diff --git a/internal/gateway/discover_mutate_test.go b/internal/gateway/discover_mutate_test.go index 32e3f91..5574f28 100644 --- a/internal/gateway/discover_mutate_test.go +++ b/internal/gateway/discover_mutate_test.go @@ -15,6 +15,11 @@ package gateway // the filter selects a non-trivial number of tests (see baseline below), and it // treats "[no tests to run]" as a hard error rather than a green run. // +// It also must not be run against a DIRTY tree: the mutant is written over +// discover.go, and a leftover mutant from a killed run would make the baseline +// green against broken code while every verdict came from the wrong source. +// See the guard at the top of TestRealMutants. +// // Run: go test -tags luajit ./internal/gateway/ -run TestRealMutants -v import ( @@ -49,6 +54,23 @@ func TestRealMutants(t *testing.T) { if err != nil { t.Fatal(err) } + + // Refuse to run against a dirty tree. + // + // The mutant is written OVER discover.go, so on a dirty file the "original" + // captured above is not the committed source — the baseline then runs against + // whatever was there, and a killed run (a background-task timeout, a Ctrl-C) + // leaves a mutant behind that the NEXT run treats as the baseline. Both make + // every verdict meaningless while the harness still reports a clean score. + // + // Refusing rather than restoring: this cannot tell "leftover mutant" apart from + // "the developer is mid-edit", and silently reverting real work is worse than + // stopping. + if dirty, derr := mutationTargetIsDirty(wd, "discover.go"); derr == nil && dirty { + t.Fatalf("%s has uncommitted changes — commit or stash them first. A mutant "+ + "left behind by a killed run would otherwise become the baseline, and "+ + "every verdict below would be about code nobody wrote.", target) + } defer func() { if werr := os.WriteFile(target, orig, 0o644); werr != nil { t.Fatalf("restore discover.go: %v", werr) @@ -234,6 +256,23 @@ func runSuite(wd, filter string) string { return string(out) } +// mutationTargetIsDirty reports whether a file under internal/gateway has +// uncommitted changes, so the harness can refuse to run instead of treating a +// leftover mutant as the baseline. +// +// git runs from the package directory with a PATH RELATIVE to it. An earlier +// version of the sibling script passed a repo-relative pathspec while running in +// the package directory; git matched nothing and the guard never fired. +func mutationTargetIsDirty(wd, name string) (bool, error) { + cmd := exec.Command("git", "diff", "--name-only", "--", "./"+name) + cmd.Dir = wd + out, err := cmd.Output() + if err != nil { + return false, err + } + return strings.TrimSpace(string(out)) != "", nil +} + func countTests(out string) int { n := 0 for _, line := range strings.Split(out, "\n") {