Files
orchestra/internal/operations/review_test.go
T
kami 57c028f94f Seal the plan as a specification instead of four bullet lists
The plan artifact was Changes{Target,Intent} plus three string lists, every
entry capped at 500 single-line characters. That bound makes a specification
impossible: a phase cannot carry a code block, a paragraph of reasoning, or a
verification command with its own argument list. renderSealed then flattened
what little survived through collapse(), so an implement session received a
summary of a summary.

plan.md replaces it. Markdown, 128 KiB, no per-line cap, sealed through the
existing path under the existing PlanRef. The parser enforces the structure the
brief states: required sections, phases numbered from 1 with no gaps, Files,
Changes and Verification per phase, and at least one automated or manual check,
because a phase nobody can verify can never be established as done. Automated
entries are JSON argv arrays, so a pipe is a literal argument rather than an
operator. Headings inside fenced blocks are content, so a plan may show
markdown without parsing its own example.

Citations resolve at seal time against the accepted research, on the
coordinator, which is the only party holding ResearchRef. A plan resting on a
finding nobody recorded fails on the planner while its session is still alive
to be told.

The plan now renders byte for byte into the implement launch, and a rotated
successor receives the same complete document. That is the property the whole
change exists for. collapse() stays for research findings, which really are
short claims.

DecodeStoredPlan reads pre-markdown refs and renders them into the same type,
labelled, so nothing downstream branches on which era a plan came from. A
legacy plan carries no phases, which is honest: the old artifact never named an
executable unit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-28 11:36:45 +04:00

219 lines
8.5 KiB
Go

package operations
import (
"errors"
"strings"
"testing"
"orchestra/internal/domain"
"orchestra/internal/registry"
"orchestra/internal/review"
"orchestra/internal/store"
)
const shaA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const shaB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
const shaBase = "0000000000000000000000000000000000000000"
func evidence(result string) review.Evidence {
return review.Evidence{
BaseSHA: shaBase, ResultSHA: result,
Diff: "--- a/internal/attr/attr.go\n+++ b/internal/attr/attr.go\n+index lookup\n",
GateCommand: "go test ./...", GateExit: 0,
}
}
// atImplement walks a task to the implementation phase with both artifacts
// sealed, which is where review becomes possible.
func atImplement(t *testing.T, project registry.Project) (*store.Store, string) {
t.Helper()
s, id := phaseStore(t)
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
t.Fatal(err)
}
if _, err := AdvanceWorkPhase(s, project, id, sealed(t, research)); err != nil {
t.Fatal(err)
}
if _, err := AdvanceWorkPhase(s, project, id, planDoc); err != nil {
t.Fatal(err)
}
return s, id
}
func finding(id string, sev review.Severity) review.Finding {
return review.Finding{
ID: id, Severity: sev, File: "internal/attr/attr.go", Line: 81,
Claim: "retry path acknowledges success before the durable append", Evidence: "line 81 returns before Append",
}
}
// Minor findings are reported and the task stays eligible. The review is bound
// to the commit it examined.
func TestMinorOnlyReviewIsAccepted(t *testing.T) {
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
s, id := atImplement(t, project)
if _, err := EnterReview(s, project, id, evidence(shaA)); err != nil {
t.Fatal(err)
}
if got, _ := s.Task(id); got.WorkPhase != domain.WorkPhaseReview {
t.Fatalf("phase = %q", got.WorkPhase)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaA, Findings: []review.Finding{finding("f1", review.Minor)}}); err != nil {
t.Fatal(err)
}
got, _ := s.Task(id)
if got.WorkPhase != domain.WorkPhaseReview {
t.Fatalf("a minor-only review must not send work back: %q", got.WorkPhase)
}
if !got.ReviewSatisfied(shaA) {
t.Fatalf("review not satisfied for its own commit: %+v", got.Review)
}
// The same review says nothing about a different tree.
if got.ReviewSatisfied(shaB) {
t.Fatal("a review of one commit must not satisfy another")
}
}
// A blocking finding returns the task to implementation, and the old review
// cannot satisfy the new commit.
func TestBlockingReviewReturnsWorkAndGoesStale(t *testing.T) {
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
s, id := atImplement(t, project)
if _, err := EnterReview(s, project, id, evidence(shaA)); err != nil {
t.Fatal(err)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaA, Findings: []review.Finding{
finding("f1", review.Important), finding("f2", review.Minor),
}}); err != nil {
t.Fatal(err)
}
got, _ := s.Task(id)
if got.WorkPhase != domain.WorkPhaseImplement {
t.Fatalf("phase = %q, want implement", got.WorkPhase)
}
if got.ReviewSatisfied(shaA) {
t.Fatal("a review with an important finding must not satisfy completion")
}
// The findings are readable for the implementation context.
r, err := TaskReview(s, got)
if err != nil || r == nil || len(r.Findings) != 2 {
t.Fatalf("findings = %+v err=%v", r, err)
}
// Fixed at a new commit: a fresh review passes, and it is bound to B.
if _, err := EnterReview(s, project, id, evidence(shaB)); err != nil {
t.Fatal(err)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaB}); err != nil {
t.Fatal(err)
}
got, _ = s.Task(id)
if !got.ReviewSatisfied(shaB) {
t.Fatalf("fresh review not satisfied: %+v", got.Review)
}
if got.ReviewSatisfied(shaA) {
t.Fatal("the superseded commit must not look reviewed")
}
}
// A review sealed against a commit other than the one under review is
// rejected while the reviewing session still exists to redo it.
func TestReviewForTheWrongCommitIsRejected(t *testing.T) {
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
s, id := atImplement(t, project)
if _, err := EnterReview(s, project, id, evidence(shaA)); err != nil {
t.Fatal(err)
}
if got, _ := s.Task(id); got.ReviewTargetSHA != shaA {
t.Fatalf("review target = %q", got.ReviewTargetSHA)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaB}); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
if got, _ := s.Task(id); got.Review != nil {
t.Fatalf("a mismatched review was recorded: %+v", got.Review)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaA}); err != nil {
t.Fatal(err)
}
}
func TestReviewEntryConditions(t *testing.T) {
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
// Wrong phase.
s, id := phaseStore(t)
if _, err := EnterReview(s, project, id, evidence(shaA)); !errors.Is(err, ErrReviewNotEligible) {
t.Fatalf("frame phase: want ErrReviewNotEligible, got %v", err)
}
// Failing gate, unanchored commits, empty diff, and a gate that never ran.
s, id = atImplement(t, project)
bad := map[string]review.Evidence{
"failing gate": func() review.Evidence { e := evidence(shaA); e.GateExit = 1; return e }(),
"no result": func() review.Evidence { e := evidence(shaA); e.ResultSHA = "short"; return e }(),
"no base": func() review.Evidence { e := evidence(shaA); e.BaseSHA = ""; return e }(),
"no diff": func() review.Evidence { e := evidence(shaA); e.Diff = ""; return e }(),
"gate skipped": func() review.Evidence { e := evidence(shaA); e.GateCommand = ""; return e }(),
}
for name, ev := range bad {
if _, err := EnterReview(s, project, id, ev); !errors.Is(err, ErrReviewNotEligible) {
t.Fatalf("%s: want ErrReviewNotEligible, got %v", name, err)
}
}
// An unresolved question blocks entry.
lease(t, s, id)
if _, err := RequestHumanDecision(s, project, id, request("which behaviour is intended?")); err != nil {
t.Fatal(err)
}
if _, err := EnterReview(s, project, id, evidence(shaA)); !errors.Is(err, ErrReviewNotEligible) {
t.Fatalf("blocked task: want ErrReviewNotEligible, got %v", err)
}
}
// A review can only be sealed by a reviewing session, and only in a shape that
// is actually reviewable.
func TestReviewResultRejections(t *testing.T) {
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
s, id := atImplement(t, project)
// Not in the review phase yet.
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaA}); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
if _, err := EnterReview(s, project, id, evidence(shaA)); err != nil {
t.Fatal(err)
}
long := strings.Repeat("x", 501)
bad := map[string]review.Result{
"no sha": {Findings: []review.Finding{finding("f1", review.Minor)}},
"short sha": {ResultSHA: "abc"},
"no id": {ResultSHA: shaA, Findings: []review.Finding{{Severity: review.Minor, File: "a.go", Claim: "c", Evidence: "e"}}},
"duplicate id": {ResultSHA: shaA, Findings: []review.Finding{finding("f1", review.Minor), finding("f1", review.Blocker)}},
"bad severity": {ResultSHA: shaA, Findings: []review.Finding{{ID: "f", Severity: "invalid", File: "a.go", Claim: "c", Evidence: "e"}}},
"absolute path": {ResultSHA: shaA, Findings: []review.Finding{{ID: "f", Severity: review.Minor, File: "/etc/passwd", Claim: "c", Evidence: "e"}}},
"no evidence": {ResultSHA: shaA, Findings: []review.Finding{{ID: "f", Severity: review.Minor, File: "a.go", Claim: "c"}}},
"essay": {ResultSHA: shaA, Findings: []review.Finding{{ID: "f", Severity: review.Minor, File: "a.go", Claim: long, Evidence: "e"}}},
"multiline": {ResultSHA: shaA, Findings: []review.Finding{{ID: "f", Severity: review.Minor, File: "a.go", Claim: "one\ntwo", Evidence: "e"}}},
}
for name, result := range bad {
if _, err := RecordReview(s, project, id, result); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("%s: want ErrInvalid, got %v", name, err)
}
}
// A rejected review left no trace.
if got, _ := s.Task(id); got.Review != nil {
t.Fatalf("a rejected review was recorded: %+v", got.Review)
}
// Too many findings is also a rejection.
flood := review.Result{ResultSHA: shaA}
for i := 0; i < 41; i++ {
flood.Findings = append(flood.Findings, finding(string(rune('a'+i%26))+strings.Repeat("z", i), review.Minor))
}
if _, err := RecordReview(s, project, id, flood); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("want ErrInvalid, got %v", err)
}
}