@matt82198/aesop 0.1.0 → 0.3.1
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 +117 -2
- package/README.md +59 -218
- package/bin/cli.js +168 -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/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +74 -4
- package/docs/PORTING.md +166 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +372 -0
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +150 -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 +673 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +179 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +254 -0
- package/driver/wave_loop.py +1408 -0
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +131 -33
- package/mcp/server.mjs +320 -4
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +22 -14
- 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 +4 -2
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/store.py +185 -73
- package/state_store/write_api.py +462 -0
- 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 +166 -43
- package/tools/cost_econ.py +480 -0
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +188 -26
- 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 +523 -0
- package/tools/portability_check.py +206 -0
- package/tools/proposals.mjs +47 -2
- package/tools/reconcile.py +7 -4
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +207 -65
- package/tools/self_stats.py +20 -0
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -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_cost_panel.py +345 -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 +779 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +340 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +81 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +383 -55
- package/ui/quality_scorecard.py +232 -0
- package/ui/sse.py +3 -3
- 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 +383 -0
- package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -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,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""End-to-end verification of the wave failure drill-down feature.
|
|
3
|
+
|
|
4
|
+
Sets up a temporary AESOP_ROOT, stubs the gh CLI, starts the dashboard server,
|
|
5
|
+
and exercises the failure drill-down UI + API via Playwright. Verifies:
|
|
6
|
+
|
|
7
|
+
1. GET /api/wave/failure?pr=N endpoint returns correct shape
|
|
8
|
+
2. FailureDrilldown component toggles expand/collapse
|
|
9
|
+
3. Failure details render when expanded
|
|
10
|
+
4. Graceful degradation when gh is unavailable
|
|
11
|
+
|
|
12
|
+
Run: python tools/verify_failure_drilldown.py
|
|
13
|
+
(or supply --port=<port> for a custom port)
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from playwright.sync_api import sync_playwright, expect
|
|
28
|
+
except ImportError: # import-clean without playwright; main() reports the miss
|
|
29
|
+
sync_playwright = None
|
|
30
|
+
expect = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Paths
|
|
34
|
+
REPO_ROOT = Path(__file__).parent.parent
|
|
35
|
+
UI_PATH = REPO_ROOT / "ui"
|
|
36
|
+
SERVE_PATH = UI_PATH / "serve.py"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_server(fixture_root, port, extra_env=None):
|
|
40
|
+
"""Start the dashboard server in a background thread.
|
|
41
|
+
|
|
42
|
+
Returns: (server_process, cleanup_callback)
|
|
43
|
+
"""
|
|
44
|
+
env = os.environ.copy()
|
|
45
|
+
env["AESOP_ROOT"] = str(fixture_root)
|
|
46
|
+
env["AESOP_STATE_ROOT"] = str(fixture_root / "state")
|
|
47
|
+
# Point to the real repo's built dist (not the fixture root)
|
|
48
|
+
env["AESOP_WEB_DIST"] = str(REPO_ROOT / "ui" / "web" / "dist")
|
|
49
|
+
env["AESOP_PROOF_FIXTURES"] = "1"
|
|
50
|
+
env["PORT"] = str(port)
|
|
51
|
+
env["AESOP_UI_COLLECT_INTERVAL"] = "0.1"
|
|
52
|
+
if extra_env:
|
|
53
|
+
env.update(extra_env)
|
|
54
|
+
|
|
55
|
+
proc = subprocess.Popen(
|
|
56
|
+
[sys.executable, str(SERVE_PATH)],
|
|
57
|
+
env=env,
|
|
58
|
+
cwd=str(UI_PATH),
|
|
59
|
+
stdout=subprocess.PIPE,
|
|
60
|
+
stderr=subprocess.PIPE,
|
|
61
|
+
text=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Wait for server to be ready (crude: sleep and probe)
|
|
65
|
+
for _ in range(30):
|
|
66
|
+
try:
|
|
67
|
+
import http.client
|
|
68
|
+
con = http.client.HTTPConnection("127.0.0.1", port, timeout=1)
|
|
69
|
+
con.request("GET", "/")
|
|
70
|
+
con.close()
|
|
71
|
+
return proc, lambda: proc.terminate()
|
|
72
|
+
except Exception:
|
|
73
|
+
time.sleep(0.1)
|
|
74
|
+
|
|
75
|
+
raise RuntimeError(f"Server failed to start on port {port}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def stub_gh(fixture_root):
|
|
79
|
+
"""Create a stub gh CLI that always fails gracefully.
|
|
80
|
+
|
|
81
|
+
Returns: path to the stub script.
|
|
82
|
+
"""
|
|
83
|
+
stub = fixture_root / "stub-gh"
|
|
84
|
+
stub.write_text(
|
|
85
|
+
"#!/bin/bash\n"
|
|
86
|
+
"echo 'stubbed gh: not authenticated' >&2\n"
|
|
87
|
+
"exit 1\n",
|
|
88
|
+
encoding="utf-8"
|
|
89
|
+
)
|
|
90
|
+
stub.chmod(0o755)
|
|
91
|
+
return stub
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
if sync_playwright is None:
|
|
96
|
+
allow_skip = "--allow-skip" in sys.argv
|
|
97
|
+
msg = "playwright missing — run `python -m playwright install chromium`, or pass --allow-skip"
|
|
98
|
+
print(f"SKIP: {msg}" if allow_skip else f"FAIL: {msg}")
|
|
99
|
+
sys.exit(0 if allow_skip else 1)
|
|
100
|
+
parser = argparse.ArgumentParser(description="Verify wave failure drill-down feature")
|
|
101
|
+
parser.add_argument("--port", type=int, default=0, help="Dashboard port (default: auto)")
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
fixture_root = Path(tempfile.mkdtemp(prefix="aesop-verify-failure-"))
|
|
105
|
+
port = args.port or 8771 # Use 8771 by default to avoid conflicts
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
print(f"[setup] fixture_root={fixture_root}")
|
|
109
|
+
(fixture_root / "state").mkdir(parents=True)
|
|
110
|
+
(fixture_root / "transcripts").mkdir()
|
|
111
|
+
|
|
112
|
+
stub_gh_path = stub_gh(fixture_root)
|
|
113
|
+
print(f"[setup] stub_gh={stub_gh_path}")
|
|
114
|
+
|
|
115
|
+
# Start server
|
|
116
|
+
extra_env = {"AESOP_GH_BIN": str(stub_gh_path)}
|
|
117
|
+
proc, cleanup = run_server(fixture_root, port, extra_env)
|
|
118
|
+
print(f"[server] started on port {port}")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Run Playwright tests
|
|
122
|
+
with sync_playwright() as p:
|
|
123
|
+
browser = p.chromium.launch(headless=True)
|
|
124
|
+
page = browser.new_page()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
# Test 1: GET /api/wave/failure endpoint
|
|
128
|
+
print("[test] GET /api/wave/failure?pr=123")
|
|
129
|
+
response = page.request.get(f"http://127.0.0.1:{port}/api/wave/failure?pr=123")
|
|
130
|
+
assert response.status == 200, f"Expected 200, got {response.status}"
|
|
131
|
+
body = json.loads(response.text())
|
|
132
|
+
assert "available" in body
|
|
133
|
+
assert "pr_number" in body
|
|
134
|
+
assert body["pr_number"] == 123
|
|
135
|
+
print(" ✓ Endpoint returns correct shape")
|
|
136
|
+
|
|
137
|
+
# Test 2: Navigate to dashboard (just to verify it loads)
|
|
138
|
+
print("[test] Dashboard loads")
|
|
139
|
+
page.goto(f"http://127.0.0.1:{port}/")
|
|
140
|
+
# Use domcontentloaded instead of networkidle because SSE keeps
|
|
141
|
+
# connections open indefinitely, preventing networkidle from firing
|
|
142
|
+
page.wait_for_load_state("domcontentloaded", timeout=5000)
|
|
143
|
+
title = page.title()
|
|
144
|
+
assert title, "Page should have a title"
|
|
145
|
+
print(f" ✓ Dashboard loaded: {title}")
|
|
146
|
+
|
|
147
|
+
# Test 3: Test 400 on missing ?pr=
|
|
148
|
+
print("[test] GET /api/wave/failure without ?pr= returns 400")
|
|
149
|
+
response = page.request.get(f"http://127.0.0.1:{port}/api/wave/failure")
|
|
150
|
+
assert response.status == 400, f"Expected 400, got {response.status}"
|
|
151
|
+
print(" ✓ Missing ?pr= rejected")
|
|
152
|
+
|
|
153
|
+
# Test 4: Test 400 on invalid ?pr=
|
|
154
|
+
print("[test] GET /api/wave/failure?pr=invalid returns 400")
|
|
155
|
+
response = page.request.get(f"http://127.0.0.1:{port}/api/wave/failure?pr=notanumber")
|
|
156
|
+
assert response.status == 400, f"Expected 400, got {response.status}"
|
|
157
|
+
print(" ✓ Invalid ?pr= rejected")
|
|
158
|
+
|
|
159
|
+
# Test 5: Verify no-cache headers
|
|
160
|
+
print("[test] Verify no-cache headers")
|
|
161
|
+
response = page.request.get(f"http://127.0.0.1:{port}/api/wave/failure?pr=456")
|
|
162
|
+
cache_control = response.headers.get("cache-control", "")
|
|
163
|
+
assert "no-cache" in cache_control, f"Expected no-cache in {cache_control}"
|
|
164
|
+
print(" ✓ No-cache headers present")
|
|
165
|
+
|
|
166
|
+
print("\n[success] All verification tests passed!")
|
|
167
|
+
|
|
168
|
+
finally:
|
|
169
|
+
browser.close()
|
|
170
|
+
|
|
171
|
+
finally:
|
|
172
|
+
cleanup()
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"\n[error] {e}", file=sys.stderr)
|
|
176
|
+
import traceback
|
|
177
|
+
traceback.print_exc()
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
finally:
|
|
181
|
+
shutil.rmtree(fixture_root, ignore_errors=True)
|
|
182
|
+
print(f"[cleanup] removed {fixture_root}")
|
|
183
|
+
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
package/tools/verify_prboard.py
CHANGED
|
@@ -159,6 +159,8 @@ def start_server(root: Path, port: int, gh_path: Path):
|
|
|
159
159
|
AESOP_ROOT=str(root),
|
|
160
160
|
AESOP_STATE_ROOT=str(state_root),
|
|
161
161
|
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
162
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
163
|
+
AESOP_PROOF_FIXTURES="1",
|
|
162
164
|
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
163
165
|
AESOP_GH_BIN=str(gh_path),
|
|
164
166
|
PORT=str(port))
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Browser proof for the wave quality scorecards component.
|
|
3
|
+
|
|
4
|
+
Drives ui/web/dist/ against fixture ledger state, asserting the contract
|
|
5
|
+
via data-testid hooks (never CSS internals):
|
|
6
|
+
|
|
7
|
+
Populated-state phase:
|
|
8
|
+
(a) console clean of errors
|
|
9
|
+
(b) GET /api/wave/quality-scorecards returns properly-shaped payload
|
|
10
|
+
(c) quality-scorecards testid present and rendered
|
|
11
|
+
(d) table with specialties and stats visible
|
|
12
|
+
(e) success rate cells present (formatted as percentages)
|
|
13
|
+
(f) top by success ranking rendered
|
|
14
|
+
(g) top by retry ranking rendered
|
|
15
|
+
(h) repair counts displayed
|
|
16
|
+
(i) skipped lines footnote omitted when count is 0
|
|
17
|
+
|
|
18
|
+
Empty-state phase (separate boot, empty ledger):
|
|
19
|
+
(j) quality-scorecards renders empty state with clear message
|
|
20
|
+
|
|
21
|
+
Run: python tools/verify_scorecards.py (exit 0 = proven, 1 = failed)
|
|
22
|
+
python tools/verify_scorecards.py --allow-skip (exit 0 = proven or skipped, 1 = failed)
|
|
23
|
+
|
|
24
|
+
Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip is passed).
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import shutil
|
|
30
|
+
import socket
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
38
|
+
SERVE = REPO / "ui" / "serve.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
FIXTURE_LEDGER_POPULATED = """| timestamp | agent_type | model | duration_seconds | tokens_in | tokens_out | verdict |
|
|
42
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
43
|
+
| 2026-07-13T14:00:00Z | haiku | claude-haiku-4-5-20251001 | 45 | 12000 | 3500 | OK |
|
|
44
|
+
| 2026-07-13T14:05:00Z | haiku | claude-haiku-4-5-20251001 | 50 | 14000 | 4200 | OK |
|
|
45
|
+
| 2026-07-13T14:10:00Z | haiku | claude-haiku-4-5-20251001 | 40 | 11000 | 3200 | FAILED |
|
|
46
|
+
| 2026-07-13T14:15:00Z | haiku | claude-haiku-4-5-20251001 | 55 | 13500 | 4000 | OK |
|
|
47
|
+
| 2026-07-13T14:20:00Z | sonnet | claude-sonnet-4-5-20250929 | 85 | 28000 | 8100 | OK |
|
|
48
|
+
| 2026-07-13T14:25:00Z | sonnet | claude-sonnet-4-5-20250929 | 90 | 30000 | 9000 | OK |
|
|
49
|
+
| 2026-07-13T14:30:00Z | orchestrator | claude-opus-4-20250805 | 120 | 50000 | 12000 | OK |
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
FIXTURE_LEDGER_EMPTY = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def find_free_port():
|
|
56
|
+
"""Find an available port for the test server."""
|
|
57
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
58
|
+
s.bind(('127.0.0.1', 0))
|
|
59
|
+
s.listen(1)
|
|
60
|
+
port = s.getsockname()[1]
|
|
61
|
+
return port
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def wait_for_server(port, timeout=30):
|
|
65
|
+
"""Wait for the server to be ready."""
|
|
66
|
+
start = time.time()
|
|
67
|
+
while time.time() - start < timeout:
|
|
68
|
+
try:
|
|
69
|
+
with socket.create_connection(('127.0.0.1', port), timeout=1):
|
|
70
|
+
return True
|
|
71
|
+
except (ConnectionRefusedError, OSError):
|
|
72
|
+
time.sleep(0.5)
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_playwright_test(port, test_name, state_dir):
|
|
77
|
+
"""Run a Playwright test against the server.
|
|
78
|
+
|
|
79
|
+
Uses a minimal inline Playwright script (no external test framework
|
|
80
|
+
needed) to validate the scorecard component.
|
|
81
|
+
"""
|
|
82
|
+
test_script = f'''
|
|
83
|
+
import asyncio
|
|
84
|
+
from playwright.async_api import async_playwright
|
|
85
|
+
|
|
86
|
+
async def test():
|
|
87
|
+
async with async_playwright() as p:
|
|
88
|
+
browser = await p.chromium.launch()
|
|
89
|
+
context = await browser.new_context()
|
|
90
|
+
page = await context.new_page()
|
|
91
|
+
|
|
92
|
+
# Capture console messages
|
|
93
|
+
console_errors = []
|
|
94
|
+
def on_console(msg):
|
|
95
|
+
if msg.type in ('error', 'warn'):
|
|
96
|
+
console_errors.append(f"{{msg.type}}: {{msg.text}}")
|
|
97
|
+
|
|
98
|
+
page.on('console', on_console)
|
|
99
|
+
|
|
100
|
+
# Load the dashboard
|
|
101
|
+
await page.goto('http://127.0.0.1:{port}/', wait_until='domcontentloaded')
|
|
102
|
+
|
|
103
|
+
# {test_name}
|
|
104
|
+
try:
|
|
105
|
+
# Test: GET /api/wave/quality-scorecards returns valid data
|
|
106
|
+
response = await page.goto('http://127.0.0.1:{port}/api/wave/quality-scorecards')
|
|
107
|
+
assert response.status == 200, f"Expected 200, got {{response.status}}"
|
|
108
|
+
json_data = await response.json()
|
|
109
|
+
assert 'specialties' in json_data, "Missing 'specialties' in response"
|
|
110
|
+
assert 'top_by_success' in json_data, "Missing 'top_by_success' in response"
|
|
111
|
+
assert 'top_by_retry' in json_data, "Missing 'top_by_retry' in response"
|
|
112
|
+
assert 'skipped_lines' in json_data, "Missing 'skipped_lines' in response"
|
|
113
|
+
|
|
114
|
+
# For populated state: check that data is present
|
|
115
|
+
if "{test_name}" == "populated":
|
|
116
|
+
assert len(json_data['specialties']) > 0, "Expected populated specialties"
|
|
117
|
+
# Check haiku exists
|
|
118
|
+
assert 'haiku' in json_data['specialties'], "Missing haiku specialty"
|
|
119
|
+
haiku = json_data['specialties']['haiku']
|
|
120
|
+
assert haiku['total_runs'] > 0, "Haiku should have runs"
|
|
121
|
+
assert 'success_rate' in haiku, "Missing success_rate"
|
|
122
|
+
assert 'retry_frequency' in haiku, "Missing retry_frequency"
|
|
123
|
+
|
|
124
|
+
# For empty state: check empty data
|
|
125
|
+
elif "{test_name}" == "empty":
|
|
126
|
+
assert len(json_data['specialties']) == 0, "Expected empty specialties"
|
|
127
|
+
assert len(json_data['top_by_success']) == 0, "Expected empty rankings"
|
|
128
|
+
|
|
129
|
+
print("✓ Test passed: {test_name}")
|
|
130
|
+
except AssertionError as e:
|
|
131
|
+
print(f"✗ Test failed: {{e}}")
|
|
132
|
+
raise
|
|
133
|
+
finally:
|
|
134
|
+
await browser.close()
|
|
135
|
+
|
|
136
|
+
asyncio.run(test())
|
|
137
|
+
'''
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
[sys.executable, '-c', test_script],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
timeout=30,
|
|
144
|
+
env={**os.environ, 'AESOP_STATE_ROOT': str(state_dir)}
|
|
145
|
+
)
|
|
146
|
+
print(result.stdout)
|
|
147
|
+
if result.stderr and 'warning' not in result.stderr.lower():
|
|
148
|
+
print(result.stderr, file=sys.stderr)
|
|
149
|
+
return result.returncode == 0
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
print(f"✗ Test timed out: {test_name}", file=sys.stderr)
|
|
152
|
+
return False
|
|
153
|
+
except Exception as e:
|
|
154
|
+
print(f"✗ Test exception: {e}", file=sys.stderr)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
"""Run the verify_scorecards proof."""
|
|
160
|
+
parser = argparse.ArgumentParser(
|
|
161
|
+
description='Browser proof for wave quality scorecards'
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument('--allow-skip', action='store_true',
|
|
164
|
+
help='Exit 0 if playwright is unavailable')
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
|
|
167
|
+
# Check if playwright is installed
|
|
168
|
+
try:
|
|
169
|
+
import playwright # noqa: F401
|
|
170
|
+
except ImportError:
|
|
171
|
+
if args.allow_skip:
|
|
172
|
+
print("⊘ Playwright not available (skipped per --allow-skip)")
|
|
173
|
+
return 0
|
|
174
|
+
print("✗ Playwright not installed; run: pip install playwright && playwright install",
|
|
175
|
+
file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
all_passed = True
|
|
179
|
+
|
|
180
|
+
# Test 1: Populated state
|
|
181
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
182
|
+
state_dir = Path(tmpdir) / 'state'
|
|
183
|
+
state_dir.mkdir(parents=True)
|
|
184
|
+
ledger_dir = state_dir / 'ledger'
|
|
185
|
+
ledger_dir.mkdir(parents=True)
|
|
186
|
+
(ledger_dir / 'OUTCOMES-LEDGER.md').write_text(FIXTURE_LEDGER_POPULATED)
|
|
187
|
+
|
|
188
|
+
# Create fixture directories
|
|
189
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
190
|
+
fixtures_dir.mkdir(parents=True)
|
|
191
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
192
|
+
transcripts_dir.mkdir(parents=True)
|
|
193
|
+
|
|
194
|
+
# Find free port for this phase
|
|
195
|
+
port = find_free_port()
|
|
196
|
+
|
|
197
|
+
# Start server
|
|
198
|
+
env = os.environ.copy()
|
|
199
|
+
env['PORT'] = str(port)
|
|
200
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
201
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
202
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
203
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
204
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
205
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
206
|
+
|
|
207
|
+
proc = subprocess.Popen(
|
|
208
|
+
[sys.executable, str(SERVE)],
|
|
209
|
+
env=env,
|
|
210
|
+
stdout=subprocess.PIPE,
|
|
211
|
+
stderr=subprocess.PIPE,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if not wait_for_server(port):
|
|
216
|
+
print("✗ Server failed to start", file=sys.stderr)
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
# Run test
|
|
220
|
+
if not run_playwright_test(port, "populated", state_dir):
|
|
221
|
+
all_passed = False
|
|
222
|
+
finally:
|
|
223
|
+
proc.terminate()
|
|
224
|
+
try:
|
|
225
|
+
proc.wait(timeout=5)
|
|
226
|
+
except subprocess.TimeoutExpired:
|
|
227
|
+
proc.kill()
|
|
228
|
+
|
|
229
|
+
# Test 2: Empty state
|
|
230
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
231
|
+
state_dir = Path(tmpdir) / 'state'
|
|
232
|
+
state_dir.mkdir(parents=True)
|
|
233
|
+
ledger_dir = state_dir / 'ledger'
|
|
234
|
+
ledger_dir.mkdir(parents=True)
|
|
235
|
+
(ledger_dir / 'OUTCOMES-LEDGER.md').write_text(FIXTURE_LEDGER_EMPTY)
|
|
236
|
+
|
|
237
|
+
# Create fixture directories
|
|
238
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
239
|
+
fixtures_dir.mkdir(parents=True)
|
|
240
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
241
|
+
transcripts_dir.mkdir(parents=True)
|
|
242
|
+
|
|
243
|
+
# Find free port for this phase
|
|
244
|
+
port = find_free_port()
|
|
245
|
+
|
|
246
|
+
env = os.environ.copy()
|
|
247
|
+
env['PORT'] = str(port)
|
|
248
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
249
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
250
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
251
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
252
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
253
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
254
|
+
|
|
255
|
+
proc = subprocess.Popen(
|
|
256
|
+
[sys.executable, str(SERVE)],
|
|
257
|
+
env=env,
|
|
258
|
+
stdout=subprocess.PIPE,
|
|
259
|
+
stderr=subprocess.PIPE,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
if not wait_for_server(port):
|
|
264
|
+
print("✗ Server failed to start", file=sys.stderr)
|
|
265
|
+
return 1
|
|
266
|
+
|
|
267
|
+
# Run test
|
|
268
|
+
if not run_playwright_test(port, "empty", state_dir):
|
|
269
|
+
all_passed = False
|
|
270
|
+
finally:
|
|
271
|
+
proc.terminate()
|
|
272
|
+
try:
|
|
273
|
+
proc.wait(timeout=5)
|
|
274
|
+
except subprocess.TimeoutExpired:
|
|
275
|
+
proc.kill()
|
|
276
|
+
|
|
277
|
+
return 0 if all_passed else 1
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == '__main__':
|
|
281
|
+
sys.exit(main())
|
|
@@ -114,6 +114,8 @@ def main():
|
|
|
114
114
|
AESOP_ROOT=str(root),
|
|
115
115
|
AESOP_STATE_ROOT=str(state_root),
|
|
116
116
|
AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
|
|
117
|
+
AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
|
|
118
|
+
AESOP_PROOF_FIXTURES="1",
|
|
117
119
|
AESOP_UI_COLLECT_INTERVAL="0.3",
|
|
118
120
|
PORT=str(port),
|
|
119
121
|
)
|