Files
Agent-System-Backend/internal/runmgr/task_checkpoint_evidence_test.go
T
weichen 6d50e487ca Canonicalize material quotes and own checkpoint status
Why: Independent re-review found single-word decision quotes, project-prefix scope mixing, revoked totals and approval status bypasses.

Evidence: Each new counterexample failed before repair; Backend go test ./... -count=1 and selected live-tag fake-stream tests passed after.

Impact: Display complete visible source sentences, parse only bounded positive count clauses, and set material progress text from the host phase.

Refs: Dev task feature/fast-slow-u2-u3-plan; r0.3 second review Important findings
2026-10-13 17:32:01 +00:00

228 lines
9.9 KiB
Go

package runmgr
import (
"agent-system-backend/internal/cardsvc"
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
core "agent-system-core"
)
func checkpointEvidenceEnvironment(t *testing.T) *runCtx {
t.Helper()
st := openRecoveryStore(t)
m := newRecoveryManager(t, st)
m.SetInstanceID("A")
m.SetCards(cardsvc.New(st, nil))
conv := recoveryConversation(t, st)
parent := recoveryRun(t, st, conv, core.RunRunning)
inputs := []struct{ name, text string }{
{"需求.txt", "交付包含12项需求,其中3项依赖外部接口。方案尚未通过。"},
{"会议.txt", "会议决定分两期交付,第一期先完成8项核心需求。"},
}
docs := make([]core.ParsedDocument, 0, len(inputs))
for _, input := range inputs {
data := []byte(input.text)
a := core.Artifact{Owner: conv.Owner, ProjectID: conv.ProjectID, Name: input.name}
if err := st.CreateArtifact(&a, data); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(data)
docs = append(docs, core.ParsedDocument{Meta: core.DocMeta{Filename: input.name, Origin: &core.DocumentOrigin{ArtifactID: a.ID, ContentHash: hex.EncodeToString(sum[:]), ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: input.text}}})
}
b, _, err := m.registerBackgroundTask(parent, bgSpawnOpts{name: "整理", material: true, documents: docs})
if err != nil {
t.Fatal(err)
}
if err := m.startTaskProjection(b, "run_material"); err != nil {
t.Fatal(err)
}
return &runCtx{mgr: m, run: core.Run{ID: "run_material", Kind: core.RunSubagent, Owner: conv.Owner, ConversationID: conv.ID, ProjectID: conv.ProjectID}, subAgent: materialSubagentID, task: b, sources: materialSources(docs, 48000)}
}
func mixedEvidenceCheckpoint(t *testing.T, c *runCtx) core.WorkCheckpointCandidate {
t.Helper()
n := func(v int) *int { return &v }
total := evidenceQuote(t, c.sources, func() string {
for _, src := range c.sources {
if strings.Contains(src.Ref.Label, "需求.txt") {
return src.Ref.ArtifactID
}
}
t.Fatal("missing requirements source")
return ""
}(), "交付包含12项需求,其中3项依赖外部接口")
phase := evidenceQuote(t, c.sources, func() string {
for _, src := range c.sources {
if strings.Contains(src.Ref.Label, "会议.txt") {
return src.Ref.ArtifactID
}
}
t.Fatal("missing meeting source")
return ""
}(), "第一期先完成8项核心需求")
return core.WorkCheckpointCandidate{CheckpointID: "r03-mixed", PhaseID: "crosscheck", Focus: "已决定第二期包含3项接口依赖", Remaining: "剩余4项均归第二期", NextStep: "按已批准的接口清单推进", Findings: []core.WorkFindingCandidate{
{FindingKey: "gap", Text: "剩余4项全部是接口依赖需求", Kind: "analysis", Assumptions: "模型推断", Sources: []core.TaskSourceQuote{total, phase}, Evidence: &core.WorkFindingEvidence{Mode: "difference", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}}, Result: n(4)}},
{FindingKey: "membership", Text: "剩余4项包含全部3项接口依赖需求", Kind: "analysis", Assumptions: "模型推断", Sources: []core.TaskSourceQuote{total, phase}, Evidence: &core.WorkFindingEvidence{Mode: "unresolved_relation", Premises: []core.WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(12)}, {Role: "phase", SourceIndex: 1, Value: n(8)}, {Role: "dependent", SourceIndex: 0, Value: n(3)}}}},
}}
}
func TestWorkCheckpointEvidenceMixedBatchIsAtomicAndHonest(t *testing.T) {
c := checkpointEvidenceEnvironment(t)
cp := mixedEvidenceCheckpoint(t, c)
before, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
receipt, err := c.PublishWorkCheckpoint(cp)
if err != nil || !strings.Contains(receipt, "待确认") {
t.Fatalf("normalization receipt absent: %q %v", receipt, err)
}
after, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
if after.Task.Revision != before.Task.Revision+1 || len(after.Task.Findings) != 2 || after.Task.Findings[0].Kind != "analysis" || after.Task.Findings[1].Kind != "needs_user" {
t.Fatalf("mixed batch did not commit once: %+v", after.Task)
}
for _, text := range []string{after.Task.Focus, after.Task.Remaining, after.Task.NextStep, after.Task.Findings[0].Text, after.Task.Findings[0].Impact, after.Task.Findings[0].Assumptions, after.Task.Findings[1].Text} {
if strings.Contains(text, "包含全部3项") || strings.Contains(text, "已批准") || strings.Contains(text, "已决定第二期") || strings.Contains(text, "均归第二期") {
t.Fatalf("unsupported assertion reached card: %q", text)
}
}
repeated, err := c.PublishWorkCheckpoint(cp)
if err != nil || repeated != receipt {
t.Fatalf("idempotent receipt changed: %q vs %q, %v", repeated, receipt, err)
}
reloaded, _ := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if reloaded.Task.Revision != after.Task.Revision || reloaded.Task.CheckpointCount != after.Task.CheckpointCount {
t.Fatal("same checkpoint advanced revision or quota")
}
}
func TestWorkCheckpointEvidenceInvalidSourceRejectsWholeBatch(t *testing.T) {
c := checkpointEvidenceEnvironment(t)
cp := mixedEvidenceCheckpoint(t, c)
before, _ := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
eventsBefore, _ := c.mgr.st.EventsAfter(c.run.ConversationID, 0, 1000)
cp.Findings[1].Sources[0].SourceID = "foreign"
if _, err := c.PublishWorkCheckpoint(cp); err == nil {
t.Fatal("invalid source was normalized instead of rejected")
}
after, _ := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
eventsAfter, _ := c.mgr.st.EventsAfter(c.run.ConversationID, 0, 1000)
if after.Task.Revision != before.Task.Revision || len(after.Task.Findings) != len(before.Task.Findings) || len(eventsAfter) != len(eventsBefore) {
t.Fatal("rejected batch changed persisted card or event stream")
}
}
func TestWorkCheckpointEvidenceSourceMustBelongToTaskInputRevision(t *testing.T) {
for _, changed := range []string{"unprovided", "parser revision"} {
t.Run(changed, func(t *testing.T) {
c := checkpointEnvironment(t)
cp := validCheckpoint(c, "scope")
if changed == "unprovided" {
data := []byte("第二期包含3项依赖外部接口")
a := core.Artifact{Owner: c.run.Owner, ProjectID: c.run.ProjectID, Name: "另一份资料.txt"}
if err := c.mgr.st.CreateArtifact(&a, data); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(data)
docs := []core.ParsedDocument{{Meta: core.DocMeta{Filename: a.Name, Origin: &core.DocumentOrigin{ArtifactID: a.ID, ContentHash: hex.EncodeToString(sum[:]), ParserRevision: "txt.v1"}}, Blocks: []core.DocBlock{{Type: core.BlockPara, Text: string(data)}}}}
for id, source := range materialSources(docs, 48000) {
c.sources[id] = source
cp.Findings[0].Sources[0] = core.TaskSourceQuote{SourceID: id, BlockIndex: 0, Quote: string(data)}
}
} else {
for id, source := range c.sources {
source.Ref.ParserRevision = "txt.v2"
c.sources[id] = source
}
}
if _, err := c.PublishWorkCheckpoint(cp); err == nil {
t.Fatal("source outside fixed task input revision was accepted")
}
})
}
}
func TestWorkCheckpointEvidenceStatusCannotAssertUnstructuredPlacement(t *testing.T) {
c := checkpointEnvironment(t)
cp := validCheckpoint(c, "unsafe-status")
cp.Focus = "接口依赖工作均安排在后续批次"
cp.Remaining = "该批次已经确定"
cp.NextStep = "全部转入后续处理"
if _, err := c.PublishWorkCheckpoint(cp); err != nil {
t.Fatal(err)
}
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
for _, value := range []string{card.Task.Focus, card.Task.Remaining, card.Task.NextStep} {
if strings.Contains(value, "均安排") || strings.Contains(value, "已经确定") || strings.Contains(value, "全部转入") {
t.Fatalf("unsupported placement escaped into status: %q", value)
}
}
}
func TestWorkCheckpointEvidenceUnknownStatusAssertionIsNeutralized(t *testing.T) {
c := checkpointEnvironment(t)
cp := validCheckpoint(c, "unknown-status")
cp.Focus = "结果全数转到下一轮"
if _, err := c.PublishWorkCheckpoint(cp); err != nil {
t.Fatal(err)
}
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
if strings.Contains(card.Task.Focus, "全数转到下一轮") {
t.Fatal("unknown status assertion escaped", card.Task.Focus)
}
}
func TestWorkCheckpointEvidenceStatusDoesNotInferApprovalFromConfirmVerb(t *testing.T) {
c := checkpointEnvironment(t)
cp := validCheckpoint(c, "approval-status")
cp.Focus = "确认方案获准"
cp.NextStep = "确认方案已生效"
if _, err := c.PublishWorkCheckpoint(cp); err != nil {
t.Fatal(err)
}
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
if card.Task.Focus == cp.Focus || card.Task.NextStep == cp.NextStep {
t.Fatalf("model approval status escaped: %q / %q", card.Task.Focus, card.Task.NextStep)
}
}
func TestWorkCheckpointEvidencePersistsFullNegativeSourceSentence(t *testing.T) {
c := checkpointEvidenceEnvironment(t)
var quote core.TaskSourceQuote
for id, source := range c.sources {
if strings.Contains(source.Ref.Label, "需求.txt") {
quote = core.TaskSourceQuote{SourceID: id, BlockIndex: source.Ref.BlockIndex, Quote: "通过"}
break
}
}
if quote.SourceID == "" {
t.Fatal("requirements source missing")
}
cp := core.WorkCheckpointCandidate{CheckpointID: "negative-record", PhaseID: "crosscheck", Findings: []core.WorkFindingCandidate{{FindingKey: "decision", Text: "方案通过", Kind: "material_record", Sources: []core.TaskSourceQuote{quote}}}}
if _, err := c.PublishWorkCheckpoint(cp); err != nil {
t.Fatal(err)
}
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
t.Fatal(err)
}
if len(card.Task.Findings) != 1 || !strings.Contains(card.Task.Findings[0].Text, "方案尚未通过") || strings.Contains(card.Task.Findings[0].Text, ":「通过」") {
t.Fatalf("short affirmative quote reached the card: %+v", card.Task.Findings)
}
}