carlos / contra Public

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

Plain git — no account needed to clone.

Download

Download this file

1package contratest
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// The JS module cap, from AGENTS.md: "small self-contained ES modules, no
12// build step, go:embed, 300 lines per module enforced by test, caps ratchet
13// down only."
14//
15// The cap is not tidiness. This is the code that holds the user's keys in the
16// one place we cannot sign or pin — a browser — and the only real defence is
17// that a person can read all of it. A 900-line module does not get read.
18const jsModuleCap = 300
19
20// jsHighWater is the ratchet: each module's current length, and the most it
21// may ever be again. Shrinking a module means lowering its number here;
22// growing one is a conversation, not an edit.
23//
24// A module missing from this map fails the test rather than defaulting to the
25// cap — a new module in the crypto path is exactly the thing that should not
26// arrive without somebody noticing.
27var jsHighWater = map[string]int{
28 // The crypto core.
29 "account.js": 172,
30 "argon2id.js": 292,
31 "blake2b.js": 99,
32 "bytes.js": 61,
33 "code.js": 165,
34 "ecies.js": 132,
35 "items.js": 124,
36 "kdf.js": 129,
37 "link.js": 131,
38 "kit.js": 203,
39 "net.js": 35,
40 "p256.js": 107,
41 "p256math.js": 143,
42 "pdf.js": 180,
43 "reissue.js": 78,
44 "sharing.js": 153,
45 "qr.js": 297,
46 "qrgf.js": 59,
47 "qrtables.js": 44,
48 "sealed.js": 91,
49 "vaultkeys.js": 217,
50
51 // Page controllers. Thin on purpose: every decision that matters lives in
52 // a crypto module, and these only move text between a form and one.
53 "pages/passkey.js": 197,
54 "pages/signin.js": 49,
55 "pages/device.js": 264,
56 "pages/devicescan.js": 96,
57 "pages/kit.js": 156,
58 "pages/signup.js": 258,
59 // 222 -> 226 on 2026-09-05, deliberately: the open vault marks itself
60 // when it belongs to somebody else, so a shared vault prints on
61 // different stock. Four lines, one of them the statement.
62 "pages/vault.js": 226,
63 "pages/vaultshare.js": 139,
64}
65
66func TestJSModulesStaySmall(t *testing.T) {
67 root := filepath.Join("..", "contra", "static")
68 seen := map[string]bool{}
69
70 // Every module this app wrote — the vendored rastrillo files in static/
71 // itself are the library's and are pinned by vendored_test.go instead.
72 var names []string
73 for _, sub := range []string{"crypto", "pages"} {
74 entries, err := os.ReadDir(filepath.Join(root, sub))
75 if err != nil {
76 t.Fatalf("read %s: %v", sub, err)
77 }
78 for _, e := range entries {
79 if e.IsDir() || !strings.HasSuffix(e.Name(), ".js") {
80 continue
81 }
82 key := e.Name()
83 if sub != "crypto" {
84 key = sub + "/" + e.Name()
85 }
86 names = append(names, key)
87 }
88 }
89
90 for _, name := range names {
91 seen[name] = true
92 rel := name
93 if !strings.Contains(rel, "/") {
94 rel = "crypto/" + rel
95 }
96 raw, err := os.ReadFile(filepath.Join(root, rel))
97 if err != nil {
98 t.Errorf("read %s: %v", name, err)
99 continue
100 }
101 lines := bytes.Count(raw, []byte("\n"))
102 if !bytes.HasSuffix(raw, []byte("\n")) {
103 lines++
104 }
105
106 limit, declared := jsHighWater[name]
107 if !declared {
108 t.Errorf("%s is not in jsHighWater. A new module in the browser crypto "+
109 "path needs a deliberate line budget, not a default one — add it "+
110 "with its current length (%d).", name, lines)
111 continue
112 }
113 if lines > jsModuleCap {
114 t.Errorf("%s is %d lines, over the %d-line cap", name, lines, jsModuleCap)
115 }
116 if lines > limit {
117 t.Errorf("%s grew from %d to %d lines. Caps ratchet down only: split it, "+
118 "or make the case for a bigger budget.", name, limit, lines)
119 }
120 if lines < limit {
121 t.Errorf("%s shrank from %d to %d lines — good. Lower its jsHighWater "+
122 "entry to %d so the ratchet keeps the ground you gained.",
123 name, limit, lines, lines)
124 }
125 }
126
127 for name := range jsHighWater {
128 if !seen[name] {
129 t.Errorf("jsHighWater lists %s, which no longer exists", name)
130 }
131 }
132}
133