Files
orchestra/internal/operations/workphase_test.go
T
kami 822f086451 Make research findings citable
The brief at agentctx.go:167 advertised findings[].id and findings[].confidence
to every research session. The struct carried neither, so encoding/json dropped
both on every seal, silently, for as long as the schema has existed. A plan
phase had nothing stable to cite and no way to tell an observation from an
assumption.

Finding gains ID and Confidence. Ids are unique within an artifact and shaped
so "research:<id>" is unambiguous in plan prose. Confidence is fact, inference,
or assumption, matching the labels the output style already uses.

DecodeStoredResearch reads what is already in the CAS and backfills both.
Refusing an artifact sealed before this change would block every task whose
research predates it, including at rotation, where the agent that could fix it
is already gone. A backfilled finding is labelled inference rather than fact:
the old schema required evidence and made no verification claim, so upgrading
it on the way in would be the same class of lie this commit removes.

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

166 lines
5.4 KiB
Go

package operations
import (
"encoding/json"
"errors"
"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"}}}
var plan = workphase.Plan{Changes: []workphase.Change{{Target: "attr.go", Intent: "aggregate per person"}}}
func TestFullPhasePathSealsEachArtifact(t *testing.T) {
s, id := phaseStore(t)
project := registry.Project{ID: "p"}
// 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, sealed(t, plan)); 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", 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", 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"}, 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)
}
}