Add federation worker and canonical handoffs
This commit is contained in:
+123
-41
@@ -3,8 +3,6 @@ package herdr
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"orchestra/internal/continuity"
|
||||
@@ -61,15 +59,20 @@ func fakeHerdr(t *testing.T) *Client {
|
||||
|
||||
func validHandoff(anchorSHA string) continuity.Handoff {
|
||||
return continuity.Handoff{
|
||||
Meta: continuity.Meta{ID: "t1", Reason: "threshold", RotationIndex: 1},
|
||||
Anchor: continuity.Anchor{GitSHA: anchorSHA, Branch: "main"},
|
||||
Goal: "finish the thing",
|
||||
DoneWhen: []string{"tests pass"},
|
||||
Action: "continue",
|
||||
Command: "go test ./...",
|
||||
Meta: continuity.Meta{ID: "t1", Reason: "threshold", RotationIndex: 1},
|
||||
Anchor: continuity.Anchor{GitSHA: anchorSHA, Branch: "main"},
|
||||
Action: "run the focused tests",
|
||||
Command: "go test ./...",
|
||||
}
|
||||
}
|
||||
|
||||
const validAnswer = `NEXT: run the focused tests
|
||||
WHY: confirm the current implementation before changing it
|
||||
REMAINING: NONE
|
||||
DEAD ENDS: NONE
|
||||
OPEN Q: NONE
|
||||
LEARNED: NONE`
|
||||
|
||||
func TestReleaseUploadsHandoffAndReleasesAgent(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
@@ -81,11 +84,7 @@ func TestReleaseUploadsHandoffAndReleasesAgent(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := continuity.Encode(validHandoff(head))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), b, 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -105,8 +104,77 @@ func TestReleaseUploadsHandoffAndReleasesAgent(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Anchor.GitSHA == head {
|
||||
t.Fatal("semantic report should be snapshotted before publication")
|
||||
if got.Anchor.GitSHA != head {
|
||||
t.Fatal("semantic report must not be included in the successor scratch anchor")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(repo, HandoffReportFile)); !os.IsNotExist(err) {
|
||||
t.Fatalf("transferred semantic report still present: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalHandoffCarriesCoordinatorReason(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "t@t")
|
||||
runGit(t, repo, "config", "user.name", "t")
|
||||
runGit(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cas := &memCAS{}
|
||||
a := CLIAdapter{Client: fakeHerdr(t), Harness: "claude", CAS: cas}
|
||||
ref, err := a.Release(context.Background(), Session{PaneID: "p1", Worktree: repo, HandoffReason: "thrash"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := continuity.Load(ref, cas)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Meta.Reason != "thrash" {
|
||||
t.Fatalf("canonical reason = %q, want thrash", got.Meta.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseUsesHarnessBindingNotDisplayName(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "t@t")
|
||||
runGit(t, repo, "config", "user.name", "t")
|
||||
runGit(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
request := make(chan Request, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
var req Request
|
||||
if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) == nil {
|
||||
request <- req
|
||||
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{}`)})
|
||||
}
|
||||
}()
|
||||
a := CLIAdapter{Client: &Client{Path: ln.Addr().String(), dial: func() (net.Conn, error) { return net.Dial("tcp", ln.Addr().String()) }}, Harness: "opencode", CAS: &memCAS{}}
|
||||
if _, err := a.Release(context.Background(), Session{PaneID: "p1", Worktree: repo, AgentName: "oc-task-specific"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := <-request
|
||||
p, _ := json.Marshal(req.Params)
|
||||
var got struct {
|
||||
Agent string `json:"agent"`
|
||||
}
|
||||
_ = json.Unmarshal(p, &got)
|
||||
if got.Agent != "opencode" {
|
||||
t.Fatalf("release agent = %q, want harness binding opencode", got.Agent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +187,43 @@ func TestReleaseRefusesWithoutHandoffFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseRefusesNarrativeOrCircularHandoffAnswer(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
runGit(t, repo, "config", "user.email", "t@t")
|
||||
runGit(t, repo, "config", "user.name", "t")
|
||||
runGit(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
bad := `NEXT: Continue the task from the handoff
|
||||
WHY: the predecessor asked for it
|
||||
REMAINING: what changed\n# a markdown report
|
||||
DEAD ENDS: NONE
|
||||
OPEN Q: NONE
|
||||
LEARNED: NONE`
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(bad), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := CLIAdapter{Client: fakeHerdr(t), Harness: "claude", CAS: &memCAS{}}
|
||||
if _, err := a.Release(context.Background(), Session{PaneID: "p1", Worktree: repo}); err == nil {
|
||||
t.Fatal("expected invalid agent answer to refuse release")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHandoffAnswerPreservesOnlyAuthoredFields(t *testing.T) {
|
||||
a, err := parseHandoffAnswer(`NEXT: inspect the failing integration test
|
||||
WHY: isolate the regression before changing production code
|
||||
REMAINING: fix the assertion after identifying the cause
|
||||
DEAD ENDS: tried rerunning the whole suite → failed because it obscures the relevant failure
|
||||
OPEN Q: whether the remote worker has the updated fixture
|
||||
LEARNED: the fixture requires a committed scratch branch
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(a.Remaining) != 1 || len(a.DeadEnds) != 1 || len(a.OpenQuestions) != 1 || len(a.Learned) != 1 {
|
||||
t.Fatalf("parsed answer lost authored fields: %#v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseDoesNotTrustAgentSuppliedAnchor(t *testing.T) {
|
||||
repo := t.TempDir()
|
||||
runGit(t, repo, "init")
|
||||
@@ -126,11 +231,7 @@ func TestReleaseDoesNotTrustAgentSuppliedAnchor(t *testing.T) {
|
||||
runGit(t, repo, "config", "user.name", "t")
|
||||
runGit(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
|
||||
b, err := continuity.Encode(validHandoff("deaddeaddeaddeaddeaddeaddeaddeaddeaddead"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), b, 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := CLIAdapter{Client: fakeHerdr(t), Harness: "claude", CAS: &memCAS{}}
|
||||
@@ -154,14 +255,7 @@ func TestReleaseScratchCommitsDirtyFilesBeforeUpload(t *testing.T) {
|
||||
if err := os.WriteFile(wipPath, []byte("in progress"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256([]byte("in progress"))
|
||||
h := validHandoff(head)
|
||||
h.Anchor.Dirty = []continuity.Dirty{{Path: "wip.txt", SHA256: hex.EncodeToString(sum[:])}}
|
||||
b, err := continuity.Encode(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), b, 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -203,22 +297,10 @@ func TestReleaseDoesNotTrustAgentSuppliedDirtyFile(t *testing.T) {
|
||||
runGit(t, repo, "config", "user.email", "t@t")
|
||||
runGit(t, repo, "config", "user.name", "t")
|
||||
runGit(t, repo, "commit", "--allow-empty", "-m", "init")
|
||||
head, err := HeadSHA(repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(repo, "wip.txt"), []byte("changed after handoff written"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stale := sha256.Sum256([]byte("original content"))
|
||||
h := validHandoff(head)
|
||||
h.Anchor.Dirty = []continuity.Dirty{{Path: "wip.txt", SHA256: hex.EncodeToString(stale[:])}}
|
||||
b, err := continuity.Encode(h)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), b, 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(repo, HandoffReportFile), []byte(validAnswer), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := CLIAdapter{Client: fakeHerdr(t), Harness: "claude", CAS: &memCAS{}}
|
||||
|
||||
Reference in New Issue
Block a user