Skip to content

Commit 87cc71a

Browse files
Gkrumbach07claude
andauthored
feat: add comprehensive test coverage (unit + E2E) with mock SDK client (#834)
## Summary Adds comprehensive test coverage for the frontend and runner, going from **0% → 74% unit test coverage** with a mock SDK client for E2E tests that doesn't require a real Anthropic API key. ## What's included ### Mock SDK Client - `MockClaudeSDKClient` in the runner — activated by `ANTHROPIC_API_KEY=mock-replay-key` - Replays pre-recorded SDK messages from JSONL fixtures through the real `ClaudeAgentAdapter` - Tests the full AG-UI translation pipeline without calling the Anthropic API - Capture script (`scripts/capture-fixtures.py`) to record new fixtures from real sessions ### Frontend Unit Tests (466 tests, ~74% coverage) - **26 test files** using Vitest + React Testing Library - Covers: event-handlers, export-chat, tool-message, ChatInputBox, FeedbackModal, stream-message, normalize-snapshot, status-colors, file-tree, input-with-history, theme-toggle, use-session-queue, use-agui-stream, use-autocomplete, use-workspace, use-projects, use-sessions, and more ### E2E Tests (58 tests) - **One comprehensive test file** (`sessions.cy.ts`) covering the full user journey - Workspace CRUD, session lifecycle, agent interaction, chat features, workspace admin, modals, feedback - Always uses `mock-replay-key` — no real API key needed - Runs in Chrome (Electron has SSE issues) ### CI Updates - `unit-tests.yml`: Added frontend vitest job alongside backend/runner/CLI tests - `e2e.yml`: Removed `ANTHROPIC_API_KEY` dependency, uses mock SDK mode - `run-tests.sh`: Uses Chrome instead of Electron ### Documentation - `CLAUDE.md`: Testing section with quick reference - `e2e/README.md`: Full E2E guide (mock SDK, test structure, writing tests) - `components/frontend/README.md`: Unit test guide (vitest patterns, mocking) ### Cleanup - Removed outdated test files: `chatbox-demo.cy.ts`, `jira-integration-demo.cy.ts`, `public-api.cy.ts`, `vteam.cy.ts` - Removed deleted admin/runtimes page - Added `data-testid` attributes to key frontend components - Added `.gitignore` for coverage output ## How to run ```bash # Unit tests (fast, ~2s) cd components/frontend && npx vitest run --coverage # E2E tests (~3min, needs running cluster) cd e2e TEST_TOKEN=$(kubectl get secret test-user-token -n ambient-code -o jsonpath='{.data.token}' | base64 -d) \ CYPRESS_BASE_URL=http://localhost:3000 \ npx cypress run --browser chrome --spec cypress/e2e/sessions.cy.ts ``` --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9180a41 commit 87cc71a

71 files changed

Lines changed: 18284 additions & 2255 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e.yml

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,24 @@
11
name: E2E Tests
22

3-
# ONLY runs when maintainer adds "safe-to-test" label to a PR
4-
# Has access to secrets (including ANTHROPIC_API_KEY)
5-
# Maintainers: Review PR code before adding label!
3+
# Runs automatically on PRs. Uses mock SDK (no real API key needed).
64

75
on:
86
pull_request:
9-
types: [labeled]
107
branches: [ main, master ]
118

9+
push:
10+
branches: [ main ]
11+
1212
concurrency:
13-
group: e2e-tests-${{ github.event.pull_request.number }}
13+
group: e2e-tests-${{ github.event.pull_request.number || github.sha }}
1414
cancel-in-progress: true
1515

1616
jobs:
17-
# Security check - only proceed if safe-to-test label was added
18-
check-label:
19-
if: github.event.pull_request.head.repo.full_name == github.repository
20-
runs-on: ubuntu-latest
21-
outputs:
22-
should-run: ${{ steps.check.outputs.should-run }}
23-
steps:
24-
- name: Check if safe-to-test label was added
25-
id: check
26-
run: |
27-
if [ "${{ github.event.label.name }}" = "safe-to-test" ]; then
28-
echo "should-run=true" >> $GITHUB_OUTPUT
29-
echo "✅ safe-to-test label added - proceeding with tests"
30-
else
31-
echo "should-run=false" >> $GITHUB_OUTPUT
32-
echo "⏭️ Label '${{ github.event.label.name }}' is not safe-to-test - skipping"
33-
fi
34-
3517
detect-changes:
18+
# Only run for same-repo PRs (not forks) and pushes to main.
19+
# Fork PRs build and execute untrusted code in the CI runner environment.
20+
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
3621
runs-on: ubuntu-latest
37-
needs: check-label
38-
if: needs.check-label.outputs.should-run == 'true'
3922
outputs:
4023
frontend: ${{ steps.filter.outputs.frontend }}
4124
backend: ${{ steps.filter.outputs.backend }}
@@ -64,8 +47,9 @@ jobs:
6447
e2e:
6548
name: End-to-End Tests
6649
runs-on: ubuntu-latest
67-
needs: [check-label, detect-changes]
68-
if: needs.check-label.outputs.should-run == 'true'
50+
needs: [detect-changes]
51+
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
52+
6953
timeout-minutes: 25
7054

7155
steps:
@@ -165,7 +149,7 @@ jobs:
165149
166150
- name: Install kind
167151
run: |
168-
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
152+
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
169153
chmod +x ./kind
170154
sudo mv ./kind /usr/local/bin/kind
171155
kind version
@@ -187,15 +171,13 @@ jobs:
187171
kind load docker-image quay.io/ambient_code/vteam_claude_runner:e2e-test --name ambient-local
188172
echo "✅ All images loaded into kind cluster"
189173
190-
- name: Update kustomization to use e2e-test images
191-
run: |
192-
sed -i 's/newTag: latest/newTag: e2e-test/g' components/manifests/overlays/e2e/kustomization.yaml
193-
echo "Updated kustomization.yaml to use e2e-test tag"
194-
195-
- name: Deploy vTeam (with ANTHROPIC_API_KEY)
174+
- name: Deploy vTeam (mock SDK mode)
196175
working-directory: e2e
197176
env:
198-
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
177+
IMAGE_FRONTEND: quay.io/ambient_code/vteam_frontend:e2e-test
178+
IMAGE_BACKEND: quay.io/ambient_code/vteam_backend:e2e-test
179+
IMAGE_OPERATOR: quay.io/ambient_code/vteam_operator:e2e-test
180+
IMAGE_RUNNER: quay.io/ambient_code/vteam_claude_runner:e2e-test
199181
run: ./scripts/deploy.sh
200182

201183
- name: Verify deployment
@@ -206,12 +188,11 @@ jobs:
206188
echo "Checking services..."
207189
kubectl get svc -n ambient-code
208190
209-
- name: Run Cypress tests (with ANTHROPIC_API_KEY)
191+
- name: Run Cypress E2E tests
210192
working-directory: e2e
211-
env:
212-
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
213193
run: ./scripts/run-tests.sh
214194

195+
215196
- name: Upload test results
216197
if: failure()
217198
uses: actions/upload-artifact@v6

.github/workflows/unit-tests.yml

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- 'components/runners/ambient-runner/**'
1010
- 'components/ambient-cli/**'
1111
- 'components/ambient-sdk/go-sdk/**'
12+
- 'components/frontend/**'
1213
- '.github/workflows/unit-tests.yml'
1314
- '!**/*.md'
1415

@@ -19,6 +20,7 @@ on:
1920
- 'components/runners/ambient-runner/**'
2021
- 'components/ambient-cli/**'
2122
- 'components/ambient-sdk/go-sdk/**'
23+
- 'components/frontend/**'
2224
- '.github/workflows/unit-tests.yml'
2325
- '!**/*.md'
2426

@@ -47,6 +49,7 @@ jobs:
4749
api-server: ${{ steps.filter.outputs.api-server }}
4850
runner: ${{ steps.filter.outputs.runner }}
4951
cli: ${{ steps.filter.outputs.cli }}
52+
frontend: ${{ steps.filter.outputs.frontend }}
5053
steps:
5154
- name: Checkout code
5255
uses: actions/checkout@v6
@@ -65,6 +68,8 @@ jobs:
6568
cli:
6669
- 'components/ambient-cli/**'
6770
- 'components/ambient-sdk/go-sdk/**'
71+
frontend:
72+
- 'components/frontend/**'
6873
6974
backend:
7075
runs-on: ubuntu-latest
@@ -221,24 +226,13 @@ jobs:
221226
- name: Install dependencies
222227
run: |
223228
python -m pip install --upgrade pip
224-
pip install -e .
225-
pip install pytest pytest-asyncio pytest-cov
229+
pip install -e '.[claude]'
230+
pip install pytest pytest-asyncio pytest-cov httpx
226231
227-
- name: Run unit tests
232+
- name: Run unit tests with coverage
228233
run: |
229-
pytest tests/ -v --tb=short --color=yes -x
234+
pytest tests/ -v --tb=short --color=yes -x --cov=ambient_runner --cov-report=term-missing --cov-report=xml
230235
231-
- name: Run tests with coverage
232-
run: |
233-
pytest tests/ --cov=ambient_runner --cov-report=term-missing --cov-report=xml
234-
235-
- name: Upload coverage to Codecov
236-
uses: codecov/codecov-action@v5
237-
with:
238-
files: ./components/runners/ambient-runner/coverage.xml
239-
flags: runner
240-
name: ambient-runner
241-
fail_ci_if_error: false
242236
243237
cli:
244238
runs-on: ubuntu-latest
@@ -280,9 +274,35 @@ jobs:
280274
path: components/ambient-cli/coverage.out
281275
retention-days: 7
282276

277+
frontend:
278+
runs-on: ubuntu-latest
279+
needs: detect-changes
280+
if: needs.detect-changes.outputs.frontend == 'true' || github.event_name == 'workflow_dispatch'
281+
name: Frontend Unit Tests (Vitest)
282+
defaults:
283+
run:
284+
working-directory: components/frontend
285+
286+
steps:
287+
- name: Checkout code
288+
uses: actions/checkout@v6
289+
290+
- name: Set up Node.js
291+
uses: actions/setup-node@v6
292+
with:
293+
node-version-file: 'components/frontend/package.json'
294+
cache: 'npm'
295+
cache-dependency-path: 'components/frontend/package-lock.json'
296+
297+
- name: Install dependencies
298+
run: npm ci
299+
300+
- name: Run unit tests with coverage
301+
run: npx vitest run --coverage
302+
283303
summary:
284304
runs-on: ubuntu-latest
285-
needs: [detect-changes, backend, api-server, runner, cli]
305+
needs: [detect-changes, backend, api-server, runner, cli, frontend]
286306
if: always()
287307
steps:
288308
- name: Check overall status
@@ -292,7 +312,8 @@ jobs:
292312
"${{ needs.backend.result }}" \
293313
"${{ needs.api-server.result }}" \
294314
"${{ needs.runner.result }}" \
295-
"${{ needs.cli.result }}"; do
315+
"${{ needs.cli.result }}" \
316+
"${{ needs.frontend.result }}"; do
296317
if [ "$result" == "failure" ] || [ "$result" == "cancelled" ]; then
297318
failed=true
298319
fi
@@ -303,6 +324,7 @@ jobs:
303324
echo " api-server: ${{ needs.api-server.result }}"
304325
echo " runner: ${{ needs.runner.result }}"
305326
echo " cli: ${{ needs.cli.result }}"
327+
echo " frontend: ${{ needs.frontend.result }}"
306328
exit 1
307329
fi
308330
echo "All unit tests passed!"

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ git push --no-verify # Skip pre-push hooks
118118
- `tsc --noEmit` and `npm run build` are **not** included (slow; CI gates on them)
119119
- Branch/push protection scripts remain in `scripts/git-hooks/` and are invoked by pre-commit
120120

121+
## Testing
122+
123+
- **Frontend unit tests**: `cd components/frontend && npx vitest run --coverage` (466 tests, ~74% coverage). See `components/frontend/README.md`.
124+
- **E2E tests**: `cd e2e && npx cypress run --browser chrome` (58 tests, mock SDK). See `e2e/README.md`.
125+
- **Runner tests**: `cd components/runners/ambient-runner && python -m pytest tests/`
126+
- **Backend tests**: `cd components/backend && make test`. See `components/backend/TEST_GUIDE.md`.
127+
121128
## More Info
122129

123130
See [BOOKMARKS.md](BOOKMARKS.md) for architecture decisions, development context, code patterns, and component-specific guides.

components/frontend/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ next-env.d.ts
6464

6565
# Previous frontend
6666
previous-frontend/
67+
68+
# Coverage
69+
coverage/

components/frontend/README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,108 @@ curl -i http://localhost:3000/api/projects/my-project/agentic-sessions \
191191
- **Error Handling**: Comprehensive error states with recovery actions
192192

193193
The frontend provides a complete user interface for the RFE (Request For Enhancement) workflow system, integrating GitHub repositories, AI runners, and real-time collaboration features.
194+
195+
## Testing
196+
197+
### Unit Tests (Vitest)
198+
199+
466 tests across 26 files. Primary coverage metric (~74%).
200+
201+
```bash
202+
# Run all tests
203+
npx vitest run
204+
205+
# With coverage report
206+
npx vitest run --coverage
207+
open coverage/index.html
208+
209+
# Watch mode
210+
npx vitest
211+
212+
# Single file
213+
npx vitest run src/utils/__tests__/export-chat.test.ts
214+
```
215+
216+
**Config**: `vitest.config.ts` — uses jsdom, Istanbul coverage, `@/` path alias.
217+
218+
### Writing Unit Tests
219+
220+
Place tests in `__tests__/` next to the source:
221+
222+
```
223+
src/
224+
components/
225+
chat/
226+
ChatInputBox.tsx
227+
__tests__/
228+
ChatInputBox.test.tsx ← test file here
229+
hooks/
230+
use-session-queue.ts
231+
__tests__/
232+
use-session-queue.test.ts ← test file here
233+
```
234+
235+
**Pure function test:**
236+
```typescript
237+
import { describe, it, expect } from 'vitest';
238+
import { convertEventsToMarkdown } from '../export-chat';
239+
240+
describe('convertEventsToMarkdown', () => {
241+
it('renders text messages', () => {
242+
const events = [
243+
{ type: 'TEXT_MESSAGE_START', role: 'user' },
244+
{ type: 'TEXT_MESSAGE_CONTENT', delta: 'Hello' },
245+
{ type: 'TEXT_MESSAGE_END' },
246+
];
247+
const md = convertEventsToMarkdown(makeExport(events), makeSession());
248+
expect(md).toContain('Hello');
249+
});
250+
});
251+
```
252+
253+
**Component test:**
254+
```typescript
255+
import { render, screen, fireEvent } from '@testing-library/react';
256+
import { describe, it, expect, vi } from 'vitest';
257+
import { MyComponent } from '../MyComponent';
258+
259+
it('handles click', () => {
260+
const onClick = vi.fn();
261+
render(<MyComponent onClick={onClick} />);
262+
fireEvent.click(screen.getByText('Click me'));
263+
expect(onClick).toHaveBeenCalled();
264+
});
265+
```
266+
267+
**Hook test:**
268+
```typescript
269+
import { renderHook, act } from '@testing-library/react';
270+
271+
it('updates state', () => {
272+
const { result } = renderHook(() => useMyHook());
273+
act(() => result.current.setValue('new'));
274+
expect(result.current.value).toBe('new');
275+
});
276+
```
277+
278+
**Mocking:**
279+
```typescript
280+
// Module mock
281+
vi.mock('@/services/api/sessions', () => ({
282+
createSession: vi.fn().mockResolvedValue({ name: 'test' }),
283+
}));
284+
285+
// Function stub
286+
const onSend = vi.fn();
287+
288+
// DOM API spy
289+
vi.spyOn(document, 'createElement').mockReturnValue(mockEl);
290+
291+
// React Query wrapper
292+
const wrapper = ({ children }) => (
293+
<QueryClientProvider client={new QueryClient()}>
294+
{children}
295+
</QueryClientProvider>
296+
);
297+
const { result } = renderHook(() => useMyQuery(), { wrapper });
298+
```

components/frontend/next.config.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
/** @type {import('next').NextConfig} */
2+
const isCoverage = process.env.CYPRESS_COVERAGE === 'true'
3+
24
const nextConfig = {
35
output: 'standalone',
46
turbopack: {
5-
root: __dirname, // Silence "inferred workspace root" warning in monorepo
7+
root: __dirname,
68
},
79
experimental: {
810
instrumentationHook: true,
9-
}
11+
// Force all static pages into a single worker to prevent QEMU SIGILL
12+
// crashes during cross-architecture Docker builds (arm64 emulation).
13+
staticGenerationMinPagesPerWorker: 100,
14+
},
15+
webpack(config) {
16+
if (isCoverage) {
17+
config.module.rules.push({
18+
test: /\.(js|ts|jsx|tsx)$/,
19+
exclude: /node_modules/,
20+
enforce: 'post',
21+
use: {
22+
loader: 'babel-loader',
23+
options: {
24+
presets: ['next/babel'],
25+
plugins: ['istanbul'],
26+
},
27+
},
28+
})
29+
}
30+
return config
31+
},
1032
}
1133

1234
module.exports = nextConfig

0 commit comments

Comments
 (0)