Files
zero/internal/trace/task_state_test.go
Anandan 9242b9e037 Add transcript-grounded structured task state (#761)
* Add transcript-grounded structured task state

Project existing plan, tool-result, changed-file, verification, and completion events into a deterministic per-run snapshot.

Emit content-free aggregate snapshots to traces. Carry bounded objective context through compaction and ground completion checks only when the projection matches the transcript; mismatches retain transcript-derived behavior and drop stale carried state.

Tested: make fmt-check

Tested: go vet ./...

Tested: go test ./...

Tested: go test -race ./internal/agent ./internal/trace

Tested: go run ./cmd/zero-release build

Tested: go run ./cmd/zero-release smoke

Tested: govulncheck ./... (no vulnerabilities found)

Advisory: pinned golangci-lint reports only unrelated pre-existing findings

Tested: git diff HEAD --check

* Preserve task objective across repeated compaction

Keep the immutable run objective when plan calls have been elided, while admitting mutable task fields only when plan parity still matches the transcript.

Make plan parsing match update_plan rejection semantics, coalesce and cap task-state trace events, rename parity to its plan-only scope, use explicit nilable dependencies, and remove confirmable objective fingerprints from traces.

Tested: make fmt-check

Tested: go vet ./...

Tested: go test ./...

Tested: go test -race ./internal/agent ./internal/trace

Tested: go run ./cmd/zero-release build

Tested: go run ./cmd/zero-release smoke

Tested: govulncheck ./... (no vulnerabilities found)

Advisory: pinned golangci-lint reports only unrelated pre-existing findings

Tested: git diff HEAD --check
2026-07-19 21:38:58 +05:30

67 lines
2.0 KiB
Go

package trace
import (
"bytes"
"strings"
"testing"
)
func TestTaskStateTraceRoundTripContainsNoTaskContent(t *testing.T) {
recorder := NewRecorder("session", "run", "")
recorder.Start()
event := TaskStateEvent{
Revision: 4,
Status: "active",
PlanPending: 2,
PlanCompleted: 1,
ToolsSucceeded: 3,
ToolsFailed: 1,
VerificationPassed: 1,
VerificationOutcome: "passed",
ChangedFileCount: 2,
PlanParity: "match",
}
recorder.EmitTaskState(event)
var output bytes.Buffer
if err := WriteNDJSON(&output, recorder.Finish()); err != nil {
t.Fatalf("WriteNDJSON: %v", err)
}
if strings.Contains(output.String(), "secret objective") || strings.Contains(output.String(), "private.go") {
t.Fatalf("task content leaked into trace: %s", output.String())
}
if strings.Contains(output.String(), "objective_hash") {
t.Fatalf("trace contains a confirmable objective fingerprint: %s", output.String())
}
parsed, err := ReadNDJSON(strings.NewReader(output.String()))
if err != nil {
t.Fatalf("ReadNDJSON: %v", err)
}
if len(parsed.TaskStates) != 1 || parsed.TaskStates[0] != event {
t.Fatalf("unexpected round trip: %#v", parsed.TaskStates)
}
}
func TestTaskStateIsDocumentedAsOptionalTraceEvent(t *testing.T) {
for _, key := range OptionalEventKeys() {
if key == "task_state" {
return
}
}
t.Fatal("task_state missing from OptionalEventKeys")
}
func TestTaskStateTraceIsBoundedAndRetainsLatestSnapshot(t *testing.T) {
recorder := NewRecorder("session", "run", "")
for revision := 1; revision <= maxTaskStateEvents+20; revision++ {
recorder.EmitTaskState(TaskStateEvent{Revision: revision})
}
events := recorder.Finish().TaskStates
if len(events) != maxTaskStateEvents {
t.Fatalf("task state events = %d, want cap %d", len(events), maxTaskStateEvents)
}
if got := events[len(events)-1].Revision; got != maxTaskStateEvents+20 {
t.Fatalf("latest revision = %d, want %d", got, maxTaskStateEvents+20)
}
}