@matt82198/aesop 0.1.0-beta.5 → 0.1.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 +158 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -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__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.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__/metrics_gate.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__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.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/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__/cost.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/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/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
package/ui/web/package.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "aesop-ui-web",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "vite",
|
|
7
|
-
"build": "vite build",
|
|
8
|
-
"test": "vitest"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"react": "19.2.7",
|
|
12
|
-
"react-dom": "19.2.7"
|
|
13
|
-
},
|
|
14
|
-
"devDependencies": {
|
|
15
|
-
"@testing-library/jest-dom": "6.9.1",
|
|
16
|
-
"@testing-library/react": "16.3.2",
|
|
17
|
-
"@testing-library/user-event": "^14.6.1",
|
|
18
|
-
"@types/react": "19.2.17",
|
|
19
|
-
"@types/react-dom": "19.2.3",
|
|
20
|
-
"@vitejs/plugin-react": "6.0.3",
|
|
21
|
-
"jsdom": "29.1.1",
|
|
22
|
-
"typescript": "6.0.2",
|
|
23
|
-
"vite": "8.1.1",
|
|
24
|
-
"vitest": "4.1.10"
|
|
25
|
-
}
|
|
26
|
-
}
|
package/ui/web/src/App.test.tsx
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { render, screen, act, fireEvent } from '@testing-library/react';
|
|
3
|
-
import App from './App';
|
|
4
|
-
import { TESTIDS } from './test/fixtures';
|
|
5
|
-
|
|
6
|
-
function setHash(hash: string) {
|
|
7
|
-
window.location.hash = hash;
|
|
8
|
-
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
window.location.hash = '';
|
|
13
|
-
document.documentElement.removeAttribute('data-theme');
|
|
14
|
-
localStorage.clear();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('App shell', () => {
|
|
18
|
-
it('renders the health header and nav', () => {
|
|
19
|
-
render(<App />);
|
|
20
|
-
expect(screen.getByTestId(TESTIDS.healthHeader)).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByRole('link', { name: 'Overview' })).toHaveAttribute('href', '#/');
|
|
22
|
-
expect(screen.getByRole('link', { name: 'Work' })).toHaveAttribute('href', '#/work');
|
|
23
|
-
expect(screen.getByRole('link', { name: 'Activity' })).toHaveAttribute('href', '#/activity');
|
|
24
|
-
expect(screen.getByRole('link', { name: 'Cost' })).toHaveAttribute('href', '#/cost');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('renders the overview placeholder by default', () => {
|
|
28
|
-
render(<App />);
|
|
29
|
-
expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('switches views on hash change', () => {
|
|
33
|
-
render(<App />);
|
|
34
|
-
act(() => setHash('#/work'));
|
|
35
|
-
expect(screen.getByTestId(TESTIDS.viewWork)).toBeInTheDocument();
|
|
36
|
-
expect(screen.queryByTestId(TESTIDS.viewOverview)).not.toBeInTheDocument();
|
|
37
|
-
|
|
38
|
-
act(() => setHash('#/cost'));
|
|
39
|
-
expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
|
|
40
|
-
|
|
41
|
-
act(() => setHash('#/activity'));
|
|
42
|
-
expect(screen.getByTestId(TESTIDS.viewActivity)).toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('marks the active tab with aria-current', () => {
|
|
46
|
-
render(<App />);
|
|
47
|
-
act(() => setHash('#/work'));
|
|
48
|
-
expect(screen.getByRole('link', { name: 'Work' })).toHaveAttribute('aria-current', 'page');
|
|
49
|
-
expect(screen.getByRole('link', { name: 'Overview' })).not.toHaveAttribute('aria-current');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('exposes the SSE connection status as a live region', () => {
|
|
53
|
-
render(<App />);
|
|
54
|
-
const status = screen.getByTestId(TESTIDS.sseStatus);
|
|
55
|
-
expect(status).toHaveAttribute('role', 'status');
|
|
56
|
-
expect(status).toHaveAttribute('aria-live', 'polite');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('theme toggle is a real button and persists to localStorage', () => {
|
|
60
|
-
render(<App />);
|
|
61
|
-
const toggle = screen.getByTestId(TESTIDS.themeToggle);
|
|
62
|
-
expect(toggle.tagName).toBe('BUTTON');
|
|
63
|
-
|
|
64
|
-
fireEvent.click(toggle);
|
|
65
|
-
const stored = localStorage.getItem('aesop-theme');
|
|
66
|
-
expect(stored === 'light' || stored === 'dark').toBe(true);
|
|
67
|
-
expect(document.documentElement.getAttribute('data-theme')).toBe(stored);
|
|
68
|
-
|
|
69
|
-
// Toggling again flips the theme
|
|
70
|
-
fireEvent.click(toggle);
|
|
71
|
-
const flipped = localStorage.getItem('aesop-theme');
|
|
72
|
-
expect(flipped).not.toBe(stored);
|
|
73
|
-
});
|
|
74
|
-
});
|
package/ui/web/src/App.tsx
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App shell — sticky HealthHeader slot + hash-tab nav + view slots (D4).
|
|
3
|
-
*
|
|
4
|
-
* U1 renders placeholder views; U4–U7 replace the placeholders with real
|
|
5
|
-
* components. The header slot, nav, theme toggle, and SSE plumbing are the
|
|
6
|
-
* stable shell contract.
|
|
7
|
-
*
|
|
8
|
-
* A11y baseline (D5): all interactive elements are real <button>/<a>,
|
|
9
|
-
* nav uses aria-current="page", the SSE status is a role="status" live region.
|
|
10
|
-
*/
|
|
11
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
-
import { useHashRoute, type Route } from './lib/useHashRoute';
|
|
13
|
-
import { useSSE } from './lib/useSSE';
|
|
14
|
-
import { HealthHeader } from './components/HealthHeader';
|
|
15
|
-
import { Overview } from './views/Overview';
|
|
16
|
-
import { Work } from './views/Work';
|
|
17
|
-
import Activity from './views/Activity';
|
|
18
|
-
import { Cost } from './views/Cost';
|
|
19
|
-
import { TESTIDS } from './test/fixtures';
|
|
20
|
-
|
|
21
|
-
const THEME_STORAGE_KEY = 'aesop-theme';
|
|
22
|
-
type Theme = 'light' | 'dark' | null; // null = follow OS preference
|
|
23
|
-
|
|
24
|
-
function readStoredTheme(): Theme {
|
|
25
|
-
try {
|
|
26
|
-
const v = localStorage.getItem(THEME_STORAGE_KEY);
|
|
27
|
-
return v === 'light' || v === 'dark' ? v : null;
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function applyTheme(theme: Theme) {
|
|
34
|
-
const root = document.documentElement;
|
|
35
|
-
if (theme === null) {
|
|
36
|
-
root.removeAttribute('data-theme');
|
|
37
|
-
} else {
|
|
38
|
-
root.setAttribute('data-theme', theme);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function useTheme() {
|
|
43
|
-
const [theme, setTheme] = useState<Theme>(readStoredTheme);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
applyTheme(theme);
|
|
47
|
-
try {
|
|
48
|
-
if (theme === null) {
|
|
49
|
-
localStorage.removeItem(THEME_STORAGE_KEY);
|
|
50
|
-
} else {
|
|
51
|
-
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
// localStorage unavailable (private mode) — theme still applies for this session
|
|
55
|
-
}
|
|
56
|
-
}, [theme]);
|
|
57
|
-
|
|
58
|
-
const toggle = useCallback(() => {
|
|
59
|
-
setTheme((prev) => {
|
|
60
|
-
const osPrefersDark =
|
|
61
|
-
typeof matchMedia !== 'undefined' &&
|
|
62
|
-
matchMedia('(prefers-color-scheme: dark)').matches;
|
|
63
|
-
const effective = prev ?? (osPrefersDark ? 'dark' : 'light');
|
|
64
|
-
return effective === 'dark' ? 'light' : 'dark';
|
|
65
|
-
});
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
return { theme, toggle };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const NAV_ITEMS: Array<{ hash: Route; label: string }> = [
|
|
72
|
-
{ hash: '#/', label: 'Overview' },
|
|
73
|
-
{ hash: '#/work', label: 'Work' },
|
|
74
|
-
{ hash: '#/activity', label: 'Activity' },
|
|
75
|
-
{ hash: '#/cost', label: 'Cost' },
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
function Placeholder({ name, testid }: { name: string; testid: string }) {
|
|
79
|
-
return (
|
|
80
|
-
<section className="view-placeholder" data-testid={testid} aria-label={`${name} view`}>
|
|
81
|
-
<h2>{name}</h2>
|
|
82
|
-
<p>{name} view lands in a later unit of wave 14.</p>
|
|
83
|
-
</section>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export default function App() {
|
|
88
|
-
const route = useHashRoute();
|
|
89
|
-
const sseState = useSSE();
|
|
90
|
-
const { toggle } = useTheme();
|
|
91
|
-
const [dataTimestamp, setDataTimestamp] = useState<number | null>(null);
|
|
92
|
-
|
|
93
|
-
const connection = sseState.connectionStatus;
|
|
94
|
-
|
|
95
|
-
// Update timestamp whenever any SSE section updates
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (sseState.data || sseState.agents || sseState.tracker || sseState.cost) {
|
|
98
|
-
setDataTimestamp(Date.now());
|
|
99
|
-
}
|
|
100
|
-
}, [sseState.data, sseState.agents, sseState.tracker, sseState.cost]);
|
|
101
|
-
|
|
102
|
-
const handleRefresh = useCallback(() => {
|
|
103
|
-
window.location.reload();
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<>
|
|
108
|
-
<HealthHeader
|
|
109
|
-
watchdog={sseState.data?.watchdog ?? null}
|
|
110
|
-
monitor={sseState.data?.monitor ?? null}
|
|
111
|
-
orchestrator={sseState.status ?? null}
|
|
112
|
-
agents={sseState.agents ?? null}
|
|
113
|
-
alerts={sseState.data?.alerts ?? null}
|
|
114
|
-
connectionStatus={connection}
|
|
115
|
-
dataTimestamp={dataTimestamp}
|
|
116
|
-
onThemeToggle={toggle}
|
|
117
|
-
onRefresh={handleRefresh}
|
|
118
|
-
/>
|
|
119
|
-
<nav className="app-nav" aria-label="Views">
|
|
120
|
-
{NAV_ITEMS.map(({ hash, label }) => (
|
|
121
|
-
<a key={hash} href={hash} aria-current={route === hash ? 'page' : undefined} className="app-nav__link">
|
|
122
|
-
{label}
|
|
123
|
-
</a>
|
|
124
|
-
))}
|
|
125
|
-
</nav>
|
|
126
|
-
<main className="app-main">
|
|
127
|
-
{route === '#/' && (
|
|
128
|
-
<Overview
|
|
129
|
-
agents={sseState.agents ?? null}
|
|
130
|
-
alerts={sseState.data?.alerts ?? null}
|
|
131
|
-
events={sseState.data?.events ?? null}
|
|
132
|
-
repos={sseState.data?.repos ?? null}
|
|
133
|
-
/>
|
|
134
|
-
)}
|
|
135
|
-
{route === '#/work' && <Work />}
|
|
136
|
-
{route === '#/activity' && <Activity state={sseState} />}
|
|
137
|
-
{route === '#/cost' &&
|
|
138
|
-
(sseState.cost ? <Cost cost={sseState.cost} /> : <Placeholder name="Cost" testid={TESTIDS.viewCost} />)}
|
|
139
|
-
</main>
|
|
140
|
-
</>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Contributing to the Aesop dashboard UI — the rules (one page)
|
|
2
|
-
|
|
3
|
-
These are the baseline rules every component in `ui/web/src/` must follow.
|
|
4
|
-
They are enforced by the wave-14 proofs (`tools/verify_dash.py` + vitest); a
|
|
5
|
-
violation is a CI failure, not a style nit.
|
|
6
|
-
|
|
7
|
-
## Interactive elements are real elements
|
|
8
|
-
|
|
9
|
-
- Anything clickable is a `<button type="button">` (actions) or an `<a href>`
|
|
10
|
-
(navigation). **Never** a `div`/`span` with `onClick`/`tabIndex` — the old
|
|
11
|
-
dashboard's div-with-tabIndex pattern is banned (plan D5).
|
|
12
|
-
- Anything focusable must show the focus ring. Do not add `outline: none`
|
|
13
|
-
anywhere; `global.css` already provides `:focus-visible` rings.
|
|
14
|
-
- Form controls get a `<label>` (visually hidden with `.sr-only` if needed).
|
|
15
|
-
|
|
16
|
-
## data-testid contract
|
|
17
|
-
|
|
18
|
-
- Every testid comes from the `TESTIDS` map in `src/test/fixtures.ts`.
|
|
19
|
-
Add new ids there first; never inline a bare string. U8's Playwright proofs
|
|
20
|
-
assert only via these hooks — renaming one breaks CI.
|
|
21
|
-
|
|
22
|
-
## Types and fixtures
|
|
23
|
-
|
|
24
|
-
- All API payload types live in `src/lib/types.ts` — import them, don't
|
|
25
|
-
redeclare shapes. New endpoint? Add the type there in the same PR.
|
|
26
|
-
- Component tests render from the fixtures in `src/test/fixtures.ts`.
|
|
27
|
-
Extend the fixtures rather than hand-rolling payload literals per test.
|
|
28
|
-
|
|
29
|
-
## Styling
|
|
30
|
-
|
|
31
|
-
- Colors/spacing/type come from the custom properties in
|
|
32
|
-
`src/styles/theme.css` (the design-tokens file — named `theme.css`, not
|
|
33
|
-
`tokens.css`, because the pre-push secret-scan gate blocks any filename
|
|
34
|
-
matching `*token*`). No hex literals in components — both themes must
|
|
35
|
-
work, and tokens are the only way that stays true.
|
|
36
|
-
- Honor `prefers-reduced-motion`: the global block handles CSS transitions;
|
|
37
|
-
don't add JS-driven animation without a reduced-motion check.
|
|
38
|
-
|
|
39
|
-
## Safety
|
|
40
|
-
|
|
41
|
-
- Any URL that ends up in an `href` goes through `src/lib/sanitizeUrl.ts`.
|
|
42
|
-
When it returns `null`, render the text without an href (inert).
|
|
43
|
-
- All mutations go through `src/lib/api.ts` (CSRF header handled there).
|
|
44
|
-
Never call `fetch` with `method: 'POST'` directly from a component.
|
|
45
|
-
|
|
46
|
-
## Build discipline (D2)
|
|
47
|
-
|
|
48
|
-
- If you change `ui/web/src/**`, run `npm run build` and commit `ui/web/dist/`
|
|
49
|
-
in the same PR — CI's drift gate rebuilds and compares.
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
.agent-row {
|
|
2
|
-
list-style: none;
|
|
3
|
-
border-bottom: 1px solid var(--color-border-subtle);
|
|
4
|
-
padding: var(--space-3) 0;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.agent-row:last-child {
|
|
8
|
-
border-bottom: none;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.agent-row__toggle {
|
|
12
|
-
display: inline-flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
justify-content: center;
|
|
15
|
-
width: var(--space-5);
|
|
16
|
-
height: var(--space-5);
|
|
17
|
-
padding: 0;
|
|
18
|
-
margin-right: var(--space-2);
|
|
19
|
-
background: transparent;
|
|
20
|
-
border: none;
|
|
21
|
-
cursor: pointer;
|
|
22
|
-
color: var(--color-text-muted);
|
|
23
|
-
transition: color var(--transition-fast);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.agent-row__toggle:hover {
|
|
27
|
-
color: var(--color-accent);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.agent-row__toggle:focus-visible {
|
|
31
|
-
outline: 2px solid var(--color-focus-ring);
|
|
32
|
-
outline-offset: 2px;
|
|
33
|
-
border-radius: var(--radius-sm);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.agent-row__toggle-icon {
|
|
37
|
-
display: inline-block;
|
|
38
|
-
font-size: var(--font-size-xs);
|
|
39
|
-
line-height: 1;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.agent-row__header {
|
|
43
|
-
display: flex;
|
|
44
|
-
align-items: center;
|
|
45
|
-
gap: var(--space-3);
|
|
46
|
-
font-size: var(--font-size-sm);
|
|
47
|
-
line-height: var(--line-height-normal);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.agent-row__status-icon {
|
|
51
|
-
display: inline-block;
|
|
52
|
-
font-size: var(--font-size-lg);
|
|
53
|
-
flex-shrink: 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.agent-row__id {
|
|
57
|
-
font-family: var(--font-mono);
|
|
58
|
-
font-weight: var(--font-weight-bold);
|
|
59
|
-
color: var(--color-accent);
|
|
60
|
-
flex-shrink: 0;
|
|
61
|
-
min-width: 100px;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.agent-row__age {
|
|
65
|
-
font-family: var(--font-mono);
|
|
66
|
-
font-size: var(--font-size-xs);
|
|
67
|
-
color: var(--color-text-muted);
|
|
68
|
-
flex-shrink: 0;
|
|
69
|
-
min-width: 50px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.agent-row__hint {
|
|
73
|
-
color: var(--color-text);
|
|
74
|
-
flex-grow: 1;
|
|
75
|
-
word-break: break-word;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.agent-row__detail {
|
|
79
|
-
margin-top: var(--space-3);
|
|
80
|
-
margin-left: calc(var(--space-5) + var(--space-2));
|
|
81
|
-
padding: var(--space-3);
|
|
82
|
-
background: var(--color-bg-subtle);
|
|
83
|
-
border-left: 2px solid var(--color-accent);
|
|
84
|
-
border-radius: var(--radius-md);
|
|
85
|
-
animation: expand-in 150ms ease;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
@keyframes expand-in {
|
|
89
|
-
from {
|
|
90
|
-
opacity: 0;
|
|
91
|
-
max-height: 0;
|
|
92
|
-
}
|
|
93
|
-
to {
|
|
94
|
-
opacity: 1;
|
|
95
|
-
max-height: 100vh;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@media (prefers-reduced-motion: reduce) {
|
|
100
|
-
.agent-row__detail {
|
|
101
|
-
animation: none;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.agent-row__detail-row {
|
|
106
|
-
display: grid;
|
|
107
|
-
grid-template-columns: 120px 1fr;
|
|
108
|
-
gap: var(--space-3);
|
|
109
|
-
align-items: baseline;
|
|
110
|
-
margin-bottom: var(--space-2);
|
|
111
|
-
font-size: var(--font-size-sm);
|
|
112
|
-
line-height: var(--line-height-normal);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.agent-row__detail-label {
|
|
116
|
-
font-weight: var(--font-weight-bold);
|
|
117
|
-
color: var(--color-text-muted);
|
|
118
|
-
text-align: right;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.agent-row__detail-value {
|
|
122
|
-
color: var(--color-text);
|
|
123
|
-
font-family: var(--font-mono);
|
|
124
|
-
word-break: break-word;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.agent-row__prompt {
|
|
128
|
-
margin-top: var(--space-4);
|
|
129
|
-
padding-top: var(--space-3);
|
|
130
|
-
border-top: 1px solid var(--color-border-subtle);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.agent-row__prompt-label {
|
|
134
|
-
display: block;
|
|
135
|
-
margin-bottom: var(--space-2);
|
|
136
|
-
font-weight: var(--font-weight-bold);
|
|
137
|
-
color: var(--color-text-muted);
|
|
138
|
-
font-size: var(--font-size-xs);
|
|
139
|
-
text-transform: uppercase;
|
|
140
|
-
letter-spacing: 0.05em;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.agent-row__prompt-text {
|
|
144
|
-
background: var(--color-bg-inset);
|
|
145
|
-
border: 1px solid var(--color-border);
|
|
146
|
-
border-radius: var(--radius-md);
|
|
147
|
-
padding: var(--space-3);
|
|
148
|
-
margin: 0;
|
|
149
|
-
font-size: var(--font-size-xs);
|
|
150
|
-
line-height: var(--line-height-normal);
|
|
151
|
-
color: var(--color-text);
|
|
152
|
-
font-family: var(--font-mono);
|
|
153
|
-
white-space: pre-wrap;
|
|
154
|
-
word-break: break-word;
|
|
155
|
-
max-height: 300px;
|
|
156
|
-
overflow-y: auto;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.agent-row__prompt-loading,
|
|
160
|
-
.agent-row__prompt-error {
|
|
161
|
-
background: var(--color-bg-inset);
|
|
162
|
-
border: 1px solid var(--color-border);
|
|
163
|
-
border-radius: var(--radius-md);
|
|
164
|
-
padding: var(--space-3);
|
|
165
|
-
font-family: var(--font-mono);
|
|
166
|
-
font-size: var(--font-size-sm);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
.agent-row__prompt-loading {
|
|
170
|
-
color: var(--color-text-muted);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.agent-row__prompt-error {
|
|
174
|
-
color: var(--color-status-error);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
@media (max-width: 768px) {
|
|
178
|
-
.agent-row__detail-row {
|
|
179
|
-
grid-template-columns: 1fr;
|
|
180
|
-
gap: var(--space-2);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.agent-row__detail-label {
|
|
184
|
-
text-align: left;
|
|
185
|
-
font-weight: var(--font-weight-bold);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
-
import { userEvent } from '@testing-library/user-event';
|
|
4
|
-
import { AgentRow } from './AgentRow';
|
|
5
|
-
import { fixtureAgents, fixtureAgentDetail, TESTIDS } from '../test/fixtures';
|
|
6
|
-
import * as api from '../lib/api';
|
|
7
|
-
|
|
8
|
-
vi.mock('../lib/api', () => ({
|
|
9
|
-
fetchAgent: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
describe('AgentRow', () => {
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.clearAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('renders agent row with basic info', () => {
|
|
18
|
-
render(<AgentRow agent={fixtureAgents[0]} />);
|
|
19
|
-
|
|
20
|
-
expect(screen.getByTestId(TESTIDS.agentRow)).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByText(fixtureAgents[0].id)).toBeInTheDocument();
|
|
22
|
-
expect(screen.getByText(fixtureAgents[0].hint)).toBeInTheDocument();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('displays status indicator with correct color mapping', () => {
|
|
26
|
-
const runningAgent = fixtureAgents[0]; // status: 'running'
|
|
27
|
-
const { container } = render(<AgentRow agent={runningAgent} />);
|
|
28
|
-
|
|
29
|
-
const statusIcon = container.querySelector('.agent-row__status-icon');
|
|
30
|
-
expect(statusIcon).toBeInTheDocument();
|
|
31
|
-
expect(statusIcon).toHaveTextContent('●');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('displays age in appropriate format (seconds/minutes)', () => {
|
|
35
|
-
const agent = { ...fixtureAgents[0], age_s: 45 };
|
|
36
|
-
render(<AgentRow agent={agent} />);
|
|
37
|
-
expect(screen.getByText('45s')).toBeInTheDocument();
|
|
38
|
-
|
|
39
|
-
const { unmount } = render(<AgentRow agent={{ ...fixtureAgents[1], age_s: 341 }} />);
|
|
40
|
-
expect(screen.getByText('5m')).toBeInTheDocument();
|
|
41
|
-
unmount();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('expands and collapses on toggle click', async () => {
|
|
45
|
-
const user = userEvent.setup();
|
|
46
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
47
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
48
|
-
|
|
49
|
-
render(<AgentRow agent={fixtureAgents[0]} />);
|
|
50
|
-
|
|
51
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
52
|
-
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
53
|
-
|
|
54
|
-
await user.click(toggleButton);
|
|
55
|
-
expect(toggleButton).toHaveAttribute('aria-expanded', 'true');
|
|
56
|
-
|
|
57
|
-
await waitFor(() => {
|
|
58
|
-
expect(screen.getByTestId(TESTIDS.agentRowDetail)).toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await user.click(toggleButton);
|
|
62
|
-
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('fetches agent detail on expansion', async () => {
|
|
66
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
67
|
-
mockFetch.mockResolvedValue(fixtureAgentDetail);
|
|
68
|
-
|
|
69
|
-
render(<AgentRow agent={fixtureAgents[0]} />);
|
|
70
|
-
|
|
71
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
72
|
-
await userEvent.click(toggleButton);
|
|
73
|
-
|
|
74
|
-
const prompt = await screen.findByText(fixtureAgentDetail.dispatch_prompt);
|
|
75
|
-
expect(prompt).toBeInTheDocument();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('displays dispatch prompt after fetch completes', async () => {
|
|
79
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
80
|
-
mockFetch.mockResolvedValue(fixtureAgentDetail);
|
|
81
|
-
|
|
82
|
-
render(<AgentRow agent={fixtureAgents[0]} />);
|
|
83
|
-
|
|
84
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
85
|
-
await userEvent.click(toggleButton);
|
|
86
|
-
|
|
87
|
-
const prompt = await screen.findByText(fixtureAgentDetail.dispatch_prompt);
|
|
88
|
-
expect(prompt).toBeInTheDocument();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('caches agent detail on subsequent expansions', async () => {
|
|
92
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
93
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
94
|
-
|
|
95
|
-
const { rerender } = render(<AgentRow agent={fixtureAgents[0]} />);
|
|
96
|
-
|
|
97
|
-
// First expansion
|
|
98
|
-
let toggleButton = screen.getByLabelText(/expand/i);
|
|
99
|
-
await userEvent.click(toggleButton);
|
|
100
|
-
|
|
101
|
-
await waitFor(() => {
|
|
102
|
-
expect(screen.getByText(fixtureAgentDetail.dispatch_prompt)).toBeInTheDocument();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const callCountAfterFirstExpand = mockFetch.mock.calls.length;
|
|
106
|
-
|
|
107
|
-
// Collapse
|
|
108
|
-
await userEvent.click(toggleButton);
|
|
109
|
-
|
|
110
|
-
// Re-render with same agent
|
|
111
|
-
rerender(<AgentRow agent={fixtureAgents[0]} />);
|
|
112
|
-
|
|
113
|
-
// Second expansion (should use cache)
|
|
114
|
-
toggleButton = screen.getByLabelText(/expand/i);
|
|
115
|
-
await userEvent.click(toggleButton);
|
|
116
|
-
|
|
117
|
-
// Should still only have been called once (from cache on second expansion)
|
|
118
|
-
expect(mockFetch.mock.calls.length).toBe(callCountAfterFirstExpand);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('is keyboard accessible (toggle button can be focused and activated)', async () => {
|
|
122
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
123
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
124
|
-
|
|
125
|
-
render(<AgentRow agent={fixtureAgents[0]} />);
|
|
126
|
-
|
|
127
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
128
|
-
|
|
129
|
-
// Should be focusable
|
|
130
|
-
toggleButton.focus();
|
|
131
|
-
expect(document.activeElement).toBe(toggleButton);
|
|
132
|
-
|
|
133
|
-
// Should be activatable with Enter
|
|
134
|
-
await userEvent.keyboard('{Enter}');
|
|
135
|
-
expect(toggleButton).toHaveAttribute('aria-expanded', 'true');
|
|
136
|
-
|
|
137
|
-
await waitFor(() => {
|
|
138
|
-
expect(screen.getByTestId(TESTIDS.agentRowDetail)).toBeInTheDocument();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('expansion survives prop updates with same agent (keyed row identity)', async () => {
|
|
143
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
144
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
145
|
-
|
|
146
|
-
const { rerender } = render(<AgentRow agent={fixtureAgents[0]} />);
|
|
147
|
-
|
|
148
|
-
// Expand
|
|
149
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
150
|
-
await userEvent.click(toggleButton);
|
|
151
|
-
|
|
152
|
-
await waitFor(() => {
|
|
153
|
-
expect(screen.getByTestId(TESTIDS.agentRowDetail)).toBeInTheDocument();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Re-render with same agent (simulating parent re-render)
|
|
157
|
-
rerender(<AgentRow agent={fixtureAgents[0]} />);
|
|
158
|
-
|
|
159
|
-
// Expanded detail should still be visible
|
|
160
|
-
expect(screen.getByTestId(TESTIDS.agentRowDetail)).toBeInTheDocument();
|
|
161
|
-
expect(toggleButton).toHaveAttribute('aria-expanded', 'true');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('displays all detail fields when expanded', async () => {
|
|
165
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
166
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
167
|
-
|
|
168
|
-
const agent = fixtureAgents[0];
|
|
169
|
-
render(<AgentRow agent={agent} />);
|
|
170
|
-
|
|
171
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
172
|
-
await userEvent.click(toggleButton);
|
|
173
|
-
|
|
174
|
-
await waitFor(() => {
|
|
175
|
-
expect(screen.getByText(agent.project)).toBeInTheDocument();
|
|
176
|
-
expect(screen.getByText(agent.taskLabel)).toBeInTheDocument();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('formats runtime duration correctly', async () => {
|
|
181
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
182
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
183
|
-
|
|
184
|
-
const agent = { ...fixtureAgents[0], runtimeSeconds: 125 }; // 2m 5s
|
|
185
|
-
render(<AgentRow agent={agent} />);
|
|
186
|
-
|
|
187
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
188
|
-
await userEvent.click(toggleButton);
|
|
189
|
-
|
|
190
|
-
await waitFor(() => {
|
|
191
|
-
expect(screen.getByText('2m 5s')).toBeInTheDocument();
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('formats token count with commas', async () => {
|
|
196
|
-
const mockFetch = vi.mocked(api.fetchAgent);
|
|
197
|
-
mockFetch.mockResolvedValueOnce(fixtureAgentDetail);
|
|
198
|
-
|
|
199
|
-
const agent = { ...fixtureAgents[0], tokensUsed: 48213 };
|
|
200
|
-
render(<AgentRow agent={agent} />);
|
|
201
|
-
|
|
202
|
-
const toggleButton = screen.getByLabelText(/expand/i);
|
|
203
|
-
await userEvent.click(toggleButton);
|
|
204
|
-
|
|
205
|
-
await waitFor(() => {
|
|
206
|
-
expect(screen.getByText('48,213')).toBeInTheDocument();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
});
|