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 ` 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()) } }