ajhahn.de
← eeco
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())
	}
}