7f12c7fc37
The v3 stack, previously an uncommitted working tree, plus this session's two units and the burn-in instrument. This commit is the burn-in build identity: coordinator and worker must both report this revision before a task is created. Workflow (earlier sessions, uncommitted until now): human decision events and reduction, source cursors and reconcile-before-launch, turn-boundary reconciliation, internal/agentctx as the single renderer, ace-fca phases with sealed artifacts, the trajectory gate, bounded grilling, independent review, task pr enforcement, and human review reflection. Capability restrictions at the agent boundary: an authz.Agent surface at GatedWrite may ask and may not act. It also fixes two bugs the unit exposed -- gated surfaces could not reach the two endpoints written for them, and RequestHumanDecision would block an unowned task while rejecting a question from the session that did own it. Turn-boundary reconcile-failure escalation: a streak of consecutive failures asks the session to hand off, fenced on the lease epoch, with reconcile_failure as a real handoff reason. The worker was dropping the coordinator's verdict on the floor; it now acts on it. Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to <worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md is the runbook. deploy/build.sh stamps both binaries from one commit. go build, go vet and go test ./... pass, 20 packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.0 KiB
Go
102 lines
3.0 KiB
Go
package workphase
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func research() Research {
|
|
return Research{
|
|
Findings: []Finding{{Claim: "attribution runs per figure", Evidence: "internal/attr/attr.go:88"}},
|
|
Code: []CodePath{{Path: "internal/attr/attr.go", Why: "aggregation happens here"}},
|
|
Invariants: []string{"identity semantics must not change"},
|
|
DeadEnds: []DeadEnd{{Tried: "figure plurality", WhyFailed: "no measured gain"}},
|
|
}
|
|
}
|
|
|
|
func plan() Plan {
|
|
return Plan{
|
|
Changes: []Change{{Target: "internal/attr/attr.go", Intent: "aggregate per person"}},
|
|
Verification: []string{"go test ./internal/attr/"},
|
|
}
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
b, err := Encode(research())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DecodeResearch(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Findings[0].Claim != "attribution runs per figure" || got.DeadEnds[0].Tried != "figure plurality" {
|
|
t.Fatalf("round trip lost content: %+v", got)
|
|
}
|
|
pb, err := Encode(plan())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gotPlan, err := DecodePlan(pb)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotPlan.Changes[0].Target != "internal/attr/attr.go" {
|
|
t.Fatalf("round trip lost content: %+v", gotPlan)
|
|
}
|
|
}
|
|
|
|
// The bound is the point. An artifact that can hold a transcript is a
|
|
// transcript, and the next phase pays for reading it.
|
|
func TestBoundsRejectUnboundedArtifacts(t *testing.T) {
|
|
cases := map[string]func() error{
|
|
"no findings": func() error { return Research{}.Validate() },
|
|
"no evidence": func() error { return Research{Findings: []Finding{{Claim: "x"}}}.Validate() },
|
|
"multiline claim": func() error { return Research{Findings: []Finding{{Claim: "a\nb", Evidence: "e"}}}.Validate() },
|
|
"long claim": func() error {
|
|
return Research{Findings: []Finding{{Claim: strings.Repeat("x", 501), Evidence: "e"}}}.Validate()
|
|
},
|
|
"too many findings": func() error {
|
|
r := Research{}
|
|
for i := 0; i < 65; i++ {
|
|
r.Findings = append(r.Findings, Finding{Claim: "c", Evidence: "e"})
|
|
}
|
|
return r.Validate()
|
|
},
|
|
"absolute path": func() error {
|
|
r := research()
|
|
r.Code = []CodePath{{Path: "/etc/passwd", Why: "no"}}
|
|
return r.Validate()
|
|
},
|
|
"blank invariant": func() error {
|
|
r := research()
|
|
r.Invariants = []string{" "}
|
|
return r.Validate()
|
|
},
|
|
"no changes": func() error { return Plan{}.Validate() },
|
|
"no change intent": func() error { return Plan{Changes: []Change{{Target: "x"}}}.Validate() },
|
|
"multiline risk": func() error {
|
|
p := plan()
|
|
p.Risks = []string{"a\nb"}
|
|
return p.Validate()
|
|
},
|
|
}
|
|
for name, fn := range cases {
|
|
if err := fn(); err == nil {
|
|
t.Fatalf("%s: expected rejection", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncodeRejectsInvalid(t *testing.T) {
|
|
if _, err := Encode(Research{}); err == nil {
|
|
t.Fatal("Encode must validate before sealing")
|
|
}
|
|
if _, err := DecodeResearch([]byte(`{"findings":[]}`)); err == nil {
|
|
t.Fatal("Decode must validate")
|
|
}
|
|
if _, err := DecodePlan([]byte(`not json`)); err == nil {
|
|
t.Fatal("Decode must reject non-JSON")
|
|
}
|
|
}
|