Files
orchestra/internal/provider/gitea_comments_test.go
T
kami 09e572f11e Carry the Gitea issue body into the task
Found by reading .orchestra/launch.md on the first burn-in task. The rendered
context said `Acceptance: Not stated.` and carried the issue title as the whole
goal, because Gitea.event parsed the issue body and then dropped it from the
TaskCreated payload.

The body is the task's own statement of what it wants, which every rendered
context ranks above continuity and below only a human decision. The store
already reads `description` from TaskCreated; only the provider was silent.

Every Gitea-sourced task so far has therefore run on its title alone. Classify
as an authority bug, not a model-following failure: no agent could have known
what it was not told.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:53:36 +04:00

121 lines
4.2 KiB
Go

package provider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"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")
}
}
// The issue body is the task's own statement of what it wants. It must reach
// the store, because every rendered context ranks it above continuity and
// below only a human decision.
func TestGiteaIngestCarriesTheIssueBody(t *testing.T) {
body := `[{"number":1,"title":"Add a --version flag","body":"Print one version line and exit 0. Keep --help working.","state":"open"}]`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(body))
}))
defer srv.Close()
out := &sink{}
g := Gitea{BaseURL: srv.URL, Owner: "kami", Repo: "test-e2e", Project: "test-e2e"}
if _, err := g.Poll(context.Background(), out); err != nil {
t.Fatal(err)
}
if len(out.events) != 1 {
t.Fatalf("events = %d", len(out.events))
}
var p struct {
Title string `json:"title"`
Description string `json:"description"`
}
if err := json.Unmarshal(out.events[0].Payload, &p); err != nil {
t.Fatal(err)
}
if p.Title == "" {
t.Fatal("title missing")
}
if !strings.Contains(p.Description, "Keep --help working") {
t.Fatalf("description = %q", p.Description)
}
}