docsite: check the examples, not just the prose
Every `carlos <verb>` inside a fence has to name a real command. A renamed verb otherwise leaves every example containing it copy-pasteable and wrong, and no other gate here would notice. The extractor has its own test — a permissive one would find nothing and pass forever.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+90
−0
internal/docsite/fences_test.go+90 −0
diff --git a/internal/docsite/fences_test.go b/internal/docsite/fences_test.go| index c33d0253d..e89ccaf21 100644 |
| --- a/internal/docsite/fences_test.go |
| +++ b/internal/docsite/fences_test.go |
| @@ -2,6 +2,7 @@ package docsite |
| import ( |
| "os" |
| + "strings" |
| "testing" |
| ) |
| @@ -53,3 +54,92 @@ func TestTestdataFencesDeclareLanguages(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// TestFencedCarlosCommandsAreReal checks the examples, not just the |
| +// prose. A renamed verb leaves every fence containing it copy-pasteable |
| +// and wrong, and nothing else in this package would notice. |
| +func TestFencedCarlosCommandsAreReal(t *testing.T) { |
| + if _, err := os.Stat(root + "/nav.json"); os.IsNotExist(err) { |
| + t.Skip("no corpus yet — Task 8 creates docs/site/nav.json") |
| + } |
| + site, err := Load(root) |
| + if err != nil { |
| + t.Fatalf("load: %v", err) |
| + } |
| + cmds, err := TopLevelCommands(mainFile) |
| + if err != nil { |
| + t.Fatalf("scan: %v", err) |
| + } |
| + real := map[string]bool{} |
| + for _, c := range cmds { |
| + real[c] = true |
| + } |
| + |
| + for _, p := range site.Pages() { |
| + for _, f := range p.Fences { |
| + for _, verb := range fencedCarlosVerbs(f.Body) { |
| + if !real[verb] { |
| + t.Errorf("%s.md:%d: example runs `carlos %s`, which is not a command", |
| + p.Slug, f.Line, verb) |
| + } |
| + } |
| + } |
| + } |
| +} |
| + |
| +// fencedCarlosVerbs returns the word after each "carlos" in a fence |
| +// body. It skips lines where "carlos" is not the command being run — |
| +// a path like /opt/carlos/carlos, or prose inside a comment. |
| +func fencedCarlosVerbs(body string) []string { |
| + var out []string |
| + for _, line := range strings.Split(body, "\n") { |
| + line = strings.TrimSpace(line) |
| + line = strings.TrimPrefix(line, "$ ") |
| + if i := strings.Index(line, "#"); i >= 0 { |
| + line = strings.TrimSpace(line[:i]) |
| + } |
| + fields := strings.Fields(line) |
| + for i, f := range fields { |
| + // Bare "carlos", or the binary called by an absolute path — |
| + // runbooks on a box invoke /opt/carlos/carlos, and those |
| + // examples go stale the same way. |
| + if f != "carlos" && !strings.HasSuffix(f, "/carlos") { |
| + continue |
| + } |
| + if i+1 >= len(fields) { |
| + continue |
| + } |
| + next := fields[i+1] |
| + if strings.HasPrefix(next, "-") { |
| + break |
| + } |
| + out = append(out, next) |
| + break |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// TestFencedCarlosVerbsIgnoresNoise pins the parsing, because a |
| +// permissive extractor turns the gate above into one that finds |
| +// nothing. |
| +func TestFencedCarlosVerbsIgnoresNoise(t *testing.T) { |
| + body := strings.Join([]string{ |
| + "carlos ship -app hello ./dist", |
| + "$ carlos promote -app hello abc123 edge", |
| + "/opt/carlos/carlos adopt", |
| + "# run carlos deploy afterwards", |
| + "carlos -h", |
| + "echo carlos", |
| + }, "\n") |
| + got := fencedCarlosVerbs(body) |
| + want := []string{"ship", "promote", "adopt"} |
| + if len(got) != len(want) { |
| + t.Fatalf("got %v, want %v", got, want) |
| + } |
| + for i := range want { |
| + if got[i] != want[i] { |
| + t.Fatalf("got %v, want %v", got, want) |
| + } |
| + } |
| +} |