Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .audit/agent-verification-loop.tsv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ts phase decision why evidence result
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
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
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
2026-08-31T15:35:30Z staging disabled authenticated traces and excluded auth state traces and storage state can contain bearer and refresh tokens playwright.config.ts, .github/workflows/staging-e2e.yml partial: inherited failure video was still uploaded
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
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
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
Expand All @@ -17,3 +17,4 @@ ts phase decision why evidence result
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
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
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
2026-08-31T19:48:03Z staging disabled traces, screenshots, and video and removed staging artifact uploads all browser captures can expose authenticated staging data playwright.config.ts, .github/workflows/staging-e2e.yml no authenticated browser artifacts retained or uploaded
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
node_modules
build
coverage
playwright-report
test-results
.git
.gitignore
.env*
Expand Down
91 changes: 86 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ concurrency:
cancel-in-progress: true

jobs:
build:
quality:
runs-on: ubuntu-latest
env:
DOCKER_PUSH: ${{ github.event_name == 'push' && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }}
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand All @@ -32,8 +30,11 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Set up Deno
uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5
with:
deno-version: 2.9.5
cache: true

- name: Lint
run: npm run lint
Expand All @@ -44,9 +45,71 @@ jobs:
- name: Test
run: npm test -- --coverage

- name: Edge function tests
run: npm run test:edge

- name: Build
run: npm run build

- name: Upload production assets
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: production-assets-${{ github.sha }}
path: dist/
if-no-files-found: error
retention-days: 1

database-tests:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22

- name: Set up Supabase CLI
uses: supabase/setup-cli@46f7f98c7f948ad727d22c1e67fab04c223a0520 # v3.0.0
with:
version: 2.115.0

- name: Start isolated database and apply migrations
run: supabase db start

- name: Database policy tests
run: npm run test:db

- name: Stop isolated database
if: always()
run: supabase stop --no-backup

browser-tests:
if: github.event_name == 'pull_request'
needs: quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22

- name: Install dependencies
run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

- name: Browser tests
env:
CI: 1
Expand All @@ -65,6 +128,24 @@ jobs:
if-no-files-found: ignore
retention-days: 7

docker:
if: github.event_name != 'pull_request'
needs: quality
runs-on: ubuntu-latest
env:
DOCKER_PUSH: ${{ github.event_name == 'push' && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }}
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Download production assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: production-assets-${{ github.sha }}
path: dist/

- name: Set up QEMU
if: env.DOCKER_PUSH == 'true'
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
Expand Down
13 changes: 0 additions & 13 deletions .github/workflows/staging-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,3 @@ jobs:
STAGING_SUPABASE_ANON_KEY: ${{ secrets.STAGING_SUPABASE_ANON_KEY }}
STAGING_DEMO_CODE: ${{ secrets.STAGING_DEMO_CODE }}
run: npm run test:e2e:staging

- name: Upload staging failure artifacts
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: staging-browser-failure-${{ github.run_id }}
path: |
test-results/**/*.png
test-results/**/*.webm
!test-results/.auth/
!test-results/**/*.zip
if-no-files-found: ignore
retention-days: 7
13 changes: 11 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ Run the smallest relevant reproduction first, then the complete gate before a PR
npm run lint
npm run typecheck
npm test -- --coverage
npm run test:edge
npm run build
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
```

`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.

Database migrations and pgTAP policies have a separate Docker-backed gate. Supabase CLI 2.115 or newer is required so `CREATE INDEX CONCURRENTLY` runs outside the migration batch:

```bash
supabase db start
npm run test:db
supabase stop --no-backup
```

WebKit is an additional release smoke gate:

```bash
Expand All @@ -46,11 +55,11 @@ STAGING_DEMO_CODE=<dedicated-test-code> \
npm run test:e2e:staging
```

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.
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/`. Staging disables traces, screenshots, and video, and its workflow uploads no browser artifacts.

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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

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.
The staging origin `http://127.0.0.1:18080` must be deployed in the CORS allowlists for `demo-code-login`, `ai`, `ai-stream`, `admin`, and `announcements` before enabling the gate. Do not deploy those functions or apply migrations to an unverified project.

## Working rules

Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1354,14 +1354,22 @@ Run the full credential-free verification ladder with one command:
npm run verify
```

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.
It runs ESLint, TypeScript, the full selected-scope coverage suite, native Deno Edge Function tests, 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.

Useful focused commands:

```bash
# Unit and Supabase function tests with enforced coverage
npm test -- --coverage

# Native Deno Edge Function tests, including the exported handler boundary
npm run test:edge

# Database migration replay and pgTAP policy tests, with local Supabase running
supabase db start
npm run test:db
supabase stop --no-backup

# One focused unit test
npx vitest run src/test/security/url-validation.test.ts

Expand All @@ -1383,7 +1391,7 @@ STAGING_DEMO_CODE=<dedicated-test-code> \
npm run test:e2e:staging
```

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.
Temporary auth state lives under `test-results/.auth/`. Staging disables traces, screenshots, and video, and its workflow uploads no browser artifacts. 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.

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.

Expand Down
31 changes: 30 additions & 1 deletion e2e/admin-announcements.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ test('user sees and permanently dismisses an active announcement', async ({ page
expect(dismissBody).toEqual({ announcementId: ANNOUNCEMENT_ID });
});

test('admin creates a targeted announcement and deletes it from history', async ({ page }) => {
test('admin can deactivate without losing history, then explicitly delete', async ({ page }) => {
let createBody: Record<string, unknown> | null = null;
let deactivateBody: Record<string, unknown> | null = null;
let deleteBody: Record<string, unknown> | null = null;

await page.route('**/functions/v1/announcements*', (route) => route.fulfill({ json: { announcements: [] } }));
Expand Down Expand Up @@ -90,6 +91,27 @@ test('admin creates a targeted announcement and deletes it from history', async
target_user_ids: [TARGET_USER_ID],
starts_at: '2026-08-31T12:00:00.000Z',
expires_at: null,
deactivated_at: null,
created_by: '99999999-9999-4999-8999-999999999999',
created_at: '2026-08-31T12:00:00.000Z',
},
},
});
return;
}
if (action === 'deactivate-announcement') {
deactivateBody = request.postDataJSON();
await route.fulfill({
json: {
announcement: {
id: ANNOUNCEMENT_ID,
title: 'Targeted note',
body: 'This message is only for the selected user.',
target_type: 'users',
target_user_ids: [TARGET_USER_ID],
starts_at: '2026-08-31T12:00:00.000Z',
expires_at: null,
deactivated_at: '2026-08-31T19:30:00.000Z',
created_by: '99999999-9999-4999-8999-999999999999',
created_at: '2026-08-31T12:00:00.000Z',
},
Expand Down Expand Up @@ -125,6 +147,13 @@ test('admin creates a targeted announcement and deletes it from history', async
targetUserIds: [TARGET_USER_ID],
});

await page.getByRole('button', { name: 'Deactivate announcement: Targeted note' }).click();
await page.getByRole('button', { name: 'Deactivate announcement' }).click();

await expect.poll(() => deactivateBody).toEqual({ announcementId: ANNOUNCEMENT_ID });
await expect(page.getByRole('heading', { name: 'Targeted note' })).toBeVisible();
await expect(page.getByText('Deactivated', { exact: true })).toBeVisible();

await page.getByRole('button', { name: 'Delete announcement: Targeted note' }).click();
await page.getByRole('button', { name: 'Delete announcement' }).click();

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"test:edge": "deno test --allow-read --allow-env=EXA_API_KEY,SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,SPARRING_INTEGRITY_SECRET,OPENAI_API_KEY --allow-net=127.0.0.1 supabase/functions/",
"test:db": "supabase test db",
"test:e2e": "playwright test --project=desktop-chromium --project=mobile-chromium",
"test:e2e:webkit": "playwright test --project=webkit",
"test:e2e:staging": "playwright test --project=staging",
"test:e2e:ui": "playwright test --ui",
"verify": "npm run lint && npm run typecheck && npm test -- --coverage && npm run build && CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e"
"verify": "npm run lint && npm run typecheck && npm test -- --coverage && npm run test:edge && npm run build && CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e"
},
"dependencies": {
"@capacitor/app": "^8.1.0",
Expand Down
8 changes: 5 additions & 3 deletions plans/admin-announcements.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Create `admin_announcements`:
- `target_user_ids uuid[] null`
- `starts_at timestamptz default now()`
- `expires_at timestamptz null`
- `deactivated_at timestamptz null`
- `created_by uuid not null`
- `created_at timestamptz default now()`

Expand All @@ -30,7 +31,8 @@ Create `user_announcement_dismissals`:
1. Add admin endpoints to existing `admin` edge function:
- create announcement.
- list announcements.
- deactivate/delete announcement.
- deactivate an announcement without deleting dismissal history.
- explicitly delete an announcement when permanent removal is intended.
2. Add authenticated non-admin endpoint:
- fetch active announcements for current user.
- dismiss announcement.
Expand All @@ -45,8 +47,8 @@ Create `user_announcement_dismissals`:

## Tests

- RLS/admin endpoint tests where practical.
- Admin UI tests for create/list/delete.
- RLS/admin endpoint tests, including deactivation and dismissal-history retention.
- Admin UI tests for create/list/deactivate/delete.
- Layout test for showing and dismissing an announcement.

## Deployment
Expand Down
5 changes: 3 additions & 2 deletions plans/agent-verification-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ must run all of these gates:
npm run lint
npm run typecheck
npm test -- --coverage
npm run test:edge
npm run build
CI=1 PORT=18080 PLAYWRIGHT_BASE_URL=http://127.0.0.1:18080 npm run test:e2e
```
Expand Down Expand Up @@ -181,8 +182,8 @@ npm run test:e2e:staging

The CI staging job may run only from a protected environment on an authorized
`main` push or explicitly authorized main-branch dispatch. It must not run on
pull requests or arbitrary branch dispatches. The job should upload the same
failure artifacts without exposing secret-bearing logs.
pull requests or arbitrary branch dispatches. It disables traces, screenshots,
and video and uploads no authenticated browser artifacts.

### 4. Add the verification playbook

Expand Down
16 changes: 12 additions & 4 deletions plans/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,25 @@ Run every step before merging this branch. Nothing here deploys anything; deploy
- [ ] `npm test` — full vitest suite green
- [ ] `npm run build` — production build succeeds
- [ ] `deno check supabase/functions/ai/index.ts supabase/functions/ai-stream/index.ts supabase/functions/admin/index.ts`
- [ ] `deno test --allow-read --allow-env=EXA_API_KEY --allow-net=127.0.0.1 supabase/functions/` — includes cost calculator, research/quality-floor, prompt-layer, and control-matrix suites
- [ ] `npm run test:edge` — native Deno coverage including the exported handler boundary
- [ ] `supabase db start; trap 'supabase stop --no-backup' EXIT; npm run test:db` — migration replay and pgTAP policy coverage with Supabase CLI 2.115 or newer, with cleanup even on failure

## 2. Migration review (before any deploy)

Two prepared migrations ship on this branch; neither is applied by the branch:
The two Quality & Truth migrations below are recorded in section 0 as applied
to both staging and production. No deployment action remains for them in this
follow-up; confirm each target's migration history again before a future deploy:

- `20260822000000_ai_request_cost_audit.sql` — audit columns on `ai_requests`
- `20260822000001_source_provenance_metadata.sql` — `metadata` jsonb on `sources`

- [ ] Confirm both files reviewed against current schema, migration order, and RLS (`ai_requests` stays service-role-write-only; `sources` keeps per-user policies)
- [ ] Apply order: migrations first, then edge functions (`ai`, `ai-stream`, `admin`), then frontend
The audit follow-up adds one migration that must run before the updated `admin`
function or frontend is deployed:

- `20260831191838_deactivate_announcements.sql` — adds `deactivated_at` and excludes deactivated announcements from authenticated reads without deleting dismissal history

- [ ] Confirm all changed migration files are reviewed against current schema, migration order, and RLS (`ai_requests` stays service-role-write-only; `sources` keeps per-user policies; announcement deactivation preserves dismissal rows)
- [ ] Apply order: pending migrations first, then edge functions (`ai`, `ai-stream`, `admin`, `announcements` as changed), then frontend
- both edge functions fall back to a base insert if audit columns are missing, so a deploy-order mistake degrades logging instead of breaking requests
- the frontend sends `metadata` on source saves; deploying it before the `sources` migration will fail those writes

Expand Down
Loading
Loading