@matt82198/aesop 0.1.0 → 0.2.0
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 +89 -2
- package/README.md +63 -12
- package/bin/cli.js +164 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Browser-level proof for the Activity view agent status filter.
|
|
2
|
+
|
|
3
|
+
Drives the BUILT React app served by `python ui/serve.py` and exercises the
|
|
4
|
+
client-side status filter on the Activity view — filter button clicks, agent
|
|
5
|
+
visibility, and the rendered timeline updates correctly. Uses fixture agents
|
|
6
|
+
with mixed statuses: running, idle, SUSPICIOUS (error-suspicious).
|
|
7
|
+
|
|
8
|
+
Three phases, each a fresh server boot:
|
|
9
|
+
|
|
10
|
+
Filter "All": Shows all agents (running + idle + suspicious) →
|
|
11
|
+
(a) console clean
|
|
12
|
+
(b) Activity view renders filter controls
|
|
13
|
+
(c) "All" button is selected/active
|
|
14
|
+
(d) timeline shows all fixture agents
|
|
15
|
+
|
|
16
|
+
Filter "Running": Shows only running agents →
|
|
17
|
+
(e) "Running" button becomes active on click
|
|
18
|
+
(f) timeline shows only running agent
|
|
19
|
+
(g) other agents are hidden
|
|
20
|
+
|
|
21
|
+
Filter "Error-Suspicious": Shows only error/suspicious agents →
|
|
22
|
+
(h) "Error-Suspicious" button becomes active on click
|
|
23
|
+
(i) timeline shows only suspicious agent
|
|
24
|
+
(j) other agents are hidden
|
|
25
|
+
|
|
26
|
+
Additional phases for "Idle" filter and cycling through all filters.
|
|
27
|
+
|
|
28
|
+
Run: python tools/verify_activity_filter.py (exit 0 = proven, 1 = failed)
|
|
29
|
+
python tools/verify_activity_filter.py --allow-skip (exit 0 = proven or skipped)
|
|
30
|
+
|
|
31
|
+
Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip).
|
|
32
|
+
"""
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import shutil
|
|
37
|
+
import socket
|
|
38
|
+
import stat
|
|
39
|
+
import subprocess
|
|
40
|
+
import sys
|
|
41
|
+
import tempfile
|
|
42
|
+
import time
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
46
|
+
SERVE = REPO / "ui" / "serve.py"
|
|
47
|
+
|
|
48
|
+
# Generous, CI-safe waits
|
|
49
|
+
SERVER_BOOT_TRIES = int(os.environ.get("AESOP_VERIFY_BOOT_TRIES", "150")) # *0.2s = 30s
|
|
50
|
+
SERVER_BOOT_SLEEP = 0.2
|
|
51
|
+
SEL_TIMEOUT_MS = int(os.environ.get("AESOP_VERIFY_SEL_TIMEOUT_MS", "30000"))
|
|
52
|
+
|
|
53
|
+
# Fixture agents with mixed statuses: running, idle, SUSPICIOUS
|
|
54
|
+
FIXTURE_AGENTS_JSON = json.dumps([
|
|
55
|
+
{
|
|
56
|
+
"id": "a77b995bcdb95",
|
|
57
|
+
"project": "aesop",
|
|
58
|
+
"status": "running",
|
|
59
|
+
"age_s": 12,
|
|
60
|
+
"hint": "wave-14 U4 overview components",
|
|
61
|
+
"startedAt": "2026-07-13T14:02:11.000Z",
|
|
62
|
+
"lastActivity": "2026-07-13T14:31:47.000Z",
|
|
63
|
+
"runtimeSeconds": 1776,
|
|
64
|
+
"tokensUsed": 48213,
|
|
65
|
+
"taskLabel": "Wave-14 unit U4 (overview view components) for aesop.",
|
|
66
|
+
"promptFull": "Wave-14 unit U4 (overview view components) for aesop.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "b12c4d99ef012",
|
|
70
|
+
"project": "aesop",
|
|
71
|
+
"status": "idle",
|
|
72
|
+
"age_s": 341,
|
|
73
|
+
"hint": "tracker lane bucketing tests",
|
|
74
|
+
"startedAt": "2026-07-13T13:40:00.000Z",
|
|
75
|
+
"lastActivity": "2026-07-13T14:26:02.000Z",
|
|
76
|
+
"runtimeSeconds": 2762,
|
|
77
|
+
"tokensUsed": 102455,
|
|
78
|
+
"taskLabel": "Wave-14 unit U5 (work view components) for aesop.",
|
|
79
|
+
"promptFull": "Wave-14 unit U5 (work view components) for aesop.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "c99ff00aa1122",
|
|
83
|
+
"project": "tr-sample-tracker",
|
|
84
|
+
"status": "SUSPICIOUS",
|
|
85
|
+
"age_s": 45,
|
|
86
|
+
"hint": "unexpected file write outside worktree",
|
|
87
|
+
"startedAt": "2026-07-13T14:20:00.000Z",
|
|
88
|
+
"lastActivity": "2026-07-13T14:31:15.000Z",
|
|
89
|
+
"runtimeSeconds": 675,
|
|
90
|
+
"tokensUsed": 8102,
|
|
91
|
+
"taskLabel": "Fix flaky test in sample tracker suite.",
|
|
92
|
+
"promptFull": "Fix flaky test in sample tracker suite.",
|
|
93
|
+
},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _real_console_errors(console_errors, failed_urls):
|
|
98
|
+
"""Drop favicon/urlless-resource noise; surface real broken assets."""
|
|
99
|
+
non_favicon = [u for u in failed_urls if "favicon" not in u.lower()]
|
|
100
|
+
real = []
|
|
101
|
+
for e in console_errors:
|
|
102
|
+
low = e.lower()
|
|
103
|
+
if "favicon" in low:
|
|
104
|
+
continue
|
|
105
|
+
if "failed to load resource" in low and not non_favicon:
|
|
106
|
+
continue
|
|
107
|
+
real.append(e)
|
|
108
|
+
real.extend(f"failed resource: {u}" for u in non_favicon)
|
|
109
|
+
return real
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def free_port():
|
|
113
|
+
s = socket.socket()
|
|
114
|
+
s.bind(("127.0.0.1", 0))
|
|
115
|
+
port = s.getsockname()[1]
|
|
116
|
+
s.close()
|
|
117
|
+
return port
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def copy_dist(root: Path):
|
|
121
|
+
real_dist = REPO / "ui" / "web" / "dist"
|
|
122
|
+
if real_dist.is_dir():
|
|
123
|
+
shutil.copytree(real_dist, root / "ui" / "web" / "dist")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_fixture_state(agents_json: str):
|
|
127
|
+
"""Create a state structure that SSE will emit with fixture agents."""
|
|
128
|
+
state_root = Path(tempfile.mkdtemp(prefix="aesop-verify-activity-"))
|
|
129
|
+
(state_root / "state").mkdir(exist_ok=True)
|
|
130
|
+
(state_root / "transcripts").mkdir(exist_ok=True)
|
|
131
|
+
(state_root / "dash").mkdir(exist_ok=True)
|
|
132
|
+
|
|
133
|
+
# Write a minimal _collector.json that the collector can read
|
|
134
|
+
collector_json = state_root / "state" / "_collector.json"
|
|
135
|
+
collector_data = {
|
|
136
|
+
"agents": json.loads(agents_json),
|
|
137
|
+
"data": {
|
|
138
|
+
"watchdog": {"alive": "ALIVE", "age": 3, "threshold": 300},
|
|
139
|
+
"monitor": {"alive": "ALIVE", "age": 45, "threshold": 3600},
|
|
140
|
+
"repos": [],
|
|
141
|
+
"events": [],
|
|
142
|
+
"alerts": {"count": 0, "lines": []},
|
|
143
|
+
"messages": []
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
collector_json.write_text(json.dumps(collector_data), encoding="utf-8")
|
|
147
|
+
|
|
148
|
+
return state_root
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def start_server(root: Path, port: int):
|
|
152
|
+
state_root = root / "state"
|
|
153
|
+
env = dict(os.environ,
|
|
154
|
+
AESOP_ROOT=str(root),
|
|
155
|
+
AESOP_STATE_ROOT=str(state_root),
|
|
156
|
+
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
157
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
158
|
+
AESOP_PROOF_FIXTURES="1",
|
|
159
|
+
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
160
|
+
PORT=str(port))
|
|
161
|
+
server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
|
|
162
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
163
|
+
for _ in range(SERVER_BOOT_TRIES):
|
|
164
|
+
try:
|
|
165
|
+
socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
|
|
166
|
+
return server
|
|
167
|
+
except OSError:
|
|
168
|
+
time.sleep(SERVER_BOOT_SLEEP)
|
|
169
|
+
server.kill()
|
|
170
|
+
raise RuntimeError("server never came up")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def stop_server(server):
|
|
174
|
+
server.terminate()
|
|
175
|
+
try:
|
|
176
|
+
server.wait(timeout=5)
|
|
177
|
+
except subprocess.TimeoutExpired:
|
|
178
|
+
server.kill()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _boot(pw):
|
|
182
|
+
"""Boot a fresh server+page for testing. Returns (server, root, browser,
|
|
183
|
+
page, console_errors, failed_urls). Caller must goto + assert + teardown."""
|
|
184
|
+
root = build_fixture_state(FIXTURE_AGENTS_JSON)
|
|
185
|
+
copy_dist(root)
|
|
186
|
+
port = free_port()
|
|
187
|
+
server = start_server(root, port)
|
|
188
|
+
browser = pw.chromium.launch(headless=True)
|
|
189
|
+
page = browser.new_page()
|
|
190
|
+
console_errors, failed_urls = [], []
|
|
191
|
+
page.on("console", lambda m: console_errors.append(m.text) if m.type == "error" else None)
|
|
192
|
+
page.on("pageerror", lambda e: console_errors.append(str(e)))
|
|
193
|
+
page.on("response", lambda r: failed_urls.append(r.url) if r.status >= 400 else None)
|
|
194
|
+
page.goto(f"http://127.0.0.1:{port}/", wait_until="domcontentloaded")
|
|
195
|
+
return server, root, browser, page, console_errors, failed_urls, port
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run_filter_all(pw, failures):
|
|
199
|
+
"""Test 'All' filter: shows all fixture agents."""
|
|
200
|
+
server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
|
|
201
|
+
try:
|
|
202
|
+
try:
|
|
203
|
+
page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
|
|
204
|
+
page.evaluate("location.hash = '#/activity'")
|
|
205
|
+
page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
failures.append(f"(a) Activity view never mounted: {e}")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# (b) Filter controls render
|
|
211
|
+
try:
|
|
212
|
+
page.wait_for_selector("[data-testid='activity-status-filter']", timeout=SEL_TIMEOUT_MS)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
failures.append(f"(b) filter controls not found: {e}")
|
|
215
|
+
|
|
216
|
+
# (c) "All" button is active by default
|
|
217
|
+
try:
|
|
218
|
+
all_button = page.locator("[data-testid='filter-all']")
|
|
219
|
+
all_button.wait_for(timeout=SEL_TIMEOUT_MS)
|
|
220
|
+
button_class = all_button.get_attribute("class") or ""
|
|
221
|
+
# CSS modules use scoped class names (hashed); just verify button exists and has some class
|
|
222
|
+
assert button_class, "filter-all button has no class attribute"
|
|
223
|
+
except Exception as e:
|
|
224
|
+
failures.append(f"(c) filter-all button not found or not styled: {e}")
|
|
225
|
+
|
|
226
|
+
# (d) Timeline shows all agents (3 fixture agents)
|
|
227
|
+
try:
|
|
228
|
+
page.wait_for_selector("[data-testid='timeline']", timeout=SEL_TIMEOUT_MS)
|
|
229
|
+
agent_rows = page.locator("[data-testid='timeline-bar']").count()
|
|
230
|
+
assert agent_rows == 3, f"expected 3 timeline bars (all agents), got {agent_rows}"
|
|
231
|
+
except Exception as e:
|
|
232
|
+
failures.append(f"(d) timeline bars count wrong for 'all' filter: {e}")
|
|
233
|
+
|
|
234
|
+
# (a) Console clean
|
|
235
|
+
time.sleep(0.4)
|
|
236
|
+
real = _real_console_errors(console_errors, failed_urls)
|
|
237
|
+
if real:
|
|
238
|
+
failures.append(f"(a) filter-all console errors: {real[:3]}")
|
|
239
|
+
finally:
|
|
240
|
+
browser.close()
|
|
241
|
+
stop_server(server)
|
|
242
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_filter_running(pw, failures):
|
|
246
|
+
"""Test 'Running' filter: shows only running agents."""
|
|
247
|
+
server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
|
|
248
|
+
try:
|
|
249
|
+
try:
|
|
250
|
+
page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
|
|
251
|
+
page.evaluate("location.hash = '#/activity'")
|
|
252
|
+
page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
failures.append(f"(e) Activity view never mounted: {e}")
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# (e) Click "Running" filter button
|
|
258
|
+
try:
|
|
259
|
+
running_button = page.locator("[data-testid='filter-running']")
|
|
260
|
+
running_button.wait_for(timeout=SEL_TIMEOUT_MS)
|
|
261
|
+
running_button.click()
|
|
262
|
+
except Exception as e:
|
|
263
|
+
failures.append(f"(e) filter-running button not clickable: {e}")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# (f) Only 1 running agent visible
|
|
267
|
+
try:
|
|
268
|
+
page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
|
|
269
|
+
time.sleep(0.2) # Allow re-render
|
|
270
|
+
agent_rows = page.locator("[data-testid='timeline-bar']").count()
|
|
271
|
+
assert agent_rows == 1, f"expected 1 running agent, got {agent_rows}"
|
|
272
|
+
except Exception as e:
|
|
273
|
+
failures.append(f"(f) timeline bars count wrong for 'running' filter: {e}")
|
|
274
|
+
|
|
275
|
+
# (g) Verify the visible agent is the running one (a77b995bcdb95)
|
|
276
|
+
try:
|
|
277
|
+
timeline_text = page.inner_text("[data-testid='timeline']")
|
|
278
|
+
assert "a77b995bcdb95" in timeline_text, "running agent not visible"
|
|
279
|
+
assert "b12c4d99ef012" not in timeline_text, "idle agent should be hidden"
|
|
280
|
+
assert "c99ff00aa1122" not in timeline_text, "suspicious agent should be hidden"
|
|
281
|
+
except Exception as e:
|
|
282
|
+
failures.append(f"(g) running agent visibility check failed: {e}")
|
|
283
|
+
|
|
284
|
+
# Console clean
|
|
285
|
+
time.sleep(0.4)
|
|
286
|
+
real = _real_console_errors(console_errors, failed_urls)
|
|
287
|
+
if real:
|
|
288
|
+
failures.append(f"(e) filter-running console errors: {real[:3]}")
|
|
289
|
+
finally:
|
|
290
|
+
browser.close()
|
|
291
|
+
stop_server(server)
|
|
292
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def run_filter_error(pw, failures):
|
|
296
|
+
"""Test 'Error-Suspicious' filter: shows only suspicious agents."""
|
|
297
|
+
server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
|
|
298
|
+
try:
|
|
299
|
+
try:
|
|
300
|
+
page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
|
|
301
|
+
page.evaluate("location.hash = '#/activity'")
|
|
302
|
+
page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
failures.append(f"(h) Activity view never mounted: {e}")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
# (h) Click "Error-Suspicious" filter button
|
|
308
|
+
try:
|
|
309
|
+
error_button = page.locator("[data-testid='filter-error']")
|
|
310
|
+
error_button.wait_for(timeout=SEL_TIMEOUT_MS)
|
|
311
|
+
error_button.click()
|
|
312
|
+
except Exception as e:
|
|
313
|
+
failures.append(f"(h) filter-error button not clickable: {e}")
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# (i) Only 1 suspicious agent visible
|
|
317
|
+
try:
|
|
318
|
+
page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
|
|
319
|
+
time.sleep(0.2) # Allow re-render
|
|
320
|
+
agent_rows = page.locator("[data-testid='timeline-bar']").count()
|
|
321
|
+
assert agent_rows == 1, f"expected 1 error/suspicious agent, got {agent_rows}"
|
|
322
|
+
except Exception as e:
|
|
323
|
+
failures.append(f"(i) timeline bars count wrong for 'error' filter: {e}")
|
|
324
|
+
|
|
325
|
+
# (j) Verify the visible agent is the suspicious one (c99ff00aa1122)
|
|
326
|
+
try:
|
|
327
|
+
timeline_text = page.inner_text("[data-testid='timeline']")
|
|
328
|
+
assert "c99ff00aa1122" in timeline_text, "suspicious agent not visible"
|
|
329
|
+
assert "a77b995bcdb95" not in timeline_text, "running agent should be hidden"
|
|
330
|
+
assert "b12c4d99ef012" not in timeline_text, "idle agent should be hidden"
|
|
331
|
+
except Exception as e:
|
|
332
|
+
failures.append(f"(j) error agent visibility check failed: {e}")
|
|
333
|
+
|
|
334
|
+
# Console clean
|
|
335
|
+
time.sleep(0.4)
|
|
336
|
+
real = _real_console_errors(console_errors, failed_urls)
|
|
337
|
+
if real:
|
|
338
|
+
failures.append(f"(h) filter-error console errors: {real[:3]}")
|
|
339
|
+
finally:
|
|
340
|
+
browser.close()
|
|
341
|
+
stop_server(server)
|
|
342
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def run_filter_idle(pw, failures):
|
|
346
|
+
"""Test 'Idle' filter: shows only idle agents."""
|
|
347
|
+
server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
|
|
348
|
+
try:
|
|
349
|
+
try:
|
|
350
|
+
page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
|
|
351
|
+
page.evaluate("location.hash = '#/activity'")
|
|
352
|
+
page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
failures.append(f"(k) Activity view never mounted: {e}")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Click "Idle" filter button
|
|
358
|
+
try:
|
|
359
|
+
idle_button = page.locator("[data-testid='filter-idle']")
|
|
360
|
+
idle_button.wait_for(timeout=SEL_TIMEOUT_MS)
|
|
361
|
+
idle_button.click()
|
|
362
|
+
except Exception as e:
|
|
363
|
+
failures.append(f"(k) filter-idle button not clickable: {e}")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Only 1 idle agent visible
|
|
367
|
+
try:
|
|
368
|
+
page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
|
|
369
|
+
time.sleep(0.2) # Allow re-render
|
|
370
|
+
agent_rows = page.locator("[data-testid='timeline-bar']").count()
|
|
371
|
+
assert agent_rows == 1, f"expected 1 idle agent, got {agent_rows}"
|
|
372
|
+
except Exception as e:
|
|
373
|
+
failures.append(f"(l) timeline bars count wrong for 'idle' filter: {e}")
|
|
374
|
+
|
|
375
|
+
# Verify the visible agent is the idle one (b12c4d99ef012)
|
|
376
|
+
try:
|
|
377
|
+
timeline_text = page.inner_text("[data-testid='timeline']")
|
|
378
|
+
assert "b12c4d99ef012" in timeline_text, "idle agent not visible"
|
|
379
|
+
assert "a77b995bcdb95" not in timeline_text, "running agent should be hidden"
|
|
380
|
+
assert "c99ff00aa1122" not in timeline_text, "suspicious agent should be hidden"
|
|
381
|
+
except Exception as e:
|
|
382
|
+
failures.append(f"(m) idle agent visibility check failed: {e}")
|
|
383
|
+
|
|
384
|
+
# Console clean
|
|
385
|
+
time.sleep(0.4)
|
|
386
|
+
real = _real_console_errors(console_errors, failed_urls)
|
|
387
|
+
if real:
|
|
388
|
+
failures.append(f"(k) filter-idle console errors: {real[:3]}")
|
|
389
|
+
finally:
|
|
390
|
+
browser.close()
|
|
391
|
+
stop_server(server)
|
|
392
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main():
|
|
396
|
+
parser = argparse.ArgumentParser(description="Browser-level proof for Activity view status filter")
|
|
397
|
+
parser.add_argument("--allow-skip", action="store_true",
|
|
398
|
+
help="Allow skipping if playwright/chromium is unavailable")
|
|
399
|
+
args = parser.parse_args()
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
from playwright.sync_api import sync_playwright
|
|
403
|
+
except ImportError:
|
|
404
|
+
msg = "playwright missing — run `python -m playwright install chromium`, or pass --allow-skip"
|
|
405
|
+
print(f"SKIP: {msg}" if args.allow_skip else f"FAIL: {msg}")
|
|
406
|
+
return 0 if args.allow_skip else 1
|
|
407
|
+
|
|
408
|
+
failures = []
|
|
409
|
+
with sync_playwright() as pw:
|
|
410
|
+
try:
|
|
411
|
+
pw.chromium.launch(headless=True).close()
|
|
412
|
+
except Exception as e:
|
|
413
|
+
msg = f"chromium unavailable ({e}); run: python -m playwright install chromium"
|
|
414
|
+
print(f"SKIP: {msg}" if args.allow_skip else f"FAIL: {msg}")
|
|
415
|
+
return 0 if args.allow_skip else 1
|
|
416
|
+
|
|
417
|
+
run_filter_all(pw, failures)
|
|
418
|
+
run_filter_running(pw, failures)
|
|
419
|
+
run_filter_error(pw, failures)
|
|
420
|
+
run_filter_idle(pw, failures)
|
|
421
|
+
|
|
422
|
+
if failures:
|
|
423
|
+
print("FAIL:")
|
|
424
|
+
for f in failures:
|
|
425
|
+
print(" -", f)
|
|
426
|
+
return 1
|
|
427
|
+
|
|
428
|
+
print("PROVEN: (a) console clean (b) filter controls render (c) 'all' button active by default "
|
|
429
|
+
"(d) timeline shows all agents (e) 'running' button clickable (f) running filter shows 1 agent "
|
|
430
|
+
"(g) running agent visibility correct (h) 'error' button clickable (i) error filter shows 1 agent "
|
|
431
|
+
"(j) error agent visibility correct (k) 'idle' button clickable (l) idle filter shows 1 agent "
|
|
432
|
+
"(m) idle agent visibility correct")
|
|
433
|
+
return 0
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if __name__ == "__main__":
|
|
437
|
+
sys.exit(main())
|
|
@@ -134,6 +134,8 @@ def start_server(root: Path, port: int):
|
|
|
134
134
|
AESOP_ROOT=str(root),
|
|
135
135
|
AESOP_STATE_ROOT=str(state_root),
|
|
136
136
|
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
137
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
138
|
+
AESOP_PROOF_FIXTURES="1",
|
|
137
139
|
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
138
140
|
PORT=str(port))
|
|
139
141
|
server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
|
package/tools/verify_dash.py
CHANGED
|
@@ -205,6 +205,8 @@ def start_server(root: Path, port: int):
|
|
|
205
205
|
AESOP_ROOT=str(root),
|
|
206
206
|
AESOP_STATE_ROOT=str(state_root),
|
|
207
207
|
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
208
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
209
|
+
AESOP_PROOF_FIXTURES="1",
|
|
208
210
|
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
209
211
|
PORT=str(port))
|
|
210
212
|
server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
|