fb7135e1d9
The brief tells the planner "a command outside its policy is refused when you seal, not later". It was not. The only caller of VerificationPolicy.Allows was PlanPhaseCommands, which runs when the implementer asks to verify: one phase, one session and one rotation after the planner could have fixed it. Run 9 sealed ["bash", "scripts/test_healthcheck.sh"] against a policy that allows neither shape, and the phase request was accepted. The check now runs beside citation resolution, on the coordinator, where the project is already in scope. A project with no verification policy can still seal a plan; it cannot seal one that declares run: lines, which matches what an absent policy already meant at verification time. Test fixtures gained a policy for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
247 lines
7.8 KiB
Go
247 lines
7.8 KiB
Go
package operations
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"orchestra/internal/authz"
|
|
"orchestra/internal/domain"
|
|
"orchestra/internal/registry"
|
|
"orchestra/internal/store"
|
|
"orchestra/internal/workphase"
|
|
)
|
|
|
|
func phaseStore(t *testing.T) (*store.Store, string) {
|
|
t.Helper()
|
|
s, err := store.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := json.Marshal(map[string]any{"source": "gitea", "external_id": "381", "project": "p"})
|
|
id := domain.NewID()
|
|
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: id, Version: 1, Payload: b, Surface: string(authz.System)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s, id
|
|
}
|
|
|
|
// lease gives the task an owning session. A question or a phase change comes
|
|
// from a live session, so a test that skips the lease is exercising a state no
|
|
// agent can be in.
|
|
func lease(t *testing.T, s *store.Store, id string) {
|
|
t.Helper()
|
|
if _, err := s.Lease(id, "h1", time.Hour); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func sealed(t *testing.T, v interface{ Validate() error }) []byte {
|
|
t.Helper()
|
|
b, err := workphase.Encode(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
var research = workphase.Research{Findings: []workphase.Finding{{ID: "r1", Confidence: workphase.Fact, Claim: "runs per figure", Evidence: "attr.go:88"}}}
|
|
|
|
// planDoc is a real sealed specification. It cites research:r1, which the
|
|
// research fixture above contains, so the reference check has something to
|
|
// resolve.
|
|
var planDoc = []byte("# Attribution plan\n" + `
|
|
## Overview
|
|
Aggregate per person.
|
|
|
|
## Current state
|
|
attr.go aggregates per figure, per research:r1.
|
|
|
|
## Desired end state
|
|
attr.go aggregates per person.
|
|
|
|
## Non-goals
|
|
No identity change.
|
|
|
|
## Approach
|
|
Change the aggregation key.
|
|
|
|
## Phase 1: Aggregate per person
|
|
|
|
### Files
|
|
- attr.go
|
|
|
|
### Changes
|
|
Change the aggregation key to the person.
|
|
|
|
### Verification
|
|
|
|
#### Automated
|
|
- run: ["go", "test", "./internal/attr/"]
|
|
|
|
## Testing strategy
|
|
Package test.
|
|
|
|
## Risks and edge cases
|
|
None known.
|
|
|
|
## Migration
|
|
None.
|
|
|
|
## References
|
|
- research:r1
|
|
`)
|
|
|
|
func TestFullPhasePathSealsEachArtifact(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
|
|
|
|
// frame -> research needs no artifact: framing produces none.
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.Task(id); got.WorkPhase != domain.WorkPhaseResearch {
|
|
t.Fatalf("phase = %q", got.WorkPhase)
|
|
}
|
|
|
|
// research -> plan must seal the research.
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); !errors.Is(err, domain.ErrInvalid) {
|
|
t.Fatalf("leaving research without an artifact must fail, got %v", err)
|
|
}
|
|
if _, err := AdvanceWorkPhase(s, project, id, []byte(`{"findings":[]}`)); err == nil {
|
|
t.Fatal("an invalid research artifact must be rejected")
|
|
}
|
|
if _, err := AdvanceWorkPhase(s, project, id, sealed(t, research)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := s.Task(id)
|
|
if got.WorkPhase != domain.WorkPhasePlan || got.ResearchRef == "" {
|
|
t.Fatalf("task = %+v", got)
|
|
}
|
|
|
|
// plan -> implement must seal the plan, and must not overwrite the
|
|
// research ref.
|
|
researchRef := got.ResearchRef
|
|
if _, err := AdvanceWorkPhase(s, project, id, planDoc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ = s.Task(id)
|
|
if got.WorkPhase != domain.WorkPhaseImplement || got.PlanRef == "" {
|
|
t.Fatalf("task = %+v", got)
|
|
}
|
|
if got.ResearchRef != researchRef {
|
|
t.Fatal("research ref was overwritten by the plan")
|
|
}
|
|
if got.PlanRef == got.ResearchRef {
|
|
t.Fatal("plan and research sealed to the same ref")
|
|
}
|
|
|
|
// implement -> review, then review sends work back to implement.
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.Task(id); got.WorkPhase != domain.WorkPhaseReview {
|
|
t.Fatalf("phase = %q", got.WorkPhase)
|
|
}
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.Task(id); got.WorkPhase != domain.WorkPhaseImplement {
|
|
t.Fatalf("phase = %q, review must be able to return work", got.WorkPhase)
|
|
}
|
|
}
|
|
|
|
// A project that declares a short path skips the phases it omits.
|
|
func TestProjectPathSkipsUndeclaredPhases(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}, WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame, domain.WorkPhaseImplement, domain.WorkPhaseReview}}
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := s.Task(id)
|
|
if got.WorkPhase != domain.WorkPhaseImplement {
|
|
t.Fatalf("phase = %q, want implement", got.WorkPhase)
|
|
}
|
|
if got.ResearchRef != "" || got.PlanRef != "" {
|
|
t.Fatal("a skipped phase must not seal an artifact")
|
|
}
|
|
}
|
|
|
|
// The store refuses a phase move that is not legal, whatever a caller asks.
|
|
func TestIllegalTransitionRejectedAtTheAppendBoundary(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
task, _ := s.Task(id)
|
|
b, _ := json.Marshal(map[string]any{"phase": string(domain.WorkPhaseReview)})
|
|
err := s.Append(domain.Event{ID: domain.NewID(), Type: domain.EventWorkPhaseChanged, TaskID: id, Version: task.Version + 1, Payload: b, Surface: string(authz.System)})
|
|
if !errors.Is(err, domain.ErrInvalid) {
|
|
t.Fatalf("frame to review must be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEndOfPathIsRefused(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}, WorkPhases: []domain.WorkPhase{domain.WorkPhaseFrame}}
|
|
if _, err := AdvanceWorkPhase(s, project, id, nil); !errors.Is(err, domain.ErrInvalid) {
|
|
t.Fatalf("want ErrInvalid at the end of the path, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPhaseChangeDoesNotTouchLifecycle(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
if _, err := s.Lease(id, "h1", 60_000_000_000); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _ := s.Task(id)
|
|
if _, err := AdvanceWorkPhase(s, registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}, id, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after, _ := s.Task(id)
|
|
if after.State != before.State {
|
|
t.Fatalf("state changed %s -> %s", before.State, after.State)
|
|
}
|
|
if after.Lease == nil || *after.Lease != *before.Lease {
|
|
t.Fatal("lease changed")
|
|
}
|
|
if after.WorkPhase != domain.WorkPhaseResearch {
|
|
t.Fatalf("phase = %q", after.WorkPhase)
|
|
}
|
|
}
|
|
|
|
// The brief promises the planner that a command outside the project's policy
|
|
// is refused when the plan seals. Run 9 sealed
|
|
// ["bash", "scripts/test_healthcheck.sh"] against a policy allowing neither
|
|
// shape: the only check lived in PlanPhaseCommands, one phase too late.
|
|
func TestPlanSealRefusesACommandOutsideProjectPolicy(t *testing.T) {
|
|
s, id := phaseStore(t)
|
|
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "vet", "./..."}}}}
|
|
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)
|
|
}
|
|
_, err := AdvanceWorkPhase(s, project, id, planDoc)
|
|
if !errors.Is(err, domain.ErrInvalid) {
|
|
t.Fatalf("a plan command outside policy must be refused at seal, got %v", err)
|
|
}
|
|
for _, want := range []string{"phase-1", "go test"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("refusal never names %q: %v", want, err)
|
|
}
|
|
}
|
|
if got, _ := s.Task(id); got.PlanRef != "" || got.WorkPhase != domain.WorkPhasePlan {
|
|
t.Fatalf("a refused plan was sealed anyway: %+v", got)
|
|
}
|
|
|
|
// The same plan seals once the project allows the command.
|
|
project.Verification.Allowed = append(project.Verification.Allowed, []string{"go", "test", "*"})
|
|
if _, err := AdvanceWorkPhase(s, project, id, planDoc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.Task(id); got.PlanRef == "" {
|
|
t.Fatal("an allowed plan did not seal")
|
|
}
|
|
}
|