返回 DeepSeek-Reasonix
complete_subtask_test.go
根目录 / internal / agent / complete_subtask_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/evidence"
11 "reasonix/internal/provider"
12 "reasonix/internal/tool"
13 )
14
15 func submitCompleteSubtask(t *testing.T, led *evidence.Ledger, args string) string {
16 t.Helper()
17 ctx := evidence.WithLedger(context.Background(), led)
18 out, err := NewCompleteSubtaskTool().Execute(ctx, json.RawMessage(args))
19 if err != nil {
20 t.Fatalf("complete_subtask: %v", err)
21 }
22 return out
23 }
24
25 // The model's report is preserved; execution facts are a separate projection.
26 func TestCompleteSubtaskPreservesModelReport(t *testing.T) {
27 led := evidence.NewLedger()
28 led.Record(evidence.Receipt{ToolName: "bash", Command: "go test ./parser", Success: true, OutputBytes: 12})
29
30 args := `{
31 "status":"complete",
32 "summary":"fixed the parser",
33 "acceptance_criteria":[
34 {"id":"AC1","status":"satisfied","evidence":[{"kind":"verification","summary":"unit tests","command":"go test ./parser"}]},
35 {"id":"AC2","status":"satisfied","evidence":[{"kind":"verification","summary":"integration suite","command":"go test ./integration"}]}
36 ]}`
37 if out := submitCompleteSubtask(t, led, args); !strings.Contains(out, "status=complete") {
38 t.Fatalf("tool result = %q, want the model's status", out)
39 }
40
41 // The agent host, not the tool, records the call; replay that receipt so the
42 // ledger lookup the report renderer uses is covered too.
43 led.Record(evidence.ReceiptFromToolCall("complete_subtask", json.RawMessage(args), true, true))
44 report, ok := led.LatestCompletionReport()
45 if !ok {
46 t.Fatal("a recorded complete_subtask call must be recoverable from the ledger")
47 }
48 if report.Status != evidence.CompletionComplete || report.Criteria[1].Status != evidence.CriterionSatisfied {
49 t.Fatalf("host rewrote the model report: %+v", report)
50 }
51 }
52
53 func TestCompleteSubtaskKeepsFullyBackedClaim(t *testing.T) {
54 led := evidence.NewLedger()
55 led.Record(evidence.Receipt{ToolName: "bash", Command: "go test ./parser", Success: true, OutputBytes: 12})
56 led.Record(evidence.Receipt{ToolName: "write_file", Success: true, Mutation: true, Write: true, Paths: []string{"parser.go"}})
57
58 out := submitCompleteSubtask(t, led, `{
59 "status":"complete",
60 "summary":"fixed the parser",
61 "acceptance_criteria":[
62 {"id":"AC1","status":"satisfied","evidence":[{"kind":"verification","summary":"tests","command":"go test ./parser"}]},
63 {"id":"AC2","status":"satisfied","evidence":[{"kind":"diff","summary":"the fix","paths":["parser.go"]}]}
64 ]}`)
65 if !strings.Contains(out, "status=complete") || strings.Contains(out, "lowered") {
66 t.Fatalf("tool result = %q, want an untouched complete status", out)
67 }
68 }
69
70 func TestCompleteSubtaskAcceptsReportWithoutHostEvidence(t *testing.T) {
71 out, err := NewCompleteSubtaskTool().Execute(context.Background(), json.RawMessage(`{
72 "status":"complete","summary":"done",
73 "acceptance_criteria":[
74 {"id":"AC1","status":"satisfied","evidence":[{"kind":"manual","summary":"I checked it"}]},
75 {"id":"AC2","status":"satisfied"}
76 ]}`))
77 if err != nil {
78 t.Fatal(err)
79 }
80 if !strings.Contains(out, "status=complete") || strings.Contains(out, "lowered") {
81 t.Fatalf("report was adjudicated: %s", out)
82 }
83 }
84
85 func TestParseCompletionReportRejectsMalformedClaims(t *testing.T) {
86 for name, args := range map[string]string{
87 "bad status": `{"status":"done","summary":"x"}`,
88 "missing summary": `{"status":"complete"}`,
89 "verification no cmd": `{"status":"complete","summary":"x","acceptance_criteria":[{"id":"AC1","status":"satisfied","evidence":[{"kind":"verification","summary":"tests"}]}]}`,
90 "diff without paths": `{"status":"complete","summary":"x","acceptance_criteria":[{"id":"AC1","status":"satisfied","evidence":[{"kind":"diff","summary":"change"}]}]}`,
91 "unknown evidence": `{"status":"complete","summary":"x","acceptance_criteria":[{"id":"AC1","status":"satisfied","evidence":[{"kind":"vibes","summary":"trust me"}]}]}`,
92 "criterion without id": `{"status":"complete","summary":"x","acceptance_criteria":[{"status":"satisfied"}]}`,
93 } {
94 if _, err := evidence.ParseCompletionReport(json.RawMessage(args)); err == nil {
95 t.Errorf("%s: accepted a malformed report", name)
96 }
97 }
98 }
99
100 func TestSubAgentAnswerSeparatesModelReportFromExecutionFacts(t *testing.T) {
101 reg := tool.NewRegistry()
102 reg.Add(fakeWriteFileTool{})
103 AttachCompleteSubtaskTool(reg)
104 prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
105 {toolCallChunk("1", "write_file", `{"path":"parser.go"}`), {Type: provider.ChunkDone}},
106 {toolCallChunk("2", "complete_subtask", `{"status":"complete","summary":"fixed the parser","acceptance_criteria":[{"id":"AC1","status":"satisfied","evidence":[{"kind":"diff","summary":"the fix","paths":["parser.go"]}]},{"id":"AC2","status":"satisfied","evidence":[{"kind":"verification","summary":"suite","command":"go test ./..."}]}],"unresolved":["integration suite not executed"]}`), {Type: provider.ChunkDone}},
107 {{Type: provider.ChunkText, Text: "all good"}, {Type: provider.ChunkDone}},
108 }}
109
110 answer, err := RunSubAgentWithSession(withNoClosedLoop(context.Background()), prov, reg, NewSession("sys"),
111 "fix the parser", Options{}, event.Discard)
112 if err != nil {
113 t.Fatalf("RunSubAgentWithSession: %v", err)
114 }
115 if !strings.HasPrefix(answer, "Model-reported status: complete") {
116 t.Fatalf("answer must label the model's assessment:\n%s", answer)
117 }
118 for _, want := range []string{
119 "AC1 satisfied",
120 "AC2 satisfied",
121 "unresolved: integration suite not executed",
122 hostReceiptsHeader,
123 } {
124 if !strings.Contains(answer, want) {
125 t.Fatalf("answer missing %q:\n%s", want, answer)
126 }
127 }
128 if strings.Contains(answer, "host lowered") {
129 t.Fatalf("host adjudication survived: %s", answer)
130 }
131 }
132
133 func TestSubAgentMayFinishWithoutCompletionReport(t *testing.T) {
134 reg := tool.NewRegistry()
135 AttachCompleteSubtaskTool(reg)
136 prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
137 {{Type: provider.ChunkText, Text: "Analysis complete."}, {Type: provider.ChunkDone}},
138 }}
139 answer, err := RunSubAgentWithSession(context.Background(), prov, reg, NewSession("sys"),
140 completeSubtaskContract, Options{}, event.Discard)
141 if err != nil || answer != "Analysis complete." {
142 t.Fatalf("plain completion = %q, %v", answer, err)
143 }
144 }
145
145 lines GO