Skip to content

Commit cf45280

Browse files
authored
test: add isolated browser verification loop (#21)
* test: add isolated browser verification loop * fix: harden protected browser verification * fix: bound Jam recorder startup
1 parent 8facb69 commit cf45280

20 files changed

Lines changed: 837 additions & 218 deletions

‎.audit/agent-verification-loop.tsv‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
ts phase decision why evidence result
2+
2026-08-31T15:27:00Z isolation created t3code/agent-verification-loop in its own T3 worktree kept the dirty main checkout and other worktrees untouched git worktree list clean isolated branch
3+
2026-08-31T15:35:00Z browser made port 18080 loopback-only and disabled server reuse prevented another checkout on port 8080 from producing false browser results playwright.config.ts isolated server configured
4+
2026-08-31T15:35:30Z staging kept real auth tokens out of traces and uploaded artifacts Playwright traces and storage state can contain bearer and refresh tokens playwright.config.ts, .github/workflows/deploy.yml secret-bearing artifacts excluded
5+
2026-08-31T15:36:00Z staging used a unique user-owned source and exact UI deletion proved persistence while limiting cleanup to the row created by the test e2e/staging/authed-flows.spec.ts bounded cleanup implemented
6+
2026-08-31T15:38:00Z local gates ran lint, typecheck, focused sparring tests, and Playwright discovery caught compile and selector problems before the full browser matrix local command output 14 focused tests and 168 browser cases listed
7+
2026-08-31T15:39:00Z staging proved partial and missing staging configuration fails closed never substitute production or a skipped fake session for missing protected values playwright.config.ts both negative preflights exited 1
8+
2026-08-31T15:44:00Z browser made the score mock briefly asynchronous after two flaky results the zero-latency mock removed the loading state before its existing assertion could observe it e2e/sparring.spec.ts preserved the loading assertion for deterministic rerun
9+
2026-08-31T16:05:00Z webkit loaded Jam recorder scripts only when MediaRecorder exists WebKit reproduced three Jam page errors on the smallest authenticated route src/jam-loader.ts, src/test/jam-loader.test.ts minimal repro passed and 56 WebKit cases passed
10+
2026-08-31T16:15:00Z local gates ran the full Node 22 verification command on the Jam-fixed tree needed fresh proof for the final source state npm run verify 85 files, 874 tests, coverage, build, and 112 Chromium cases passed
11+
2026-08-31T16:22:00Z staging audited staging before any remote mutation confirmed target and identified exact synchronization and credential blockers Supabase function, secret-name, migration, and CORS reads 2 migrations and 3 function deploys stale; protected test inputs absent
12+
2026-08-31T16:30:00Z review fixed all four Luna xhigh pre-commit findings kept auth traces out of artifacts, isolated staging concurrency, tightened AI proof, and guaranteed cleanup after insert playwright.config.ts, .github/workflows/staging-e2e.yml, e2e/staging/authed-flows.spec.ts focused lint, typecheck, tests, YAML parse, and staging discovery passed
13+
2026-08-31T16:31:00Z ci recorded exhausted GitHub Actions minutes as an external exception Adam confirmed hosted CI cannot run and required local CI-equivalent proof user instruction local gates remain authoritative; GitHub runs must not be called green
14+
2026-08-31T16:35:00Z integration rebased onto analytics and telemetry main changes kept the PR current before push and invalidated older verification origin/main 7bca126 clean rebase; full gates rerun
15+
2026-08-31T16:40:00Z local gates ran fresh CI-equivalent checks after rebase proved the exact integrated source before PR creation npm ci, npm run verify 89 files, 930 tests, coverage, build, and 112 Chromium cases passed
16+
2026-08-31T16:42:00Z webkit reran the full Safari-engine matrix after rebase main changed routing and AI code since the first WebKit proof Playwright 1.60 Noble container with Node 22 56 WebKit cases passed
17+
2026-08-31T16:56:00Z CodeRabbit fixed all four current-head review comments restricted trusted staging triggers and loopback auth, updated plan state, and sequenced Jam before app startup PR 21 review on 5f0f67a all findings implemented or bounded with deterministic proof
18+
2026-08-31T16:59:00Z local gates reran CI-equivalent checks after CodeRabbit fixes review changes affected workflow security and application bootstrap npm run verify, Playwright 1.60 Noble container 89 files, 930 tests, coverage, build, 112 Chromium and 56 WebKit cases passed
19+
2026-08-31T17:06:00Z CodeRabbit added a bounded Jam CDN timeout prevented a stalled recorder request from blocking application startup src/jam-loader.ts, src/test/jam-loader.test.ts 20 focused tests, lint, typecheck, build, Chromium and WebKit startup passed

‎.github/workflows/deploy.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
- name: Install dependencies
3333
run: npm ci
3434

35+
- name: Install Chromium
36+
run: npx playwright install --with-deps chromium
37+
3538
- name: Lint
3639
run: npm run lint
3740

@@ -44,6 +47,24 @@ jobs:
4447
- name: Build
4548
run: npm run build
4649

50+
- name: Browser tests
51+
env:
52+
CI: 1
53+
PORT: 18080
54+
PLAYWRIGHT_BASE_URL: http://127.0.0.1:18080
55+
run: npm run test:e2e
56+
57+
- name: Upload browser failure artifacts
58+
if: failure()
59+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
60+
with:
61+
name: chromium-browser-failure-${{ github.run_id }}
62+
path: |
63+
test-results/
64+
playwright-report/
65+
if-no-files-found: ignore
66+
retention-days: 7
67+
4768
- name: Set up QEMU
4869
if: env.DOCKER_PUSH == 'true'
4970
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3

‎.github/workflows/staging-e2e.yml‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Protected staging E2E
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Build and Push Docker Image"]
6+
types: [completed]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: protected-staging-e2e-main
14+
cancel-in-progress: false
15+
16+
jobs:
17+
staging-e2e:
18+
if: >-
19+
(
20+
(github.event_name == 'workflow_run' &&
21+
github.event.workflow_run.conclusion == 'success' &&
22+
github.event.workflow_run.event == 'push' &&
23+
github.event.workflow_run.head_branch == 'main') ||
24+
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
25+
) && vars.STAGING_E2E_ENABLED == 'true'
26+
runs-on: ubuntu-latest
27+
environment: staging
28+
steps:
29+
- name: Checkout code
30+
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
31+
with:
32+
persist-credentials: false
33+
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
34+
35+
- name: Set up Node
36+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
37+
with:
38+
node-version: 22
39+
40+
- name: Install dependencies
41+
run: npm ci
42+
43+
- name: Install Chromium
44+
run: npx playwright install --with-deps chromium
45+
46+
- name: Verify protected variables are present
47+
shell: bash
48+
env:
49+
STAGING_SUPABASE_URL: ${{ secrets.STAGING_SUPABASE_URL }}
50+
STAGING_SUPABASE_ANON_KEY: ${{ secrets.STAGING_SUPABASE_ANON_KEY }}
51+
STAGING_DEMO_CODE: ${{ secrets.STAGING_DEMO_CODE }}
52+
run: |
53+
missing=0
54+
for name in STAGING_SUPABASE_URL STAGING_SUPABASE_ANON_KEY STAGING_DEMO_CODE; do
55+
if [ -z "${!name}" ]; then
56+
echo "::error::$name is not configured in the staging environment"
57+
missing=1
58+
fi
59+
done
60+
exit "$missing"
61+
62+
- name: Protected staging browser tests
63+
env:
64+
CI: 1
65+
PORT: 18080
66+
PLAYWRIGHT_BASE_URL: http://127.0.0.1:18080
67+
STAGING_SUPABASE_URL: ${{ secrets.STAGING_SUPABASE_URL }}
68+
STAGING_SUPABASE_ANON_KEY: ${{ secrets.STAGING_SUPABASE_ANON_KEY }}
69+
STAGING_DEMO_CODE: ${{ secrets.STAGING_DEMO_CODE }}
70+
run: npm run test:e2e:staging
71+
72+
- name: Upload staging failure artifacts
73+
if: failure()
74+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
75+
with:
76+
name: staging-browser-failure-${{ github.run_id }}
77+
path: |
78+
test-results/**/*.png
79+
test-results/**/*.webm
80+
!test-results/.auth/
81+
!test-results/**/*.zip
82+
if-no-files-found: ignore
83+
retention-days: 7

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ humanizer.md
6161
*.txt
6262
*.info
6363
!README.md
64+
!AGENTS.md
6465
!**/README.md
6566
!docs/pricing-snapshots/*.md
6667
!plans/**/*.md

‎AGENTS.md‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# DebateLab agent guide
2+
3+
## Runtime and install
4+
5+
- Use Node 22 and npm. The lockfile is `package-lock.json`.
6+
- Start a clean checkout with `npm ci`.
7+
- The development server normally uses port 8080. Automated browser verification owns `127.0.0.1:18080` and never reuses an existing server.
8+
- Local Supabase development uses port 54321. Do not point staging checks at the production project.
9+
10+
## Verification ladder
11+
12+
Run the smallest relevant reproduction first, then the complete gate before a PR:
13+
14+
```bash
15+
npm run lint
16+
npm run typecheck
17+
npm test -- --coverage
18+
npm run build
19+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
20+
```
21+
22+
`npm run verify` runs that full credential-free ladder. It starts an isolated Vite server and executes the desktop and mobile Chromium projects. Browser failures stay in ignored `test-results/` and `playwright-report/` directories. CI uploads those directories for seven days when the browser step fails.
23+
24+
WebKit is an additional release smoke gate:
25+
26+
```bash
27+
npx playwright install --with-deps webkit
28+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e:webkit
29+
```
30+
31+
## Protected staging verification
32+
33+
The staging suite uses a real staging user and backend. It never uses a service-role key. Supply all three protected values together:
34+
35+
- `STAGING_SUPABASE_URL`
36+
- `STAGING_SUPABASE_ANON_KEY`
37+
- `STAGING_DEMO_CODE`
38+
39+
Then run:
40+
41+
```bash
42+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 \
43+
STAGING_SUPABASE_URL=<staging-url> \
44+
STAGING_SUPABASE_ANON_KEY=<staging-anon-key> \
45+
STAGING_DEMO_CODE=<dedicated-test-code> \
46+
npm run test:e2e:staging
47+
```
48+
49+
The suite rejects the production project, checks that the anon key belongs to the supplied staging URL, authenticates through `demo-code-login`, proves session reload, writes and reads back one unique source, makes one bounded AI request, and deletes only that source. It saves temporary auth state under `test-results/.auth/`. Never upload that directory or a staging trace because both can contain session tokens.
50+
51+
GitHub runs staging only for `main` push or a main-branch manual dispatch, through the protected `staging` environment, after the credential-free build job passes. Set the repository variable `STAGING_E2E_ENABLED=true` only after the protected environment has the three secrets, current staging functions, schema, provider secret, dedicated unlimited-use test code, and a main-only deployment rule. A skipped staging job is not a pass.
52+
53+
The staging origin `http://127.0.0.1:18080` must be deployed in the CORS allowlists for `demo-code-login`, `ai`, and `ai-stream` before enabling the gate. Do not deploy those functions or apply migrations to an unverified project.
54+
55+
## Working rules
56+
57+
- Start bug fixes with a failing test or a repeatable browser reproduction when the repository has a suitable test layer.
58+
- Keep stub sessions limited to render checks. Do not describe them as authenticated backend proof.
59+
- Never put staging or production secrets in PR jobs, logs, traces, screenshots, commits, or comments.
60+
- A local pass proves only the local commit. Inspect the current-head GitHub run before calling CI green. Account billing or spending-limit failures are external exceptions, not passing checks.
61+
- State exactly which browser and staging checks ran. If credentials, provider access, or staging synchronization are missing, stop that gate and report it instead of substituting production or mocks.

‎README.md‎

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,57 +1346,47 @@ jobs:
13461346

13471347
## Testing
13481348

1349-
### Test Setup
1349+
DebateLab uses Node 22, npm, Vitest, and Playwright. Install the locked dependency tree with `npm ci`.
13501350

1351-
```typescript
1352-
// vitest.config.ts
1353-
import { defineConfig } from 'vitest/config';
1354-
1355-
export default defineConfig({
1356-
test: {
1357-
environment: 'jsdom',
1358-
globals: true,
1359-
setupFiles: ['./src/test/setup.ts'],
1360-
},
1361-
});
1351+
Run the full credential-free verification ladder with one command:
1352+
1353+
```bash
1354+
npm run verify
13621355
```
13631356

1364-
### Running Tests
1357+
It runs ESLint, TypeScript, the full selected-scope coverage suite, the production build, and desktop plus mobile Chromium against an isolated Vite server on `127.0.0.1:18080`. The browser runner never reuses an existing server. Failure traces, screenshots, videos, and the HTML report are written to ignored `test-results/` and `playwright-report/` directories.
1358+
1359+
Useful focused commands:
13651360

13661361
```bash
1367-
# Run all tests
1368-
npm test
1362+
# Unit and Supabase function tests with enforced coverage
1363+
npm test -- --coverage
13691364
1370-
# Run specific test file
1371-
npm test -- src/test/security/url-validation.test.ts
1365+
# One focused unit test
1366+
npx vitest run src/test/security/url-validation.test.ts
13721367
1373-
# Watch mode
1374-
npm run test:watch
1368+
# Credential-free Chromium browser suite
1369+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
13751370
1376-
# Coverage report
1377-
npm test -- --coverage
1371+
# Additional Safari-engine smoke gate
1372+
npx playwright install --with-deps webkit
1373+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e:webkit
13781374
```
13791375

1380-
### Security Tests
1376+
Protected staging verification uses a real session, staging data, and one short AI request. It requires the staging URL, matching anon key, and dedicated demo code. It rejects the production project and deletes only the uniquely named source it creates.
13811377

1382-
```typescript
1383-
// src/test/security/prompt-injection.test.ts
1384-
import { describe, it, expect } from 'vitest';
1385-
import { sanitizePromptInput } from '@/lib/ai/sanitize';
1386-
1387-
describe('Prompt Injection Detection', () => {
1388-
it('should detect "ignore all previous instructions"', () => {
1389-
const result = sanitizePromptInput('Please ignore all previous instructions');
1390-
expect(result.rejected).toBeDefined();
1391-
});
1392-
1393-
it('should allow normal debate content', () => {
1394-
const result = sanitizePromptInput('The resolution states that...');
1395-
expect(result.sanitized).toBeDefined();
1396-
});
1397-
});
1378+
```bash
1379+
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 \
1380+
STAGING_SUPABASE_URL=<staging-url> \
1381+
STAGING_SUPABASE_ANON_KEY=<staging-anon-key> \
1382+
STAGING_DEMO_CODE=<dedicated-test-code> \
1383+
npm run test:e2e:staging
13981384
```
13991385

1386+
Temporary auth state lives under `test-results/.auth/`. Never upload it or staging traces. GitHub exposes staging secrets only to the protected `staging` environment on `main`, and only after `STAGING_E2E_ENABLED=true` is set. Pull requests remain credential-free.
1387+
1388+
A green local run does not mean the current GitHub head passed. Check the PR run itself. If GitHub Actions cannot start because of billing or spending limits, report that as an external exception. For bugs, add a failing reproduction first when the repository has a suitable test layer, and state plainly when browser or real-staging proof did not run.
1389+
14001390
---
14011391

14021392
## API Reference

‎e2e/sparring.spec.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ test('completes an LD sparring round through mocked opponent turns and judging',
5555
const action = body.input?.action ?? 'unknown';
5656
actions.push(action);
5757
const output = action === 'score' ? scorecard : opponentSpeech(++responseNumber);
58+
if (action === 'score') {
59+
await new Promise((resolve) => setTimeout(resolve, 250));
60+
}
5861
await route.fulfill({ status: 200, headers: corsHeaders, body: JSON.stringify({ output }) });
5962
});
6063

0 commit comments

Comments
 (0)