Why: U3 needs bounded, source-indexed premises before the host can judge numeric and membership claims. Evidence: TestWorkCheckpointEvidenceModesAndBounds failed on missing types, then go test ./... -count=1 and git diff --check passed. Impact: Extend only the candidate wire contract; persisted task snapshots and display kinds stay unchanged. Refs: Dev task feature/fast-slow-u2-u3-plan; r0.3 evidence implementation plan Task 1
148 lines
7.3 KiB
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)
|
|
}
|
|
}
|
|
}
|