返回 DeepSeek-Reasonix
claim_test.go
根目录 / internal / completion / claim_test.go
1 package completion
2
3 import (
4 "slices"
5 "strings"
6 "testing"
7
8 "reasonix/internal/evidence"
9 )
10
11 func claimed(status string, completion string) evidence.Receipt {
12 args := `{"status":"` + status + `","completion":` + completion + `}`
13 return evidence.Receipt{ToolName: "update_goal", Success: true, Args: []byte(args)}
14 }
15
16 func TestClaimedVerificationThatNeverRanIsUnbacked(t *testing.T) {
17 rep := Build(nil, ledgerOf(
18 wrote("parser.go"),
19 read("parser.go"),
20 claimed("complete", `{"verified":["go test ./..."]}`),
21 ))
22 if got := gapKinds(rep); !slices.Contains(got, "unbacked_claim") {
23 t.Fatalf("gap kinds = %v, want the fabricated verification caught", got)
24 }
25 if !strings.Contains(rep.Gaps[0].Detail, "no run of it was recorded") {
26 t.Fatalf("gap detail = %q, want the reason named", rep.Gaps[0].Detail)
27 }
28 if rep.Gaps[0].Kind != GapUnbackedClaim {
29 t.Fatalf("an unbacked claim must lead the gap list, got %v", rep.Gaps[0].Kind)
30 }
31 }
32
33 func TestClaimedVerificationThatFailedIsUnbacked(t *testing.T) {
34 rep := Build(nil, ledgerOf(
35 wrote("parser.go"),
36 read("parser.go"),
37 ran("go test ./...", false),
38 claimed("complete", `{"verified":["go test ./..."]}`),
39 ))
40 if !strings.Contains(rep.Gaps[0].Detail, "its last run failed") {
41 t.Fatalf("gap = %+v, want the failed run named", rep.Gaps[0])
42 }
43 }
44
45 func TestClaimedVerificationBeforeTheLatestChangeIsUnbacked(t *testing.T) {
46 rep := Build(nil, ledgerOf(
47 ran("go test ./...", true),
48 wrote("parser.go"),
49 read("parser.go"),
50 claimed("complete", `{"verified":["go test ./..."]}`),
51 ))
52 if !strings.Contains(rep.Gaps[0].Detail, "before the latest change") {
53 t.Fatalf("gap = %+v, want the stale claim named", rep.Gaps[0])
54 }
55 }
56
57 func TestBackedClaimAddsNoGap(t *testing.T) {
58 rep := Build(nil, ledgerOf(
59 wrote("parser.go"),
60 read("parser.go"),
61 ran("go test ./...", true),
62 claimed("complete", `{"verified":["go test ./..."]}`),
63 ))
64 if len(rep.Gaps) != 0 {
65 t.Fatalf("gaps = %+v, want none: the claim matches a fresh successful receipt", rep.Gaps)
66 }
67 if rep.Verdict != VerdictUnknown {
68 t.Fatalf("verdict = %v, want unknown", rep.Verdict)
69 }
70 if len(rep.Claimed.Verified) != 1 {
71 t.Fatalf("claim not recorded: %+v", rep.Claimed)
72 }
73 }
74
75 // A cited command need not be byte-identical to the one that ran; the shared
76 // segment matcher decides, exactly as complete_step does.
77 func TestClaimMatchesTheCommandAsItActuallyRan(t *testing.T) {
78 rep := Build(nil, ledgerOf(
79 wrote("parser.go"),
80 read("parser.go"),
81 ran("cd /repo && go test ./...", true),
82 claimed("complete", `{"verified":["go test ./..."]}`),
83 ))
84 if len(rep.Gaps) != 0 {
85 t.Fatalf("gaps = %+v, want none: a cd prefix is not a different command", rep.Gaps)
86 }
87 }
88
89 func TestDeclaredUnverifiedAndRisksSurvive(t *testing.T) {
90 rep := Build(nil, ledgerOf(
91 wrote("parser.go"),
92 read("parser.go"),
93 ran("go test ./...", true),
94 claimed("complete", `{"unverified":["desktop UI never exercised"],"risks":["the migration is one-way"]}`),
95 ))
96 if got := gapKinds(rep); !slices.Equal(got, []string{"declared_unverified"}) {
97 t.Fatalf("gap kinds = %v, want the self-declared gap kept", got)
98 }
99 if rep.Verdict != VerdictUnknown {
100 t.Fatalf("verdict = %v, want unknown: declarations are not a quality verdict", rep.Verdict)
101 }
102 if !slices.Equal(rep.Risks, []string{"the migration is one-way"}) {
103 t.Fatalf("risks = %v", rep.Risks)
104 }
105 }
106
107 // The invariant that makes the claim safe to accept at all.
108 func TestClaimCannotClearFailedExecution(t *testing.T) {
109 rep := Build(nil, ledgerOf(wrote("parser.go"), ran("go test ./...", false), claimed("complete", `{"verified":["go test ./..."]}`)))
110 if len(rep.Verifications) != 1 || rep.Verifications[0].Passed || !slices.Contains(gapKinds(rep), "unbacked_claim") {
111 t.Fatalf("claim changed facts: %+v", rep)
112 }
113 }
114
115 func TestFailedUpdateGoalClaimsNothing(t *testing.T) {
116 rejected := evidence.Receipt{
117 ToolName: "update_goal", Success: false,
118 Args: []byte(`{"status":"complete","completion":{"verified":["go test ./..."]}}`),
119 }
120 rep := Build(nil, ledgerOf(wrote("parser.go"), read("parser.go"), rejected))
121 if !rep.Claimed.Empty() {
122 t.Fatalf("a rejected update_goal must claim nothing, got %+v", rep.Claimed)
123 }
124 if got := gapKinds(rep); slices.Contains(got, "unbacked_claim") {
125 t.Fatalf("gap kinds = %v, want no claim gap from a failed call", got)
126 }
127 }
128
129 func TestLatestCompleteClaimIgnoresContinueAndFailedCalls(t *testing.T) {
130 led := ledgerOf(
131 wrote("parser.go"),
132 claimed("continue", `{"unverified":["nothing run yet"]}`),
133 claimed("complete", `{"unverified":["real-engine e2e"]}`),
134 )
135 got, ok := LatestCompleteClaim(led)
136 if !ok || len(got.Unverified) != 1 || got.Unverified[0] != "real-engine e2e" {
137 t.Fatalf("LatestCompleteClaim = (%+v, %v)", got, ok)
138 }
139 rejected := evidence.Receipt{
140 ToolName: "update_goal", Success: false,
141 Args: []byte(`{"status":"complete","completion":{"unverified":["should not count"]}}`),
142 }
143 if _, ok := LatestCompleteClaim(ledgerOf(rejected)); ok {
144 t.Fatal("a failed update_goal must not yield a complete claim")
145 }
146 }
147
148 func TestLatestClaimWins(t *testing.T) {
149 rep := Build(nil, ledgerOf(
150 wrote("parser.go"),
151 read("parser.go"),
152 claimed("continue", `{"unverified":["nothing run yet"]}`),
153 ran("go test ./...", true),
154 claimed("complete", `{"verified":["go test ./..."]}`),
155 ))
156 if len(rep.Gaps) != 0 {
157 t.Fatalf("gaps = %+v, want the superseded claim dropped", rep.Gaps)
158 }
159 }
160
160 lines GO