amicus 4.3.0 → 4.4.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +64 -0
- package/README.md +6 -3
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +200 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +371 -0
- package/docs/council.md +911 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +55 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +699 -0
- package/electron/fold.js +1 -1
- package/electron/ipc-workspace.js +283 -0
- package/electron/main.js +31 -1
- package/electron/preload-workspace.js +40 -0
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-shell.js +85 -0
- package/electron/workspace-ui/index.html +111 -0
- package/electron/workspace-ui/live-model.js +112 -0
- package/electron/workspace-ui/md-lite.js +163 -0
- package/electron/workspace-ui/workspace-app.js +240 -0
- package/electron/workspace-ui/workspace-matrix.js +249 -0
- package/electron/workspace-ui/workspace-panels.js +237 -0
- package/electron/workspace-ui/workspace-render.js +277 -0
- package/electron/workspace-ui/workspace-verbs.js +293 -0
- package/electron/workspace-ui/workspace.css +172 -0
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +25 -1
- package/schemas/council-run.schema.json +34 -0
- package/schemas/progress.schema.json +26 -1
- package/schemas/spend.schema.json +52 -4
- package/skills/second-opinion/MODEL-NOTES.md +53 -5
- package/src/cli-handlers-council-run.js +25 -3
- package/src/cli-handlers-spend.js +50 -5
- package/src/cli-handlers-watch.js +48 -10
- package/src/cli.js +4 -2
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +59 -3
- package/src/council/findings.js +236 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +277 -0
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +73 -7
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +148 -113
- package/src/council/run-state.js +23 -1
- package/src/council/run.js +52 -53
- package/src/council/tally.js +10 -0
- package/src/headless.js +519 -17
- package/src/mcp-council-awareness.js +53 -3
- package/src/observe/council-legs.js +240 -0
- package/src/observe/live-doc.js +39 -4
- package/src/observe/watch-render.js +23 -1
- package/src/opencode-client.js +15 -3
- package/src/sidecar/child-sessions.js +197 -0
- package/src/sidecar/conversation-mirror.js +111 -37
- package/src/sidecar/fanout-budget.js +71 -0
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +29 -1
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +65 -81
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +8 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/sidecar/tool-part.js +196 -0
- package/src/sidecar/workspace-window.js +62 -0
- package/src/spend-query.js +33 -6
- package/src/utils/env-num.js +42 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +120 -0
- package/src/utils/pricing.js +114 -9
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +208 -0
- package/src/workspace/blind-mode.js +32 -0
- package/src/workspace/fold-format.js +124 -0
- package/src/workspace/live-normalize.js +169 -0
- package/src/workspace/matrix-model.js +94 -0
- package/src/workspace/run-detail.js +229 -0
- package/src/workspace/run-scan.js +148 -0
package/docs/testing.md
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to Amicus's test infrastructure, covering unit tests, integration tests, E2E tests, and the agentic eval system.
|
|
4
|
+
|
|
5
|
+
## Test Architecture
|
|
6
|
+
|
|
7
|
+
Amicus uses a three-tier testing strategy plus an eval system:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Tier 1: Unit Tests (mocked) ~1200 tests, <2 min
|
|
11
|
+
└─ Business logic, parsing, validation, session management
|
|
12
|
+
|
|
13
|
+
Tier 2: Integration Tests (source) ~30 tests, <5 sec
|
|
14
|
+
└─ Source-level verification, module wiring, config checks
|
|
15
|
+
|
|
16
|
+
Tier 3: E2E Tests (real LLM) ~15 tests, ~3 min each
|
|
17
|
+
└─ CLI headless, MCP headless, Electron CDP
|
|
18
|
+
└─ Requires OPENROUTER_API_KEY, skipped when missing
|
|
19
|
+
|
|
20
|
+
Eval System: Agentic Evals ~3 scenarios, ~5 min each
|
|
21
|
+
└─ Full Claude Code + Amicus interaction grading
|
|
22
|
+
└─ Programmatic checks + LLM-as-judge scoring
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Quick Reference
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm test # Unit tests only (*.integration.test.js excluded by jest.config.js)
|
|
29
|
+
npm test tests/context.test.js # Single file (preferred during dev)
|
|
30
|
+
npm test -- --coverage # Coverage report
|
|
31
|
+
npm test -- -t "should extract" # Run tests matching pattern
|
|
32
|
+
|
|
33
|
+
npm run test:integration # Integration tier, KEYLESS — credentials scrubbed, paid suites skip (free, ~10s)
|
|
34
|
+
npm run test:integration:live # Integration tier with real keys — SPENDS MONEY (serial: --runInBand is baked in)
|
|
35
|
+
npm run test:all # Unit + integration with real keys — SPENDS MONEY (not a gate anywhere)
|
|
36
|
+
npm run test:e2e:mcp # MCP E2E with real repomix (requires OPENROUTER_API_KEY)
|
|
37
|
+
npm run lint # ESLint on src/
|
|
38
|
+
|
|
39
|
+
# Run individual E2E test files (require OPENROUTER_API_KEY)
|
|
40
|
+
npm test tests/cli-headless-e2e.integration.test.js
|
|
41
|
+
npm test tests/mcp-headless-e2e.integration.test.js
|
|
42
|
+
npm test tests/electron-toolbar-e2e.integration.test.js
|
|
43
|
+
|
|
44
|
+
# Evals
|
|
45
|
+
node evals/run_eval.js --eval-id 1
|
|
46
|
+
node evals/run_eval.js --all --dry-run
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Tier 1: Unit Tests
|
|
52
|
+
|
|
53
|
+
Unit tests mock all external dependencies (OpenCode SDK, filesystem, network) and run fast. They form the bulk of the test suite.
|
|
54
|
+
|
|
55
|
+
### What to Unit Test
|
|
56
|
+
|
|
57
|
+
| Area | Example Files | Focus |
|
|
58
|
+
|------|--------------|-------|
|
|
59
|
+
| CLI parsing | `cli.test.js` | Command validation, flag handling, error messages |
|
|
60
|
+
| Context filtering | `context.test.js` | Turn extraction, token estimation, JSONL parsing |
|
|
61
|
+
| Session management | `session-manager.test.js` | CRUD operations, metadata persistence |
|
|
62
|
+
| Prompt construction | `prompt-builder.test.js` | Template assembly, mode-specific prompts |
|
|
63
|
+
| Conflict detection | `conflict.test.js` | mtime comparison, warning formatting |
|
|
64
|
+
| Drift calculation | `drift.test.js` | Staleness scoring, turn counting |
|
|
65
|
+
| Headless mode | `headless.test.js` | Polling logic, fold marker detection, timeout |
|
|
66
|
+
| MCP tools | `mcp-tools.test.js`, `mcp-server.test.js` | Zod schemas, tool handlers |
|
|
67
|
+
| Session operations | `sidecar/*.test.js` | Start, resume, continue, read, context-builder |
|
|
68
|
+
| Config/utils | Various `utils/*.test.js` | Agent mapping, config loading, validation |
|
|
69
|
+
|
|
70
|
+
### What NOT to Unit Test
|
|
71
|
+
|
|
72
|
+
Do not write unit tests for:
|
|
73
|
+
- DOM manipulation in `renderer.js`
|
|
74
|
+
- UI picker components (`model-picker.js`, `mode-picker.js`)
|
|
75
|
+
- Electron window configuration (`main.js`)
|
|
76
|
+
- CSS class assignments and styling
|
|
77
|
+
|
|
78
|
+
DOM mock tests are ineffective. They test mock behavior, not real rendering. Use CDP E2E tests for UI verification instead.
|
|
79
|
+
|
|
80
|
+
### Mocking Patterns
|
|
81
|
+
|
|
82
|
+
The codebase uses Jest's `jest.mock()` for external dependencies:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Mock the OpenCode SDK (used in headless.test.js, e2e.test.js)
|
|
86
|
+
jest.mock('../src/opencode-client', () => ({
|
|
87
|
+
startServer: jest.fn(),
|
|
88
|
+
createSession: jest.fn(),
|
|
89
|
+
sendPromptAsync: jest.fn(),
|
|
90
|
+
getMessages: jest.fn(),
|
|
91
|
+
checkHealth: jest.fn(),
|
|
92
|
+
}));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Key rule:** The OpenCode SDK uses ESM dynamic imports (`await import()`) which fail under Jest without `--experimental-vm-modules`. Always mock the SDK in unit tests. For E2E tests that need a real server, use `tests/helpers/start-server.js` (runs in a separate Node.js process).
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Tier 2: Integration Tests
|
|
100
|
+
|
|
101
|
+
Integration tests verify source-level invariants without mocking. They read actual source files to assert code structure, ensuring critical patterns aren't accidentally removed.
|
|
102
|
+
|
|
103
|
+
| Test File | What It Verifies |
|
|
104
|
+
|-----------|-----------------|
|
|
105
|
+
| `spawn-pipe-deadlock.integration.test.js` | `spawnSidecarProcess()` in `src/mcp-server.js` uses `ignore` (not `pipe`) for stdio, no `detached: true`, uses `child.unref()` |
|
|
106
|
+
| `electron-headless-mode.test.js` | `electron/main.js` gates `mainWindow.show()` behind `AMICUS_HEADLESS_TEST` env var |
|
|
107
|
+
|
|
108
|
+
These tests catch regressions in critical spawn/process configuration that would be hard to debug in production.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Tier 3: E2E Tests
|
|
113
|
+
|
|
114
|
+
E2E tests spawn real processes, call real LLMs, and verify end-to-end behavior. They require `OPENROUTER_API_KEY` and are automatically skipped when the key is missing.
|
|
115
|
+
|
|
116
|
+
### Skip Behavior
|
|
117
|
+
|
|
118
|
+
All E2E tests use this pattern:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const HAS_API_KEY = !!(
|
|
122
|
+
process.env.OPENROUTER_API_KEY ||
|
|
123
|
+
(() => {
|
|
124
|
+
try {
|
|
125
|
+
const envPath = path.join(os.homedir(), '.config', 'amicus', '.env');
|
|
126
|
+
return fs.readFileSync(envPath, 'utf-8').includes('OPENROUTER_API_KEY=');
|
|
127
|
+
} catch { return false; }
|
|
128
|
+
})()
|
|
129
|
+
);
|
|
130
|
+
const describeE2E = HAS_API_KEY ? describe : describe.skip;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The key can be in the environment or in `~/.config/amicus/.env`.
|
|
134
|
+
|
|
135
|
+
### CLI Headless E2E (`cli-headless-e2e.integration.test.js`)
|
|
136
|
+
|
|
137
|
+
Spawns the real `amicus` CLI binary with `start --no-ui` and verifies the full headless lifecycle.
|
|
138
|
+
|
|
139
|
+
**What it tests:**
|
|
140
|
+
1. `start --no-ui` runs to completion with real LLM
|
|
141
|
+
2. Session files created on disk (metadata.json, summary.md, initial_context.md)
|
|
142
|
+
3. `list` command shows the completed session
|
|
143
|
+
4. `read` command returns the summary
|
|
144
|
+
5. `read --metadata` returns valid JSON metadata
|
|
145
|
+
|
|
146
|
+
**Architecture:**
|
|
147
|
+
```
|
|
148
|
+
Test process
|
|
149
|
+
└─ spawn(node, [amicus.js, start, --no-ui, ...])
|
|
150
|
+
└─ OpenCode server (auto port)
|
|
151
|
+
└─ Real LLM call (gemini)
|
|
152
|
+
└─ Session files written to tmpDir
|
|
153
|
+
└─ spawn(node, [amicus.js, list, ...]) // verify list
|
|
154
|
+
└─ spawn(node, [amicus.js, read, ...]) // verify read
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### MCP Headless E2E (`mcp-headless-e2e.integration.test.js`)
|
|
158
|
+
|
|
159
|
+
Spawns a real MCP server over stdio, sends JSON-RPC tool calls, and verifies the full MCP lifecycle.
|
|
160
|
+
|
|
161
|
+
**What it tests:**
|
|
162
|
+
1. `amicus_start` with `noUi: true` launches a headless session
|
|
163
|
+
2. `amicus_status` polling until completion
|
|
164
|
+
3. `amicus_read` returns the summary
|
|
165
|
+
4. `amicus_list` shows the completed session
|
|
166
|
+
5. Session files exist on disk with correct metadata
|
|
167
|
+
|
|
168
|
+
**Architecture:**
|
|
169
|
+
```
|
|
170
|
+
Test process
|
|
171
|
+
└─ spawn(node, [amicus.js, mcp]) // MCP server over stdio
|
|
172
|
+
├─ JSON-RPC: initialize
|
|
173
|
+
├─ JSON-RPC: tools/call (amicus_start)
|
|
174
|
+
│ └─ OpenCode server (auto port)
|
|
175
|
+
│ └─ Real LLM call
|
|
176
|
+
├─ JSON-RPC: tools/call (amicus_status) // poll loop
|
|
177
|
+
├─ JSON-RPC: tools/call (amicus_read)
|
|
178
|
+
└─ JSON-RPC: tools/call (amicus_list)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Electron CDP E2E (`electron-toolbar-e2e.integration.test.js`)
|
|
182
|
+
|
|
183
|
+
Spawns a real Electron window (hidden) with a real OpenCode server, connects via Chrome DevTools Protocol, and asserts toolbar DOM state.
|
|
184
|
+
|
|
185
|
+
**What it tests:**
|
|
186
|
+
1. Brand name renders ("Amicus")
|
|
187
|
+
2. Task ID displayed in toolbar
|
|
188
|
+
3. Timer ticks (changes after 2 seconds)
|
|
189
|
+
4. Fold button exists with shortcut label
|
|
190
|
+
5. Settings gear button exists
|
|
191
|
+
6. Update banner hidden by default
|
|
192
|
+
7. Update banner visible when `AMICUS_MOCK_UPDATE=available` (the legacy `SIDECAR_MOCK_UPDATE` name was removed in v2.0.0)
|
|
193
|
+
8. Screenshots captured as PNG files
|
|
194
|
+
|
|
195
|
+
**Architecture:**
|
|
196
|
+
```
|
|
197
|
+
Test process
|
|
198
|
+
├─ spawn(node, [start-server.js]) // Real OpenCode server (separate process)
|
|
199
|
+
│ └─ Outputs { port, sessionId }
|
|
200
|
+
├─ spawn(electron, [main.js]) // Hidden Electron window
|
|
201
|
+
│ ├─ BrowserView → http://localhost:<port> (OpenCode UI)
|
|
202
|
+
│ └─ Main window → data:text/html (toolbar)
|
|
203
|
+
└─ CdpClient.toolbar(9224) // CDP WebSocket connection
|
|
204
|
+
├─ Runtime.evaluate(...) // DOM assertions
|
|
205
|
+
└─ Page.captureScreenshot(...) // Screenshot capture
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## CDP Helper (`tests/helpers/cdp-client.js`)
|
|
211
|
+
|
|
212
|
+
Thin class (~190 lines) wrapping `ws` + `http` for Chrome DevTools Protocol communication. No external dependencies beyond `ws` (already a project dependency).
|
|
213
|
+
|
|
214
|
+
### API
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
const { CdpClient } = require('./helpers/cdp-client');
|
|
218
|
+
|
|
219
|
+
// Factory methods (with retry/polling)
|
|
220
|
+
const cdp = await CdpClient.toolbar(port, timeoutMs); // data: URL target
|
|
221
|
+
const cdp = await CdpClient.content(port, timeoutMs); // http://localhost target
|
|
222
|
+
|
|
223
|
+
// Core methods
|
|
224
|
+
const targets = await cdp.getTargets(); // GET /json
|
|
225
|
+
const target = await cdp.findTarget(t => t.url.startsWith('data:'));
|
|
226
|
+
await cdp.connect(targetId); // WebSocket
|
|
227
|
+
const value = await cdp.evaluate('document.title'); // Runtime.evaluate
|
|
228
|
+
await cdp.waitForSelector('.brand', 10000); // Poll until element exists
|
|
229
|
+
await cdp.screenshot('/tmp/toolbar.png'); // Page.captureScreenshot
|
|
230
|
+
cdp.close(); // Cleanup
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Electron Debug Targets
|
|
234
|
+
|
|
235
|
+
Electron creates two CDP targets per window:
|
|
236
|
+
|
|
237
|
+
| Target | URL Pattern | Contains |
|
|
238
|
+
|--------|-------------|----------|
|
|
239
|
+
| **Content** | `http://localhost:<port>` | OpenCode web UI (BrowserView) |
|
|
240
|
+
| **Toolbar** | `data:text/html,...` | Amicus toolbar (brand, timer, fold button) |
|
|
241
|
+
|
|
242
|
+
Use `CdpClient.toolbar()` or `CdpClient.content()` to connect to the right one.
|
|
243
|
+
|
|
244
|
+
### Server Helper (`tests/helpers/start-server.js`)
|
|
245
|
+
|
|
246
|
+
Starts a real OpenCode server in a separate Node.js process, working around Jest's inability to handle ESM dynamic imports. Outputs `{ port, sessionId }` as JSON on stdout, then stays alive until killed.
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
const child = spawn(process.execPath, ['tests/helpers/start-server.js']);
|
|
250
|
+
// Parse JSON from stdout to get { port, sessionId }
|
|
251
|
+
// Kill child when done to stop the server
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Environment Variables for Testing
|
|
257
|
+
|
|
258
|
+
### Required for E2E Tests
|
|
259
|
+
|
|
260
|
+
| Variable | Purpose |
|
|
261
|
+
|----------|---------|
|
|
262
|
+
| `OPENROUTER_API_KEY` | API key for real LLM calls. Without it, E2E tests are skipped. Can also be in `~/.config/amicus/.env` |
|
|
263
|
+
|
|
264
|
+
### Test Infrastructure Variables
|
|
265
|
+
|
|
266
|
+
| Variable | Default | Purpose |
|
|
267
|
+
|----------|---------|---------|
|
|
268
|
+
| `AMICUS_HEADLESS_TEST` | unset | Set to `1` to suppress `mainWindow.show()` in Electron. Window is created but never made visible. CDP screenshots still work (captures off-screen renderer). |
|
|
269
|
+
| `AMICUS_DEBUG_PORT` | `9222` | CDP remote debugging port (the legacy `SIDECAR_DEBUG_PORT` name was removed in v2.0.0 — see [docs/SHIMS.md](./SHIMS.md)). Use `9223`+ to avoid conflicts with Chrome browser. E2E tests use `9224`. |
|
|
270
|
+
| `AMICUS_MOCK_UPDATE` | unset | Mock update banner state: `available`, `updating`, `success`, `error`. The legacy `SIDECAR_MOCK_UPDATE` name was removed in v2.0.0. Used in Electron toolbar E2E tests. |
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Cross-Platform: macOS / Linux / Windows
|
|
275
|
+
|
|
276
|
+
### macOS
|
|
277
|
+
|
|
278
|
+
No extra dependencies needed. Electron runs natively. The window is created with `show: false` when `AMICUS_HEADLESS_TEST=1`, so no visible window pops up. CDP screenshots capture the off-screen renderer via `Page.captureScreenshot`.
|
|
279
|
+
|
|
280
|
+
The visual testing docs reference `screencapture -x` and AppleScript window positioning — those are macOS-specific tools. See the Windows section for the cross-platform equivalent.
|
|
281
|
+
|
|
282
|
+
### Linux (VPS / CI)
|
|
283
|
+
|
|
284
|
+
Electron requires an X server to create a renderer, even with `show: false`. The E2E tests auto-detect headless Linux and manage Xvfb:
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
function ensureDisplay() {
|
|
288
|
+
if (process.platform !== 'linux' || process.env.DISPLAY) {
|
|
289
|
+
return { display: process.env.DISPLAY, cleanup: () => {} };
|
|
290
|
+
}
|
|
291
|
+
// Auto-launch Xvfb on :99
|
|
292
|
+
const xvfbProcess = spawn('Xvfb', [':99', '-screen', '0', '1280x720x24']);
|
|
293
|
+
return { display: ':99', cleanup: () => xvfbProcess.kill() };
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Prerequisites for Linux CI:**
|
|
298
|
+
```bash
|
|
299
|
+
apt-get install -y xvfb libgtk-3-0 libnotify4 libnss3 libxss1 libasound2
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Windows 11
|
|
303
|
+
|
|
304
|
+
The unit suite runs fully green on Windows (verified F2). A few platform-specific details:
|
|
305
|
+
|
|
306
|
+
**Path encoding (`src/session.js`, `src/environment.js`):** `encodeProjectPath` / `encodePath` replace `/`, `\`, `:`, and `_` with dashes. On Windows, `C:\Users\x` encodes to `C--Users-x` — the drive-colon and both backslashes each become a dash. This matches Claude Code's directory naming behavior.
|
|
307
|
+
|
|
308
|
+
**OpenCode binary on PATH (`src/utils/path-setup.js`):** Node's `spawn()` without `shell: true` cannot run `.cmd` shims. `ensureNodeModulesBinInPath()` adds `opencode-windows-x64/bin` and `opencode-windows-x64-baseline/bin` to `PATH` before spawning, so `opencode.exe` resolves directly. The baseline variant supports pre-AVX2 CPUs; normal-before-baseline order in `PATH` is intentional.
|
|
309
|
+
|
|
310
|
+
**Screenshots:** `screencapture` is macOS-only. On Windows, screenshots in E2E tests still work because they go through CDP `Page.captureScreenshot` (rendered off-screen). For manual visual inspection use:
|
|
311
|
+
```powershell
|
|
312
|
+
# Check window visibility
|
|
313
|
+
Get-Process electron | Select-Object MainWindowTitle, MainWindowHandle
|
|
314
|
+
# CDP screenshot (cross-platform — use the CdpClient helper)
|
|
315
|
+
const cdp = await CdpClient.toolbar(9223);
|
|
316
|
+
await cdp.screenshot('C:\\tmp\\amicus-test.png');
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**X server:** Not required. Electron on Windows creates a renderer natively without a display server. The `ensureDisplay()` / Xvfb logic in E2E tests is Linux-only and is skipped on `win32`.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Screenshots
|
|
324
|
+
|
|
325
|
+
CDP E2E tests capture screenshots to `tests/screenshots/` (gitignored). Screenshots are PNG files generated via `Page.captureScreenshot`:
|
|
326
|
+
|
|
327
|
+
| Screenshot | Generated By | Shows |
|
|
328
|
+
|------------|-------------|-------|
|
|
329
|
+
| `toolbar-default.png` | Toolbar E2E default tests | Default toolbar state |
|
|
330
|
+
| `toolbar-update-banner.png` | Toolbar E2E update banner tests | Toolbar with update banner visible |
|
|
331
|
+
|
|
332
|
+
Screenshots are not committed to git. They're generated fresh on each test run for visual verification. Future work: pixel-diff comparison against committed baselines.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Agentic Eval System
|
|
337
|
+
|
|
338
|
+
The eval system tests whether an LLM (Claude) can correctly use Amicus as a tool. Each eval spawns a real Claude Code process in an isolated sandbox.
|
|
339
|
+
|
|
340
|
+
See [evals/README.md](../evals/README.md) for full documentation.
|
|
341
|
+
|
|
342
|
+
### Quick Start
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
node evals/run_eval.js --eval-id 1 # Single eval
|
|
346
|
+
node evals/run_eval.js --all # All evals
|
|
347
|
+
node evals/run_eval.js --eval-id 1 --mode mcp # MCP mode only
|
|
348
|
+
node evals/run_eval.js --eval-id 1 --mode cli # CLI mode only
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Scoring
|
|
352
|
+
|
|
353
|
+
Two-stage: programmatic checks (gate) then LLM-as-judge (quality). All programmatic checks must pass before the LLM judge runs.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Writing New Tests
|
|
358
|
+
|
|
359
|
+
### Choosing the Right Tier
|
|
360
|
+
|
|
361
|
+
| Scenario | Tier | Example |
|
|
362
|
+
|----------|------|---------|
|
|
363
|
+
| New parsing logic | Unit | Mock inputs, assert outputs |
|
|
364
|
+
| New CLI flag | Unit | Test `parseArgs()` with the new flag |
|
|
365
|
+
| New MCP tool | Unit | Test Zod schema + handler with mocked SDK |
|
|
366
|
+
| Critical spawn config | Integration | Read source, assert pattern present |
|
|
367
|
+
| New headless workflow | E2E | Real LLM, verify session files |
|
|
368
|
+
| New toolbar UI element | E2E (CDP) | Real Electron, assert DOM via CDP |
|
|
369
|
+
| LLM decision quality | Eval | Claude + Amicus in sandbox |
|
|
370
|
+
|
|
371
|
+
### Naming Conventions
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
tests/
|
|
375
|
+
foo.test.js # Unit test for src/foo.js
|
|
376
|
+
foo.integration.test.js # Integration test (source-level)
|
|
377
|
+
foo-e2e.integration.test.js # E2E test (real processes/LLM)
|
|
378
|
+
sidecar/foo.test.js # Unit test for src/sidecar/foo.js
|
|
379
|
+
helpers/ # Test utilities (not test files)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### CDP E2E Test Pattern
|
|
383
|
+
|
|
384
|
+
When adding a new Electron E2E test:
|
|
385
|
+
|
|
386
|
+
1. Reuse the `startRealServer()` + `spawnElectron()` + `CdpClient` pattern from `electron-toolbar-e2e.integration.test.js`
|
|
387
|
+
2. Use `AMICUS_HEADLESS_TEST=1` to suppress the window
|
|
388
|
+
3. Use a unique `AMICUS_DEBUG_PORT` (currently `9224` for toolbar tests)
|
|
389
|
+
4. Always clean up in `afterAll`: kill Electron, kill server, kill Xvfb
|
|
390
|
+
5. Save screenshots to `tests/screenshots/` with descriptive names
|
|
391
|
+
|
|
392
|
+
```javascript
|
|
393
|
+
describeE2E('My New E2E Test', () => {
|
|
394
|
+
let serverInfo, electronProcess, cdp;
|
|
395
|
+
|
|
396
|
+
beforeAll(async () => {
|
|
397
|
+
serverInfo = await startRealServer();
|
|
398
|
+
electronProcess = spawnElectron({
|
|
399
|
+
opencodePort: serverInfo.port,
|
|
400
|
+
sessionId: serverInfo.sessionId,
|
|
401
|
+
taskId: 'my-test',
|
|
402
|
+
});
|
|
403
|
+
cdp = await CdpClient.toolbar(CDP_PORT, 20000);
|
|
404
|
+
}, 30000);
|
|
405
|
+
|
|
406
|
+
afterAll(async () => {
|
|
407
|
+
cdp?.close();
|
|
408
|
+
electronProcess?.kill('SIGTERM');
|
|
409
|
+
serverInfo?.cleanup();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('verifies some DOM state', async () => {
|
|
413
|
+
await cdp.waitForSelector('.my-element');
|
|
414
|
+
const text = await cdp.evaluate(`document.querySelector('.my-element')?.textContent`);
|
|
415
|
+
expect(text).toContain('expected');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Test File Location
|
|
421
|
+
|
|
422
|
+
All test files go in `tests/`. Test helpers go in `tests/helpers/`. The Jest config matches `**/tests/**/*.test.js`.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Jest Configuration
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
// jest.config.js
|
|
430
|
+
module.exports = {
|
|
431
|
+
testEnvironment: 'node',
|
|
432
|
+
testMatch: ['**/tests/**/*.test.js'],
|
|
433
|
+
testPathIgnorePatterns: [
|
|
434
|
+
'/node_modules/',
|
|
435
|
+
'\\.integration\\.test\\.js$', // E2E/integration tests excluded from default gate
|
|
436
|
+
'\\.worktrees/'
|
|
437
|
+
],
|
|
438
|
+
collectCoverageFrom: ['src/**/*.js', 'bin/**/*.js', 'electron/**/*.js'],
|
|
439
|
+
coverageDirectory: 'coverage',
|
|
440
|
+
coverageReporters: ['text', 'lcov'],
|
|
441
|
+
verbose: true
|
|
442
|
+
};
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`npm test` runs only `*.test.js` files that do NOT match `*.integration.test.js`. E2E and integration tests must be run explicitly via `npm run test:integration` (keyless), `npm run test:integration:live` (paid), `npm run test:all`, or by naming the file directly.
|
|
446
|
+
|
|
447
|
+
### Which gate runs which tier
|
|
448
|
+
|
|
449
|
+
| Rail | Runs | Trigger | Cost |
|
|
450
|
+
|------|------|---------|------|
|
|
451
|
+
| `npm test` | unit suite only | every push (pre-push hook), `ci.yml` `test` job on the 3x2 OS/Node matrix | free |
|
|
452
|
+
| `npm run test:integration` | integration tier, keyless | `ci.yml` `integration` job, every push + PR (ubuntu only) | free |
|
|
453
|
+
| `npm run test:integration:live` | integration tier, real keys | `integration-live.yml`, `workflow_dispatch` only | **spends money** |
|
|
454
|
+
|
|
455
|
+
**Why the live rail runs serially.** `test:integration:live` carries `--runInBand`, and it is the only
|
|
456
|
+
rail that does. It is also the only configuration in the repo that spawns several **real OpenCode
|
|
457
|
+
servers** at once: the keyless rail is parallel-safe only because those suites skip there. Run in
|
|
458
|
+
parallel, the live rail flakes on `fanout-e2e` — real legs erroring in ~1.2 s under contention from
|
|
459
|
+
concurrent servers, which reads exactly like a dead model alias and is not one. A worker cap would
|
|
460
|
+
only narrow that window; the contention is over ports and processes, not CPU, so the flag closes it
|
|
461
|
+
instead. Do **not** pass `--maxWorkers` to "speed it up": a paid rail that lies costs more than the
|
|
462
|
+
~2 minutes serialization spends.
|
|
463
|
+
|
|
464
|
+
The pre-push hook gates on **`npm test` only** — it does not run the integration tier. That is deliberate: `test:all` lets the paid suites see your real credentials, so wiring it into pre-push would bill you on every push. CI watches the tier instead.
|
|
465
|
+
|
|
466
|
+
**How the keyless run stays free.** `npm run test:integration` goes through `scripts/run-integration-keyless.js`, which builds a scrubbed environment before spawning jest: it deletes every name in `PROVIDER_ENV_MAP` (plus legacy aliases), drops `AMICUS_ENV_DIR`/`AMICUS_CONFIG_DIR`, and sandboxes **every credential-path root** — `HOME`, `USERPROFILE`, `XDG_DATA_HOME`, `XDG_CONFIG_HOME`, and `APPDATA` — by repointing each one inside an empty sandbox directory.
|
|
467
|
+
|
|
468
|
+
That last part is the one that matters, because the paid suites can find a credential through **three** doors, not one:
|
|
469
|
+
|
|
470
|
+
1. the process env (`OPENROUTER_API_KEY` etc.);
|
|
471
|
+
2. `~/.config/amicus/.env`, read directly by each suite's `HAS_API_KEY` check via `os.homedir()`;
|
|
472
|
+
3. OpenCode's `auth.json`, reached by suites that call `loadCredentials()` at module scope (e.g. `fanout-e2e`), via `utils/auth-json.js`'s `resolveAuthJsonPath()`.
|
|
473
|
+
|
|
474
|
+
Door 2 resolves through `os.homedir()` alone, which is a libuv call against the **real** process environment — a jest `--setupFiles` shim cannot move it, since jest hands each test environment a *copy* of `process.env` while `os.homedir()` reads the real one. **Door 3 is not a single path either**: `resolveAuthJsonPath()` checks `XDG_DATA_HOME` FIRST, and falls back to `APPDATA` on win32, before it ever falls back to the `os.homedir()`-relative `~/.local/share/opencode/auth.json`. Repointing only `HOME`/`USERPROFILE` — the fix this wrapper originally shipped with — closes the home-relative fallback but leaves `XDG_DATA_HOME`/`APPDATA` open: anyone with either exported in their real environment (XDG_DATA_HOME is common on Nix/home-manager and several Linux distros; APPDATA is always set on Windows) escapes the sandbox entirely, regardless of `os.homedir()`, through exactly the shape of gap that billed the live 2-model wave described next. **This was tried and failed in production, not just in theory:** the `--setupFiles` shim version was tried first and produced two distinct wrong outcomes in a single run — suites gated on door 2 saw `HAS_API_KEY` true with no env credentials, so they ran and failed (17 spurious failures), while `fanout-e2e` pulled a real key through door 3 and billed for a live wave. A whole-branch review of the `HOME`/`USERPROFILE`-only wrapper fix later reproduced the same class of leak end to end with a planted fake credential: with `XDG_DATA_HOME` set, `resolveAuthJsonPath()` resolved OUTSIDE the sandbox and `loadCredentials()` picked the fake key straight up — confirming the gap without spending anything real. Scrubbing every root in the parent process before jest is spawned closes all three doors at once, because the workers — and the CLI/MCP subprocesses the tests themselves spawn — inherit the real, scrubbed environment.
|
|
475
|
+
|
|
476
|
+
The scrub is derived from the engine's own `PROVIDER_ENV_MAP` rather than a hand-maintained list of paid test files, so a provider added later is covered automatically, and a paid suite added later self-skips automatically as long as it follows the existing key-gate pattern.
|
|
477
|
+
|
|
478
|
+
**Timeouts:** E2E tests set per-test timeouts of 180 seconds (3 minutes) for real LLM calls. Unit tests use Jest's default 5-second timeout.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Test File Index
|
|
483
|
+
|
|
484
|
+
Complete mapping of test files to their targets and focus areas.
|
|
485
|
+
|
|
486
|
+
| Test File | Target Module | Focus |
|
|
487
|
+
|-----------|--------------|-------|
|
|
488
|
+
| `cli.test.js` | Argument parsing | Command validation, flag handling |
|
|
489
|
+
| `context.test.js` | Context filtering | Turn extraction, token estimation |
|
|
490
|
+
| `session.test.js` | Session resolution | Primary/fallback paths |
|
|
491
|
+
| `session-manager.test.js` | Persistence layer | CRUD operations, metadata |
|
|
492
|
+
| `conflict.test.js` | File conflicts | mtime comparison, warning format |
|
|
493
|
+
| `drift.test.js` | Drift calculation | Age, turn count, significance |
|
|
494
|
+
| `headless.test.js` | OpenCode HTTP API | Spawn, polling, timeout |
|
|
495
|
+
| `prompt-builder.test.js` | System prompts | Template construction |
|
|
496
|
+
| `index.test.js` | Main API | Re-export smoke tests, generateTaskId |
|
|
497
|
+
| `e2e.test.js` | End-to-end | Full workflow |
|
|
498
|
+
| `sidecar/start.test.js` | Session starting | Task ID generation, metadata creation, MCP config |
|
|
499
|
+
| `sidecar/resume.test.js` | Session resumption | Drift detection, metadata loading |
|
|
500
|
+
| `sidecar/continue.test.js` | Session continuation | Previous session loading, context building |
|
|
501
|
+
| `read-json.test.js` | Session reading | `read --json` run/wave documents, wave-aware list |
|
|
502
|
+
| `sidecar/context-builder.test.js` | Context building | Session resolution, message filtering |
|
|
503
|
+
| `sidecar/session-utils.test.js` | Shared utilities | Session paths, finalization, heartbeat |
|
|
504
|
+
| `sidecar/progress.test.js` | Progress reader | Message counts, latest activity, last activity |
|
|
505
|
+
| `sidecar/exit-handler.test.js` | Crash handler | Metadata update on crash, status transitions |
|
|
506
|
+
| `mcp-headless-lifecycle.test.js` | MCP headless lifecycle | Start, poll, progress, crash, abort, read |
|
|
507
|
+
| `mcp-discovery.test.js` | MCP discovery | Plugin chain, `~/.claude.json` mcpServers, merge priority, sidecar exclusion |
|
|
508
|
+
| `mcp-discovery-integration.test.js` | buildMcpConfig merge | Discovery + file + CLI merge, --no-mcp, --exclude-mcp |
|
|
509
|
+
| `mcp-repomix-e2e.integration.test.js` | MCP E2E (real LLM + repomix) | Real discovery -> headless sidecar -> repomix tool call |
|
|
510
|
+
| `auth-json.test.js` | Auth JSON reader | Import discovery, provider mapping, smart delete check |
|
|
511
|
+
| `opencode-client-cowork.test.js` | OpenCode client config | Client-aware prompt, systemPrompt, port handling, provider model sync |
|
|
512
|
+
| `config.test.js` | Config core | Config I/O, aliases, getEffectiveAliases, tryResolveModel, buildProviderModels |
|
|
513
|
+
| `config-fallback.test.js` | Config fallback | Direct API fallback with persisted keys |
|
|
514
|
+
| `config-hash.test.js` | Config hashing | Config hashing, alias table, change detection |
|
|
515
|
+
| `config-null-alias.test.js` | Config null alias | Null alias protection and auto-repair |
|
|
516
|
+
| `config-resolve.test.js` | Config resolution | Model resolution, default aliases, direct API fallback, detectFallback |
|
|
517
|
+
| `model-validator.test.js` | Model validator | Validation, filtering, interactive prompting, headless errors |
|
|
518
|
+
| `model-fetcher.test.js` | Model fetcher | Provider API fetching, normalization, grouping, error handling |
|
|
519
|
+
| `updater.test.js` | Update checker | Mock states, performUpdate spawn, CLI integration |
|
|
520
|
+
| `evals/tests/transcript_parser.test.js` | Stream-json parsing | Tool call extraction, token usage, error capture |
|
|
521
|
+
| `evals/tests/evaluator.test.js` | Eval criteria | Programmatic checks (7 types), LLM-as-judge prompt/response |
|
|
522
|
+
| `evals/tests/claude_runner.test.js` | Claude runner | MCP config, sandbox creation, CLI command building |
|
|
523
|
+
| `evals/tests/result_writer.test.js` | Result output | Summary formatting, file writing |
|
|
524
|
+
| `scripts/check-secrets.test.js` | Secret detection | Pattern matching, allowlist, multi-secret |
|
|
525
|
+
| `scripts/check-file-sizes.test.js` | File size limits | Line counting, batch checking |
|
|
526
|
+
| `scripts/validate-docs.test.js` | Doc drift detection | Section extraction, drift comparison, staged file check |
|
|
527
|
+
| `helpers/cdp-client.test.js` | CDP helper | Mock HTTP+WebSocket CDP server, factory methods |
|
|
528
|
+
| `electron-headless-mode.test.js` | Electron headless | Source-level verify `AMICUS_HEADLESS_TEST` guard |
|
|
529
|
+
| `cli-headless-e2e.integration.test.js` | CLI E2E (real LLM) | `start --no-ui`, `list`, `read`, `read --metadata` |
|
|
530
|
+
| `electron-toolbar-e2e.integration.test.js` | Electron CDP E2E (real LLM) | Brand, task ID, timer, fold button, settings, update banner, screenshots |
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## UI Testing Approach (Autonomous Verification Required)
|
|
535
|
+
|
|
536
|
+
**MANDATORY: Any UI feature change MUST be visually verified before considering it complete.** Do not rely solely on unit tests for UI work -- launch the Electron app, inspect via CDP, and take a screenshot.
|
|
537
|
+
|
|
538
|
+
For UI changes, follow this autonomous verification process:
|
|
539
|
+
|
|
540
|
+
1. **Launch the app** with appropriate mock env vars (e.g., `AMICUS_MOCK_UPDATE=available`)
|
|
541
|
+
2. **Use `AMICUS_DEBUG_PORT=9223`** to avoid port conflicts with Chrome
|
|
542
|
+
3. **Inspect via Chrome DevTools Protocol**: Connect to `http://127.0.0.1:9223/json`, find the target page, query DOM state via WebSocket
|
|
543
|
+
4. **Take a screenshot**: Use CDP `Page.captureScreenshot` (cross-platform) via the `CdpClient` helper. On macOS you can also use `screencapture -x /tmp/amicus-<feature>.png`. On Windows use the `CdpClient` approach (no `screencapture` binary available).
|
|
544
|
+
5. **Check both targets**: The Electron window has two pages -- the OpenCode content (`http://localhost:...`) and the toolbar (`data:text/html`). Test each as needed.
|
|
545
|
+
|
|
546
|
+
**Key gotcha:** `contextBridge` does not work with `data:` URLs. The toolbar (`data:text/html`) cannot use `window.sidecar` IPC. Use `executeJavaScript()` polling from the main process instead.
|
|
547
|
+
|
|
548
|
+
See [electron-testing.md](electron-testing.md) for full CDP patterns, toolbar-specific testing, and known limitations.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## Image / Diagram QA (Mandatory Visual Loop)
|
|
553
|
+
|
|
554
|
+
**When creating or modifying any image (SVG, PNG, diagram, screenshot), you MUST:**
|
|
555
|
+
|
|
556
|
+
1. Render / convert the image
|
|
557
|
+
2. Read it back visually (use `Read` tool on the PNG) and inspect the output
|
|
558
|
+
3. Check for: text clipping, alignment issues, correct labels, layout balance, readability
|
|
559
|
+
4. Fix any issues found
|
|
560
|
+
5. Re-render and re-inspect -- **loop until fully QA'd**
|
|
561
|
+
|
|
562
|
+
Never commit an image without completing visual verification. GitHub strips `<style>` and `<filter>` from SVGs, so always convert to PNG (use `sharp`) for any image referenced in README or docs.
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Update Banner Mock Testing
|
|
567
|
+
|
|
568
|
+
Use `AMICUS_MOCK_UPDATE` to test update UI states without real npm operations (the legacy `SIDECAR_MOCK_UPDATE` name was removed in v2.0.0):
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
AMICUS_MOCK_UPDATE=available amicus start --model gemini --prompt "test" # Shows banner
|
|
572
|
+
AMICUS_MOCK_UPDATE=success amicus start --model gemini --prompt "test" # Update succeeds
|
|
573
|
+
AMICUS_MOCK_UPDATE=error amicus start --model gemini --prompt "test" # Update fails
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Troubleshooting
|
|
579
|
+
|
|
580
|
+
| Problem | Cause | Solution |
|
|
581
|
+
|---------|-------|---------|
|
|
582
|
+
| E2E tests skipped | No `OPENROUTER_API_KEY` | Set the key in env or `~/.config/amicus/.env` |
|
|
583
|
+
| CDP connection refused | Wrong port or Chrome conflict | Use `AMICUS_DEBUG_PORT=9224` (not 9222/9223) |
|
|
584
|
+
| Electron E2E skipped | `electron` not installed | `npm install` (it's a devDependency) |
|
|
585
|
+
| Linux E2E crash | No X server | Install Xvfb: `apt-get install xvfb` |
|
|
586
|
+
| Jest ESM error | Dynamic import in test | Use `tests/helpers/start-server.js` child process |
|
|
587
|
+
| `waitForSelector` timeout | Element not rendered yet | Increase timeout or check selector name |
|
|
588
|
+
| Screenshot empty/small | Window not created | Verify `AMICUS_HEADLESS_TEST=1` is set |
|
|
589
|
+
| Stale CDP target ID | Electron restarted | Always use `CdpClient.toolbar()` factory (retries) |
|