oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package guardstest turns the prose rules in AGENTS.md into tests.
2//
3// CARLOS's own rule: "a guard a shell step can satisfy is a guard a
4// shell step can be wrong about — put guards inside the test binary."
5// Every rule here is one AGENTS.md states, and the point is that
6// nobody has to remember it.
7//
8// There is no production code in this package.
9package guardstest
10
11import (
12 "go/ast"
13 "go/parser"
14 "go/token"
15 "os"
16 "path/filepath"
17 "regexp"
18 "runtime"
19 "strings"
20 "testing"
21)
22
23// repoRoot is found from this file's own path, never from the working
24// directory: `go test ./...` and a run from an editor have different
25// working directories and only one of them would be right.
26func repoRoot(t *testing.T) string {
27 t.Helper()
28 _, file, _, ok := runtime.Caller(0)
29 if !ok {
30 t.Fatal("runtime.Caller failed")
31 }
32 return filepath.Dir(filepath.Dir(filepath.Dir(file)))
33}
34
35// walk visits every file under root with one of the given extensions,
36// skipping the directories that hold nothing we wrote.
37func walk(t *testing.T, root string, exts []string, fn func(path string, body []byte)) {
38 t.Helper()
39 err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
40 if err != nil {
41 return err
42 }
43 if info.IsDir() {
44 switch info.Name() {
45 case ".git", "node_modules", "releases", ".claude", "vendor":
46 return filepath.SkipDir
47 }
48 return nil
49 }
50 for _, ext := range exts {
51 if strings.HasSuffix(path, ext) {
52 b, err := os.ReadFile(path)
53 if err != nil {
54 return err
55 }
56 fn(path, b)
57 return nil
58 }
59 }
60 return nil
61 })
62 if err != nil {
63 t.Fatalf("walk: %v", err)
64 }
65}
66
67// The 300-line cap, on everything we write.
68//
69// Exemptions are by EXACT PATH and each carries its reason. The
70// vendored scripts are rastrillo's, delivered by the scaffold and
71// pinned byte-for-byte by vendored_test.go — editing them to satisfy a
72// line cap would break that pin, which is the more valuable guard.
73var lineCapExempt = map[string]string{
74 "internal/calendar/static/datetime.js": "vendored from rastrillo; pinned byte-for-byte",
75 "internal/calendar/static/rastrillo.js": "rastrillo's, written from estilo's busy by scripts/writeestilo; pinned byte-for-byte",
76 "internal/calendar/static/select.js": "vendored from rastrillo; pinned byte-for-byte",
77 "internal/calendar/static/tokens.css": "vendored from rastrillo; pinned byte-for-byte",
78 "internal/calendar/static/theme.css": "written from estilo by scripts/writeestilo; pinned byte-for-byte",
79}
80
81func TestNoFileExceedsTheLineCap(t *testing.T) {
82 const cap = 300
83 root := repoRoot(t)
84 walk(t, root, []string{".go", ".js"}, func(path string, body []byte) {
85 rel, _ := filepath.Rel(root, path)
86 rel = filepath.ToSlash(rel)
87 if _, ok := lineCapExempt[rel]; ok {
88 return
89 }
90 if n := len(strings.Split(string(body), "\n")); n > cap {
91 t.Errorf("%s is %d lines, cap is %d — split it", rel, n, cap)
92 }
93 })
94}
95
96// An exemption for a file that no longer exists proves nothing, and
97// quietly accumulates. Sheets shipped a walk that skipped vendor/ and
98// therefore proved nothing at all; this is the test that catches the
99// equivalent here.
100func TestEveryLineCapExemptionIsLive(t *testing.T) {
101 root := repoRoot(t)
102 for rel, reason := range lineCapExempt {
103 if _, err := os.Stat(filepath.Join(root, rel)); err != nil {
104 t.Errorf("line-cap exemption %q (%s) names a file that does not exist", rel, reason)
105 }
106 }
107}
108
109// sessions.UserID returns (0, false) under the magic-link plugin,
110// because the Subject is a verified address rather than a numeric id.
111// Every owner-scoped query built on it silently filters to person 0 —
112// a calendar that looks empty rather than broken, which is the more
113// expensive failure.
114//
115// An AST walk rather than a grep, so this comment — which names the
116// call it forbids — does not need an exemption from its own rule.
117func TestNeverSessionsUserID(t *testing.T) {
118 root := repoRoot(t)
119 fset := token.NewFileSet()
120
121 walk(t, root, []string{".go"}, func(path string, body []byte) {
122 rel, _ := filepath.Rel(root, path)
123 f, err := parser.ParseFile(fset, path, body, 0)
124 if err != nil {
125 return // a file that does not parse is the compiler's problem
126 }
127 // Only if the file actually imports the sessions package.
128 imported := ""
129 for _, im := range f.Imports {
130 if p := strings.Trim(im.Path.Value, `"`); p == "amadan.net/rastrillo/rastrillo/sessions" || p == preMoveRastrillo+"/sessions" {
131 imported = "sessions"
132 if im.Name != nil {
133 imported = im.Name.Name
134 }
135 }
136 }
137 if imported == "" {
138 return
139 }
140 ast.Inspect(f, func(n ast.Node) bool {
141 sel, ok := n.(*ast.SelectorExpr)
142 if !ok || sel.Sel.Name != "UserID" {
143 return true
144 }
145 if id, ok := sel.X.(*ast.Ident); ok && id.Name == imported {
146 t.Errorf("%s calls %s.UserID — use auth.From(r), and CurrentPerson(r) for the id",
147 filepath.ToSlash(rel), imported)
148 }
149 return true
150 })
151 })
152}
153
154// A cgo SQLite driver would break CGO_ENABLED=0, and the failure is a
155// binary that will not build for the platform rather than one that
156// misbehaves — but it is cheap to catch here and expensive to discover
157// at ship time.
158func TestNoCgoSQLiteDriver(t *testing.T) {
159 root := repoRoot(t)
160 forbidden := []string{"gorm.io/driver/sqlite", "github.com/mattn/go-sqlite3", "glebarez"}
161 walk(t, root, []string{".go", ".mod"}, func(path string, body []byte) {
162 rel, _ := filepath.Rel(root, path)
163 if strings.HasSuffix(rel, "guards_test.go") {
164 return // this file names them in order to forbid them
165 }
166 for _, f := range forbidden {
167 if strings.Contains(string(body), f) {
168 t.Errorf("%s references %s — the stack is cgo-free", filepath.ToSlash(rel), f)
169 }
170 }
171 })
172}
173
174// Every var(--rst-*) this app's own CSS uses must actually be declared
175// by the vendored token or theme files.
176//
177// Written because an invented token name is invisible: the property
178// falls back to nothing, the element renders unstyled-ish, and nobody
179// notices until a screen looks slightly wrong on a canary.
180func TestEveryTokenWeUseIsDefined(t *testing.T) {
181 root := repoRoot(t)
182 static := filepath.Join(root, "internal", "calendar", "static")
183
184 declared := map[string]bool{}
185 for _, name := range []string{"tokens.css", "theme.css"} {
186 b, err := os.ReadFile(filepath.Join(static, name))
187 if err != nil {
188 t.Fatalf("read %s: %v", name, err)
189 }
190 for _, m := range regexp.MustCompile(`(--rst-[A-Za-z0-9-]+)\s*:`).FindAllStringSubmatch(string(b), -1) {
191 declared[m[1]] = true
192 }
193 }
194 if len(declared) < 20 {
195 t.Fatalf("only %d tokens found in the vendored CSS; the parse is wrong, so this test proves nothing", len(declared))
196 }
197
198 used := regexp.MustCompile(`var\((--rst-[A-Za-z0-9-]+)`)
199 for _, name := range []string{"calendar.css"} {
200 b, err := os.ReadFile(filepath.Join(static, name))
201 if err != nil {
202 t.Fatalf("read %s: %v", name, err)
203 }
204 for _, m := range used.FindAllStringSubmatch(string(b), -1) {
205 if !declared[m[1]] {
206 t.Errorf("%s uses %s, which no vendored stylesheet declares", name, m[1])
207 }
208 }
209 }
210}
211
212// This app's own components take cal-. rst- is the framework's
213// namespace and a future release may claim a name we took; estilo- is
214// the suite's, and a month grid is not a suite component until a second
215// app copies one.
216func TestOurCSSDoesNotClaimAnotherPrefix(t *testing.T) {
217 root := repoRoot(t)
218 b, err := os.ReadFile(filepath.Join(root, "internal", "calendar", "static", "calendar.css"))
219 if err != nil {
220 t.Fatalf("read calendar.css: %v", err)
221 }
222 // Selectors only. var(--rst-*) is a token reference and entirely
223 // correct — this app is meant to use the framework's tokens.
224 bad := regexp.MustCompile(`\[\s*(rst|estilo)-|\.(rst|estilo)-`)
225 for _, m := range bad.FindAllString(string(b), -1) {
226 t.Errorf("calendar.css defines a selector for another namespace: %q — this app's prefix is cal-", m)
227 }
228}
229
230// Seven rastrillo utilities keep the class spelling and have no
231// attribute twin. Written in attribute position they silently do
232// nothing — the failure that put a visible "sr-only" label in sheets'
233// toolbar.
234func TestUtilitiesAreNeverWrittenAsAttributes(t *testing.T) {
235 root := repoRoot(t)
236 utilities := []string{"rst-sr-only", "rst-mono", "rst-grow", "rst-m-hide", "rst-danger", "rst-nm", "rst-cell-mut"}
237 walk(t, filepath.Join(root, "internal", "calendar", "templates"), []string{".html"},
238 func(path string, body []byte) {
239 rel, _ := filepath.Rel(root, path)
240 for _, u := range utilities {
241 // In attribute position it appears as a bare word
242 // preceded by whitespace inside a tag, never as
243 // class="...".
244 bare := regexp.MustCompile(`<[^>]*\s` + regexp.QuoteMeta(u) + `[\s=>]`)
245 for _, m := range bare.FindAllString(string(body), -1) {
246 if strings.Contains(m, `class=`) {
247 continue
248 }
249 t.Errorf("%s writes %s in attribute position (%q); it is a class-only utility",
250 filepath.ToSlash(rel), u, strings.TrimSpace(m))
251 }
252 }
253 })
254}
255
256// preMoveRastrillo is rastrillo's module path before it moved to
257// amadan.net/rastrillo/rastrillo (v0.25.0). Nothing after v0.23.0 was
258// released under it, so code importing it gets no rastrillo fix, and
259// its own vendored-asset check cannot tell. That is how Calendar kept
260// zooming iOS on every tap for a month after rastrillo fixed it
261// (polish audit r3, item 1).
262const preMoveRastrillo = "github.com/carlosframework/rastrillo"
263
264// preMoveAllowed is the one file that may still import it, and why:
265// rastrillo/native's server takes the old path's *sessions.Sessions.
266// Delete the entry when native/server moves.
267var preMoveAllowed = map[string]string{
268 "internal/calendar/native.go": preMoveRastrillo + "/sessions",
269}
270
271// TestNoPreMoveRastrillo fails on any import of rastrillo's old module
272// path outside preMoveAllowed.
273func TestNoPreMoveRastrillo(t *testing.T) {
274 root := repoRoot(t)
275 fset := token.NewFileSet()
276 walk(t, root, []string{".go"}, func(path string, body []byte) {
277 rel, _ := filepath.Rel(root, path)
278 rel = filepath.ToSlash(rel)
279 f, err := parser.ParseFile(fset, path, body, parser.ImportsOnly)
280 if err != nil {
281 return // a file that does not parse is the compiler's problem
282 }
283 for _, im := range f.Imports {
284 imp := strings.Trim(im.Path.Value, `"`)
285 if (imp == preMoveRastrillo || strings.HasPrefix(imp, preMoveRastrillo+"/")) && preMoveAllowed[rel] != imp {
286 t.Errorf("%s: imports %q, rastrillo's frozen pre-move path; use amadan.net/rastrillo/rastrillo", rel, imp)
287 }
288 }
289 })
290}
291