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
133 lines
5.2 KiB
Go
133 lines
5.2 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"orchestra/internal/domain"
|
|
"orchestra/internal/human"
|
|
"orchestra/internal/operations"
|
|
"orchestra/internal/orchestrator"
|
|
"orchestra/internal/registry"
|
|
"orchestra/internal/router"
|
|
"orchestra/internal/workphase"
|
|
)
|
|
|
|
// F23. Human steering must work throughout the task, not only after a pull
|
|
// request exists. Pull-request feedback is a separate concept with a narrower
|
|
// window; conflating them would leave every pre-submission phase unsteerable,
|
|
// which is the state run 3 was diagnosed in.
|
|
//
|
|
// The task here never submits anything. It sits in its first phase, leased and
|
|
// running, and a comment on its own issue still becomes a standing decision
|
|
// that the live session is handed at its next verified turn boundary.
|
|
func TestPreSubmissionCommentSteersALiveSession(t *testing.T) {
|
|
s, reg, _ := setup(t)
|
|
task := ingest(t, s, "381")
|
|
src := &tracingSource{tr: &trace{}}
|
|
rec := &human.Reconciler{Store: s, Sources: map[string]human.Source{"gitea": src}}
|
|
s.PreLease = func(id string) error { return rec.Reconcile(context.Background(), id) }
|
|
|
|
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{}, Adapters: adapters{&harness{occupancy: .1}}, StatePath: t.TempDir() + "/sessions.json", Hard: .8}
|
|
c.ReconcileHumanInput = rec.Reconcile
|
|
rt := router.Router{Store: s, Registry: reg, Reachability: alwaysReachable{}, OnLease: func(e domain.Event) error {
|
|
return c.Start(context.Background(), e)
|
|
}}
|
|
if leased, err := rt.AssignPending(); err != nil || len(leased) != 1 {
|
|
t.Fatalf("leased=%d err=%v", len(leased), err)
|
|
}
|
|
leasedTask, _ := s.Task(task.ID)
|
|
if leasedTask.Lease == nil {
|
|
t.Fatal("task is not leased")
|
|
}
|
|
// Still in the first phase, and nothing has been submitted.
|
|
if leasedTask.WorkPhase != "" && leasedTask.WorkPhase != domain.WorkPhaseFrame {
|
|
t.Fatalf("phase = %q", leasedTask.WorkPhase)
|
|
}
|
|
epoch := leasedTask.Lease.Epoch
|
|
|
|
// No human input yet: the boundary answers with nothing to deliver.
|
|
_, decisions, err := c.RemoteTurn(context.Background(), task.ID, epoch, orchestrator.TurnContinue, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(decisions) != 0 {
|
|
t.Fatalf("decisions before any comment = %+v", decisions)
|
|
}
|
|
|
|
// The human comments on the issue while the session runs.
|
|
src.next = "918"
|
|
src.inputs = []human.Input{{Provider: "gitea", ExternalID: "918", Author: "kami", Body: "no, use b"}}
|
|
|
|
_, decisions, err = c.RemoteTurn(context.Background(), task.ID, epoch, orchestrator.TurnContinue, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(decisions) != 1 || decisions[0].Value != "no, use b" {
|
|
t.Fatalf("decisions = %+v", decisions)
|
|
}
|
|
|
|
// Reported as delivered, so the same correction is not re-sent every turn.
|
|
_, decisions, err = c.RemoteTurn(context.Background(), task.ID, epoch, orchestrator.TurnContinue, []string{decisions[0].ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(decisions) != 0 {
|
|
t.Fatalf("a delivered decision repeated: %+v", decisions)
|
|
}
|
|
}
|
|
|
|
// F21 and F22 at the coordinator boundary: an accepted request moves the phase
|
|
// and seals what the phase produced, and a stale session's request is refused.
|
|
func TestPhaseRequestPathSealsAndFences(t *testing.T) {
|
|
s, reg, _ := setup(t)
|
|
task := ingest(t, s, "381")
|
|
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{}, Adapters: adapters{&harness{occupancy: .1}}, StatePath: t.TempDir() + "/sessions.json", Hard: .8}
|
|
rt := router.Router{Store: s, Registry: reg, Reachability: alwaysReachable{}, OnLease: func(e domain.Event) error {
|
|
return c.Start(context.Background(), e)
|
|
}}
|
|
if _, err := rt.AssignPending(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
leased, _ := s.Task(task.ID)
|
|
epoch := leased.Lease.Epoch
|
|
project := registry.Project{ID: "p", Verification: registry.VerificationPolicy{Allowed: [][]string{{"go", "test", "*"}}}}
|
|
|
|
if _, err := operations.RequestWorkPhase(s, project, task.ID, epoch, "op-1", domain.WorkPhaseFrame, domain.WorkPhaseResearch, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := s.Task(task.ID)
|
|
if got.WorkPhase != domain.WorkPhaseResearch {
|
|
t.Fatalf("phase = %q", got.WorkPhase)
|
|
}
|
|
|
|
// A request carrying a lease epoch that no longer owns the task is a stale
|
|
// opinion from a session that has been superseded.
|
|
if _, err := operations.RequestWorkPhase(s, project, task.ID, "stale-epoch", "op-2", domain.WorkPhaseResearch, domain.WorkPhasePlan, sealed(t, researchArtifact)); err == nil {
|
|
t.Fatal("a stale epoch advanced the phase")
|
|
}
|
|
if got, _ := s.Task(task.ID); got.WorkPhase != domain.WorkPhaseResearch {
|
|
t.Fatalf("phase moved on a stale request: %q", got.WorkPhase)
|
|
}
|
|
|
|
// The real owner's request seals the research the next phase will read.
|
|
if _, err := operations.RequestWorkPhase(s, project, task.ID, epoch, "op-3", domain.WorkPhaseResearch, domain.WorkPhasePlan, sealed(t, researchArtifact)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ = s.Task(task.ID)
|
|
if got.WorkPhase != domain.WorkPhasePlan || got.ResearchRef == "" {
|
|
t.Fatalf("task = %+v", got)
|
|
}
|
|
}
|
|
|
|
var researchArtifact = workphase.Research{Findings: []workphase.Finding{{ID: "r1", Confidence: workphase.Fact, Claim: "runs per figure", Evidence: "attr.go:88"}}}
|
|
|
|
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
|
|
}
|