Files
Agent-System-Core/task_snapshot_test.go

148 lines
7.3 KiB
Go

package core
import (
"encoding/json"
"strings"
"testing"
)
// Loss of the optional task object makes refresh discard stage findings.
func TestTaskSnapshotCardRoundTrip(t *testing.T) {
raw := []byte(`{"id":"card1","projectId":"p1","conversationId":"c1","kind":"progress","status":"active","task":{"schemaVersion":1,"taskId":"t1","attemptId":"a1","cardId":"card1","revision":4,"executionState":"running","focus":"核对材料","findings":[{"id":"f1","text":"日期不同","kind":"material_record","status":"active","revision":1}]}}`)
var card ActivityCard
if err := json.Unmarshal(raw, &card); err != nil {
t.Fatal(err)
}
encoded, err := json.Marshal(card)
if err != nil {
t.Fatal(err)
}
var restored map[string]json.RawMessage
if err := json.Unmarshal(encoded, &restored); err != nil {
t.Fatal(err)
}
if _, ok := restored["task"]; !ok {
t.Fatal("task findings were lost in card persistence")
}
var task map[string]json.RawMessage
if err := json.Unmarshal(restored["task"], &task); err != nil {
t.Fatal(err)
}
if string(task["revision"]) != "4" || string(task["focus"]) != `"核对材料"` {
t.Fatal("revision/focus did not survive")
}
}
func validTaskSnapshot() TaskSnapshot {
return TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: "card1", ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", StreamScope: "conversation:c1", Title: "整理材料"}
}
func TestTaskSnapshotRejectsUnsafeAndUnboundState(t *testing.T) {
for _, mutate := range []func(*TaskSnapshot){
func(s *TaskSnapshot) { s.TaskID = "" }, func(s *TaskSnapshot) { s.AttemptID = "" },
func(s *TaskSnapshot) { s.Revision = 9007199254740992 }, func(s *TaskSnapshot) { s.SourceSeq = -1 },
func(s *TaskSnapshot) { s.ExecutionState = "imagined" }, func(s *TaskSnapshot) { s.StreamScope = "conversation:other" },
func(s *TaskSnapshot) { s.ExecutionState = "running" },
} {
s := validTaskSnapshot()
mutate(&s)
if s.Validate() == nil {
t.Errorf("invalid snapshot accepted: %+v", s)
}
}
if err := validTaskSnapshot().Validate(); err != nil {
t.Fatal(err)
}
}
func TestWorkCheckpointRejectsVerifiedAndEmptyEvidence(t *testing.T) {
c := WorkCheckpointCandidate{CheckpointID: "cp1", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{FindingKey: "date", Text: "日期不同", Kind: "verified", Sources: []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}}}}
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "kind") {
t.Fatal("model granted verified status", err)
}
c.Findings[0].Kind = "material_record"
c.Findings[0].Sources = nil
if ValidateWorkCheckpoint(c) == nil {
t.Fatal("source-free fact accepted")
}
c.Findings[0].Sources = []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}
if err := ValidateWorkCheckpoint(c); err != nil {
t.Fatal(err)
}
c.Findings[0].Text = strings.Repeat("x", 17000)
if ValidateWorkCheckpoint(c) == nil {
t.Fatal("unbounded candidate accepted")
}
}
func TestWorkCheckpointEvidenceModesAndBounds(t *testing.T) {
base := func() WorkCheckpointCandidate {
return WorkCheckpointCandidate{CheckpointID: "cp-evidence", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{
FindingKey: "phase", Text: "待核对", Kind: "needs_user",
Sources: []TaskSourceQuote{{SourceID: "total", Quote: "总12项"}, {SourceID: "phase", Quote: "一期8项"}, {SourceID: "dependent", Quote: "其中3项依赖接口"}},
}}}
}
n := func(v int) *int { return &v }
total := WorkEvidencePremise{Role: "total", SourceIndex: 0, Value: n(12)}
phase := WorkEvidencePremise{Role: "phase", SourceIndex: 1, Value: n(8)}
dependent := WorkEvidencePremise{Role: "dependent", SourceIndex: 2, Value: n(3)}
for _, tc := range []struct {
name string
evidence WorkFindingEvidence
valid bool
}{
{"direct", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}, true},
{"difference", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}, true},
{"unresolved", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}}, true},
{"unknown mode", WorkFindingEvidence{Mode: "prediction", Premises: []WorkEvidencePremise{total, phase}}, false},
{"missing result", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}}, false},
{"extra result", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}, Result: n(3)}, false},
{"duplicate role", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, total}, Result: n(4)}, false},
{"missing premise", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase}}, false},
{"negative index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: -1}}}, false},
{"foreign index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 3}}}, false},
{"negative value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(-1)}, phase}, Result: n(4)}, false},
{"large value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(1000001)}, phase}, Result: n(4)}, false},
{"direct value", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0, Value: n(1)}}}, false},
{"missing value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0}, phase}, Result: n(4)}, false},
} {
t.Run(tc.name, func(t *testing.T) {
c := base()
c.Findings[0].Evidence = &tc.evidence
err := ValidateWorkCheckpoint(c)
if (err == nil) != tc.valid {
t.Fatalf("valid=%v err=%v", tc.valid, err)
}
})
}
c := base()
c.Findings[0].Evidence = &WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}
c.Findings[0].Text = strings.Repeat("字", 5500)
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "16KiB") {
t.Fatal("evidence bypassed checkpoint size cap", err)
}
}
func TestTaskSnapshotBodyPreservesLimitsAndHonestStates(t *testing.T) {
s := validTaskSnapshot()
s.Remaining = "图片内容未纳入"
s.ExecutionState = "failed"
s.Conclusion = "处理失败,已有发现可查看"
s.Findings = []TaskFinding{{ID: "f1", Text: "金额缺失", Kind: "material_record", Status: "active", Revision: 1}}
raw, _ := json.Marshal(s.SummaryBody())
for _, want := range []string{"执行失败", "图片内容未纳入", "金额缺失", "处理失败"} {
if !strings.Contains(string(raw), want) {
t.Errorf("lost critical summary %s", want)
}
}
}
func TestTaskSnapshotUnstartedAndFinishedDoNotClaimReading(t *testing.T) {
for _, state := range []string{"queued", "completed", "interrupted"} {
s := validTaskSnapshot()
s.ExecutionState = state
s.Conclusion = "处理已结束"
b, _ := json.Marshal(s.SummaryBody())
if strings.Contains(string(b), "正在读取") {
t.Errorf("%s falsely claims active reading", state)
}
}
}