An intent verb for the trunk-based loop: branch from base → write the file → open a PR → auto-merge (squash) → delete the branch. One call instead of orchestrating file_write_branch + pr_create + workflow_run_status + pr_merge + branch_delete and remembering the conventions each time. The load-bearing safety is a pure, fail-closed CI gate (evaluateShipGate): merge=true ONLY when every workflow run for the PR head commit is completed+success AND the base branch is not review-protected. Every other state — CI pending / red / absent, review-required base, or an unclean merge — fails closed to PR-only and returns the PR with a reason. A change with no CI gate is never auto-merged to trunk (cf. agentsquad#36: non-compiling code reviewer-approved straight to main with no CI wall). - ci_timeout_seconds polls the head commit's runs to completion (default 0 = snapshot, returns pending right after opening the PR). - Derives a deterministic short-lived branch (tbd/<slug>-<hash>); handles new and existing files (fetches the blob sha for updates). - Adds head.sha + mergeable to the PR struct. - Tests: full gate matrix (green/pending/red/cancelled/none/mixed/protected) + Call happy-merge, no-CI fail-closed, red fail-closed, allowlist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 lines
2.2 KiB
Go
54 lines
2.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.d-ma.be/mathias/gitea-mcp/internal/gitea"
|
|
)
|
|
|
|
// The ship gate is the load-bearing safety of tbd_ship: it must NEVER return
|
|
// merge=true unless CI is fully green AND the base isn't review-protected.
|
|
// Every non-green / unknown / protected state fails closed to PR-only (#40).
|
|
func TestEvaluateShipGate(t *testing.T) {
|
|
run := func(status, concl string) gitea.WorkflowRun {
|
|
return gitea.WorkflowRun{Status: status, Conclusion: concl}
|
|
}
|
|
protected := &gitea.BranchProtection{Protected: true, RequiredApprovals: 1}
|
|
open := &gitea.BranchProtection{Protected: false}
|
|
|
|
tests := []struct {
|
|
name string
|
|
runs []gitea.WorkflowRun
|
|
bp *gitea.BranchProtection
|
|
wantMerge bool
|
|
wantCI string
|
|
}{
|
|
{"all green → merge", []gitea.WorkflowRun{run("completed", "success")}, open, true, "success"},
|
|
{"in_progress → no merge", []gitea.WorkflowRun{run("in_progress", "")}, open, false, "pending"},
|
|
{"queued → no merge", []gitea.WorkflowRun{run("queued", "")}, open, false, "pending"},
|
|
{"failed → no merge", []gitea.WorkflowRun{run("completed", "failure")}, open, false, "failed"},
|
|
{"cancelled → no merge (fail-closed)", []gitea.WorkflowRun{run("completed", "cancelled")}, open, false, "failed"},
|
|
{"no runs → no merge (no CI gate)", nil, open, false, "none"},
|
|
{"mixed success+queued → no merge", []gitea.WorkflowRun{run("completed", "success"), run("queued", "")}, open, false, "pending"},
|
|
{"green but protected → no merge", []gitea.WorkflowRun{run("completed", "success")}, protected, false, "success"},
|
|
{"no runs + protected → no merge", nil, protected, false, "none"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
g := evaluateShipGate(tc.runs, tc.bp)
|
|
if g.merge != tc.wantMerge {
|
|
t.Errorf("merge = %v, want %v (reason: %q)", g.merge, tc.wantMerge, g.reason)
|
|
}
|
|
if g.ciStatus != tc.wantCI {
|
|
t.Errorf("ciStatus = %q, want %q", g.ciStatus, tc.wantCI)
|
|
}
|
|
if !tc.wantMerge && g.reason == "" {
|
|
t.Errorf("no-merge decision must carry a reason")
|
|
}
|
|
if tc.wantMerge && g.reason != "" {
|
|
t.Errorf("merge decision must have empty reason, got %q", g.reason)
|
|
}
|
|
})
|
|
}
|
|
}
|