Go 159 lines
package workflow
import (
"os/exec"
"strings"
"testing"
)
// requireGo skips a test when the Go toolchain is not on PATH. The gate
// workflow tests drive real subprocesses; `go` is the one command
// guaranteed present while `go test` runs and behaves identically on
// every platform — `go version` exits 0, `go help <bogus>` exits 2.
func requireGo(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go toolchain not on PATH")
}
}
func TestGate_NameAndSummary(t *testing.T) {
if got := (buildGate{}).Name(); got != "gate" {
t.Errorf("Name() = %q, want gate", got)
}
if (buildGate{}).Summary() == "" {
t.Error("Summary() is empty")
}
}
func TestGate_NoGateDeclaredIsClean(t *testing.T) {
cfg := newCfg(t)
cfg.Gate = nil
res, err := buildGate{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Fatalf("no gate -> %d (%s)", res.Code, res.Summary)
}
if res.Summary != "no gate declared" {
t.Errorf("summary = %q", res.Summary)
}
}
func TestGate_SingleStepPasses(t *testing.T) {
requireGo(t)
cfg := newCfg(t)
cfg.Gate = [][]string{{"go", "version"}}
res, err := buildGate{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Fatalf("single passing step -> %d (%s) %+v", res.Code, res.Summary, res.Findings)
}
if res.Summary != "1 gate step(s) passed" {
t.Errorf("summary = %q", res.Summary)
}
}
func TestGate_SingleStepFailsIsFinding(t *testing.T) {
requireGo(t)
cfg := newCfg(t)
cfg.Gate = [][]string{{"go", "help", "eeco-bogus-topic-xyz"}}
res, err := buildGate{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("failing step -> %d (%s), want CodeFinding", res.Code, res.Summary)
}
if len(res.Findings) != 1 {
t.Fatalf("findings = %+v, want one", res.Findings)
}
if res.Findings[0].Path != "go help eeco-bogus-topic-xyz" {
t.Errorf("finding path = %q", res.Findings[0].Path)
}
}
func TestGate_MultiStepAllPass(t *testing.T) {
requireGo(t)
cfg := newCfg(t)
cfg.Gate = [][]string{{"go", "version"}, {"go", "version"}}
res, err := buildGate{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Fatalf("two passing steps -> %d (%s)", res.Code, res.Summary)
}
if res.Summary != "2 gate step(s) passed" {
t.Errorf("summary = %q", res.Summary)
}
}
func TestGate_StopsAtFirstFailure(t *testing.T) {
requireGo(t)
cfg := newCfg(t)
// Step 2 fails; step 3 must never run.
cfg.Gate = [][]string{
{"go", "version"},
{"go", "help", "eeco-bogus-topic-xyz"},
{"go", "version"},
}
var out strings.Builder
res, err := buildGate{}.Run(Env{Config: cfg, Out: &out})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("chain with a failing step -> %d (%s)", res.Code, res.Summary)
}
if !strings.Contains(res.Summary, "step 2/3") {
t.Errorf("summary = %q, want it to name step 2/3", res.Summary)
}
// The progress log announces steps 1 and 2 but never step 3 — the
// chain stopped at the first failure.
if strings.Contains(out.String(), "gate step 3/3") {
t.Errorf("step 3 ran after the chain should have stopped:\n%s", out.String())
}
}
func TestGate_MissingToolBlocks(t *testing.T) {
cfg := newCfg(t)
cfg.Gate = [][]string{{"eeco-definitely-absent-cmd-zzz"}}
res, err := buildGate{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeBlocked {
t.Fatalf("missing tool -> %d (%s), want CodeBlocked", res.Code, res.Summary)
}
if !strings.Contains(res.Summary, "eeco-definitely-absent-cmd-zzz") {
t.Errorf("summary = %q, want it to name the missing tool", res.Summary)
}
}
func TestGate_MissingToolInChainBlocksBeforeAnyStepRuns(t *testing.T) {
requireGo(t)
cfg := newCfg(t)
// Step 1 is runnable, step 2 is not. Pre-flight must block the whole
// chain before step 1 runs — a chain that cannot complete is blocked,
// not a finding (a missing tool outranks a finding).
cfg.Gate = [][]string{
{"go", "version"},
{"eeco-definitely-absent-cmd-zzz"},
}
var out strings.Builder
res, err := buildGate{}.Run(Env{Config: cfg, Out: &out})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeBlocked {
t.Fatalf("missing tool in chain -> %d (%s), want CodeBlocked", res.Code, res.Summary)
}
if out.Len() != 0 {
t.Errorf("a step ran before the pre-flight block:\n%s", out.String())
}
}