7f12c7fc37
The v3 stack, previously an uncommitted working tree, plus this session's two units and the burn-in instrument. This commit is the burn-in build identity: coordinator and worker must both report this revision before a task is created. Workflow (earlier sessions, uncommitted until now): human decision events and reduction, source cursors and reconcile-before-launch, turn-boundary reconciliation, internal/agentctx as the single renderer, ace-fca phases with sealed artifacts, the trajectory gate, bounded grilling, independent review, task pr enforcement, and human review reflection. Capability restrictions at the agent boundary: an authz.Agent surface at GatedWrite may ask and may not act. It also fixes two bugs the unit exposed -- gated surfaces could not reach the two endpoints written for them, and RequestHumanDecision would block an unowned task while rejecting a question from the session that did own it. Turn-boundary reconcile-failure escalation: a streak of consecutive failures asks the session to hand off, fenced on the lease epoch, with reconcile_failure as a real handoff reason. The worker was dropping the coordinator's verdict on the floor; it now acts on it. Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to <worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md is the runbook. deploy/build.sh stamps both binaries from one commit. go build, go vet and go test ./... pass, 20 packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"orchestra/internal/domain"
|
|
"orchestra/internal/store"
|
|
)
|
|
|
|
func TestGiteaCommentsFetchAfterCursor(t *testing.T) {
|
|
var path string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
path = r.URL.Path
|
|
// Deliberately out of order, to prove the source sorts by id.
|
|
w.Write([]byte(`[
|
|
{"id":920,"body":"and keep the flag","user":{"login":"kami"},"created_at":"2026-08-26T12:02:00Z"},
|
|
{"id":917,"body":"older","user":{"login":"kami"},"created_at":"2026-08-26T11:00:00Z"},
|
|
{"id":918,"body":"no, use b","user":{"login":"kami"},"created_at":"2026-08-26T12:00:00Z"}
|
|
]`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
g := GiteaComments{Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "orchestra", Project: "p"}}
|
|
task := domain.Task{ID: "t1", Source: g.SourceName(), ExternalID: "381"}
|
|
|
|
got, next, err := g.FetchAfter(context.Background(), task, store.SourceCursor{Cursor: "917"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if path != "/api/v1/repos/kami/orchestra/issues/381/comments" {
|
|
t.Fatalf("path = %s", path)
|
|
}
|
|
if len(got) != 2 || got[0].ExternalID != "918" || got[1].ExternalID != "920" {
|
|
t.Fatalf("inputs = %+v", got)
|
|
}
|
|
if got[0].Body != "no, use b" || got[0].Author != "kami" || got[0].Provider != "gitea:p" {
|
|
t.Fatalf("first input = %+v", got[0])
|
|
}
|
|
if next.Cursor != "920" || next.TaskID != "t1" {
|
|
t.Fatalf("next = %+v", next)
|
|
}
|
|
|
|
// Nothing new: the cursor must stay put rather than regress.
|
|
got, next, err = g.FetchAfter(context.Background(), task, store.SourceCursor{Cursor: "920"})
|
|
if err != nil || len(got) != 0 {
|
|
t.Fatalf("inputs=%+v err=%v", got, err)
|
|
}
|
|
if next.Cursor != "920" {
|
|
t.Fatalf("next = %+v", next)
|
|
}
|
|
}
|
|
|
|
func TestGiteaCommentsIgnoresForeignAndUnkeyedTasks(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Errorf("unexpected request to %s", r.URL.Path)
|
|
}))
|
|
defer srv.Close()
|
|
g := GiteaComments{Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "orchestra", Project: "p"}}
|
|
for name, task := range map[string]domain.Task{
|
|
"other source": {ID: "t1", Source: "vikunja", ExternalID: "381"},
|
|
"no issue": {ID: "t1", Source: g.SourceName()},
|
|
} {
|
|
got, _, err := g.FetchAfter(context.Background(), task, store.SourceCursor{})
|
|
if err != nil || len(got) != 0 {
|
|
t.Fatalf("%s: inputs=%+v err=%v", name, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGiteaCommentsRejectsUnparsableCursorAndHTTPError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "boom", 500)
|
|
}))
|
|
defer srv.Close()
|
|
g := GiteaComments{Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "orchestra", Project: "p"}}
|
|
task := domain.Task{ID: "t1", Source: g.SourceName(), ExternalID: "381"}
|
|
if _, _, err := g.FetchAfter(context.Background(), task, store.SourceCursor{Cursor: "abc"}); err == nil {
|
|
t.Fatal("bad cursor must not be treated as zero")
|
|
}
|
|
if _, _, err := g.FetchAfter(context.Background(), task, store.SourceCursor{}); err == nil {
|
|
t.Fatal("http failure must be reported")
|
|
}
|
|
}
|