package movementtest import ( "bytes" "os" "path/filepath" "strings" "testing" ) // The rules in this file are the ones that would otherwise depend on // somebody remembering them. AGENTS.md says: if you find yourself relying on // remembering a rule, make it a test instead. // TestOnlyLoggingFileImportsSlog keeps the sensitive pair unreachable from a // log call. // // Email and stance together are a political position attached to a named // person, and carlos logs is platform-held storage that cannot be grepped // clean afterwards. Pattern-matching for "a call that takes both an address // and a stance" is not decidable over arbitrary Go — wrappers, structs, a %v // of a Signup — so the boundary is drawn one level up: only logging.go may // reach the logger at all, and its surface takes typed reason codes rather // than strings or errors. // // cmd/ is out of scope. It logs its own startup failures before any request // exists, and it has no access to a Signup. func TestOnlyLoggingFileImportsSlog(t *testing.T) { const pkg = "../movement" err := filepath.Walk(pkg, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || !strings.HasSuffix(path, ".go") { return nil } if filepath.Base(path) == "logging.go" { return nil } src, readErr := os.ReadFile(path) if readErr != nil { return readErr } if bytes.Contains(src, []byte(`"log/slog"`)) { t.Errorf("%s imports log/slog directly.\n"+ "Route it through logging.go: the log surface takes typed Reason "+ "codes so an address and a stance cannot meet in one line.", path) } return nil }) if err != nil { t.Fatal(err) } } // TestTemplatesCarryNoLiteralCopy enforces the copy table. // // Every user-facing string resolves through internal/copy, so that the whole // engine can be built and reviewed on a canary before copy review has // happened — and so that unreviewed words cannot reach production by being // quietly typed into markup. Markup, attribute values and template actions // are fine; sentences are not. func TestTemplatesCarryNoLiteralCopy(t *testing.T) { dir := filepath.Join("..", "movement", "templates") entries, err := os.ReadDir(dir) if err != nil { t.Skipf("no templates yet: %v", err) } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".html") { continue } src, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { t.Fatal(err) } for _, run := range prose(string(src)) { t.Errorf("%s contains literal copy: %q\n"+ "Move it into internal/copy and render it with {{t \"some.id\"}}.", e.Name(), run) } } } // prose returns runs of three or more consecutive words left after template // actions, tags, comments and entities are removed. Three is the threshold // where a string stops looking like a class name and starts looking like a // sentence somebody wrote for a reader. func prose(src string) []string { src = strip(src, "{{", "}}") src = strip(src, "") src = strip(src, "<", ">") var out []string for _, line := range strings.Split(src, "\n") { words := 0 var run []string flush := func() { if words >= 3 { out = append(out, strings.Join(run, " ")) } words, run = 0, nil } for _, f := range strings.Fields(line) { f = strings.Trim(f, ".,:;!?\"'()") if f != "" && isWord(f) { words++ run = append(run, f) continue } flush() } flush() } return out } func isWord(s string) bool { for _, r := range s { if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '-' && r != '\'' { return false } } return true } // strip removes every open..close span, including unterminated trailing ones. func strip(s, open, close string) string { var b strings.Builder for { i := strings.Index(s, open) if i < 0 { b.WriteString(s) return b.String() } b.WriteString(s[:i]) b.WriteString(" ") rest := s[i+len(open):] j := strings.Index(rest, close) if j < 0 { return b.String() } s = rest[j+len(close):] } }