carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movementtest
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// The rules in this file are the ones that would otherwise depend on
12// somebody remembering them. AGENTS.md says: if you find yourself relying on
13// remembering a rule, make it a test instead.
14
15// TestOnlyLoggingFileImportsSlog keeps the sensitive pair unreachable from a
16// log call.
17//
18// Email and stance together are a political position attached to a named
19// person, and carlos logs is platform-held storage that cannot be grepped
20// clean afterwards. Pattern-matching for "a call that takes both an address
21// and a stance" is not decidable over arbitrary Go — wrappers, structs, a %v
22// of a Signup — so the boundary is drawn one level up: only logging.go may
23// reach the logger at all, and its surface takes typed reason codes rather
24// than strings or errors.
25//
26// cmd/ is out of scope. It logs its own startup failures before any request
27// exists, and it has no access to a Signup.
28func TestOnlyLoggingFileImportsSlog(t *testing.T) {
29 const pkg = "../movement"
30
31 err := filepath.Walk(pkg, func(path string, info os.FileInfo, err error) error {
32 if err != nil {
33 return err
34 }
35 if info.IsDir() || !strings.HasSuffix(path, ".go") {
36 return nil
37 }
38 if filepath.Base(path) == "logging.go" {
39 return nil
40 }
41 src, readErr := os.ReadFile(path)
42 if readErr != nil {
43 return readErr
44 }
45 if bytes.Contains(src, []byte(`"log/slog"`)) {
46 t.Errorf("%s imports log/slog directly.\n"+
47 "Route it through logging.go: the log surface takes typed Reason "+
48 "codes so an address and a stance cannot meet in one line.", path)
49 }
50 return nil
51 })
52 if err != nil {
53 t.Fatal(err)
54 }
55}
56
57// TestTemplatesCarryNoLiteralCopy enforces the copy table.
58//
59// Every user-facing string resolves through internal/copy, so that the whole
60// engine can be built and reviewed on a canary before copy review has
61// happened — and so that unreviewed words cannot reach production by being
62// quietly typed into markup. Markup, attribute values and template actions
63// are fine; sentences are not.
64func TestTemplatesCarryNoLiteralCopy(t *testing.T) {
65 dir := filepath.Join("..", "movement", "templates")
66 entries, err := os.ReadDir(dir)
67 if err != nil {
68 t.Skipf("no templates yet: %v", err)
69 }
70
71 for _, e := range entries {
72 if e.IsDir() || !strings.HasSuffix(e.Name(), ".html") {
73 continue
74 }
75 src, err := os.ReadFile(filepath.Join(dir, e.Name()))
76 if err != nil {
77 t.Fatal(err)
78 }
79 for _, run := range prose(string(src)) {
80 t.Errorf("%s contains literal copy: %q\n"+
81 "Move it into internal/copy and render it with {{t \"some.id\"}}.",
82 e.Name(), run)
83 }
84 }
85}
86
87// prose returns runs of three or more consecutive words left after template
88// actions, tags, comments and entities are removed. Three is the threshold
89// where a string stops looking like a class name and starts looking like a
90// sentence somebody wrote for a reader.
91func prose(src string) []string {
92 src = strip(src, "{{", "}}")
93 src = strip(src, "<!--", "-->")
94 src = strip(src, "<", ">")
95
96 var out []string
97 for _, line := range strings.Split(src, "\n") {
98 words := 0
99 var run []string
100 flush := func() {
101 if words >= 3 {
102 out = append(out, strings.Join(run, " "))
103 }
104 words, run = 0, nil
105 }
106 for _, f := range strings.Fields(line) {
107 f = strings.Trim(f, ".,:;!?\"'()")
108 if f != "" && isWord(f) {
109 words++
110 run = append(run, f)
111 continue
112 }
113 flush()
114 }
115 flush()
116 }
117 return out
118}
119
120func isWord(s string) bool {
121 for _, r := range s {
122 if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '-' && r != '\'' {
123 return false
124 }
125 }
126 return true
127}
128
129// strip removes every open..close span, including unterminated trailing ones.
130func strip(s, open, close string) string {
131 var b strings.Builder
132 for {
133 i := strings.Index(s, open)
134 if i < 0 {
135 b.WriteString(s)
136 return b.String()
137 }
138 b.WriteString(s[:i])
139 b.WriteString(" ")
140 rest := s[i+len(open):]
141 j := strings.Index(rest, close)
142 if j < 0 {
143 return b.String()
144 }
145 s = rest[j+len(close):]
146 }
147}
148