oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1// Package guards turns rules that live as prose in AGENTS.md into
2// tests. CARLOS's own rule for this: "A guard a shell step can satisfy
3// is a guard a shell step can be wrong about — put guards inside the
4// test binary." So these run as part of `go test ./...`, not as
5// separate shell greps that a CI step could get subtly wrong (or that a
6// shell rewrite could quietly stop running).
7//
8// This file holds the shared plumbing (repo-root discovery, file
9// walking, import parsing) that every rule's test file uses. It carries
10// no Test functions of its own. The shape is ported from Sheets'
11// internal/guardstest/repo_test.go deliberately: two apps disagreeing
12// about how to find their own repo root is a bug waiting to differ.
13package guards
14
15import (
16 "go/parser"
17 "go/token"
18 "os"
19 "path/filepath"
20 "runtime"
21 "strconv"
22 "strings"
23 "testing"
24)
25
26// module is the module path declared in this repo's go.mod. Used to
27// confirm repoRoot found the right go.mod, not some unrelated one on a
28// parent path (e.g. if the worktree lives under another Go module).
29const module = "amadan.net/oficina/home"
30
31// repoRoot locates the module root by walking up from this source
32// file's own location (via runtime.Caller), not from the working
33// directory. `go test ./...` runs every package's tests with that
34// package's own directory as cwd, so cwd is not a reliable anchor —
35// runtime.Caller(0) always points back to this file, wherever it was
36// invoked from.
37func repoRoot(t *testing.T) string {
38 t.Helper()
39 _, thisFile, _, ok := runtime.Caller(0)
40 if !ok {
41 t.Fatal("guards: runtime.Caller(0) failed")
42 }
43 dir := filepath.Dir(thisFile)
44 for {
45 if b, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil {
46 if strings.Contains(string(b), "module "+module+"\n") {
47 return dir
48 }
49 }
50 parent := filepath.Dir(dir)
51 if parent == dir {
52 t.Fatalf("guards: no go.mod for module %q found above %s", module, filepath.Dir(thisFile))
53 }
54 dir = parent
55 }
56}
57
58// skipDir names directories goFiles never descends into: version
59// control, dependency/vendor trees, and dotfiles-style tooling
60// directories that hold no Go source of ours. Skipping dot-directories
61// also keeps .claude/worktrees — which holds full checkouts of this
62// same repo — out of the walk, so a guard never reports another
63// branch's files as this one's.
64func skipDir(name string) bool {
65 if strings.HasPrefix(name, ".") {
66 return true
67 }
68 switch name {
69 case "vendor", "node_modules", "testdata":
70 return true
71 }
72 return false
73}
74
75// goFiles returns the path of every *.go file under root.
76func goFiles(t *testing.T, root string) []string {
77 t.Helper()
78 var files []string
79 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
80 if err != nil {
81 return err
82 }
83 if d.IsDir() {
84 if path != root && skipDir(d.Name()) {
85 return filepath.SkipDir
86 }
87 return nil
88 }
89 if strings.HasSuffix(path, ".go") {
90 files = append(files, path)
91 }
92 return nil
93 })
94 if err != nil {
95 t.Fatalf("guards: walk %s: %v", root, err)
96 }
97 return files
98}
99
100// relPath is filepath.Rel with the guards-package error-handling
101// boilerplate folded in, since every rule needs a repo-relative path
102// for its failure messages.
103func relPath(t *testing.T, root, path string) string {
104 t.Helper()
105 rel, err := filepath.Rel(root, path)
106 if err != nil {
107 t.Fatalf("guards: rel %s: %v", path, err)
108 }
109 return filepath.ToSlash(rel)
110}
111
112// fileImports returns the import paths a Go source file declares. It
113// parses in ImportsOnly mode — the cheapest mode that still sees the
114// import block — so walking every file in the tree on every test run
115// stays fast.
116func fileImports(t *testing.T, path string) []string {
117 t.Helper()
118 fset := token.NewFileSet()
119 f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly)
120 if err != nil {
121 t.Fatalf("guards: parse %s: %v", path, err)
122 }
123 imports := make([]string, 0, len(f.Imports))
124 for _, imp := range f.Imports {
125 p, err := strconv.Unquote(imp.Path.Value)
126 if err != nil {
127 t.Fatalf("guards: unquote import in %s: %v", path, err)
128 }
129 imports = append(imports, p)
130 }
131 return imports
132}
133
134// migrationFiles returns the path of every *.sql file under root. It
135// walks rather than globbing one directory: a second migrations
136// directory added later must be covered by the schema guards, not fall
137// silently outside them.
138func migrationFiles(t *testing.T, root string) []string {
139 t.Helper()
140 var files []string
141 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
142 if err != nil {
143 return err
144 }
145 if d.IsDir() {
146 if path != root && skipDir(d.Name()) {
147 return filepath.SkipDir
148 }
149 return nil
150 }
151 if strings.HasSuffix(path, ".sql") {
152 files = append(files, path)
153 }
154 return nil
155 })
156 if err != nil {
157 t.Fatalf("guards: walk %s: %v", root, err)
158 }
159 return files
160}
161
162// readFile is os.ReadFile with the guards-package error-handling
163// boilerplate folded in.
164func readFile(t *testing.T, path string) string {
165 t.Helper()
166 b, err := os.ReadFile(path)
167 if err != nil {
168 t.Fatalf("guards: read %s: %v", path, err)
169 }
170 return string(b)
171}
172