// Package guards turns rules that live as prose in AGENTS.md into // tests. CARLOS's own rule for this: "A guard a shell step can satisfy // is a guard a shell step can be wrong about — put guards inside the // test binary." So these run as part of `go test ./...`, not as // separate shell greps that a CI step could get subtly wrong (or that a // shell rewrite could quietly stop running). // // This file holds the shared plumbing (repo-root discovery, file // walking, import parsing) that every rule's test file uses. It carries // no Test functions of its own. The shape is ported from Sheets' // internal/guardstest/repo_test.go deliberately: two apps disagreeing // about how to find their own repo root is a bug waiting to differ. package guards import ( "go/parser" "go/token" "os" "path/filepath" "runtime" "strconv" "strings" "testing" ) // module is the module path declared in this repo's go.mod. Used to // confirm repoRoot found the right go.mod, not some unrelated one on a // parent path (e.g. if the worktree lives under another Go module). const module = "amadan.net/oficina/home" // repoRoot locates the module root by walking up from this source // file's own location (via runtime.Caller), not from the working // directory. `go test ./...` runs every package's tests with that // package's own directory as cwd, so cwd is not a reliable anchor — // runtime.Caller(0) always points back to this file, wherever it was // invoked from. func repoRoot(t *testing.T) string { t.Helper() _, thisFile, _, ok := runtime.Caller(0) if !ok { t.Fatal("guards: runtime.Caller(0) failed") } dir := filepath.Dir(thisFile) for { if b, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil { if strings.Contains(string(b), "module "+module+"\n") { return dir } } parent := filepath.Dir(dir) if parent == dir { t.Fatalf("guards: no go.mod for module %q found above %s", module, filepath.Dir(thisFile)) } dir = parent } } // skipDir names directories goFiles never descends into: version // control, dependency/vendor trees, and dotfiles-style tooling // directories that hold no Go source of ours. Skipping dot-directories // also keeps .claude/worktrees — which holds full checkouts of this // same repo — out of the walk, so a guard never reports another // branch's files as this one's. func skipDir(name string) bool { if strings.HasPrefix(name, ".") { return true } switch name { case "vendor", "node_modules", "testdata": return true } return false } // goFiles returns the path of every *.go file under root. func goFiles(t *testing.T, root string) []string { t.Helper() var files []string err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if path != root && skipDir(d.Name()) { return filepath.SkipDir } return nil } if strings.HasSuffix(path, ".go") { files = append(files, path) } return nil }) if err != nil { t.Fatalf("guards: walk %s: %v", root, err) } return files } // relPath is filepath.Rel with the guards-package error-handling // boilerplate folded in, since every rule needs a repo-relative path // for its failure messages. func relPath(t *testing.T, root, path string) string { t.Helper() rel, err := filepath.Rel(root, path) if err != nil { t.Fatalf("guards: rel %s: %v", path, err) } return filepath.ToSlash(rel) } // fileImports returns the import paths a Go source file declares. It // parses in ImportsOnly mode — the cheapest mode that still sees the // import block — so walking every file in the tree on every test run // stays fast. func fileImports(t *testing.T, path string) []string { t.Helper() fset := token.NewFileSet() f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) if err != nil { t.Fatalf("guards: parse %s: %v", path, err) } imports := make([]string, 0, len(f.Imports)) for _, imp := range f.Imports { p, err := strconv.Unquote(imp.Path.Value) if err != nil { t.Fatalf("guards: unquote import in %s: %v", path, err) } imports = append(imports, p) } return imports } // migrationFiles returns the path of every *.sql file under root. It // walks rather than globbing one directory: a second migrations // directory added later must be covered by the schema guards, not fall // silently outside them. func migrationFiles(t *testing.T, root string) []string { t.Helper() var files []string err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { if path != root && skipDir(d.Name()) { return filepath.SkipDir } return nil } if strings.HasSuffix(path, ".sql") { files = append(files, path) } return nil }) if err != nil { t.Fatalf("guards: walk %s: %v", root, err) } return files } // readFile is os.ReadFile with the guards-package error-handling // boilerplate folded in. func readFile(t *testing.T, path string) string { t.Helper() b, err := os.ReadFile(path) if err != nil { t.Fatalf("guards: read %s: %v", path, err) } return string(b) }