personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6import re
7import shutil
8import subprocess
9from pathlib import Path
10
11import pytest
12
13STATIC_ROOT = Path("solstone/convey/static")
14BODY_WORKSPACE = Path("solstone/apps/body/workspace.html")
15GATE_HARNESS = STATIC_ROOT / "tests" / "gate-drawer.html"
16
17
18def _owner_regions(source: str) -> list[str]:
19 return re.findall(
20 r"// --- owner-facing strings ---\n(.*?)// --- end owner-facing strings ---",
21 source,
22 flags=re.DOTALL,
23 )
24
25
26def _node_or_skip() -> str:
27 node = shutil.which("node")
28 if node is None:
29 pytest.skip("node is not available")
30 return node
31
32
33def test_drawer_static_headers_match_day_grid_byte_for_byte():
34 assert (STATIC_ROOT / "drawer.js").read_bytes().splitlines(keepends=True)[:2] == (
35 STATIC_ROOT / "day-grid.js"
36 ).read_bytes().splitlines(keepends=True)[:2]
37 assert (STATIC_ROOT / "gate-drawer.js").read_bytes().splitlines(keepends=True)[
38 :2
39 ] == (STATIC_ROOT / "day-grid.js").read_bytes().splitlines(keepends=True)[:2]
40 assert (STATIC_ROOT / "drawer.css").read_bytes().splitlines(keepends=True)[:2] == (
41 STATIC_ROOT / "day-grid.css"
42 ).read_bytes().splitlines(keepends=True)[:2]
43
44
45def test_drawer_shell_links_follow_day_grid():
46 source = (STATIC_ROOT / "shell.html").read_text(encoding="utf-8")
47
48 assert source.index("/static/day-grid.css") < source.index("/static/drawer.css")
49 assert source.index("/static/day-grid.js") < source.index("/static/drawer.js")
50 assert source.index("/static/drawer.js") < source.index("/static/gate-drawer.js")
51 assert source.index("/static/gate-drawer.js") < source.index(
52 "/static/shell_boot.js"
53 )
54 assert source.index("/static/drawer.js") < source.index("/static/shell_boot.js")
55
56
57def test_drawer_js_contract_and_constraints():
58 source = (STATIC_ROOT / "drawer.js").read_text(encoding="utf-8")
59
60 assert "(function () {" in source
61 assert "'use strict';" in source
62 assert (
63 "window.Drawer = Object.freeze({ render, preserveOpen, formatLine });" in source
64 )
65 assert "Storage" not in source
66 assert "localStorage" not in source
67 assert "sessionStorage" not in source
68 assert "aria-" not in source
69
70
71def test_drawer_owner_facing_region_is_empty_and_clean():
72 source = (STATIC_ROOT / "drawer.js").read_text(encoding="utf-8")
73 regions = _owner_regions(source)
74 banned = {"capture", "watch", "record", "monitor", "track", "collect", "user"}
75
76 assert regions == [" "]
77 for region in regions:
78 lowered = region.lower()
79 assert region == lowered
80 assert {word for word in banned if word in lowered} == set()
81
82
83def test_gate_drawer_js_contract_and_owner_facing_region():
84 source = (STATIC_ROOT / "gate-drawer.js").read_text(encoding="utf-8")
85 regions = _owner_regions(source)
86 banned = {"capture", "watch", "record", "monitor", "track", "collect", "user"}
87 carried_strings = {"Manual tags:", "Segments with audio:", "Embeddings:"}
88
89 assert "(function () {" in source
90 assert "'use strict';" in source
91 assert "window.GateDrawer = Object.freeze({ render });" in source
92 assert "Storage" not in source
93 assert "localStorage" not in source
94 assert "sessionStorage" not in source
95 assert "<b>" not in source
96 assert "drawer-chip" not in source
97 assert "next_generic" not in source
98 assert "tag more owner statements, then build from manual tags." not in source
99 assert regions and len(regions) == 1
100 assert "SPK_OVERVIEW_OWNER_COHESION_LABEL" in regions[0]
101 assert "payload-key test" in regions[0]
102 for reason in (
103 "too_few_stmts",
104 "median_duration_too_short",
105 "cluster_too_diffuse",
106 ):
107 assert reason in regions[0]
108 strings = re.findall(r"'([^']*)'", regions[0])
109 assert strings
110 assert carried_strings.issubset(strings)
111 for text in strings:
112 if text in carried_strings:
113 continue
114 assert text == text.lower()
115 lowered = text.lower()
116 assert {word for word in banned if word in lowered} == set()
117
118
119def test_drawer_css_has_no_generated_owner_text_or_user_select():
120 source = (STATIC_ROOT / "drawer.css").read_text(encoding="utf-8")
121
122 assert "user-select" not in source
123 generated = re.findall(r"content\s*:\s*([^;]+);", source)
124 assert all(value.strip() in {'""', "''"} for value in generated)
125
126
127def test_drawer_css_classes_have_consumers():
128 css = (STATIC_ROOT / "drawer.css").read_text(encoding="utf-8")
129 consumers = "\n".join(
130 [
131 (STATIC_ROOT / "drawer.js").read_text(encoding="utf-8"),
132 (STATIC_ROOT / "gate-drawer.js").read_text(encoding="utf-8"),
133 (STATIC_ROOT / "tests" / "drawer.html").read_text(encoding="utf-8"),
134 GATE_HARNESS.read_text(encoding="utf-8"),
135 BODY_WORKSPACE.read_text(encoding="utf-8"),
136 ]
137 )
138 classes = set(re.findall(r"\.((?:drawer|gate)(?:-[A-Za-z0-9_]+)*|ev-meta)\b", css))
139
140 assert classes
141 assert {name for name in classes if name not in consumers} == set()
142
143
144def test_drawer_smoke_harness_covers_contract():
145 source = (STATIC_ROOT / "tests" / "drawer.html").read_text(encoding="utf-8")
146
147 assert '<link rel="stylesheet" href="../drawer.css">' in source
148 assert '<script src="../drawer.js"></script>' in source
149 assert "function assert(name, condition, detail)" in source
150 assert "function equal(name, actual, expected)" in source
151 assert "data-drawer-id" in source
152 assert "drawer-chev" in source
153 assert "drawer-summary-text" in source
154 assert "drawer-body" in source
155 assert "drawer-chip--warn" in source
156 assert "drawer-chip--danger" in source
157 assert "emphasized line text" in source
158 assert "digit runs emphasized" in source
159 assert "label has no emphasis" in source
160 assert "chip has no emphasis" in source
161 assert "prose line has no emphasis" in source
162 assert "no line omits line span" in source
163 assert "preserve restores open id" in source
164 assert "vanished id is clean no-op" in source
165
166
167def test_gate_drawer_smoke_harness_covers_contract():
168 source = GATE_HARNESS.read_text(encoding="utf-8")
169
170 assert source.startswith(
171 "<!-- SPDX-License-Identifier: AGPL-3.0-only -->\n"
172 "<!-- Copyright (c) 2026 sol pbc -->\n"
173 "<!doctype html>"
174 )
175 assert '<link rel="stylesheet" href="../tokens.css">' in source
176 assert '<link rel="stylesheet" href="../drawer.css">' in source
177 assert source.index('<script src="../drawer.js"></script>') < source.index(
178 '<script src="../gate-drawer.js"></script>'
179 )
180 assert "function assert(name, condition, detail)" in source
181 assert "function equal(name, actual, expected)" in source
182 assert "speakers-owner-gate-diagnostics" in source
183 assert "gate-row" in source
184 assert "gate-need" in source
185 assert "gate-bar" in source
186 assert "gate-next" in source
187 assert "too few line emphasizes observed first" in source
188 assert "median line emphasizes seconds" in source
189 assert "cohesion line has no digits" in source
190 assert "zero observed renders" in source
191 assert "missing threshold omits needs" in source
192 assert "incomplete triple omits bar" in source
193 assert "bar caps at one hundred" in source
194 assert "action html renders" in source
195 assert "preserve restores gate drawer" in source
196
197
198def test_gate_drawer_render_contract_under_node():
199 node = _node_or_skip()
200 drawer_source = (STATIC_ROOT / "drawer.js").read_text(encoding="utf-8")
201 gate_source = (STATIC_ROOT / "gate-drawer.js").read_text(encoding="utf-8")
202 script = "\n".join(
203 [
204 "const window = {};",
205 drawer_source,
206 gate_source,
207 "function assert(condition, message) { if (!condition) throw new Error(message); }",
208 r"""
209function decodeEntities(text) {
210 const named = { amp: "&", lt: "<", gt: ">", quot: "\"", apos: "'" };
211 return String(text).replace(/&(#\d+|#x[0-9a-fA-F]+|\w+);/g, (entity, body) => {
212 if (body[0] === "#") {
213 const value = body[1]?.toLowerCase() === "x"
214 ? Number.parseInt(body.slice(2), 16)
215 : Number.parseInt(body.slice(1), 10);
216 return Number.isFinite(value) ? String.fromCodePoint(value) : entity;
217 }
218 return Object.prototype.hasOwnProperty.call(named, body) ? named[body] : entity;
219 });
220}
221
222function textContentAfterUnescape(html) {
223 return String(html).split(/(<[^>]*>)/g)
224 .filter((chunk) => chunk && !chunk.startsWith("<"))
225 .map(decodeEntities)
226 .join("");
227}
228
229function lineHtml(rendered) {
230 const match = rendered.match(/<span class="drawer-line">([\s\S]*?)<\/span>/);
231 return match ? match[1] : "";
232}
233
234function bodyHtml(rendered) {
235 const match = rendered.match(/<div class="drawer-body">([\s\S]*?)<\/div><\/details>$/);
236 return match ? match[1] : "";
237}
238
239function payload(overrides = {}) {
240 return {
241 status: "low_quality",
242 source: "candidate_pool",
243 low_quality_reason: "too_few_stmts",
244 observed_value: 5,
245 threshold_value: 30,
246 manual_tags_count: 11,
247 segments_available: 12,
248 embeddings_available: 13,
249 can_build_from_tags: false,
250 ...overrides,
251 };
252}
253
254function formatMetric(value) {
255 if (typeof value !== "number" || !Number.isFinite(value)) return "n/a";
256 return Number.isInteger(value) ? String(value) : value.toFixed(2);
257}
258
259function formatWithUnit(value, unit) {
260 const text = formatMetric(value);
261 return text === "n/a" ? text : `${text}${unit}`;
262}
263
264function boldTexts(html) {
265 return Array.from(html.matchAll(/<b>(.*?)<\/b>/g)).map((match) => match[1]);
266}
267
268function assertDeclined(name, rendered) {
269 assert(rendered === "", `${name} renders no drawer`);
270 assert(!rendered.includes("gate-bar"), `${name} has no bar`);
271 assert(!rendered.includes("gate-row"), `${name} has no row`);
272}
273
274assertDeclined("route default low quality", window.GateDrawer.render(payload({
275 low_quality_reason: "",
276 observed_value: 0.0,
277 threshold_value: 0.0,
278})));
279
280assertDeclined("unknown low quality reason", window.GateDrawer.render(payload({
281 low_quality_reason: "some_new_gate",
282})));
283
284const directDrawer = window.Drawer.render({
285 id: "direct",
286 label: "direct",
287 line: "median statement length 1.5s — needs 2s",
288 bodyHtml: "",
289});
290assert(Object.keys(window.Drawer).join("|") === "render|preserveOpen|formatLine", "drawer exports formatLine");
291assert(window.Drawer.formatLine("2 files created · <5>").includes("<b>2</b> files created · <<b>5</b>>"), "formatLine escapes and emphasizes digits");
292assert(lineHtml(directDrawer).includes("<b>1.5s</b>"), "drawer emphasizes compact seconds");
293
294const tooFewPayload = payload();
295const tooFew = window.GateDrawer.render(tooFewPayload, {
296 actionHtml: '<button id="spkOwnerBuildFromTags">build</button>',
297});
298const tooFewLine = lineHtml(tooFew);
299const tooFewBody = bodyHtml(tooFew);
300const tooFewLineText = textContentAfterUnescape(tooFewLine);
301const tooFewBodyText = textContentAfterUnescape(tooFewBody);
302const tooFewObserved = formatMetric(tooFewPayload.observed_value);
303const tooFewThreshold = formatMetric(tooFewPayload.threshold_value);
304assert(tooFew.includes('data-drawer-id="speakers-owner-gate-diagnostics"'), "drawer id renders");
305assert(!tooFew.includes("drawer-chip"), "chip omitted");
306assert(tooFewLineText.includes(tooFewObserved), "too few line includes observed value");
307assert(tooFewLineText.includes(tooFewThreshold), "too few line includes threshold value");
308assert(tooFewLineText.indexOf(tooFewObserved) < tooFewLineText.indexOf(tooFewThreshold), "too few line is observed first");
309assert(boldTexts(tooFewLine).join("|") === [tooFewObserved, tooFewThreshold].join("|"), "too few emphasis follows line order");
310assert(tooFewBody.includes("<span>statements</span>"), "statements row label renders");
311assert(!tooFewBody.includes("too_few_stmts"), "raw reason token omitted");
312assert(!tooFewLineText.includes(tooFewPayload.low_quality_reason), "raw reason token omitted from line");
313assert(tooFewBodyText.includes(tooFewPayload.source), "source value is retained");
314assert(tooFewBodyText.includes(String(tooFewPayload.manual_tags_count)), "manual tags value is retained");
315assert(tooFewBodyText.includes(String(tooFewPayload.segments_available)), "segments value is retained");
316assert(tooFewBodyText.includes(String(tooFewPayload.embeddings_available)), "embeddings value is retained");
317assert(tooFewBody.includes("spkOwnerBuildFromTags"), "action html renders");
318assert(!tooFewBody.includes("<b>"), "body has no authored emphasis");
319
320const medianPayload = payload({
321 low_quality_reason: "median_duration_too_short",
322 observed_value: 1.5,
323 threshold_value: 2,
324});
325const median = window.GateDrawer.render(medianPayload);
326const medianLine = lineHtml(median);
327const medianLineText = textContentAfterUnescape(medianLine);
328const medianObserved = formatWithUnit(medianPayload.observed_value, "s");
329const medianThreshold = formatWithUnit(medianPayload.threshold_value, "s");
330assert(medianLineText.includes(medianObserved), "median line includes observed value");
331assert(medianLineText.includes(medianThreshold), "median line includes threshold value");
332assert(medianLineText.indexOf(medianObserved) < medianLineText.indexOf(medianThreshold), "median line is observed first");
333assert(boldTexts(medianLine).join("|") === [medianObserved, medianThreshold].join("|"), "median seconds are emphasized");
334assert(bodyHtml(median).includes("<span>median length</span>"), "median row label renders");
335
336const diffuse = window.GateDrawer.render(payload({
337 low_quality_reason: "cluster_too_diffuse",
338 observed_value: 0.2,
339 threshold_value: 0.3,
340}));
341const diffuseLine = lineHtml(diffuse);
342assert(textContentAfterUnescape(diffuseLine).trim().length > 0, "diffuse line text renders");
343assert(!/\d/.test(textContentAfterUnescape(diffuseLine)), "diffuse line has no digits");
344assert(!diffuseLine.includes("<b>"), "diffuse line has no emphasis");
345assert(bodyHtml(diffuse).includes("<span>consistency</span>"), "consistency row label renders");
346
347const zeroPayload = payload({
348 low_quality_reason: "median_duration_too_short",
349 observed_value: 0,
350 threshold_value: 1.5,
351});
352const zeroObserved = window.GateDrawer.render(zeroPayload);
353assert(textContentAfterUnescape(lineHtml(zeroObserved)).includes(formatWithUnit(zeroPayload.observed_value, "s")), "zero observed renders");
354
355const missingThreshold = window.GateDrawer.render(payload({ threshold_value: 0 }));
356assert(!textContentAfterUnescape(lineHtml(missingThreshold)).includes("needs"), "threshold zero omits needs clause");
357assert(!missingThreshold.includes("gate-bar"), "threshold zero omits bar");
358
359const absentObserved = window.GateDrawer.render(payload({ observed_value: Number.NaN }));
360assert(textContentAfterUnescape(lineHtml(absentObserved)).includes("n/a"), "absent observed renders n/a");
361assert(!absentObserved.includes("NaN"), "nan never reaches html");
362assert(!absentObserved.includes("undefined"), "undefined never reaches html");
363
364const cappedPayload = payload({ observed_value: payload().threshold_value * 2 });
365const capped = window.GateDrawer.render(cappedPayload);
366assert(capped.includes('style="width:100.00%"'), "bar caps at one hundred");
367""",
368 ]
369 )
370
371 subprocess.run([node, "-e", script], check=True, text=True)