personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_convey_drawer.py
15 kB 371 lines
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 · &lt;<b>5</b>&gt;"), "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)