640 lines
24 KiB
Go
640 lines
24 KiB
Go
package evidence
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
)
|
||
|
||
func TestLedgerRecordsSuccessAndFailureReceipts(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "bash",
|
||
Args: json.RawMessage(`{"command":"go test ./..."}`),
|
||
Success: true,
|
||
Command: "go test ./...",
|
||
})
|
||
ledger.Record(Receipt{
|
||
ToolName: "bash",
|
||
Args: json.RawMessage(`{"command":"go test ./internal/..."}`),
|
||
Success: false,
|
||
Command: "go test ./internal/...",
|
||
})
|
||
|
||
if !ledger.HasSuccessfulCommand("go test ./...") {
|
||
t.Fatal("successful bash command should verify")
|
||
}
|
||
if ledger.HasSuccessfulCommand("go test ./internal/...") {
|
||
t.Fatal("failed bash command must not verify")
|
||
}
|
||
}
|
||
|
||
func TestLedgerHasWriteOrCommandSince(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{ToolName: "todo_write", Success: true, Todos: []TodoItem{{Content: "edit", Status: "in_progress"}}})
|
||
ledger.Record(Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}})
|
||
ledger.Record(Receipt{ToolName: "bash", Success: false, Command: "go test ./..."})
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "edit"})
|
||
|
||
if got := ledger.Len(); got != 4 {
|
||
t.Fatalf("Len() = %d, want 4", got)
|
||
}
|
||
if !ledger.HasWriteOrCommandSince(0) {
|
||
t.Fatal("write receipt at index 1 should count from index 0")
|
||
}
|
||
if !ledger.HasWriteOrCommandSince(-1) {
|
||
t.Fatal("negative index should behave like 0")
|
||
}
|
||
if ledger.HasWriteOrCommandSince(2) {
|
||
t.Fatal("failed command and bookkeeping receipts must not count as progress")
|
||
}
|
||
ledger.Record(Receipt{ToolName: "bash", Success: true, Command: "go test ./..."})
|
||
if !ledger.HasWriteOrCommandSince(2) {
|
||
t.Fatal("successful command receipt after index should count as progress")
|
||
}
|
||
var nilLedger *Ledger
|
||
if nilLedger.HasWriteOrCommandSince(0) {
|
||
t.Fatal("nil ledger must report no progress")
|
||
}
|
||
}
|
||
|
||
func TestLedgerMatchesFileReadAndWriteReceipts(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{ToolName: "read_file", Success: true, Paths: []string{`internal/tool/builtin/completestep.go`}, Read: true})
|
||
ledger.Record(Receipt{ToolName: "write_file", Success: true, Paths: []string{`internal/evidence/evidence.go`}, Write: true})
|
||
ledger.Record(Receipt{ToolName: "edit_file", Success: false, Paths: []string{`failed.go`}, Write: true})
|
||
|
||
if !ledger.HasSuccessfulReadOrWrite([]string{`internal\tool\builtin\completestep.go`}) {
|
||
t.Fatal("read receipt should verify the same path across separators")
|
||
}
|
||
if !ledger.HasSuccessfulWrite([]string{`internal/evidence/evidence.go`}) {
|
||
t.Fatal("write receipt should verify written path")
|
||
}
|
||
if ledger.HasSuccessfulWrite([]string{`failed.go`}) {
|
||
t.Fatal("failed write receipt must not verify")
|
||
}
|
||
}
|
||
|
||
func TestLedgerReportsAnchorRefreshReadsAfterWrites(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{ToolName: "write_file", Success: true, Paths: []string{`src\a.go`}, Write: true})
|
||
writeIndex, ok := ledger.LatestSuccessfulWriteIndex([]string{`src/a.go`})
|
||
if !ok {
|
||
t.Fatal("expected latest write index")
|
||
}
|
||
if ledger.HasSuccessfulAnchorRefreshReadAfter([]string{`src/a.go`}, writeIndex) {
|
||
t.Fatal("read-after-write should be false before a read")
|
||
}
|
||
|
||
ledger.Record(Receipt{ToolName: "grep", Success: true, Paths: []string{`src/a.go`}, Read: true, Args: json.RawMessage(`{"path":"src/a.go","pattern":"func"}`)})
|
||
if ledger.HasSuccessfulAnchorRefreshReadAfter([]string{`src/a.go`}, writeIndex) {
|
||
t.Fatal("grep should not refresh anchor edit state")
|
||
}
|
||
ledger.Record(Receipt{ToolName: "read_file", Success: true, Paths: []string{`src/a.go`}, Read: true, Args: json.RawMessage(`{"path":"src/a.go","offset":100,"limit":20}`)})
|
||
if ledger.HasSuccessfulAnchorRefreshReadAfter([]string{`src/a.go`}, writeIndex) {
|
||
t.Fatal("windowed read_file should not refresh anchor edit state")
|
||
}
|
||
ledger.Record(Receipt{ToolName: "read_file", Success: true, Paths: []string{`src/a.go`}, Read: true, Args: json.RawMessage(`{"path":"src/a.go"}`)})
|
||
if !ledger.HasSuccessfulAnchorRefreshReadAfter([]string{`src/a.go`}, writeIndex) {
|
||
t.Fatal("read-after-write should be true after a successful read")
|
||
}
|
||
}
|
||
|
||
func TestLedgerReportsFinalReadinessReceiptsAfterWriter(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{ToolName: "bash", Success: true, Command: "go test ./..."})
|
||
ledger.Record(Receipt{ToolName: "write_file", Success: true, Paths: []string{"changed.go"}, Write: true})
|
||
ledger.Record(Receipt{ToolName: "bash", Success: false, Command: "git diff --check"})
|
||
ledger.Record(Receipt{ToolName: "todo_write", Success: true, Todos: []TodoItem{{Content: "Edit code", Status: "in_progress"}}})
|
||
|
||
writer, ok := ledger.LatestSuccessfulWriterIndex()
|
||
if !ok {
|
||
t.Fatal("expected latest successful writer")
|
||
}
|
||
if ledger.HasSuccessfulCommandAfter("go test ./...", writer) {
|
||
t.Fatal("command before latest writer must not satisfy final readiness")
|
||
}
|
||
if ledger.HasSuccessfulCommandAfter("git diff --check", writer) {
|
||
t.Fatal("failed command must not satisfy final readiness")
|
||
}
|
||
if ledger.HasSuccessfulCompleteStepAfter(writer) {
|
||
t.Fatal("missing complete_step must not satisfy final readiness")
|
||
}
|
||
if !ledger.HasSuccessfulTodoWrite() {
|
||
t.Fatal("successful todo_write receipt should be reported")
|
||
}
|
||
|
||
ledger.Record(Receipt{ToolName: "bash", Success: true, Command: "git diff --check"})
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "Edit code"})
|
||
if !ledger.HasSuccessfulCommandAfter("git diff --check", writer) {
|
||
t.Fatal("command after latest writer should satisfy final readiness")
|
||
}
|
||
if !ledger.HasSuccessfulCompleteStepAfter(writer) {
|
||
t.Fatal("complete_step after latest writer should satisfy final readiness")
|
||
}
|
||
}
|
||
|
||
func TestLedgerResetClearsTurnReceipts(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{ToolName: "bash", Success: true, Command: "go test ./..."})
|
||
|
||
ledger.Reset()
|
||
|
||
if ledger.HasSuccessfulCommand("go test ./...") {
|
||
t.Fatal("reset should clear prior-turn evidence")
|
||
}
|
||
}
|
||
|
||
func TestContextCarriesLedger(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ctx := WithLedger(context.Background(), ledger)
|
||
|
||
got, ok := FromContext(ctx)
|
||
if !ok {
|
||
t.Fatal("ledger missing from context")
|
||
}
|
||
if got != ledger {
|
||
t.Fatal("context returned a different ledger")
|
||
}
|
||
}
|
||
|
||
func TestReceiptFromToolCallExtractsEvidenceFields(t *testing.T) {
|
||
bash := ReceiptFromToolCall("bash", json.RawMessage(`{"command":"git diff --check"}`), true, false)
|
||
if bash.Command != "git diff --check" {
|
||
t.Fatalf("bash command = %q", bash.Command)
|
||
}
|
||
if bash.Write {
|
||
t.Fatal("bash should not be treated as a verified file writer")
|
||
}
|
||
|
||
write := ReceiptFromToolCall("write_file", json.RawMessage(`{"path":"internal/evidence/evidence.go","content":"x"}`), true, false)
|
||
if !write.Write || len(write.Paths) != 1 || write.Paths[0] != `internal/evidence/evidence.go` {
|
||
t.Fatalf("write receipt not extracted: %+v", write)
|
||
}
|
||
|
||
read := ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/tool/builtin/completestep.go"}`), true, true)
|
||
if !read.Read || len(read.Paths) != 1 {
|
||
t.Fatalf("read receipt not extracted: %+v", read)
|
||
}
|
||
|
||
glob := ReceiptFromToolCall("glob", json.RawMessage(`{"pattern":"**/*.go"}`), true, true)
|
||
if !glob.Read {
|
||
t.Fatalf("generic read-only tool should be treated as read context: %+v", glob)
|
||
}
|
||
}
|
||
|
||
func TestReceiptFromToolCallExtractsTodoWriteItems(t *testing.T) {
|
||
receipt := ReceiptFromToolCall("todo_write", json.RawMessage(`{"todos":[
|
||
{"content":"Add parser","status":"in_progress","activeForm":"Adding parser"},
|
||
{"content":"Wire parser","status":"pending","level":1}
|
||
]}`), true, true)
|
||
|
||
if len(receipt.Todos) != 2 {
|
||
t.Fatalf("todos not extracted: %+v", receipt)
|
||
}
|
||
if receipt.Todos[0].Content != "Add parser" || receipt.Todos[0].Status != "in_progress" || receipt.Todos[0].ActiveForm != "Adding parser" {
|
||
t.Fatalf("first todo not extracted: %+v", receipt.Todos[0])
|
||
}
|
||
if receipt.Todos[1].Level != 1 {
|
||
t.Fatalf("todo level not extracted: %+v", receipt.Todos[1])
|
||
}
|
||
}
|
||
|
||
func TestReceiptFromToolCallExtractsCompleteStep(t *testing.T) {
|
||
receipt := ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"Add parser",
|
||
"result":"parser added",
|
||
"evidence":[{"kind":"manual","summary":"checked manually"}]
|
||
}`), true, true)
|
||
|
||
if receipt.Step != "Add parser" {
|
||
t.Fatalf("complete_step step = %q", receipt.Step)
|
||
}
|
||
if !receipt.StepProof {
|
||
t.Fatalf("complete_step evidence proof not extracted: %+v", receipt)
|
||
}
|
||
if receipt.Read {
|
||
t.Fatalf("complete_step should not be treated as read-only context: %+v", receipt)
|
||
}
|
||
|
||
missingResult := ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"Add parser",
|
||
"evidence":[{"kind":"manual","summary":"checked manually"}]
|
||
}`), false, true)
|
||
if missingResult.StepProof {
|
||
t.Fatalf("complete_step without result should not count as proof: %+v", missingResult)
|
||
}
|
||
|
||
missingCommand := ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"Add parser",
|
||
"result":"parser added",
|
||
"evidence":[{"kind":"verification","summary":"checked manually"}]
|
||
}`), false, true)
|
||
if missingCommand.StepProof {
|
||
t.Fatalf("verification evidence without command should not count as proof: %+v", missingCommand)
|
||
}
|
||
|
||
emptyProof := ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"Add parser",
|
||
"result":"parser added",
|
||
"evidence":[]
|
||
}`), false, true)
|
||
if emptyProof.StepProof {
|
||
t.Fatalf("empty complete_step evidence should not count as proof: %+v", emptyProof)
|
||
}
|
||
}
|
||
|
||
func TestReceiptFromToolCallExtractsCompleteStepIndex(t *testing.T) {
|
||
receipt := ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step_index":2,
|
||
"result":"done",
|
||
"evidence":[{"kind":"manual","summary":"checked"}]
|
||
}`), true, true)
|
||
|
||
if receipt.Step != "2" {
|
||
t.Fatalf("step index not extracted as step identity: %+v", receipt)
|
||
}
|
||
if !receipt.StepProof {
|
||
t.Fatalf("step proof not detected: %+v", receipt)
|
||
}
|
||
}
|
||
|
||
func TestLedgerMatchesLatestSuccessfulTodoStep(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: false,
|
||
Todos: []TodoItem{{Content: "Failed only", Status: "in_progress"}},
|
||
})
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Add parser", Status: "in_progress", ActiveForm: "Adding parser"},
|
||
{Content: "Wire parser", Status: "completed"},
|
||
{Content: "Document parser", Status: "pending"},
|
||
},
|
||
})
|
||
|
||
for _, step := range []string{"Add parser", "Adding parser", "2"} {
|
||
match, ok := ledger.MatchLatestTodoStep(step)
|
||
if !ok {
|
||
t.Fatalf("latest todo receipt missing for %q", step)
|
||
}
|
||
if !match.Found {
|
||
t.Fatalf("step %q did not match latest todo list", step)
|
||
}
|
||
if step == "2" && match.Content != "Wire parser" {
|
||
t.Fatalf("numeric step matched %q, want Wire parser", match.Content)
|
||
}
|
||
}
|
||
|
||
match, ok := ledger.MatchLatestTodoStep("Failed only")
|
||
if !ok {
|
||
t.Fatal("successful todo receipt should exist")
|
||
}
|
||
if match.Found {
|
||
t.Fatal("failed todo_write receipt must not match")
|
||
}
|
||
}
|
||
|
||
func TestMatchTodoStepToleratesCitationDrift(t *testing.T) {
|
||
// Verbatim shape from discussion #3970: todo authored with a fullwidth
|
||
// colon, cited back with a halfwidth one — and stuck forever.
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Phase 4:环境准备", Status: "completed"},
|
||
{Content: "Phase 5:脚本编辑与执行代码", Status: "in_progress"},
|
||
{Content: "Review notes", Status: "pending"},
|
||
},
|
||
})
|
||
|
||
matches := map[string]int{
|
||
"Phase 5: 脚本编辑与执行代码": 2,
|
||
"phase 5:脚本编辑与执行代码": 2,
|
||
" Phase 5:脚本编辑与执行代码": 2,
|
||
"脚本编辑与执行代码": 2,
|
||
"Phase 4:环境": 1,
|
||
"REVIEW NOTES": 3,
|
||
"2": 2,
|
||
}
|
||
for step, want := range matches {
|
||
match, ok := ledger.MatchLatestTodoStep(step)
|
||
if !ok || !match.Found {
|
||
t.Fatalf("step %q should match todo %d, got found=%v", step, want, match.Found)
|
||
}
|
||
if match.Index != want {
|
||
t.Errorf("step %q matched todo %d, want %d", step, match.Index, want)
|
||
}
|
||
}
|
||
|
||
for _, step := range []string{"deploy backend", "代码", "Phase 9:不存在的阶段"} {
|
||
if match, _ := ledger.MatchLatestTodoStep(step); match.Found {
|
||
t.Errorf("step %q should not match, got todo %d (%q)", step, match.Index, match.Content)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestMatchTodoStepAmbiguousContainmentStaysUnmatched(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Deploy backend service", Status: "in_progress"},
|
||
{Content: "Deploy backend worker", Status: "pending"},
|
||
},
|
||
})
|
||
if match, _ := ledger.MatchLatestTodoStep("Deploy backend"); match.Found {
|
||
t.Fatalf("ambiguous citation should stay unmatched, got todo %d (%q)", match.Index, match.Content)
|
||
}
|
||
if match, _ := ledger.MatchLatestTodoStep("Deploy backend worker"); !match.Found || match.Index != 2 {
|
||
t.Fatal("exact citation must still resolve despite shared prefix")
|
||
}
|
||
}
|
||
|
||
func TestLedgerRequiresCompleteStepForNewCompletedTodos(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Add parser", Status: "in_progress"},
|
||
{Content: "Already done", Status: "completed"},
|
||
},
|
||
})
|
||
|
||
current := []TodoItem{
|
||
{Content: "Add parser", Status: "completed"},
|
||
{Content: "Already done", Status: "completed"},
|
||
}
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos(current)
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 1 || missing[0].Content != "Add parser" {
|
||
t.Fatalf("missing = %+v, want only Add parser", missing)
|
||
}
|
||
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: false, Step: "Add parser"})
|
||
missing, hasBaseline = ledger.UnverifiedCompletedTodos(current)
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline after failed complete_step")
|
||
}
|
||
if len(missing) != 1 || missing[0].Content != "Add parser" {
|
||
t.Fatalf("failed complete_step without proof-bearing recovery should not authorize completion, missing = %+v", missing)
|
||
}
|
||
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "Add parser"})
|
||
missing, hasBaseline = ledger.UnverifiedCompletedTodos(current)
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline after successful complete_step")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("successful complete_step should authorize completion, missing = %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestLedgerMatchesCompletionByActiveFormAndNumber(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Add parser", Status: "in_progress", ActiveForm: "Adding parser"},
|
||
{Content: "Wire parser", Status: "in_progress"},
|
||
},
|
||
})
|
||
|
||
current := []TodoItem{
|
||
{Content: "Add parser", Status: "completed", ActiveForm: "Adding parser"},
|
||
{Content: "Wire parser", Status: "in_progress"},
|
||
}
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "Adding parser"})
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos(current)
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("activeForm complete_step should authorize completion, missing = %+v", missing)
|
||
}
|
||
|
||
current = []TodoItem{
|
||
{Content: "Add parser", Status: "completed", ActiveForm: "Adding parser"},
|
||
{Content: "Wire parser", Status: "completed"},
|
||
}
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "2"})
|
||
missing, hasBaseline = ledger.UnverifiedCompletedTodos(current)
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("numeric complete_step should authorize completion, missing = %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestLedgerNumericCompleteStepDoesNotAuthorizeReplacedTodo(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{{Content: "Add parser", Status: "in_progress"}},
|
||
})
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "1"})
|
||
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos([]TodoItem{
|
||
{Content: "Ship parser", Status: "completed"},
|
||
})
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 1 || missing[0].Content != "Ship parser" {
|
||
t.Fatalf("numeric complete_step should not authorize a replaced todo, missing = %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestLedgerNumericCompleteStepFollowsReorderedSignedTodo(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Add parser", Status: "in_progress"},
|
||
{Content: "Write tests", Status: "pending"},
|
||
},
|
||
})
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "1"})
|
||
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos([]TodoItem{
|
||
{Content: "Write tests", Status: "pending"},
|
||
{Content: "Add parser", Status: "completed"},
|
||
})
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("numeric complete_step should follow the signed todo identity after reorder, missing = %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestLedgerNoBaselineDoesNotConstrainCompletedTodos(t *testing.T) {
|
||
ledger := NewLedger()
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos([]TodoItem{
|
||
{Content: "Add parser", Status: "completed"},
|
||
})
|
||
|
||
if hasBaseline {
|
||
t.Fatal("empty ledger should not report a prior todo_write baseline")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("no baseline should not report missing completions, got %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestLedgerNumericCompleteStepAuthorizesRephrasedTodo(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(Receipt{
|
||
ToolName: "todo_write",
|
||
Success: true,
|
||
Todos: []TodoItem{
|
||
{Content: "Add parser", Status: "in_progress"},
|
||
{Content: "Write tests", Status: "pending"},
|
||
},
|
||
})
|
||
ledger.Record(Receipt{ToolName: "complete_step", Success: true, Step: "1"})
|
||
|
||
// The model rephrased item 1 (added detail) but it's the same task.
|
||
missing, hasBaseline := ledger.UnverifiedCompletedTodos([]TodoItem{
|
||
{Content: "Add parser with streaming support", Status: "completed"},
|
||
{Content: "Write tests", Status: "pending"},
|
||
})
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline")
|
||
}
|
||
if len(missing) != 0 {
|
||
t.Fatalf("rephrased todo at same index should be authorized by content overlap, missing = %+v", missing)
|
||
}
|
||
|
||
// The model also rephrased item 2; still ok because the new text contains the old.
|
||
missing, hasBaseline = ledger.UnverifiedCompletedTodos([]TodoItem{
|
||
{Content: "Add parser with streaming support", Status: "completed"},
|
||
{Content: "Write tests and benchmarks", Status: "completed"},
|
||
})
|
||
if !hasBaseline {
|
||
t.Fatal("expected prior todo_write baseline for second rephrase")
|
||
}
|
||
// Item 1 is already authorized; item 2 is also rephrased but lacks a
|
||
// complete_step — so it should still be flagged.
|
||
if len(missing) != 1 || missing[0].Content != "Write tests and benchmarks" {
|
||
t.Fatalf("rephrased todo without complete_step should still be missing, got %+v", missing)
|
||
}
|
||
}
|
||
|
||
func TestToolCallMutatesForDeliveryProfile(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
toolName string
|
||
args string
|
||
readOnly bool
|
||
want bool
|
||
}{
|
||
{name: "trusted reader", toolName: "read_file", args: `{"path":"a.go"}`, readOnly: true},
|
||
{name: "file writer", toolName: "edit_file", args: `{"path":"a.go"}`, want: true},
|
||
{name: "delegated task meta", toolName: "task", args: `{"prompt":"fix it"}`},
|
||
{name: "run_skill meta", toolName: "run_skill", args: `{"name":"review"}`},
|
||
{name: "review meta", toolName: "review", args: `{"task":"review changes"}`},
|
||
{name: "security_review meta", toolName: "security_review", args: `{"task":"security"}`},
|
||
{name: "use_capability meta", toolName: "use_capability", args: `{"action":"inspect","capability_id":"mcp-server:github"}`},
|
||
{name: "test command", toolName: "bash", args: `{"command":"go test ./..."}`},
|
||
{name: "diff review", toolName: "bash", args: `{"command":"git diff --check"}`},
|
||
{name: "formatter write", toolName: "bash", args: `{"command":"gofmt -w internal/a.go"}`, want: true},
|
||
{name: "file redirect", toolName: "bash", args: `{"command":"printf x > generated.txt"}`, want: true},
|
||
{name: "compound verification", toolName: "bash", args: `{"command":"go test ./... 2>&1 | tail -20"}`},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := ToolCallMutates(tt.toolName, json.RawMessage(tt.args), tt.readOnly); got != tt.want {
|
||
t.Fatalf("ToolCallMutates(%q, %s) = %v, want %v", tt.toolName, tt.args, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestToolCallRequiresDeliveryCriteriaForExecutionCommands(t *testing.T) {
|
||
if !ToolCallRequiresDeliveryCriteria("bash", json.RawMessage(`{"command":"go test ./..."}`), false) {
|
||
t.Fatal("verification command should require delivery acceptance criteria")
|
||
}
|
||
if !ToolCallRequiresDeliveryCriteria("bash", json.RawMessage(`{"command":"npm run test"}`), false) {
|
||
t.Fatal("npm run test should require delivery acceptance criteria")
|
||
}
|
||
if !ToolCallRequiresDeliveryCriteria("bash", json.RawMessage(`{"command":"git diff --check"}`), false) {
|
||
t.Fatal("git diff --check is a verification command and should require acceptance criteria")
|
||
}
|
||
}
|
||
|
||
func TestLedgerDeliverySignoffRequiresPostMutationVerificationAndReview(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(ReceiptFromToolCall("todo_write", json.RawMessage(`{"todos":[{"content":"Ship parser","status":"in_progress"}]}`), true, true))
|
||
ledger.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/parser.go"}`), true, false))
|
||
mutation, ok := ledger.LatestSuccessfulMutationIndex()
|
||
if !ok {
|
||
t.Fatal("expected mutation receipt")
|
||
}
|
||
ledger.Record(ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/parser.go"}`), true, true))
|
||
ledger.Record(ReceiptFromToolCall("bash", json.RawMessage(`{"command":"go test ./internal/..."}`), true, false))
|
||
ledger.Record(ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"Ship parser",
|
||
"result":"parser shipped",
|
||
"evidence":[{"kind":"verification","summary":"tests passed","command":"go test ./internal/..."}]
|
||
}`), true, true))
|
||
|
||
if !ledger.HasSuccessfulAcceptanceCriteria() {
|
||
t.Fatal("expected non-empty todo_write to establish acceptance criteria")
|
||
}
|
||
if !ledger.HasSuccessfulReviewAfter(mutation) {
|
||
t.Fatal("expected post-mutation read to satisfy review")
|
||
}
|
||
if !ledger.HasSuccessfulDeliverySignoffAfter(mutation) {
|
||
t.Fatal("expected post-mutation verification cited by complete_step")
|
||
}
|
||
}
|
||
|
||
func TestLedgerDeliverySignoffRejectsPreMutationVerification(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(ReceiptFromToolCall("bash", json.RawMessage(`{"command":"go test ./..."}`), true, false))
|
||
ledger.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"main.go"}`), true, false))
|
||
mutation, ok := ledger.LatestSuccessfulMutationIndex()
|
||
if !ok {
|
||
t.Fatal("expected mutation receipt")
|
||
}
|
||
ledger.Record(ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"change",
|
||
"result":"changed",
|
||
"evidence":[{"kind":"verification","summary":"tests passed before edit","command":"go test ./..."}]
|
||
}`), true, true))
|
||
if ledger.HasSuccessfulDeliverySignoffAfter(mutation) {
|
||
t.Fatal("pre-mutation verification must not sign off changed work")
|
||
}
|
||
}
|
||
|
||
func TestLedgerDeliverySignoffRejectsInspectionCommandMasqueradingAsVerification(t *testing.T) {
|
||
ledger := NewLedger()
|
||
ledger.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"main.go"}`), true, false))
|
||
mutation, ok := ledger.LatestSuccessfulMutationIndex()
|
||
if !ok {
|
||
t.Fatal("expected mutation receipt")
|
||
}
|
||
ledger.Record(ReceiptFromToolCall("bash", json.RawMessage(`{"command":"git status --short"}`), true, false))
|
||
ledger.Record(ReceiptFromToolCall("complete_step", json.RawMessage(`{
|
||
"step":"change",
|
||
"result":"changed",
|
||
"evidence":[{"kind":"verification","summary":"claimed verification","command":"git status --short"}]
|
||
}`), true, true))
|
||
if ledger.HasSuccessfulDeliverySignoffAfter(mutation) {
|
||
t.Fatal("inspection-only git status must not count as delivery verification")
|
||
}
|
||
}
|