228 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|