package operations import ( "encoding/json" "errors" "strings" "testing" "orchestra/internal/authz" "orchestra/internal/domain" "orchestra/internal/registry" "orchestra/internal/store" ) func gatedProject() registry.Project { return registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}, TrajectoryGate: map[string]string{"plan_to_implement": "required"}} } func humanReply(t *testing.T, s *store.Store, taskID, id, value string) { t.Helper() task, _ := s.Task(taskID) if err := s.Append(domain.Event{ ID: domain.NewID(), Type: domain.EventHumanDecisionRecorded, TaskID: taskID, Version: task.Version + 1, Surface: string(authz.System), Payload: mustJSONBytes(t, map[string]any{ "decision_id": id, "kind": "correction", "subject": "operator_instruction", "value": value, "source": map[string]any{"provider": "gitea", "external_id": "c-" + id}, }), }); err != nil { t.Fatal(err) } } func mustJSONBytes(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return b } // The gate stops plan to implement, hands the human a packet built from state // that already exists, and lets the work through once they answer. func TestTrajectoryGateBlocksThenClears(t *testing.T) { s, id := phaseStore(t) project := gatedProject() 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) } // plan to implement is gated. proposal := []byte("# Attribution cache plan\n" + ` ## Overview add the cache ## Current state runs per figure, per research:r1. ## Desired end state Aggregation is cached per person. ## Non-goals No identity change. ## Approach Memoise in the aggregation loop. ## Phase 1: Add the cache ### Files - internal/attr/attr.go ### Changes add the cache to internal/attr/attr.go ### Verification #### Automated - run: ["go", "test", "./internal/attr/"] ## Testing strategy go test ./internal/attr/ ## Risks and edge cases cache invalidation on rename ## Migration None. ## References - research:r1 `) _, err := AdvanceWorkPhase(s, project, id, proposal) if !errors.Is(err, ErrTrajectoryGate) { t.Fatalf("want ErrTrajectoryGate, got %v", err) } blocked, _ := s.Task(id) if blocked.State != domain.StateBlocked || blocked.BlockReason != domain.BlockReasonTrajectoryGate { t.Fatalf("task = %+v", blocked) } if blocked.WorkPhase != domain.WorkPhasePlan { t.Fatalf("phase moved before the human answered: %q", blocked.WorkPhase) } // The packet carries the proposal that is not sealed yet, plus the // research it came from. for _, want := range []string{ "Trajectory gate: plan to implement", "add the cache", "go test ./internal/attr/", "cache invalidation on rename", "runs per figure", } { if !strings.Contains(blocked.Blocker, want) { t.Fatalf("packet missing %q:\n%s", want, blocked.Blocker) } } // Asking again while waiting must not re-raise the gate. before := len(s.Events(0)) if _, err := AdvanceWorkPhase(s, project, id, proposal); !errors.Is(err, ErrTrajectoryGate) { t.Fatalf("want ErrTrajectoryGate, got %v", err) } if len(s.Events(0)) != before { t.Fatal("a second gate event was appended while waiting") } // The human answers. Any wording counts: an imported comment carries no // gate-specific subject. humanReply(t, s, id, "d1", "keep the per-person aggregation, but do not add the cache, add the index") if _, err := AdvanceWorkPhase(s, project, id, proposal); err != nil { t.Fatal(err) } got, _ := s.Task(id) if got.State != domain.StateLeased && got.State != domain.StateQueued { t.Fatalf("state = %s, want queued after the gate cleared", got.State) } if got.WorkPhase != domain.WorkPhaseImplement { t.Fatalf("phase = %q", got.WorkPhase) } if got.PlanRef == "" { t.Fatal("the plan was not sealed once the gate cleared") } } // An ungated project never stops. func TestUngatedProjectAdvances(t *testing.T) { s, id := phaseStore(t) project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}} 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) } if got, _ := s.Task(id); got.WorkPhase != domain.WorkPhaseImplement { t.Fatalf("phase = %q", got.WorkPhase) } } // A decision recorded before the gate was raised is not an answer to it. func TestOlderDecisionDoesNotOpenTheGate(t *testing.T) { s, id := phaseStore(t) project := gatedProject() humanReply(t, s, id, "d0", "an earlier instruction") 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); !errors.Is(err, ErrTrajectoryGate) { t.Fatalf("want ErrTrajectoryGate, got %v", err) } }