giteactl: Actions runs, CI logs, wait/wait-quiet, releases
runs/log/wait/wait-quiet/release against the Gitea API. Log fetch tries API (token) -> public web route -> ssh+zstd file storage (hex-bucket path verified against the real Pi5 layout). wait-quiet encodes the "serialize heavy builds" rule with private repos as warnings, not failures. Injectable fetchers make the wait logic testable. Spec: doc/tool-parity.md 4.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
This commit is contained in:
149
giteactl/gitea/gitea_test.go
Normal file
149
giteactl/gitea/gitea_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTasksParsesAndSorts(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/repos/brasse/agent-tools/actions/tasks") {
|
||||
t.Errorf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "token tok" {
|
||||
t.Errorf("token header missing")
|
||||
}
|
||||
fmt.Fprint(w, `{"workflow_runs":[
|
||||
{"id":110,"name":"build","run_number":5,"status":"success"},
|
||||
{"id":111,"name":"build-release","run_number":6,"status":"running"}]}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
tasks, err := New(srv.URL, "brasse", "tok").Tasks("agent-tools", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tasks) != 2 || tasks[0].ID != 111 {
|
||||
t.Errorf("tasks not sorted newest first: %+v", tasks)
|
||||
}
|
||||
if !tasks[0].Active() || tasks[1].Active() {
|
||||
t.Error("Active() wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateRepoHint(t *testing.T) {
|
||||
srv := httptest.NewServer(http.NotFoundHandler())
|
||||
defer srv.Close()
|
||||
_, err := New(srv.URL, "brasse", "").Tasks("infra-Doc", 5)
|
||||
if err == nil || !strings.Contains(err.Error(), "token") {
|
||||
t.Errorf("404 should hint about tokens, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogWebRejectsHTML(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "<!DOCTYPE html><html>login page</html>")
|
||||
}))
|
||||
defer srv.Close()
|
||||
_, err := New(srv.URL, "brasse", "").LogWeb("x", 1, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "HTML") {
|
||||
t.Errorf("HTML response should error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZstPathHexBucket(t *testing.T) {
|
||||
// verified against the real Pi5 layout: task 53 -> 35/53.log.zst
|
||||
cases := map[int64]string{
|
||||
53: "/logs/brasse/FitnessDroid/35/53.log.zst",
|
||||
52: "/logs/brasse/FitnessDroid/34/52.log.zst",
|
||||
86: "/logs/brasse/FitnessDroid/56/86.log.zst",
|
||||
2: "/logs/brasse/FitnessDroid/02/2.log.zst",
|
||||
258: "/logs/brasse/FitnessDroid/02/258.log.zst",
|
||||
}
|
||||
for id, want := range cases {
|
||||
if got := ZstPath("/logs", "brasse", "FitnessDroid", id); got != want {
|
||||
t.Errorf("ZstPath(%d) = %s, want %s", id, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitRunFinishes(t *testing.T) {
|
||||
calls := 0
|
||||
fetch := func(repo string) ([]Task, error) {
|
||||
calls++
|
||||
status := "running"
|
||||
if calls >= 3 {
|
||||
status = "success"
|
||||
}
|
||||
return []Task{
|
||||
{ID: 2, RunNumber: 7, Status: status},
|
||||
{ID: 1, RunNumber: 6, Status: "failure"}, // older run must not matter
|
||||
}, nil
|
||||
}
|
||||
res, err := WaitRun("x", fetch, time.Millisecond, time.Second, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Done || !res.AllOK || res.Attempts != 3 {
|
||||
t.Errorf("unexpected: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitRunRedFailure(t *testing.T) {
|
||||
fetch := func(repo string) ([]Task, error) {
|
||||
return []Task{{ID: 2, RunNumber: 7, Status: "failure"}}, nil
|
||||
}
|
||||
res, err := WaitRun("x", fetch, time.Millisecond, time.Second, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Done || res.AllOK {
|
||||
t.Errorf("failure must give Done && !AllOK: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitRunTimeout(t *testing.T) {
|
||||
fetch := func(repo string) ([]Task, error) {
|
||||
return []Task{{ID: 2, RunNumber: 7, Status: "running"}}, nil
|
||||
}
|
||||
res, err := WaitRun("x", fetch, 5*time.Millisecond, 15*time.Millisecond, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Done {
|
||||
t.Errorf("should have timed out: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitQuietCountsAcrossRepos(t *testing.T) {
|
||||
step := 0
|
||||
fetch := func(repo string) ([]Task, error) {
|
||||
// step 0: both repos busy; step >= 1: only one
|
||||
if repo == "a" && step > 0 {
|
||||
return []Task{{ID: 1, Status: "success"}}, nil
|
||||
}
|
||||
return []Task{{ID: 2, Status: "running"}}, nil
|
||||
}
|
||||
log := func(string) { step++ }
|
||||
ok, err := WaitQuiet([]string{"a", "b"}, 1, fetch, time.Millisecond, time.Second, log)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("should reach quiet state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDuration(t *testing.T) {
|
||||
start := time.Now().Add(-90 * time.Second)
|
||||
tk := Task{Status: "success", RunStartedAt: start, UpdatedAt: start.Add(75 * time.Second)}
|
||||
if d := tk.Duration(); d != 75*time.Second {
|
||||
t.Errorf("duration = %s", d)
|
||||
}
|
||||
if (Task{Status: "running", RunStartedAt: start}).Duration() != 0 {
|
||||
t.Error("active task should have zero duration")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user