package main import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // Tests run with cwd = package dir (cmd/verify-reqs), so `testdata/...` // paths resolve relative to the package. The real-repo tests use a // repoRoot helper to locate `.ciagent/...`. func repoRoot(t *testing.T) string { t.Helper() wd, err := os.Getwd() if err != nil { t.Fatalf("getwd: %v", err) } return filepath.Join(wd, "..", "..") } // case (1): clean pair → no drift. func TestVerify_clean(t *testing.T) { diff, count, err := verify( filepath.Join("testdata", "roadmap_clean.md"), filepath.Join("testdata", "requirements_clean.md"), ) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(diff) != 0 { t.Fatalf("expected no drift, got:\n%s", strings.Join(diff, "\n")) } if count != 5 { t.Fatalf("expected 5 consistent rows, got %d", count) } } // case (2)+(3): drift pair → all drifts reported, both directions. func TestVerify_drift(t *testing.T) { diff, _, err := verify( filepath.Join("testdata", "roadmap_drift.md"), filepath.Join("testdata", "requirements_drift.md"), ) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(diff) != 4 { t.Fatalf("expected 4 drifts (REQ-002 forward, REQ-003 reverse, REQ-004 forward, REQ-005 forward), got %d:\n%s", len(diff), strings.Join(diff, "\n")) } joined := strings.Join(diff, "\n") for _, want := range []string{"REQ-002", "REQ-003", "REQ-004", "REQ-005"} { if !strings.Contains(joined, want) { t.Errorf("diff missing %s:\n%s", want, joined) } } if !strings.Contains(joined, "direction=reverse") { t.Errorf("expected a reverse-direction drift, got:\n%s", joined) } if !strings.Contains(joined, "direction=forward") { t.Errorf("expected a forward-direction drift, got:\n%s", joined) } } // case (4): missing args → defaults resolve to the repo's .ciagent/ files. // Runs the program as a subprocess from the repo root. func TestVerify_defaultArgs(t *testing.T) { if testing.Short() { t.Skip("subprocess test skipped in -short mode") } root := repoRoot(t) cmd := exec.Command("go", "run", "./cmd/verify-reqs") cmd.Dir = root out := &strings.Builder{} cmd.Stdout = out cmd.Stderr = out if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { t.Fatalf("verify-reqs on real repo exited %d (should be 0): %s", exitErr.ExitCode(), out.String()) } t.Fatalf("go run failed: %v", err) } } // case (5): malformed markdown (no REQ rows) → clear error, not silent pass. func TestVerify_malformed(t *testing.T) { _, _, err := verify( filepath.Join("testdata", "roadmap_clean.md"), filepath.Join("testdata", "requirements_malformed.md"), ) if err == nil { t.Fatal("expected error on malformed (no REQ rows) requirements, got nil") } if !strings.Contains(err.Error(), "no REQ rows") { t.Errorf("expected 'no REQ rows' error, got: %v", err) } } // case (6): missing file → clear error, not silent pass. func TestVerify_missingFile(t *testing.T) { _, _, err := verify( filepath.Join("testdata", "roadmap_clean.md"), filepath.Join("testdata", "does_not_exist.md"), ) if err == nil { t.Fatal("expected error on missing file, got nil") } if !strings.Contains(err.Error(), "does_not_exist.md") { t.Errorf("expected error to mention the missing file, got: %v", err) } } // GRILL #4 golden test: the v0.2-style // `**COMPLETE (merged to main via v0.3)**` header MUST be recognized as a // complete milestone by the substring-tolerant regex. The drift fixture's // roadmap carries exactly this header on its v0.2 line, and the drift // fixture's REQ-002 (v0.2 P1, Pending) would be silently skipped if the // regex regressed to the exact `\*\*COMPLETE\*\*` form. TestVerify_drift // already asserts REQ-002 is flagged forward-drift; this test makes the // intent explicit and guards against a regex regression. func TestVerify_v02SubstringTolerantHeader(t *testing.T) { diff, _, err := verify( filepath.Join("testdata", "roadmap_drift.md"), filepath.Join("testdata", "requirements_drift.md"), ) if err != nil { t.Fatalf("unexpected error: %v", err) } // REQ-002 references milestone v0.2, whose header uses the // `**COMPLETE (merged to main via v0.3)**` variant. If the regex // regressed, v0.2 would not be marked complete and REQ-002 (Pending) // would NOT be flagged as forward drift. found := false for _, d := range diff { if strings.Contains(d, "REQ-002") && strings.Contains(d, "direction=forward") { found = true break } } if !found { t.Fatalf("REQ-002 forward drift not reported — substring-tolerant regex may have regressed; diff:\n%s", strings.Join(diff, "\n")) } } // Verify the actual repo passes (regression guard for the real docs). func TestVerify_realRepo(t *testing.T) { if testing.Short() { t.Skip("real-repo test skipped in -short mode") } root := repoRoot(t) diff, count, err := verify( filepath.Join(root, ".ciagent", "ROADMAP.md"), filepath.Join(root, ".ciagent", "REQUIREMENTS.md"), ) if err != nil { t.Fatalf("verify on real repo errored: %v", err) } if len(diff) != 0 { t.Fatalf("real repo has requirements drift (should be clean after SPECIFY):\n%s", strings.Join(diff, "\n")) } if count <= 0 { t.Fatalf("real repo reported %d consistent rows (expected > 0)", count) } }