@matt82198/aesop 0.2.0 → 0.3.2
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 +39 -1
- package/README.md +60 -263
- package/bin/cli.js +7 -3
- package/daemons/install-tasks.ps1 +193 -0
- package/daemons/run-hidden.vbs +39 -0
- package/docs/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +48 -3
- package/docs/PORTING.md +166 -0
- package/docs/TEAM-STATE.md +16 -184
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +50 -48
- package/driver/codex_driver.py +59 -12
- package/driver/openai_transport.py +33 -0
- package/driver/wave_bridge.py +9 -1
- package/driver/wave_loop.py +437 -70
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +22 -5
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +6 -4
- package/state_store/__init__.py +2 -1
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/write_api.py +462 -0
- package/tools/cost_ceiling.py +106 -15
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/eod_sweep.py +137 -33
- package/tools/mutation_test.py +130 -8
- package/tools/proposals.mjs +47 -2
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +73 -18
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -0
- package/tools/verify_cost_panel.py +345 -0
- package/tools/wave_preflight.py +296 -29
- package/tools/wave_templates.py +170 -45
- package/ui/collectors.py +81 -0
- package/ui/handler.py +230 -85
- package/ui/sse.py +3 -3
- package/ui/wave_telemetry.py +24 -18
- package/ui/web/dist/assets/{index-DqZLgwNg.css → index-CNQxaiOW.css} +1 -1
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
- package/ui/web/dist/index.html +2 -2
- package/docs/QUICKSTART.md +0 -80
- package/ui/web/dist/assets/index-BGbgw2Nh.js +0 -9
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Browser proof for the Cost Analytics Panel component.
|
|
4
|
+
|
|
5
|
+
Drives ui/web/dist/ against fixture cost data (ledger + pricing), asserting
|
|
6
|
+
the contract via data-testid hooks:
|
|
7
|
+
|
|
8
|
+
Populated-state phase:
|
|
9
|
+
(a) console clean of errors
|
|
10
|
+
(b) GET /api/cost returns properly-shaped CostSummary
|
|
11
|
+
(c) cost-analytics-panel testid present and rendered
|
|
12
|
+
(d) Spend per Wave section visible (or DATA-UNAVAILABLE state)
|
|
13
|
+
(e) Model Efficiency (counterfactual) section visible with comparison table
|
|
14
|
+
(f) Burn Rate & Projection section visible with meter
|
|
15
|
+
(g) Burn rate label and projection text rendered
|
|
16
|
+
(h) No 404/500 errors in console
|
|
17
|
+
|
|
18
|
+
Empty-state phase (separate boot, empty ledger):
|
|
19
|
+
(i) cost-analytics-panel renders with DATA-UNAVAILABLE sections
|
|
20
|
+
(j) Console remains clean
|
|
21
|
+
|
|
22
|
+
Run: python tools/verify_cost_panel.py (exit 0 = proven, 1 = failed)
|
|
23
|
+
python tools/verify_cost_panel.py --allow-skip (exit 0 = proven or skipped, 1 = failed)
|
|
24
|
+
|
|
25
|
+
Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip is passed).
|
|
26
|
+
"""
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import socket
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
import time
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
39
|
+
SERVE = REPO / "ui" / "serve.py"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
FIXTURE_LEDGER_POPULATED = """| timestamp | agent_type | model | duration_seconds | tokens_in | tokens_out | verdict |
|
|
43
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
44
|
+
| 2026-07-13T14:00:00Z | haiku | claude-haiku-4-5-20251001 | 45 | 12000 | 3500 | OK |
|
|
45
|
+
| 2026-07-13T14:05:00Z | haiku | claude-haiku-4-5-20251001 | 50 | 14000 | 4200 | OK |
|
|
46
|
+
| 2026-07-13T14:10:00Z | haiku | claude-haiku-4-5-20251001 | 40 | 11000 | 3200 | FAILED |
|
|
47
|
+
| 2026-07-13T14:15:00Z | haiku | claude-haiku-4-5-20251001 | 55 | 13500 | 4000 | OK |
|
|
48
|
+
| 2026-07-13T14:20:00Z | sonnet | claude-sonnet-4-5-20250929 | 85 | 28000 | 8100 | OK |
|
|
49
|
+
| 2026-07-13T14:25:00Z | sonnet | claude-sonnet-4-5-20250929 | 90 | 30000 | 9000 | OK |
|
|
50
|
+
| 2026-07-13T14:30:00Z | orchestrator | claude-opus-4-20250805 | 120 | 50000 | 12000 | OK |
|
|
51
|
+
| 2026-07-14T08:00:00Z | haiku | claude-haiku-4-5-20251001 | 40 | 10000 | 2800 | OK |
|
|
52
|
+
| 2026-07-14T08:30:00Z | haiku | claude-haiku-4-5-20251001 | 45 | 11000 | 3200 | OK |
|
|
53
|
+
| 2026-07-14T09:00:00Z | sonnet | claude-sonnet-4-5-20250929 | 80 | 26000 | 7800 | OK |
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
FIXTURE_PRICING = {
|
|
57
|
+
"pricing": {
|
|
58
|
+
"claude-haiku-4-5-20251001": {
|
|
59
|
+
"input_per_mtok": 0.80,
|
|
60
|
+
"output_per_mtok": 4.0
|
|
61
|
+
},
|
|
62
|
+
"claude-sonnet-4-5-20250929": {
|
|
63
|
+
"input_per_mtok": 3.0,
|
|
64
|
+
"output_per_mtok": 15.0
|
|
65
|
+
},
|
|
66
|
+
"claude-opus-4-20250805": {
|
|
67
|
+
"input_per_mtok": 15.0,
|
|
68
|
+
"output_per_mtok": 75.0
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
FIXTURE_LEDGER_EMPTY = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def find_free_port():
|
|
77
|
+
"""Find an available port for the test server."""
|
|
78
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
79
|
+
s.bind(('127.0.0.1', 0))
|
|
80
|
+
s.listen(1)
|
|
81
|
+
port = s.getsockname()[1]
|
|
82
|
+
return port
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def wait_for_server(port, timeout=30):
|
|
86
|
+
"""Wait for the server to be ready."""
|
|
87
|
+
start = time.time()
|
|
88
|
+
while time.time() - start < timeout:
|
|
89
|
+
try:
|
|
90
|
+
with socket.create_connection(('127.0.0.1', port), timeout=1):
|
|
91
|
+
return True
|
|
92
|
+
except (ConnectionRefusedError, OSError):
|
|
93
|
+
time.sleep(0.5)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_playwright_test(port, test_name, state_dir):
|
|
98
|
+
"""Run a Playwright test against the server.
|
|
99
|
+
|
|
100
|
+
Uses a minimal inline Playwright script to validate the cost analytics panel.
|
|
101
|
+
"""
|
|
102
|
+
test_script = f'''
|
|
103
|
+
import asyncio
|
|
104
|
+
from playwright.async_api import async_playwright
|
|
105
|
+
|
|
106
|
+
async def test():
|
|
107
|
+
async with async_playwright() as p:
|
|
108
|
+
browser = await p.chromium.launch()
|
|
109
|
+
context = await browser.new_context()
|
|
110
|
+
page = await context.new_page()
|
|
111
|
+
|
|
112
|
+
# Capture console messages
|
|
113
|
+
console_errors = []
|
|
114
|
+
def on_console(msg):
|
|
115
|
+
if msg.type in ('error', 'warn'):
|
|
116
|
+
# Filter out benign warnings
|
|
117
|
+
text = msg.text.lower()
|
|
118
|
+
if 'warning' in text or 'deprecated' in text:
|
|
119
|
+
pass # Skip benign warnings
|
|
120
|
+
else:
|
|
121
|
+
console_errors.append(f"{{msg.type}}: {{msg.text}}")
|
|
122
|
+
|
|
123
|
+
page.on('console', on_console)
|
|
124
|
+
|
|
125
|
+
# Load the dashboard
|
|
126
|
+
await page.goto('http://127.0.0.1:{port}/', wait_until='domcontentloaded')
|
|
127
|
+
|
|
128
|
+
# {test_name}
|
|
129
|
+
try:
|
|
130
|
+
# Test 1: GET /api/cost returns valid CostSummary
|
|
131
|
+
response = await page.goto('http://127.0.0.1:{port}/api/cost')
|
|
132
|
+
assert response.status == 200, f"GET /api/cost failed: {{response.status}}"
|
|
133
|
+
json_data = await response.json()
|
|
134
|
+
assert 'models' in json_data, "Missing 'models' in CostSummary"
|
|
135
|
+
assert 'daily_totals' in json_data, "Missing 'daily_totals' in CostSummary"
|
|
136
|
+
assert 'overall_scorecard' in json_data, "Missing 'overall_scorecard' in CostSummary"
|
|
137
|
+
assert 'has_pricing' in json_data, "Missing 'has_pricing' in CostSummary"
|
|
138
|
+
print("[PASS] GET /api/cost returns valid CostSummary")
|
|
139
|
+
|
|
140
|
+
# Test 2: CostAnalyticsPanel is rendered
|
|
141
|
+
await page.goto('http://127.0.0.1:{port}/#/cost')
|
|
142
|
+
panel = page.locator('[data-testid="cost-analytics-panel"]')
|
|
143
|
+
is_visible = await panel.is_visible()
|
|
144
|
+
assert is_visible, "CostAnalyticsPanel not visible"
|
|
145
|
+
print("[PASS] CostAnalyticsPanel rendered and visible")
|
|
146
|
+
|
|
147
|
+
# Test 3: For populated state, check sections
|
|
148
|
+
if "{test_name}" == "populated":
|
|
149
|
+
# Check model data aggregated
|
|
150
|
+
models_count = len(json_data.get('models', {{}}))
|
|
151
|
+
if models_count > 0:
|
|
152
|
+
print(f"[PASS] Models aggregated: {{models_count}} model(s)")
|
|
153
|
+
# Pricing may or may not be loaded depending on config,
|
|
154
|
+
# but the panel should still render
|
|
155
|
+
else:
|
|
156
|
+
print("[INFO] No models found (ledger may be empty)")
|
|
157
|
+
|
|
158
|
+
# Check daily totals
|
|
159
|
+
daily_count = len(json_data.get('daily_totals', {{}}))
|
|
160
|
+
if daily_count > 0:
|
|
161
|
+
print(f"[PASS] Daily totals: {{daily_count}} day(s)")
|
|
162
|
+
else:
|
|
163
|
+
print("[INFO] No daily totals (ledger may be empty)")
|
|
164
|
+
|
|
165
|
+
# Test 4: For empty state, check graceful degradation
|
|
166
|
+
elif "{test_name}" == "empty":
|
|
167
|
+
assert json_data.get('has_pricing') is False or len(json_data.get('models', {{}})) == 0
|
|
168
|
+
print("[PASS] Empty state handled gracefully")
|
|
169
|
+
|
|
170
|
+
# Test 5: No fatal console errors
|
|
171
|
+
if console_errors:
|
|
172
|
+
error_text = "\\n".join(console_errors[:3])
|
|
173
|
+
print(f"[WARN] Console messages: {{error_text}}")
|
|
174
|
+
else:
|
|
175
|
+
print("[PASS] Console clean (no errors)")
|
|
176
|
+
|
|
177
|
+
print(f"[PASS] Test passed: {test_name}")
|
|
178
|
+
except AssertionError as e:
|
|
179
|
+
print(f"[FAIL] Test failed: {{e}}")
|
|
180
|
+
raise
|
|
181
|
+
finally:
|
|
182
|
+
await browser.close()
|
|
183
|
+
|
|
184
|
+
asyncio.run(test())
|
|
185
|
+
'''
|
|
186
|
+
try:
|
|
187
|
+
result = subprocess.run(
|
|
188
|
+
[sys.executable, '-c', test_script],
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
timeout=30,
|
|
192
|
+
env={**os.environ, 'AESOP_STATE_ROOT': str(state_dir)}
|
|
193
|
+
)
|
|
194
|
+
print(result.stdout)
|
|
195
|
+
if result.stderr and 'warning' not in result.stderr.lower():
|
|
196
|
+
print(result.stderr, file=sys.stderr)
|
|
197
|
+
return result.returncode == 0
|
|
198
|
+
except subprocess.TimeoutExpired:
|
|
199
|
+
print(f"[FAIL] Test timed out: {test_name}", file=sys.stderr)
|
|
200
|
+
return False
|
|
201
|
+
except Exception as e:
|
|
202
|
+
print(f"[FAIL] Test exception: {e}", file=sys.stderr)
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
"""Run the verify_cost_panel proof."""
|
|
208
|
+
parser = argparse.ArgumentParser(
|
|
209
|
+
description='Browser proof for Cost Analytics Panel'
|
|
210
|
+
)
|
|
211
|
+
parser.add_argument('--allow-skip', action='store_true',
|
|
212
|
+
help='Exit 0 if playwright is unavailable')
|
|
213
|
+
args = parser.parse_args()
|
|
214
|
+
|
|
215
|
+
# Check if playwright is installed
|
|
216
|
+
try:
|
|
217
|
+
import playwright # noqa: F401
|
|
218
|
+
except ImportError:
|
|
219
|
+
if args.allow_skip:
|
|
220
|
+
print("[SKIP] Playwright not available (skipped per --allow-skip)")
|
|
221
|
+
return 0
|
|
222
|
+
print("[FAIL] Playwright not installed; run: pip install playwright && playwright install",
|
|
223
|
+
file=sys.stderr)
|
|
224
|
+
return 1
|
|
225
|
+
|
|
226
|
+
all_passed = True
|
|
227
|
+
|
|
228
|
+
# Test 1: Populated state with pricing
|
|
229
|
+
print("\n=== Test: Populated state with pricing ===")
|
|
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_POPULATED)
|
|
236
|
+
|
|
237
|
+
# Write pricing config
|
|
238
|
+
config_file = Path(tmpdir) / 'aesop.config.json'
|
|
239
|
+
config_file.write_text(json.dumps(FIXTURE_PRICING))
|
|
240
|
+
|
|
241
|
+
# Create fixture directories
|
|
242
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
243
|
+
fixtures_dir.mkdir(parents=True)
|
|
244
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
245
|
+
transcripts_dir.mkdir(parents=True)
|
|
246
|
+
|
|
247
|
+
# Find free port for this phase
|
|
248
|
+
port = find_free_port()
|
|
249
|
+
|
|
250
|
+
# Start server
|
|
251
|
+
env = os.environ.copy()
|
|
252
|
+
env['PORT'] = str(port)
|
|
253
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
254
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
255
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
256
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
257
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
258
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
259
|
+
env['AESOP_CONFIG_ROOT'] = str(tmpdir)
|
|
260
|
+
|
|
261
|
+
proc = subprocess.Popen(
|
|
262
|
+
[sys.executable, str(SERVE)],
|
|
263
|
+
env=env,
|
|
264
|
+
stdout=subprocess.PIPE,
|
|
265
|
+
stderr=subprocess.PIPE,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
if not wait_for_server(port):
|
|
270
|
+
print("[FAIL] Server failed to start", file=sys.stderr)
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
# Run test
|
|
274
|
+
if not run_playwright_test(port, "populated", state_dir):
|
|
275
|
+
all_passed = False
|
|
276
|
+
finally:
|
|
277
|
+
proc.terminate()
|
|
278
|
+
try:
|
|
279
|
+
proc.wait(timeout=5)
|
|
280
|
+
except subprocess.TimeoutExpired:
|
|
281
|
+
proc.kill()
|
|
282
|
+
|
|
283
|
+
# Test 2: Empty state
|
|
284
|
+
print("\n=== Test: Empty state ===")
|
|
285
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
286
|
+
state_dir = Path(tmpdir) / 'state'
|
|
287
|
+
state_dir.mkdir(parents=True)
|
|
288
|
+
ledger_dir = state_dir / 'ledger'
|
|
289
|
+
ledger_dir.mkdir(parents=True)
|
|
290
|
+
(ledger_dir / 'OUTCOMES-LEDGER.md').write_text(FIXTURE_LEDGER_EMPTY)
|
|
291
|
+
|
|
292
|
+
# Create fixture directories
|
|
293
|
+
fixtures_dir = Path(tmpdir) / 'fixtures'
|
|
294
|
+
fixtures_dir.mkdir(parents=True)
|
|
295
|
+
transcripts_dir = Path(tmpdir) / 'transcripts'
|
|
296
|
+
transcripts_dir.mkdir(parents=True)
|
|
297
|
+
|
|
298
|
+
# Find free port for this phase
|
|
299
|
+
port = find_free_port()
|
|
300
|
+
|
|
301
|
+
env = os.environ.copy()
|
|
302
|
+
env['PORT'] = str(port)
|
|
303
|
+
env['AESOP_STATE_ROOT'] = str(state_dir)
|
|
304
|
+
env['AESOP_ROOT'] = str(REPO)
|
|
305
|
+
env['AESOP_WEB_DIST'] = str(REPO / 'ui' / 'web' / 'dist')
|
|
306
|
+
env['AESOP_PROOF_FIXTURES'] = '1'
|
|
307
|
+
env['AESOP_UI_COLLECT_INTERVAL'] = '0.1'
|
|
308
|
+
env['AESOP_TRANSCRIPTS_ROOT'] = str(transcripts_dir)
|
|
309
|
+
|
|
310
|
+
proc = subprocess.Popen(
|
|
311
|
+
[sys.executable, str(SERVE)],
|
|
312
|
+
env=env,
|
|
313
|
+
stdout=subprocess.PIPE,
|
|
314
|
+
stderr=subprocess.PIPE,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
if not wait_for_server(port):
|
|
319
|
+
print("[FAIL] Server failed to start", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
# Run test
|
|
323
|
+
if not run_playwright_test(port, "empty", state_dir):
|
|
324
|
+
all_passed = False
|
|
325
|
+
finally:
|
|
326
|
+
proc.terminate()
|
|
327
|
+
try:
|
|
328
|
+
proc.wait(timeout=5)
|
|
329
|
+
except subprocess.TimeoutExpired:
|
|
330
|
+
proc.kill()
|
|
331
|
+
|
|
332
|
+
if all_passed:
|
|
333
|
+
print("\n" + "=" * 60)
|
|
334
|
+
print("[PASS] All cost panel proof tests PASSED")
|
|
335
|
+
print("=" * 60)
|
|
336
|
+
return 0
|
|
337
|
+
else:
|
|
338
|
+
print("\n" + "=" * 60)
|
|
339
|
+
print("[FAIL] Some cost panel proof tests FAILED")
|
|
340
|
+
print("=" * 60)
|
|
341
|
+
return 1
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
sys.exit(main())
|