@matt82198/aesop 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -94
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +89 -264
- package/aesop.config.example.json +27 -26
- package/bin/CLAUDE.md +39 -0
- package/bin/cli.js +368 -24
- package/daemons/CLAUDE.md +33 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +90 -10
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/spikes/tiered-cognition/README.md +29 -0
- package/docs/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/hooks/CLAUDE.md +38 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +29 -8
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +529 -62
- package/package.json +12 -3
- package/skills/CLAUDE.md +3 -0
- package/skills/power/SKILL.md +161 -0
- package/tools/CLAUDE.md +191 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +204 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +131 -31
- package/tools/stall_check.py +172 -0
- package/tools/verify_dash.py +694 -0
- package/tools/verify_submit_encoding.py +194 -0
- package/ui/CLAUDE.md +109 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +179 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +58 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +586 -0
- package/ui/config.py +114 -0
- package/ui/csrf.py +140 -0
- package/ui/handler.py +426 -0
- package/ui/render.py +29 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +168 -0
- package/ui/templates/dashboard.html +1202 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"""Browser-level proof for the realtime SSE dashboard (ui/serve.py).
|
|
2
|
+
|
|
3
|
+
Drives a real headless Chromium (python-playwright) against a fixture fleet and
|
|
4
|
+
asserts the contract the unit tests can't see:
|
|
5
|
+
(a) zero console errors on load and across the run
|
|
6
|
+
(b) backlog panel renders the seeded tiers/items
|
|
7
|
+
(c) clicking an agent row expands detail containing the actual dispatch prompt
|
|
8
|
+
(d) file changes push to the page over SSE within ~5s WITHOUT reload
|
|
9
|
+
(e) the expanded row is STILL expanded after those live updates
|
|
10
|
+
|
|
11
|
+
Run: python tools/verify_dash.py (exit 0 = proven, 1 = failed)
|
|
12
|
+
python tools/verify_dash.py --allow-skip (exit 0 = proven or skipped, 1 = failed)
|
|
13
|
+
|
|
14
|
+
Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip is passed).
|
|
15
|
+
Use --allow-skip for environments that legitimately can't run a browser.
|
|
16
|
+
"""
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import socket
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
import time
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
30
|
+
SERVE = REPO / "ui" / "serve.py"
|
|
31
|
+
|
|
32
|
+
FIXTURE_BACKLOG = """# Audit backlog — verify_dash fixture
|
|
33
|
+
|
|
34
|
+
**Status legend:** ⬜ unclaimed · 🔵 dispatched · ✅ merged · ⏸ user call
|
|
35
|
+
|
|
36
|
+
## P0 — correctness / security
|
|
37
|
+
|
|
38
|
+
- ✅ **[sec] BACKLOG-SEED-ALPHA item.** done.
|
|
39
|
+
- 🔵 **[js] BACKLOG-SEED-BETA item.** in flight.
|
|
40
|
+
|
|
41
|
+
## Landing log
|
|
42
|
+
- fixture
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
AGENT_FULL_ID = "verifyagent0123456789ab"
|
|
46
|
+
# Long, multi-line prompt so the .dispatch-prompt box (max-height 300px) actually
|
|
47
|
+
# overflows and is scrollable — required to test scroll-position preservation.
|
|
48
|
+
PROMPT_MARKER = "FIXTURE-PROMPT-MARKER: rebuild the flux capacitor\n" + "\n".join(
|
|
49
|
+
f"line {i}: recalibrate subsystem {i} and verify each tolerance band carefully"
|
|
50
|
+
for i in range(60))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def free_port():
|
|
54
|
+
s = socket.socket()
|
|
55
|
+
s.bind(("127.0.0.1", 0))
|
|
56
|
+
port = s.getsockname()[1]
|
|
57
|
+
s.close()
|
|
58
|
+
return port
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_fixture(root: Path, hint: str, with_high_alerts: bool = False):
|
|
62
|
+
(root / "state").mkdir(exist_ok=True)
|
|
63
|
+
(root / "transcripts").mkdir(exist_ok=True)
|
|
64
|
+
(root / "dash").mkdir(exist_ok=True)
|
|
65
|
+
(root / "AUDIT-BACKLOG.md").write_text(FIXTURE_BACKLOG, encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
# Optional: seed with HIGH and MED severity alerts for testing
|
|
68
|
+
if with_high_alerts:
|
|
69
|
+
alerts_log = root / "state" / "SECURITY-ALERTS.log"
|
|
70
|
+
alerts_log.write_text(
|
|
71
|
+
"2025-07-12T14:32:01Z | HIGH | API secret exposed in logs\n"
|
|
72
|
+
"2025-07-12T14:30:05Z | MED | Unvalidated user input detected\n",
|
|
73
|
+
encoding="utf-8"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Fake detector: reads hint.txt so live agent updates are deterministic.
|
|
77
|
+
(root / "hint.txt").write_text(hint, encoding="utf-8")
|
|
78
|
+
fake = (
|
|
79
|
+
"import { readFileSync } from 'node:fs';\n"
|
|
80
|
+
"const hint = readFileSync(new URL('../hint.txt', import.meta.url), 'utf8').trim();\n"
|
|
81
|
+
"console.log(JSON.stringify([{id:'" + AGENT_FULL_ID[:13] + "',"
|
|
82
|
+
"status:'running',age_s:4,hint:hint,taskLabel:hint}]));\n"
|
|
83
|
+
)
|
|
84
|
+
(root / "dash" / "dash-extra.mjs").write_text(fake, encoding="utf-8")
|
|
85
|
+
transcript = root / "transcripts" / f"{AGENT_FULL_ID}.output"
|
|
86
|
+
lines = [
|
|
87
|
+
json.dumps({"type": "user", "parentUuid": None,
|
|
88
|
+
"message": {"content": PROMPT_MARKER}}),
|
|
89
|
+
json.dumps({"type": "assistant", "model": "claude-haiku-4-5",
|
|
90
|
+
"message": {"content": "working"}}),
|
|
91
|
+
]
|
|
92
|
+
transcript.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
93
|
+
# fingerprint seed so the collector re-invokes the fake detector on touch
|
|
94
|
+
(root / "transcripts" / "agent-seed.jsonl").write_text("{}\n", encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
parser = argparse.ArgumentParser(
|
|
99
|
+
description="Browser-level proof for the realtime SSE dashboard"
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--allow-skip",
|
|
103
|
+
action="store_true",
|
|
104
|
+
help="Allow skipping if playwright/chromium is unavailable (exit 0 instead of 1)"
|
|
105
|
+
)
|
|
106
|
+
args = parser.parse_args()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from playwright.sync_api import sync_playwright
|
|
110
|
+
except ImportError:
|
|
111
|
+
msg = "playwright missing — run `python -m playwright install chromium`, or pass --allow-skip"
|
|
112
|
+
if args.allow_skip:
|
|
113
|
+
print(f"SKIP: {msg}")
|
|
114
|
+
return 0
|
|
115
|
+
else:
|
|
116
|
+
print(f"FAIL: {msg}")
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
root = Path(tempfile.mkdtemp(prefix="aesop-verify-dash-"))
|
|
120
|
+
state_root = root / "state"
|
|
121
|
+
|
|
122
|
+
# HARD GUARD: refuse to run if state_root looks like the real repo state dir
|
|
123
|
+
# (e.g., ~/aesop/state or an absolute path ending with /aesop/state)
|
|
124
|
+
real_state = Path.home() / "aesop" / "state"
|
|
125
|
+
if state_root.resolve() == real_state.resolve():
|
|
126
|
+
print("FAIL: state dir resolved to real repo state (~aesop/state), refusing to run")
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
port = free_port()
|
|
130
|
+
env = dict(os.environ,
|
|
131
|
+
AESOP_ROOT=str(root),
|
|
132
|
+
AESOP_STATE_ROOT=str(state_root),
|
|
133
|
+
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
134
|
+
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
135
|
+
PORT=str(port))
|
|
136
|
+
# Build with HIGH/MED severity alerts for testing alarm color semantics
|
|
137
|
+
build_fixture(root, hint="initial fixture task", with_high_alerts=True)
|
|
138
|
+
server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
|
|
139
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
140
|
+
console_errors = []
|
|
141
|
+
failures = []
|
|
142
|
+
try:
|
|
143
|
+
# wait for server
|
|
144
|
+
for _ in range(50):
|
|
145
|
+
try:
|
|
146
|
+
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
|
147
|
+
break
|
|
148
|
+
except OSError:
|
|
149
|
+
time.sleep(0.2)
|
|
150
|
+
else:
|
|
151
|
+
print("FAIL: server never came up")
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
with sync_playwright() as pw:
|
|
155
|
+
try:
|
|
156
|
+
browser = pw.chromium.launch(headless=True)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
msg = f"chromium unavailable ({e}); run: python -m playwright install chromium"
|
|
159
|
+
if args.allow_skip:
|
|
160
|
+
print(f"SKIP: {msg}")
|
|
161
|
+
return 0
|
|
162
|
+
else:
|
|
163
|
+
print(f"FAIL: {msg}")
|
|
164
|
+
return 1
|
|
165
|
+
page = browser.new_page()
|
|
166
|
+
page.on("console", lambda m: console_errors.append(m.text)
|
|
167
|
+
if m.type == "error" else None)
|
|
168
|
+
page.on("pageerror", lambda e: console_errors.append(str(e)))
|
|
169
|
+
page.goto(f"http://127.0.0.1:{port}/", wait_until="domcontentloaded")
|
|
170
|
+
|
|
171
|
+
# (b) backlog panel renders seeded items
|
|
172
|
+
|
|
173
|
+
# (P2-UX) Layout order verification: Fleet Agents + Security Alerts in top third
|
|
174
|
+
try:
|
|
175
|
+
page.wait_for_selector("#agents-list", timeout=8000)
|
|
176
|
+
page.wait_for_selector("#alerts-list", timeout=8000)
|
|
177
|
+
page.wait_for_selector("#backlog-tiers", timeout=8000)
|
|
178
|
+
|
|
179
|
+
# Verify DOM order: agents should appear before backlog
|
|
180
|
+
agents_index = page.evaluate(
|
|
181
|
+
"Array.from(document.querySelectorAll('[id]')).findIndex(el => el.id === 'agents-list')"
|
|
182
|
+
)
|
|
183
|
+
alerts_index = page.evaluate(
|
|
184
|
+
"Array.from(document.querySelectorAll('[id]')).findIndex(el => el.id === 'alerts-list')"
|
|
185
|
+
)
|
|
186
|
+
backlog_index = page.evaluate(
|
|
187
|
+
"Array.from(document.querySelectorAll('[id]')).findIndex(el => el.id === 'backlog-tiers')"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert agents_index < backlog_index, f"Fleet Agents should appear BEFORE Audit Backlog in DOM (agents={agents_index}, backlog={backlog_index})"
|
|
191
|
+
assert alerts_index < backlog_index, f"Security Alerts should appear BEFORE Audit Backlog in DOM (alerts={alerts_index}, backlog={backlog_index})"
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
failures.append(f"(P2-UX) layout order verification failed: {e}")
|
|
195
|
+
|
|
196
|
+
# (P2-UX) Collapsed done backlog items: verify reduced padding and opacity
|
|
197
|
+
try:
|
|
198
|
+
# Ensure backlog items render with the fixture
|
|
199
|
+
page.wait_for_function(
|
|
200
|
+
"document.querySelector('.backlog-item.done') !== null",
|
|
201
|
+
timeout=8000
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Get height and opacity of done item vs active item
|
|
205
|
+
done_item_styles = page.evaluate("""
|
|
206
|
+
(() => {
|
|
207
|
+
const done = document.querySelector('.backlog-item.done');
|
|
208
|
+
const cs = window.getComputedStyle(done);
|
|
209
|
+
return {
|
|
210
|
+
height: done.offsetHeight,
|
|
211
|
+
opacity: cs.opacity,
|
|
212
|
+
padding: cs.padding
|
|
213
|
+
};
|
|
214
|
+
})()
|
|
215
|
+
""")
|
|
216
|
+
|
|
217
|
+
active_item_styles = page.evaluate("""
|
|
218
|
+
(() => {
|
|
219
|
+
const active = Array.from(document.querySelectorAll('.backlog-item'))
|
|
220
|
+
.find(el => !el.classList.contains('done'));
|
|
221
|
+
if (!active) return {height: 0, opacity: '1', padding: '0px'};
|
|
222
|
+
const cs = window.getComputedStyle(active);
|
|
223
|
+
return {
|
|
224
|
+
height: active.offsetHeight,
|
|
225
|
+
opacity: cs.opacity,
|
|
226
|
+
padding: cs.padding
|
|
227
|
+
};
|
|
228
|
+
})()
|
|
229
|
+
""")
|
|
230
|
+
|
|
231
|
+
# Done items should be more compact (lower height, lower opacity)
|
|
232
|
+
assert done_item_styles['height'] <= active_item_styles['height'], f"Done items should be more compact (height {done_item_styles['height']} > {active_item_styles['height']})"
|
|
233
|
+
assert float(done_item_styles['opacity']) < 1.0, f"Done items should have reduced opacity (got {done_item_styles['opacity']})"
|
|
234
|
+
|
|
235
|
+
except Exception as e:
|
|
236
|
+
failures.append(f"(P2-UX) done item visual collapse verification failed: {e}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
page.wait_for_selector("#backlog-tiers:not(.loading)", timeout=8000)
|
|
241
|
+
assert "BACKLOG-SEED-ALPHA" in page.inner_text("#backlog-tiers")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
failures.append(f"(b) backlog panel did not render seed items: {e}")
|
|
244
|
+
|
|
245
|
+
# (b2) alarm color semantics: alert count renders in alarm color when HIGH alerts exist
|
|
246
|
+
try:
|
|
247
|
+
# Wait for the alert count to get a class (indicating data has loaded)
|
|
248
|
+
page.wait_for_function(
|
|
249
|
+
"document.getElementById('alert-count').className !== ''",
|
|
250
|
+
timeout=8000
|
|
251
|
+
)
|
|
252
|
+
# Check that the element has the alarm-high class (red color)
|
|
253
|
+
class_list = page.evaluate("document.getElementById('alert-count').className")
|
|
254
|
+
assert "alarm-high" in class_list or "alarm-med" in class_list, \
|
|
255
|
+
f"Alert count should have 'alarm-high' or 'alarm-med' class for severity, got: {class_list}"
|
|
256
|
+
# Verify computed color is NOT neutral gray (should be red/amber)
|
|
257
|
+
color = page.evaluate("window.getComputedStyle(document.getElementById('alert-count')).color")
|
|
258
|
+
# Color should be high-alert red (not gray/neutral) — just verify it's a color
|
|
259
|
+
assert "rgb(" in color, f"Alert count should have computed color, got: {color}"
|
|
260
|
+
except Exception as e:
|
|
261
|
+
failures.append(f"(b2) alert count alarm color not set: {e}")
|
|
262
|
+
|
|
263
|
+
# (b3) Security Alerts panel has distinct alarm styling when HIGH alerts exist
|
|
264
|
+
try:
|
|
265
|
+
alerts_box = page.query_selector(".alerts-box")
|
|
266
|
+
alerts_box_class = page.evaluate("document.querySelector('.alerts-box').className")
|
|
267
|
+
assert "has-high-alerts" in alerts_box_class or "has-alerts" in alerts_box_class, \
|
|
268
|
+
f"Alerts box should have alarm styling class, got: {alerts_box_class}"
|
|
269
|
+
# Verify border changed from neutral #333 to alarm #f44
|
|
270
|
+
border_color = page.evaluate("window.getComputedStyle(document.querySelector('.alerts-box')).borderColor")
|
|
271
|
+
assert "rgb(" in border_color, f"Alerts box should have computed border color: {border_color}"
|
|
272
|
+
except Exception as e:
|
|
273
|
+
failures.append(f"(b3) alerts panel alarm styling not applied: {e}")
|
|
274
|
+
|
|
275
|
+
# (b4) affordances: stronger expand-toggle + responsive header wrap (no horizontal overflow when narrow)
|
|
276
|
+
try:
|
|
277
|
+
page.wait_for_selector(".agent-row", timeout=8000)
|
|
278
|
+
toggle_size = page.evaluate(
|
|
279
|
+
"parseFloat(getComputedStyle(document.querySelector('.agent-expand-toggle')).fontSize)")
|
|
280
|
+
assert toggle_size >= 13, f"expand toggle should be a stronger affordance (>=13px), got {toggle_size}px"
|
|
281
|
+
# narrow the viewport: header must wrap, body must not scroll horizontally
|
|
282
|
+
page.set_viewport_size({"width": 600, "height": 800})
|
|
283
|
+
page.wait_for_timeout(200)
|
|
284
|
+
overflow = page.evaluate(
|
|
285
|
+
"document.documentElement.scrollWidth - document.documentElement.clientWidth")
|
|
286
|
+
assert overflow <= 2, f"body overflows horizontally at 600px (header not wrapping): {overflow}px"
|
|
287
|
+
page.set_viewport_size({"width": 1280, "height": 900})
|
|
288
|
+
except Exception as e:
|
|
289
|
+
failures.append(f"(b4) affordance/responsive-header check failed: {e}")
|
|
290
|
+
|
|
291
|
+
# (c) click agent row -> expands with the real dispatch prompt
|
|
292
|
+
try:
|
|
293
|
+
page.wait_for_selector(".agent-row", timeout=8000)
|
|
294
|
+
page.click(".agent-row")
|
|
295
|
+
page.wait_for_selector(".agent-row.expanded", timeout=4000)
|
|
296
|
+
page.wait_for_function(
|
|
297
|
+
"document.querySelector('.agent-row.expanded .agent-details')"
|
|
298
|
+
f" && document.querySelector('.agent-row.expanded .agent-details').innerText.includes('FIXTURE-PROMPT-MARKER')",
|
|
299
|
+
timeout=8000)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
failures.append(f"(c) click-to-expand with prompt failed: {e}")
|
|
302
|
+
|
|
303
|
+
# (d) live updates over SSE, no reload: backlog file + agent hint change
|
|
304
|
+
try:
|
|
305
|
+
bl = root / "AUDIT-BACKLOG.md"
|
|
306
|
+
content = bl.read_text(encoding="utf-8").replace(
|
|
307
|
+
"## Landing log",
|
|
308
|
+
"- ⬜ **[test] LIVE-BACKLOG-MARKER item.** pushed live.\n\n## Landing log")
|
|
309
|
+
bl.write_text(content, encoding="utf-8")
|
|
310
|
+
(root / "hint.txt").write_text("LIVE-AGENT-MARKER task", encoding="utf-8")
|
|
311
|
+
(root / "transcripts" / "agent-live.jsonl").write_text("{}\n", encoding="utf-8")
|
|
312
|
+
page.wait_for_function(
|
|
313
|
+
"document.querySelector('#backlog-tiers').innerText.includes('LIVE-BACKLOG-MARKER')",
|
|
314
|
+
timeout=8000)
|
|
315
|
+
page.wait_for_function(
|
|
316
|
+
"document.querySelector('#agents-list').innerText.includes('LIVE-AGENT-MARKER')",
|
|
317
|
+
timeout=8000)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
failures.append(f"(d) live SSE update did not reach the page: {e}")
|
|
320
|
+
|
|
321
|
+
# (e) expansion survived the live updates
|
|
322
|
+
try:
|
|
323
|
+
assert page.query_selector(".agent-row.expanded") is not None, \
|
|
324
|
+
"expanded row lost after live updates"
|
|
325
|
+
except Exception as e:
|
|
326
|
+
failures.append(f"(e) {e}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# (f) scroll position and text selection survive live updates (bugfix P2 #1)
|
|
330
|
+
try:
|
|
331
|
+
# Expand an agent again to get its prompt box visible
|
|
332
|
+
expanded_row = page.query_selector(".agent-row.expanded")
|
|
333
|
+
if not expanded_row:
|
|
334
|
+
# Re-expand if needed
|
|
335
|
+
page.click(".agent-row")
|
|
336
|
+
page.wait_for_selector(".agent-row.expanded", timeout=4000)
|
|
337
|
+
|
|
338
|
+
# Get the prompt box and scroll it down
|
|
339
|
+
prompt_box = page.query_selector(".agent-row.expanded .dispatch-prompt")
|
|
340
|
+
assert prompt_box is not None, "Prompt box not found"
|
|
341
|
+
|
|
342
|
+
# Scroll the prompt box to bottom
|
|
343
|
+
initial_scroll = page.evaluate(
|
|
344
|
+
"document.querySelector('.agent-row.expanded .dispatch-prompt').scrollTop || 0")
|
|
345
|
+
page.evaluate(
|
|
346
|
+
"document.querySelector('.agent-row.expanded .dispatch-prompt').scrollTop = 999")
|
|
347
|
+
scroll_before = page.evaluate(
|
|
348
|
+
"document.querySelector('.agent-row.expanded .dispatch-prompt').scrollTop")
|
|
349
|
+
assert scroll_before > initial_scroll, f"Failed to scroll; before={initial_scroll}, after={scroll_before}"
|
|
350
|
+
|
|
351
|
+
# Trigger a live update by touching the backlog
|
|
352
|
+
bl = root / "AUDIT-BACKLOG.md"
|
|
353
|
+
content = bl.read_text(encoding="utf-8").replace(
|
|
354
|
+
"## Landing log",
|
|
355
|
+
"- ⬜ **[test] SCROLL-PERSIST-MARKER item.** live update.\n\n## Landing log")
|
|
356
|
+
bl.write_text(content, encoding="utf-8")
|
|
357
|
+
|
|
358
|
+
# Wait for the update to arrive
|
|
359
|
+
page.wait_for_function(
|
|
360
|
+
"document.querySelector('#backlog-tiers').innerText.includes('SCROLL-PERSIST-MARKER')",
|
|
361
|
+
timeout=8000)
|
|
362
|
+
|
|
363
|
+
# Check that scroll position survived the update
|
|
364
|
+
scroll_after = page.evaluate(
|
|
365
|
+
"document.querySelector('.agent-row.expanded .dispatch-prompt').scrollTop")
|
|
366
|
+
assert scroll_after >= scroll_before - 2, f"Scroll position lost during live update: before={scroll_before}, after={scroll_after}"
|
|
367
|
+
except Exception as e:
|
|
368
|
+
failures.append(f"(f) scroll position/selection not preserved during live update: {e}")
|
|
369
|
+
|
|
370
|
+
# (g) promptCache eviction works: removed agents don't stay cached (bugfix P2 #2)
|
|
371
|
+
try:
|
|
372
|
+
# Get initial cache size
|
|
373
|
+
cache_size_before = page.evaluate("window.__getPromptCacheSize()")
|
|
374
|
+
assert cache_size_before > 0, "Cache should have entries for expanded agents"
|
|
375
|
+
|
|
376
|
+
# Change agent hint to force a new agent to appear
|
|
377
|
+
(root / "hint.txt").write_text("NEW-AGENT-AFTER-EVICT", encoding="utf-8")
|
|
378
|
+
(root / "transcripts" / "agent-evict-marker.jsonl").write_text("{}", encoding="utf-8")
|
|
379
|
+
page.wait_for_function(
|
|
380
|
+
"document.querySelector('#agents-list').innerText.includes('NEW-AGENT-AFTER-EVICT')",
|
|
381
|
+
timeout=8000)
|
|
382
|
+
|
|
383
|
+
# Now change it again to a different agent (old one gets removed from DOM)
|
|
384
|
+
(root / "hint.txt").write_text("FINAL-AGENT-STATE", encoding="utf-8")
|
|
385
|
+
(root / "transcripts" / "agent-final-marker.jsonl").write_text("{}", encoding="utf-8")
|
|
386
|
+
page.wait_for_function(
|
|
387
|
+
"document.querySelector('#agents-list').innerText.includes('FINAL-AGENT-STATE')",
|
|
388
|
+
timeout=8000)
|
|
389
|
+
|
|
390
|
+
# Cache should have evicted old entries
|
|
391
|
+
cache_size_after = page.evaluate("window.__getPromptCacheSize()")
|
|
392
|
+
assert cache_size_after <= cache_size_before + 1, f"Cache grew unbounded: before={cache_size_before}, after={cache_size_after}"
|
|
393
|
+
except Exception as e:
|
|
394
|
+
failures.append(f"(g) promptCache not evicting removed agents: {e}")
|
|
395
|
+
|
|
396
|
+
# (h) /submit writes the inbox file as valid UTF-8, even on first write
|
|
397
|
+
# (regression for PR #36: an encoding-less header write corrupted the file)
|
|
398
|
+
try:
|
|
399
|
+
marker = "SUBMIT-ENC-MARKER café ✓ orchestrator"
|
|
400
|
+
page.fill("#inbox-input", marker)
|
|
401
|
+
page.click("#inbox-button")
|
|
402
|
+
inbox_file = root / "state" / "ui-inbox.md"
|
|
403
|
+
deadline = time.time() + 6
|
|
404
|
+
ok = False
|
|
405
|
+
while time.time() < deadline:
|
|
406
|
+
if inbox_file.exists():
|
|
407
|
+
# Must decode as UTF-8 without raising UnicodeDecodeError.
|
|
408
|
+
text = inbox_file.read_text(encoding="utf-8")
|
|
409
|
+
if marker in text:
|
|
410
|
+
ok = True
|
|
411
|
+
break
|
|
412
|
+
time.sleep(0.2)
|
|
413
|
+
assert ok, f"submitted UTF-8 marker not found in {inbox_file}"
|
|
414
|
+
except Exception as e:
|
|
415
|
+
failures.append(f"(h) /submit did not write a valid UTF-8 inbox file: {e}")
|
|
416
|
+
|
|
417
|
+
# (i) tracker panel renders with lanes (proposed, ranked, in-progress, done)
|
|
418
|
+
try:
|
|
419
|
+
page.wait_for_selector("#tracker-lanes", timeout=8000)
|
|
420
|
+
# Verify lane headers exist
|
|
421
|
+
page.wait_for_function(
|
|
422
|
+
"document.querySelector('[data-lane=\"proposed\"]') !== null",
|
|
423
|
+
timeout=5000)
|
|
424
|
+
lanes = page.evaluate(
|
|
425
|
+
"Array.from(document.querySelectorAll('[data-lane]')).map(el => el.dataset.lane)")
|
|
426
|
+
expected_lanes = ['proposed', 'ranked', 'in-progress', 'done']
|
|
427
|
+
for lane in expected_lanes:
|
|
428
|
+
assert lane in lanes, f"Lane '{lane}' not found in tracker"
|
|
429
|
+
except Exception as e:
|
|
430
|
+
failures.append(f"(i) tracker panel did not render lanes: {e}")
|
|
431
|
+
|
|
432
|
+
# (j) POST tracker item via form → appears in proposed lane via SSE (no reload)
|
|
433
|
+
try:
|
|
434
|
+
page.fill("#tracker-title", "Tracker Test Item")
|
|
435
|
+
page.select_option("#tracker-priority", "P1")
|
|
436
|
+
page.fill("#tracker-notes", "Test notes for tracker item")
|
|
437
|
+
page.click("#tracker-add-btn")
|
|
438
|
+
# Wait for item to appear in proposed lane via SSE (no page reload)
|
|
439
|
+
page.wait_for_function(
|
|
440
|
+
"document.querySelector('[data-lane=\"proposed\"]')?.innerText.includes('Tracker Test Item')",
|
|
441
|
+
timeout=8000)
|
|
442
|
+
# Verify priority chip rendered
|
|
443
|
+
page.wait_for_function(
|
|
444
|
+
"document.querySelector('.priority-p1') !== null",
|
|
445
|
+
timeout=5000)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
failures.append(f"(j) tracker add-item form did not create/SSE item: {e}")
|
|
448
|
+
|
|
449
|
+
# (m) tracker item pr_link XSS: javascript: URLs must never reach a DOM href
|
|
450
|
+
# (wave-10 P0 — render-side defense-in-depth for buildTrackerItem)
|
|
451
|
+
try:
|
|
452
|
+
render = page.evaluate(
|
|
453
|
+
"""
|
|
454
|
+
(() => {
|
|
455
|
+
const el = buildTrackerItem({
|
|
456
|
+
id: 'xss-test-item',
|
|
457
|
+
title: 'XSS Probe',
|
|
458
|
+
priority: 'P1',
|
|
459
|
+
lane: 'proposed',
|
|
460
|
+
pr_link: 'javascript:alert(1)'
|
|
461
|
+
});
|
|
462
|
+
const a = el.querySelector('a');
|
|
463
|
+
return {
|
|
464
|
+
html: el.innerHTML,
|
|
465
|
+
anchorHref: a ? a.getAttribute('href') : null,
|
|
466
|
+
anchorPresent: a !== null
|
|
467
|
+
};
|
|
468
|
+
})()
|
|
469
|
+
"""
|
|
470
|
+
)
|
|
471
|
+
# The raw pr_link text may still be SHOWN (escaped, inert) to the user
|
|
472
|
+
# for visibility — that's fine. What must never happen is the scheme
|
|
473
|
+
# landing inside an href="..." attribute, which is what makes it
|
|
474
|
+
# click-to-execute.
|
|
475
|
+
assert 'href="javascript' not in render["html"].lower(), \
|
|
476
|
+
f"(m) javascript: scheme leaked into a rendered href attribute: {render['html']}"
|
|
477
|
+
# No <a> should be emitted for a rejected scheme (label falls back to
|
|
478
|
+
# a plain, non-clickable span), and no href value may resolve to
|
|
479
|
+
# anything other than the empty/neutralized string.
|
|
480
|
+
assert not (render["anchorPresent"] and render["anchorHref"]
|
|
481
|
+
and "javascript:" in render["anchorHref"].lower()), \
|
|
482
|
+
f"(m) tracker item rendered an executable javascript: href: {render['anchorHref']}"
|
|
483
|
+
assert not render["anchorPresent"], \
|
|
484
|
+
f"(m) expected pr_link with unsafe scheme to render inert (no <a>), got href={render['anchorHref']}"
|
|
485
|
+
|
|
486
|
+
# Sanity check: a legitimate https:// pr_link must still render as a
|
|
487
|
+
# real, clickable link so the fix doesn't break valid PR links.
|
|
488
|
+
safe_render = page.evaluate(
|
|
489
|
+
"""
|
|
490
|
+
(() => {
|
|
491
|
+
const el = buildTrackerItem({
|
|
492
|
+
id: 'safe-link-item',
|
|
493
|
+
title: 'Safe Link',
|
|
494
|
+
priority: 'P1',
|
|
495
|
+
lane: 'proposed',
|
|
496
|
+
pr_link: 'https://github.com/example/repo/pull/1'
|
|
497
|
+
});
|
|
498
|
+
const a = el.querySelector('a');
|
|
499
|
+
return { anchorHref: a ? a.getAttribute('href') : null };
|
|
500
|
+
})()
|
|
501
|
+
"""
|
|
502
|
+
)
|
|
503
|
+
assert safe_render["anchorHref"] == "https://github.com/example/repo/pull/1", \
|
|
504
|
+
f"(m) valid https pr_link should still render a real href, got: {safe_render['anchorHref']}"
|
|
505
|
+
except Exception as e:
|
|
506
|
+
failures.append(f"(m) tracker pr_link XSS probe failed: {e}")
|
|
507
|
+
|
|
508
|
+
# (k) orchestrator status shows "no active session" when file absent
|
|
509
|
+
try:
|
|
510
|
+
orch_status = page.inner_text("#orchestrator-status")
|
|
511
|
+
assert "no active session" in orch_status or orch_status == "—", \
|
|
512
|
+
f"Expected 'no active session' when status file absent, got: '{orch_status}'"
|
|
513
|
+
except Exception as e:
|
|
514
|
+
failures.append(f"(k) orchestrator status did not show 'no active session': {e}")
|
|
515
|
+
|
|
516
|
+
# (l) write orchestrator-status.json with phase=audit → ASCII banner appears
|
|
517
|
+
try:
|
|
518
|
+
status_data = {
|
|
519
|
+
"id": "main",
|
|
520
|
+
"role": "orchestrator",
|
|
521
|
+
"activity": "running audit",
|
|
522
|
+
"phase": "audit",
|
|
523
|
+
"updated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
524
|
+
}
|
|
525
|
+
status_file = root / "state" / "orchestrator-status.json"
|
|
526
|
+
status_file.write_text(json.dumps(status_data, indent=2), encoding="utf-8")
|
|
527
|
+
# Wait for banner to appear via SSE
|
|
528
|
+
page.wait_for_function(
|
|
529
|
+
"document.getElementById('audit-banner').style.display === 'block'",
|
|
530
|
+
timeout=8000)
|
|
531
|
+
# Verify ASCII art is present
|
|
532
|
+
banner_text = page.inner_text("#audit-banner")
|
|
533
|
+
assert "AUDIT CYCLE RUNNING" in banner_text and "scanning" in banner_text, \
|
|
534
|
+
f"Audit banner missing expected content: {banner_text}"
|
|
535
|
+
except Exception as e:
|
|
536
|
+
failures.append(f"(l) orchestrator status audit banner did not appear: {e}")
|
|
537
|
+
|
|
538
|
+
# (n) Wave-13 UX/a11y fixes validation
|
|
539
|
+
# (n1) header-label contrast: verify color changed from #666 to #999
|
|
540
|
+
try:
|
|
541
|
+
header_label_color = page.evaluate(
|
|
542
|
+
"window.getComputedStyle(document.querySelector('.header-label')).color")
|
|
543
|
+
# #999 = rgb(153, 153, 153)
|
|
544
|
+
assert "153" in header_label_color, \
|
|
545
|
+
f"(n1) header-label should have #999 contrast fix (rgb ~153,153,153), got: {header_label_color}"
|
|
546
|
+
except Exception as e:
|
|
547
|
+
failures.append(f"(n1) header-label contrast check failed: {e}")
|
|
548
|
+
|
|
549
|
+
# (n2) priority select has visual affordance (chevron)
|
|
550
|
+
try:
|
|
551
|
+
select_bg = page.evaluate(
|
|
552
|
+
"window.getComputedStyle(document.getElementById('tracker-priority')).backgroundImage")
|
|
553
|
+
assert "url(" in select_bg and ("svg" in select_bg.lower() or "data:" in select_bg), \
|
|
554
|
+
f"(n2) tracker-priority should have chevron background, got: {select_bg}"
|
|
555
|
+
except Exception as e:
|
|
556
|
+
failures.append(f"(n2) priority select chevron not found: {e}")
|
|
557
|
+
|
|
558
|
+
# (n3) empty tracker lane shows placeholder
|
|
559
|
+
try:
|
|
560
|
+
# Create a tracker with no items to get empty state
|
|
561
|
+
page.evaluate("""
|
|
562
|
+
(() => {
|
|
563
|
+
const container = document.getElementById('tracker-lanes');
|
|
564
|
+
container.innerHTML = '';
|
|
565
|
+
const laneEl = document.createElement('div');
|
|
566
|
+
laneEl.className = 'tracker-lane';
|
|
567
|
+
laneEl.innerHTML = `
|
|
568
|
+
<div class="lane-header">
|
|
569
|
+
<span>Empty Test Lane</span>
|
|
570
|
+
<span class="lane-count" aria-label="Empty Test Lane: 0 items">0</span>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="lane-items empty" data-lane="test"></div>
|
|
573
|
+
`;
|
|
574
|
+
const itemsContainer = laneEl.querySelector('.lane-items');
|
|
575
|
+
const placeholder = document.createElement('div');
|
|
576
|
+
placeholder.className = 'lane-empty-placeholder';
|
|
577
|
+
placeholder.textContent = 'empty';
|
|
578
|
+
itemsContainer.appendChild(placeholder);
|
|
579
|
+
container.appendChild(laneEl);
|
|
580
|
+
})()
|
|
581
|
+
""")
|
|
582
|
+
empty_lane_text = page.inner_text('[data-lane="test"]')
|
|
583
|
+
assert "empty" in empty_lane_text.lower(), \
|
|
584
|
+
f"(n3) empty lane should show 'empty' placeholder, got: '{empty_lane_text}'"
|
|
585
|
+
except Exception as e:
|
|
586
|
+
failures.append(f"(n3) empty lane placeholder check failed: {e}")
|
|
587
|
+
|
|
588
|
+
# (n4) prefers-reduced-motion media query is in CSS
|
|
589
|
+
try:
|
|
590
|
+
css_contains_prefers = page.evaluate("""
|
|
591
|
+
(() => {
|
|
592
|
+
const styles = Array.from(document.styleSheets)
|
|
593
|
+
.filter(s => !s.href || s.href.includes('http://localhost'))
|
|
594
|
+
.map(s => {
|
|
595
|
+
try { return s.cssText; } catch { return ''; }
|
|
596
|
+
})
|
|
597
|
+
.join(' ');
|
|
598
|
+
const pageContent = document.documentElement.outerHTML;
|
|
599
|
+
return pageContent.includes('prefers-reduced-motion');
|
|
600
|
+
})()
|
|
601
|
+
""")
|
|
602
|
+
assert css_contains_prefers, \
|
|
603
|
+
"(n4) CSS should include @media (prefers-reduced-motion: reduce) query"
|
|
604
|
+
except Exception as e:
|
|
605
|
+
failures.append(f"(n4) prefers-reduced-motion query check failed: {e}")
|
|
606
|
+
|
|
607
|
+
# (n5) tracker-live-region exists for a11y announcements (sr-only)
|
|
608
|
+
try:
|
|
609
|
+
live_region = page.query_selector("#tracker-live-region")
|
|
610
|
+
assert live_region is not None, \
|
|
611
|
+
"(n5) tracker-live-region should exist for screen reader announcements"
|
|
612
|
+
# Verify it's sr-only (absolutely positioned, off-screen)
|
|
613
|
+
sr_only_styles = page.evaluate("""
|
|
614
|
+
(() => {
|
|
615
|
+
const el = document.getElementById('tracker-live-region');
|
|
616
|
+
const cs = window.getComputedStyle(el);
|
|
617
|
+
return {
|
|
618
|
+
position: cs.position,
|
|
619
|
+
width: cs.width,
|
|
620
|
+
height: cs.height
|
|
621
|
+
};
|
|
622
|
+
})()
|
|
623
|
+
""")
|
|
624
|
+
assert sr_only_styles["position"] == "absolute", \
|
|
625
|
+
f"(n5) tracker-live-region should be sr-only (position:absolute), got: {sr_only_styles}"
|
|
626
|
+
except Exception as e:
|
|
627
|
+
failures.append(f"(n5) tracker-live-region sr-only check failed: {e}")
|
|
628
|
+
|
|
629
|
+
# (n6) lane-count elements have aria-labels
|
|
630
|
+
try:
|
|
631
|
+
page.wait_for_selector(".lane-count", timeout=5000)
|
|
632
|
+
lane_count_labels = page.evaluate("""
|
|
633
|
+
(() => {
|
|
634
|
+
return Array.from(document.querySelectorAll('.lane-count'))
|
|
635
|
+
.map(el => el.getAttribute('aria-label') || 'missing')
|
|
636
|
+
.filter(l => l !== 'missing');
|
|
637
|
+
})()
|
|
638
|
+
""")
|
|
639
|
+
assert len(lane_count_labels) > 0 and all(": " in l for l in lane_count_labels), \
|
|
640
|
+
f"(n6) lane-count elements should have 'Lane Name: N items' aria-labels, got: {lane_count_labels}"
|
|
641
|
+
except Exception as e:
|
|
642
|
+
failures.append(f"(n6) lane-count aria-label check failed: {e}")
|
|
643
|
+
|
|
644
|
+
# (n7) running-count has toned down styling (not 18px/900)
|
|
645
|
+
try:
|
|
646
|
+
running_count_style = page.evaluate("""
|
|
647
|
+
(() => {
|
|
648
|
+
const el = document.getElementById('running-count');
|
|
649
|
+
const cs = window.getComputedStyle(el);
|
|
650
|
+
return { fontSize: cs.fontSize, fontWeight: cs.fontWeight };
|
|
651
|
+
})()
|
|
652
|
+
""")
|
|
653
|
+
# After fix: 16px/600, not 18px/900
|
|
654
|
+
font_size = int(running_count_style["fontSize"].replace("px", ""))
|
|
655
|
+
assert font_size <= 16, \
|
|
656
|
+
f"(n7) running-count should have toned down font-size (<=16px), got: {font_size}px"
|
|
657
|
+
# Font-weight 600 should be "600" or similar, not "900"
|
|
658
|
+
assert "900" not in str(running_count_style["fontWeight"]), \
|
|
659
|
+
f"(n7) running-count should have reduced font-weight (not 900), got: {running_count_style['fontWeight']}"
|
|
660
|
+
except Exception as e:
|
|
661
|
+
failures.append(f"(n7) running-count styling check failed: {e}")
|
|
662
|
+
|
|
663
|
+
# (a) console clean across the whole run
|
|
664
|
+
time.sleep(1.0)
|
|
665
|
+
real_errors = [e for e in console_errors if "favicon" not in e.lower()]
|
|
666
|
+
if real_errors:
|
|
667
|
+
failures.append(f"(a) console errors: {real_errors[:5]}")
|
|
668
|
+
|
|
669
|
+
browser.close()
|
|
670
|
+
finally:
|
|
671
|
+
server.terminate()
|
|
672
|
+
try:
|
|
673
|
+
server.wait(timeout=5)
|
|
674
|
+
except subprocess.TimeoutExpired:
|
|
675
|
+
server.kill()
|
|
676
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
677
|
+
|
|
678
|
+
if failures:
|
|
679
|
+
print("FAIL:")
|
|
680
|
+
for f in failures:
|
|
681
|
+
print(" -", f)
|
|
682
|
+
return 1
|
|
683
|
+
print("PROVEN: (a) console clean (b) backlog rendered (b2) alert-count alarm color "
|
|
684
|
+
"(b3) alerts-box alarm styling (c) click-expand with prompt (d) SSE live updates (e) expansion survived "
|
|
685
|
+
"(i) tracker lanes rendered (j) tracker add-item SSE (k) orchestrator status (l) audit banner ASCII "
|
|
686
|
+
"(m) tracker pr_link javascript: XSS neutralized, https pr_link still clickable "
|
|
687
|
+
"(n1) header-label contrast #999 (7.5:1 WCAG AA) (n2) priority select chevron affordance "
|
|
688
|
+
"(n3) empty lane placeholder (n4) prefers-reduced-motion animations disabled "
|
|
689
|
+
"(n5) tracker sr-only live region (n6) lane-count aria-labels (n7) running-count toned down")
|
|
690
|
+
return 0
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
if __name__ == "__main__":
|
|
694
|
+
sys.exit(main())
|