@matt82198/aesop 0.1.0-beta.4 → 0.1.0-beta.5
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 +46 -5
- package/README.md +57 -1
- package/aesop.config.example.json +8 -1
- package/bin/CLAUDE.md +30 -2
- package/bin/cli.js +286 -73
- package/daemons/CLAUDE.md +4 -1
- package/daemons/run-watchdog.sh +5 -2
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +23 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/collect-signals.mjs +85 -0
- package/package.json +4 -1
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- 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/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +30 -149
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.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__/metrics_gate.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/tools/alert_bridge.py +449 -0
- package/tools/ci_merge_wait.py +121 -8
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/secret_scan.py +8 -1
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_submit_encoding.py +12 -4
- package/ui/CLAUDE.md +57 -41
- 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/agents.py +9 -5
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/submit.py +44 -5
- package/ui/collectors.py +184 -35
- package/ui/config.py +9 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +7 -3
- package/ui/handler.py +254 -4
- package/ui/render.py +26 -6
- package/ui/sse.py +16 -1
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost view tests — composition of CostTable, CostChart, and Scorecard.
|
|
3
|
+
* Tests empty state (has_pricing=false) with configure callout.
|
|
4
|
+
* Tests full state rendering.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import { Cost } from './Cost';
|
|
10
|
+
import { fixtureCost, fixtureCostWithPricing, TESTIDS } from '../test/fixtures';
|
|
11
|
+
|
|
12
|
+
describe('Cost view', () => {
|
|
13
|
+
it('renders with testid', () => {
|
|
14
|
+
render(<Cost cost={fixtureCost} />);
|
|
15
|
+
expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders cost table, chart, and scorecard', () => {
|
|
19
|
+
render(<Cost cost={fixtureCostWithPricing} />);
|
|
20
|
+
expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByTestId(TESTIDS.scorecard)).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('tokens-only mode (has_pricing=false)', () => {
|
|
26
|
+
it('shows "configure pricing" empty-state callout', () => {
|
|
27
|
+
render(<Cost cost={fixtureCost} />);
|
|
28
|
+
const view = screen.getByTestId(TESTIDS.viewCost);
|
|
29
|
+
// Should mention configure or pricing
|
|
30
|
+
expect(view.textContent).toMatch(/configure|pricing|aesop\.config/i);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('callout references aesop.config.json', () => {
|
|
34
|
+
render(<Cost cost={fixtureCost} />);
|
|
35
|
+
const view = screen.getByTestId(TESTIDS.viewCost);
|
|
36
|
+
expect(view.textContent).toContain('aesop.config.json');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('provides instructions for pricing configuration', () => {
|
|
40
|
+
render(<Cost cost={fixtureCost} />);
|
|
41
|
+
const view = screen.getByTestId(TESTIDS.viewCost);
|
|
42
|
+
// Should have some instructional text
|
|
43
|
+
expect(view.textContent?.length).toBeGreaterThan(50);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('still renders table and chart even without pricing', () => {
|
|
47
|
+
render(<Cost cost={fixtureCost} />);
|
|
48
|
+
// Should show token data even without pricing
|
|
49
|
+
expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('pricing mode (has_pricing=true)', () => {
|
|
55
|
+
it('does not show "configure pricing" callout', () => {
|
|
56
|
+
render(<Cost cost={fixtureCostWithPricing} />);
|
|
57
|
+
const view = screen.getByTestId(TESTIDS.viewCost);
|
|
58
|
+
// Should NOT emphasize configuration when pricing exists
|
|
59
|
+
expect(view.textContent).not.toMatch(/configure pricing/i);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders cost columns with dollar amounts', () => {
|
|
63
|
+
render(<Cost cost={fixtureCostWithPricing} />);
|
|
64
|
+
const table = screen.getByTestId(TESTIDS.costTable);
|
|
65
|
+
// Should display pricing info
|
|
66
|
+
expect(table.innerHTML).toContain('$');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders as a proper section element', () => {
|
|
71
|
+
render(<Cost cost={fixtureCost} />);
|
|
72
|
+
const view = screen.getByTestId(TESTIDS.viewCost) as HTMLElement;
|
|
73
|
+
expect(['SECTION', 'DIV'].includes(view.tagName)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('has aria-label or heading describing the view', () => {
|
|
77
|
+
render(<Cost cost={fixtureCost} />);
|
|
78
|
+
const view = screen.getByTestId(TESTIDS.viewCost);
|
|
79
|
+
// Should have descriptive content or aria-label
|
|
80
|
+
expect(view.getAttribute('aria-label') || view.textContent).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('layout is readable with all three components visible', () => {
|
|
84
|
+
render(<Cost cost={fixtureCostWithPricing} />);
|
|
85
|
+
// Should have all three sections rendered
|
|
86
|
+
expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByTestId(TESTIDS.scorecard)).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('scorecard displays verdict statistics', () => {
|
|
92
|
+
render(<Cost cost={fixtureCost} />);
|
|
93
|
+
// Should show total runs count
|
|
94
|
+
expect(screen.getByTestId(TESTIDS.scorecard).textContent).toContain('142');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('chart shows per-day trend', () => {
|
|
98
|
+
render(<Cost cost={fixtureCost} />);
|
|
99
|
+
const chart = screen.getByTestId(TESTIDS.costChart);
|
|
100
|
+
// Should render 6 bars for the 3 days (2 per day: in/out)
|
|
101
|
+
const bars = chart.querySelectorAll('rect[data-day]');
|
|
102
|
+
expect(bars.length).toBe(6);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('table shows per-model breakdown', () => {
|
|
106
|
+
render(<Cost cost={fixtureCost} />);
|
|
107
|
+
const table = screen.getByTestId(TESTIDS.costTable);
|
|
108
|
+
// Should list haiku and sonnet models
|
|
109
|
+
expect(table.textContent).toContain('haiku');
|
|
110
|
+
expect(table.textContent).toContain('sonnet');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles cost object with skipped_lines footnote', () => {
|
|
114
|
+
render(<Cost cost={fixtureCost} />);
|
|
115
|
+
// scorecard mentions skipped lines when > 0
|
|
116
|
+
// At least the component renders without error
|
|
117
|
+
expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('empty cost (0 runs) still renders all panels', () => {
|
|
121
|
+
const empty = {
|
|
122
|
+
...fixtureCost,
|
|
123
|
+
models: {},
|
|
124
|
+
daily_totals: {},
|
|
125
|
+
overall_scorecard: {
|
|
126
|
+
total_runs: 0,
|
|
127
|
+
ok_count: 0,
|
|
128
|
+
failed_count: 0,
|
|
129
|
+
empty_count: 0,
|
|
130
|
+
hung_count: 0,
|
|
131
|
+
ok_rate: 0,
|
|
132
|
+
failed_rate: 0,
|
|
133
|
+
empty_rate: 0,
|
|
134
|
+
hung_rate: 0,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
render(<Cost cost={empty} />);
|
|
138
|
+
expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost view — composition of cost table, chart, and scorecard.
|
|
3
|
+
* Shows per-model breakdowns, per-day trends, and verdict quality metrics.
|
|
4
|
+
* When has_pricing=false, shows a "configure pricing" empty-state callout.
|
|
5
|
+
* When has_pricing=true, displays dollar estimates alongside tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CostSummary } from '../lib/types';
|
|
9
|
+
import { CostTable } from '../components/CostTable';
|
|
10
|
+
import { CostChart } from '../components/CostChart';
|
|
11
|
+
import { Scorecard } from '../components/Scorecard';
|
|
12
|
+
import { TESTIDS } from '../test/fixtures';
|
|
13
|
+
import './Cost.css';
|
|
14
|
+
|
|
15
|
+
interface CostProps {
|
|
16
|
+
cost: CostSummary;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Cost({ cost }: CostProps) {
|
|
20
|
+
return (
|
|
21
|
+
<section className="view-cost" data-testid={TESTIDS.viewCost} aria-label="Cost analytics">
|
|
22
|
+
<h2>Cost Analytics</h2>
|
|
23
|
+
|
|
24
|
+
{!cost.has_pricing && (
|
|
25
|
+
<div className="cost-callout cost-callout--info" role="status">
|
|
26
|
+
<h3>Configure Pricing</h3>
|
|
27
|
+
<p>
|
|
28
|
+
To see cost estimates, add a <code>pricing</code> map to your{' '}
|
|
29
|
+
<code>aesop.config.json</code> with per-model input and output rates (e.g.{' '}
|
|
30
|
+
<code>{'{input: 0.003, output: 0.015}'}</code>). Without pricing, token counts are
|
|
31
|
+
shown; no estimates are computed.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
<div className="cost-layout">
|
|
37
|
+
<div className="cost-section">
|
|
38
|
+
<h3>By Model</h3>
|
|
39
|
+
<CostTable cost={cost} />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div className="cost-section">
|
|
43
|
+
<h3>Daily Trend</h3>
|
|
44
|
+
<CostChart cost={cost} />
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="cost-section">
|
|
48
|
+
<h3>Quality Scorecard</h3>
|
|
49
|
+
<Scorecard cost={cost} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.overview {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-6);
|
|
5
|
+
padding: var(--space-6);
|
|
6
|
+
max-width: var(--content-max-width);
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.overview__section {
|
|
12
|
+
display: flex;
|
|
13
|
+
gap: var(--space-5);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.overview__section--full {
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.overview__section--row {
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: repeat(3, 1fr);
|
|
23
|
+
gap: var(--space-5);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (max-width: 1200px) {
|
|
27
|
+
.overview__section--row {
|
|
28
|
+
grid-template-columns: repeat(2, 1fr);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 768px) {
|
|
33
|
+
.overview {
|
|
34
|
+
gap: var(--space-4);
|
|
35
|
+
padding: var(--space-4);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.overview__section {
|
|
39
|
+
gap: var(--space-3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.overview__section--row {
|
|
43
|
+
grid-template-columns: 1fr;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (prefers-reduced-motion: reduce) {
|
|
48
|
+
.overview {
|
|
49
|
+
scroll-behavior: auto;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Overview } from './Overview';
|
|
4
|
+
import {
|
|
5
|
+
fixtureAgents,
|
|
6
|
+
fixtureAlerts,
|
|
7
|
+
fixtureEvents,
|
|
8
|
+
fixtureRepos,
|
|
9
|
+
TESTIDS,
|
|
10
|
+
} from '../test/fixtures';
|
|
11
|
+
|
|
12
|
+
vi.mock('../components/AgentsPanel', () => ({
|
|
13
|
+
AgentsPanel: () => <div data-testid="agents-panel-mock">AgentsPanel</div>,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../components/AlertsPanel', () => ({
|
|
17
|
+
AlertsPanel: () => <div data-testid="alerts-panel-mock">AlertsPanel</div>,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../components/EventsFeed', () => ({
|
|
21
|
+
EventsFeed: () => <div data-testid="events-feed-mock">EventsFeed</div>,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../components/ReposPanel', () => ({
|
|
25
|
+
ReposPanel: () => <div data-testid="repos-panel-mock">ReposPanel</div>,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../components/InboxForm', () => ({
|
|
29
|
+
InboxForm: () => <div data-testid="inbox-form-mock">InboxForm</div>,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('Overview', () => {
|
|
33
|
+
it('renders overview view with all components', () => {
|
|
34
|
+
render(
|
|
35
|
+
<Overview
|
|
36
|
+
agents={fixtureAgents}
|
|
37
|
+
alerts={fixtureAlerts}
|
|
38
|
+
events={fixtureEvents}
|
|
39
|
+
repos={fixtureRepos}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByTestId('agents-panel-mock')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByTestId('alerts-panel-mock')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByTestId('events-feed-mock')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByTestId('repos-panel-mock')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByTestId('inbox-form-mock')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles null values gracefully', () => {
|
|
52
|
+
render(
|
|
53
|
+
<Overview
|
|
54
|
+
agents={null}
|
|
55
|
+
alerts={null}
|
|
56
|
+
events={null}
|
|
57
|
+
repos={null}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders with empty arrays', () => {
|
|
65
|
+
render(
|
|
66
|
+
<Overview
|
|
67
|
+
agents={[]}
|
|
68
|
+
alerts={{ count: 0, lines: [] }}
|
|
69
|
+
events={[]}
|
|
70
|
+
repos={[]}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview view — main dashboard combining all overview components.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - Top: AgentsPanel (full width)
|
|
6
|
+
* - Middle row: AlertsPanel | EventsFeed | ReposPanel
|
|
7
|
+
* - Bottom: InboxForm
|
|
8
|
+
*
|
|
9
|
+
* Props passed from App.tsx via SSE state.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Agent, Alert } from '../lib/types';
|
|
13
|
+
import { AgentsPanel } from '../components/AgentsPanel';
|
|
14
|
+
import { AlertsPanel } from '../components/AlertsPanel';
|
|
15
|
+
import { EventsFeed } from '../components/EventsFeed';
|
|
16
|
+
import { ReposPanel } from '../components/ReposPanel';
|
|
17
|
+
import { InboxForm } from '../components/InboxForm';
|
|
18
|
+
import { TESTIDS } from '../test/fixtures';
|
|
19
|
+
import './Overview.css';
|
|
20
|
+
|
|
21
|
+
interface OverviewProps {
|
|
22
|
+
agents: Agent[] | null;
|
|
23
|
+
alerts: Alert | null;
|
|
24
|
+
events: string[] | null;
|
|
25
|
+
repos: any[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Overview({ agents, alerts, events, repos }: OverviewProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="overview" data-testid={TESTIDS.viewOverview}>
|
|
31
|
+
<section className="overview__section overview__section--full">
|
|
32
|
+
<AgentsPanel agents={agents} />
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<section className="overview__section overview__section--row">
|
|
36
|
+
<AlertsPanel alerts={alerts} />
|
|
37
|
+
<EventsFeed events={events} />
|
|
38
|
+
<ReposPanel repos={repos} />
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section className="overview__section overview__section--full">
|
|
42
|
+
<InboxForm />
|
|
43
|
+
</section>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Work } from './Work';
|
|
4
|
+
import { fixtureTrackerItems, fixtureBacklog, TESTIDS } from '../test/fixtures';
|
|
5
|
+
|
|
6
|
+
// Mock useSSE hook
|
|
7
|
+
vi.mock('../lib/useSSE', () => ({
|
|
8
|
+
useSSE: () => ({
|
|
9
|
+
tracker: { items: fixtureTrackerItems },
|
|
10
|
+
backlog: fixtureBacklog,
|
|
11
|
+
data: null,
|
|
12
|
+
agents: null,
|
|
13
|
+
status: null,
|
|
14
|
+
cost: null,
|
|
15
|
+
connectionStatus: { status: 'live' as const },
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('Work view', () => {
|
|
20
|
+
it('renders the work view with correct testid', () => {
|
|
21
|
+
render(<Work />);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByTestId(TESTIDS.viewWork)).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders tracker board heading', () => {
|
|
27
|
+
render(<Work />);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('Tracker Kanban')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders the add item button', () => {
|
|
33
|
+
render(<Work />);
|
|
34
|
+
|
|
35
|
+
const addButton = screen.getByRole('button', { name: /Add Item/ });
|
|
36
|
+
expect(addButton).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('shows form when add item button is clicked', () => {
|
|
40
|
+
render(<Work />);
|
|
41
|
+
|
|
42
|
+
const addButton = screen.getByRole('button', { name: /Add Item/ });
|
|
43
|
+
expect(addButton).toHaveAttribute('aria-expanded', 'false');
|
|
44
|
+
|
|
45
|
+
// Form inputs should not be visible initially
|
|
46
|
+
expect(screen.queryByLabelText('Title')).not.toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('displays tracker board with items', () => {
|
|
50
|
+
render(<Work />);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByTestId(TESTIDS.trackerBoard)).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('displays backlog panel', () => {
|
|
56
|
+
render(<Work />);
|
|
57
|
+
|
|
58
|
+
expect(screen.getByTestId(TESTIDS.backlogPanel)).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders all tracker lanes', () => {
|
|
62
|
+
render(<Work />);
|
|
63
|
+
|
|
64
|
+
// Be specific to avoid duplicate "Done" matching
|
|
65
|
+
// Lane headers include counts, e.g., "Proposed1", so check by prefix
|
|
66
|
+
const sections = screen.getAllByRole('heading', { level: 2 });
|
|
67
|
+
const laneTexts = sections.map((s) => s.textContent || '');
|
|
68
|
+
|
|
69
|
+
expect(laneTexts.some((t) => t.startsWith('Proposed'))).toBe(true);
|
|
70
|
+
expect(laneTexts.some((t) => t.startsWith('Ranked'))).toBe(true);
|
|
71
|
+
expect(laneTexts.some((t) => t.startsWith('In Progress'))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders backlog audit tier headers', () => {
|
|
75
|
+
render(<Work />);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText('Audit Backlog')).toBeInTheDocument();
|
|
78
|
+
// Use context to avoid matching P0 in tracker cards
|
|
79
|
+
const backlogPanel = screen.getByTestId(TESTIDS.backlogPanel);
|
|
80
|
+
expect(backlogPanel.textContent).toContain('P0');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work view — tracker kanban board + backlog panel + form.
|
|
3
|
+
* Binds TrackerBoard, TrackerCard, TrackerForm, BacklogPanel.
|
|
4
|
+
* Reads tracker + backlog from SSE, allows mutations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useSSE } from '../lib/useSSE';
|
|
9
|
+
import { TrackerBoard } from '../components/TrackerBoard';
|
|
10
|
+
import { TrackerForm } from '../components/TrackerForm';
|
|
11
|
+
import { BacklogPanel } from '../components/BacklogPanel';
|
|
12
|
+
import { TESTIDS } from '../test/fixtures';
|
|
13
|
+
import type { TrackerItem, AuditBacklog } from '../lib/types';
|
|
14
|
+
import '../styles/work.css';
|
|
15
|
+
|
|
16
|
+
export function Work() {
|
|
17
|
+
const sse = useSSE();
|
|
18
|
+
const [trackerItems, setTrackerItems] = useState<TrackerItem[]>([]);
|
|
19
|
+
const [backlog, setBacklog] = useState<AuditBacklog | null>(null);
|
|
20
|
+
const [showForm, setShowForm] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (sse.tracker?.items !== undefined && sse.tracker?.items !== null) {
|
|
24
|
+
setTrackerItems(sse.tracker.items);
|
|
25
|
+
} else if (sse.tracker === null) {
|
|
26
|
+
// Treat null items as empty array
|
|
27
|
+
setTrackerItems([]);
|
|
28
|
+
}
|
|
29
|
+
}, [sse.tracker]);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (sse.backlog) {
|
|
33
|
+
setBacklog(sse.backlog);
|
|
34
|
+
}
|
|
35
|
+
}, [sse.backlog]);
|
|
36
|
+
|
|
37
|
+
const handleItemUpdate = (updated: TrackerItem) => {
|
|
38
|
+
setTrackerItems((prev) =>
|
|
39
|
+
prev.map((item) => (item.id === updated.id ? updated : item))
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleFormSuccess = () => {
|
|
44
|
+
setShowForm(false);
|
|
45
|
+
// Tracker board will update via SSE
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<section className="work-view" data-testid={TESTIDS.viewWork} aria-label="Work view">
|
|
50
|
+
<div className="work-container">
|
|
51
|
+
<div className="work-board">
|
|
52
|
+
<div className="board-header">
|
|
53
|
+
<h2>Tracker Kanban</h2>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className="add-item-button"
|
|
57
|
+
onClick={() => setShowForm(!showForm)}
|
|
58
|
+
aria-expanded={showForm}
|
|
59
|
+
>
|
|
60
|
+
{showForm ? 'Cancel' : '+ Add Item'}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{showForm && (
|
|
65
|
+
<div className="form-container">
|
|
66
|
+
<TrackerForm onSuccess={handleFormSuccess} />
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<TrackerBoard items={trackerItems} onUpdate={handleItemUpdate} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<aside className="work-sidebar">
|
|
74
|
+
<BacklogPanel backlog={backlog} />
|
|
75
|
+
</aside>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface Window {
|
|
4
|
+
/**
|
|
5
|
+
* CSRF token injected by ui/render.py's sentinel substitution in the built
|
|
6
|
+
* index.html; absent on the raw Vite dev server (api.ts then falls back to
|
|
7
|
+
* GET /api/session).
|
|
8
|
+
*/
|
|
9
|
+
__AESOP_CSRF_TOKEN__?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"jsxImportSource": "react",
|
|
16
|
+
"moduleResolution": "bundler",
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"],
|
|
21
|
+
"exclude": ["node_modules"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: 5173,
|
|
8
|
+
proxy: {
|
|
9
|
+
'/data': 'http://localhost:8770',
|
|
10
|
+
'/api': 'http://localhost:8770',
|
|
11
|
+
'/agent': 'http://localhost:8770',
|
|
12
|
+
'/events': 'http://localhost:8770',
|
|
13
|
+
'/submit': 'http://localhost:8770',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
build: {
|
|
17
|
+
outDir: 'dist',
|
|
18
|
+
sourcemap: false,
|
|
19
|
+
rollupOptions: {
|
|
20
|
+
output: {
|
|
21
|
+
manualChunks: undefined,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
test: {
|
|
7
|
+
environment: 'jsdom',
|
|
8
|
+
setupFiles: ['src/test/setup.ts'],
|
|
9
|
+
globals: true,
|
|
10
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
11
|
+
},
|
|
12
|
+
});
|