Skip to content

Commit 6c6a1be

Browse files
Merge pull request #198 from BjoernSchotte/codex/parallel-browser-evidence
test(extension): parallel browser evidence lanes
2 parents cec8aac + 35148ff commit 6c6a1be

23 files changed

Lines changed: 2186 additions & 78 deletions

.github/workflows/ci.yml

Lines changed: 104 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,11 @@ jobs:
309309
- name: Sink commit-protocol tests
310310
run: bun --conditions=development test apps/cli/src/commands/export-pdf-sink.test.ts
311311

312-
browser-export-harness:
312+
browser-export-assets:
313313
needs: changes
314314
if: needs.changes.outputs.proofMode == 'required' && needs.changes.outputs.browserHarness == 'true'
315315
runs-on: ubuntu-latest
316-
timeout-minutes: 20
316+
timeout-minutes: 15
317317
steps:
318318
- uses: actions/checkout@v7
319319

@@ -343,21 +343,12 @@ jobs:
343343
${{ runner.os }}-turbo-browser-
344344
${{ runner.os }}-turbo-
345345
346-
- name: Restore Playwright Chromium
347-
uses: actions/cache@v6
348-
with:
349-
path: ~/.cache/ms-playwright
350-
key: ${{ runner.os }}-playwright-chromium-1.55.0
351-
352346
- name: Install dependencies
353347
run: bun install --frozen-lockfile
354348

355349
- name: Provision pinned PDF fonts
356350
run: bun run fonts:ensure
357351

358-
- name: Install Playwright Chromium
359-
run: bunx playwright@1.55.0 install --with-deps chromium
360-
361352
- name: Type check browser export harness
362353
run: bun run typecheck:browser-export-harness
363354

@@ -370,29 +361,19 @@ jobs:
370361
- name: Assert conformance-case registry
371362
run: bun run assert:conformance-cases
372363

373-
- name: Run browser export harness E2E
374-
run: bun run test:browser-export-harness
375-
376364
- name: Build packed MV3 extension once
377365
run: bun run build:extension
378366

379-
- name: Verify PDF.js worker in the built MV3 extension
380-
run: bun run --cwd apps/extension test:worker-extension-browser:prebuilt
381-
382-
- name: Verify durable export jobs in the built MV3 extension
383-
run: bun run --cwd apps/extension test:jobs-extension-browser:prebuilt
384-
385-
- name: Verify Research in the built MV3 extension
386-
run: bun run --cwd apps/extension test:research-extension-browser:prebuilt
387-
388-
- name: Verify Rovo visibility in the built MV3 extension
389-
run: bun run --cwd apps/extension test:rovo-extension-browser:prebuilt
390-
391-
- name: Verify action palette in the built MV3 extension
392-
run: bun run --cwd apps/extension test:palette-extension-browser:prebuilt
393-
394-
- name: Shape-parity gate
395-
run: bun run check:parity
367+
- name: Upload prebuilt browser assets
368+
uses: actions/upload-artifact@v7
369+
with:
370+
name: browser-assets-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
371+
path: |
372+
apps/browser-export-harness/dist
373+
apps/extension/.output/chrome-mv3
374+
if-no-files-found: error
375+
include-hidden-files: true
376+
retention-days: 1
396377

397378
- name: Sanitize Turbo run summaries
398379
if: always()
@@ -412,6 +393,98 @@ jobs:
412393
if-no-files-found: error
413394
retention-days: 14
414395

396+
browser-export-harness:
397+
name: Browser evidence / ${{ matrix.lane }}
398+
needs: [changes, browser-export-assets]
399+
if: always() && needs.changes.outputs.proofMode == 'required' && needs.changes.outputs.browserHarness == 'true' && needs.browser-export-assets.result == 'success'
400+
strategy:
401+
fail-fast: false
402+
matrix:
403+
lane: [neutral-palette, research-worker-rovo, jobs]
404+
runs-on: ubuntu-latest
405+
timeout-minutes: 15
406+
env:
407+
ATLCLI_BROWSER_EVIDENCE_ROOT: ${{ github.workspace }}/.artifacts/browser-evidence
408+
# Shared GitHub runners record timing but do not enforce local/homelab
409+
# latency budgets. Functional, network, long-task, and size gates remain.
410+
ATLCLI_BROWSER_ASSERT_TIMING: "0"
411+
steps:
412+
- uses: actions/checkout@v7
413+
414+
- name: Setup Bun
415+
uses: oven-sh/setup-bun@v2
416+
with:
417+
bun-version: 1.3.14
418+
419+
- name: Restore Bun package cache
420+
uses: actions/cache@v6
421+
with:
422+
path: ~/.bun/install/cache
423+
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
424+
425+
- name: Restore pinned PDF fonts
426+
uses: actions/cache@v6
427+
with:
428+
path: packages/pdf/.fonts
429+
key: ${{ runner.os }}-pdf-fonts-v1-${{ hashFiles('packages/pdf/src/runtime-assets.ts', 'packages/pdf/scripts/ensure-fonts.ts') }}
430+
431+
- name: Restore Playwright Chromium
432+
uses: actions/cache@v6
433+
with:
434+
path: ~/.cache/ms-playwright
435+
key: ${{ runner.os }}-playwright-chromium-1.55.0
436+
437+
- name: Install dependencies
438+
run: bun install --frozen-lockfile
439+
440+
- name: Provision pinned PDF fonts
441+
run: bun run fonts:ensure
442+
443+
- name: Install Playwright Chromium
444+
run: bunx playwright@1.55.0 install --with-deps chromium
445+
446+
- name: Download exact prebuilt browser assets
447+
uses: actions/download-artifact@v8
448+
with:
449+
name: browser-assets-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
450+
# upload-artifact stores both inputs relative to their common apps/
451+
# root, so restore that root explicitly in each isolated runner.
452+
path: apps
453+
454+
- name: Verify prebuilt browser assets
455+
run: |
456+
test -d apps/browser-export-harness/dist
457+
test -f apps/extension/.output/chrome-mv3/manifest.json
458+
459+
- name: Run isolated browser lane
460+
run: bun scripts/ci/run-browser-lane.ts "${{ matrix.lane }}"
461+
462+
- name: Validate synthetic browser evidence
463+
id: browser-evidence-validation
464+
if: always()
465+
run: bun scripts/ci/validate-browser-evidence.ts "$ATLCLI_BROWSER_EVIDENCE_ROOT"
466+
467+
- name: Upload browser evidence summaries
468+
if: always() && steps.browser-evidence-validation.outcome == 'success'
469+
uses: actions/upload-artifact@v7
470+
with:
471+
name: browser-evidence-summary-${{ matrix.lane }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
472+
path: |
473+
.artifacts/browser-evidence/*/junit.xml
474+
.artifacts/browser-evidence/*/summary.json
475+
.artifacts/browser-evidence/*/manifest.json
476+
if-no-files-found: error
477+
retention-days: 14
478+
479+
- name: Upload browser failure evidence
480+
if: failure() && steps.browser-evidence-validation.outcome == 'success'
481+
uses: actions/upload-artifact@v7
482+
with:
483+
name: browser-evidence-failures-${{ matrix.lane }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
484+
path: .artifacts/browser-evidence/*/failures
485+
if-no-files-found: error
486+
retention-days: 7
487+
415488
# Compatibility signal only. GitHub runner Chrome is intentionally not a
416489
# substitute for Playwright-matched Chromium in the required MV3 proof.
417490
browser-system-chrome-canary:

apps/browser-export-harness/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
"check:output": "bun --conditions=development scripts/check-output.ts",
1313
"assert:cases": "bun scripts/assert-case-manifest.ts",
1414
"precheck:parity": "bun run --cwd ../.. fonts:ensure && bun run --cwd ../.. vendor:typst && bun run --cwd ../.. vendor:pdfium",
15-
"check:parity": "bun --conditions=development scripts/check-parity.ts",
15+
"check:parity": "bun --conditions=development run check:parity:prebuilt",
16+
"check:parity:prebuilt": "bun --conditions=development scripts/check-parity.ts",
1617
"test:unit": "bun --conditions=development test tests/boundaries.test.ts tests/output-scan.test.ts tests/pdf-worker-client.test.ts tests/pdf-job-parity.test.ts tests/docx-job-parity.test.ts scripts/check-parity.test.ts",
1718
"pretest:e2e": "bun run --cwd ../.. fonts:ensure && bun run --cwd ../.. vendor:typst && bun run --cwd ../.. vendor:pdfium",
18-
"test:e2e": "playwright test"
19+
"test:e2e": "bun --conditions=development run test:e2e:prebuilt",
20+
"test:e2e:prebuilt": "playwright test"
1921
},
2022
"dependencies": {
2123
"@atlcli/code-highlight": "workspace:*",

apps/browser-export-harness/playwright.config.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { join, resolve } from "node:path";
12
import { defineConfig, devices } from "@playwright/test";
23

34
const port = Number(process.env.ATLCLI_HARNESS_PORT ?? "4179");
@@ -9,6 +10,10 @@ const browserChannel = process.env.ATLCLI_PLAYWRIGHT_CHANNEL as
910
| "chrome"
1011
| "chromium"
1112
| undefined;
13+
const evidenceDir = resolve(
14+
process.env.ATLCLI_BROWSER_EVIDENCE_DIR ??
15+
join(import.meta.dirname, "../../.artifacts/browser-evidence/neutral"),
16+
);
1217

1318
export default defineConfig({
1419
testDir: "./tests",
@@ -18,12 +23,23 @@ export default defineConfig({
1823
retries: 0,
1924
timeout: 120_000,
2025
expect: { timeout: 60_000 },
21-
reporter: "line",
26+
reporter: [
27+
["line"],
28+
["junit", { outputFile: join(evidenceDir, "junit.xml") }],
29+
],
30+
outputDir: join(evidenceDir, ".playwright"),
2231
use: {
2332
...devices["Desktop Chrome"],
2433
baseURL: mountUrl,
2534
headless: true,
26-
trace: "retain-on-failure",
35+
trace: {
36+
mode: "retain-on-failure",
37+
screenshots: true,
38+
snapshots: true,
39+
sources: false,
40+
},
41+
screenshot: "only-on-failure",
42+
video: "retain-on-failure",
2743
},
2844
webServer: {
2945
command: "bun scripts/serve-dist.ts",

apps/extension/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,43 @@ The repository-level `bun run check:browser-export-harness` and
169169
`bun run test:browser-export-harness` commands prove the reusable DOCX/PDF package contracts in
170170
an independent production Vite/Chromium host.
171171

172+
### Parallel packed-browser checks
173+
174+
Build the production artifacts once, then run either the complete local
175+
orchestrator or one fixed lane:
176+
177+
```bash
178+
bun run build:browser-export-harness
179+
bun run build:extension
180+
181+
# Performance-sensitive checks first; long jobs/research checks then overlap.
182+
ATLCLI_BROWSER_EVIDENCE_ROOT="$PWD/.artifacts/browser-evidence" \
183+
bun scripts/ci/run-browser-lanes.ts
184+
185+
# Useful when separate worktrees or homelab workers own separate lanes.
186+
ATLCLI_BROWSER_EVIDENCE_ROOT="$PWD/.artifacts/browser-evidence-jobs" \
187+
bun scripts/ci/run-browser-lane.ts jobs
188+
```
189+
190+
The fixed lanes are `neutral-palette`, `research-worker-rovo`, and `jobs`.
191+
Give every concurrently running worktree or worker its own evidence root; the
192+
default is already worktree-local, but an explicit path makes the ownership
193+
visible. Local and homelab runs enforce the palette latency budget by default.
194+
Set `ATLCLI_BROWSER_ASSERT_TIMING=0` only on an uncontrolled shared runner;
195+
functional, network-isolation, long-task, and bundle-size checks still run.
196+
197+
Each suite writes JUnit, a summary, and a SHA/run/digest-bound manifest. Passed
198+
suites discard browser media. Failed suites retain an opaque test directory
199+
with a Playwright trace plus screenshots and videos. GitHub CI builds the
200+
browser artifacts once and fans the three lanes out to separate runners. It
201+
uploads evidence only after the synthetic-evidence validator has rejected live
202+
tenant data, credentials, private paths, unsafe archives, and unexpected files.
203+
204+
Live Atlassian-account checks are intentionally outside GitHub CI and its
205+
artifact publication path. Run those only on an authorized local or homelab
206+
worker, keep profiles and captures outside Git, and clean up created tenant
207+
resources after the run.
208+
172209
## Design notes
173210

174211
- The compact shell has a scalable product-area switcher. **Publishing** is active;

apps/extension/tests/jobs/packed/job-recovery.e2e.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { join } from "node:path";
66
import { fileURLToPath } from "node:url";
77
import { buildDocx, para } from "@atlcli/docx/fixtures";
88
import { EXTENSION_ROOT, OUTPUT_DIR } from "../../build-helper.js";
9+
import { createPackedBrowserEvidence } from "../../support/packed-browser-evidence.js";
910

1011
const JOB_A = "123e4567-e89b-42d3-a456-426614174000";
1112
const JOB_B = "223e4567-e89b-42d3-a456-426614174000";
@@ -35,6 +36,7 @@ let baseExtensionDir: string;
3536
let userDataDir: string;
3637
let page: Page;
3738
let packedOffscreen: ChildTargetSession | undefined;
39+
const browserEvidence = createPackedBrowserEvidence("jobs");
3840

3941
class ChildTargetSession {
4042
private id = 0;
@@ -355,11 +357,12 @@ async function createPackedPngBytes(): Promise<number[]> {
355357
}
356358

357359
async function launchPackedBrowser(): Promise<void> {
358-
context = await chromium.launchPersistentContext(userDataDir, {
360+
context = await chromium.launchPersistentContext(userDataDir, browserEvidence.launchOptions({
359361
channel: "chromium",
360362
headless: true,
361363
args: [`--disable-extensions-except=${baseExtensionDir}`, `--load-extension=${baseExtensionDir}`],
362-
});
364+
}));
365+
await browserEvidence.attachContext(context);
363366
let serviceWorker = context.serviceWorkers()[0];
364367
serviceWorker ??= await context.waitForEvent("serviceworker", { timeout: 30_000 });
365368
extensionId = new URL(serviceWorker.url()).host;
@@ -471,6 +474,10 @@ test.beforeAll(async () => {
471474
await launchPackedBrowser();
472475
});
473476

477+
test.beforeEach(async ({}, testInfo) => {
478+
await browserEvidence.startTest(testInfo);
479+
});
480+
474481
test.beforeEach(async () => {
475482
await packedOffscreen?.close();
476483
packedOffscreen = undefined;
@@ -508,10 +515,18 @@ test.beforeEach(async () => {
508515
});
509516
});
510517

518+
test.afterEach(async ({}, testInfo) => {
519+
await browserEvidence.finishTest(testInfo);
520+
});
521+
511522
test.afterAll(async () => {
512-
await packedOffscreen?.close();
513-
await context?.close();
514-
rmSync(suiteRoot, { recursive: true, force: true });
523+
try {
524+
await packedOffscreen?.close();
525+
if (context) await browserEvidence.closeContext(context);
526+
} finally {
527+
browserEvidence.finalize();
528+
rmSync(suiteRoot, { recursive: true, force: true });
529+
}
515530
});
516531

517532
async function sendWake(ids?: string[]): Promise<{ kind: string; claimedJobId?: string }> {
@@ -2217,7 +2232,7 @@ test("a full persistent-browser restart automatically reclaims checkpointed work
22172232
const originalExtensionId = extensionId;
22182233
await packedOffscreen?.close();
22192234
packedOffscreen = undefined;
2220-
await context.close();
2235+
await browserEvidence.closeContext(context);
22212236
installBrowserRestartFetchStub();
22222237
await launchPackedBrowser();
22232238

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tmpdir } from "node:os";
22
import { join } from "node:path";
33
import { defineConfig } from "@playwright/test";
4+
import { browserEvidenceSuiteDir } from "../../support/packed-browser-evidence.js";
45

56
export default defineConfig({
67
testDir: ".",
@@ -9,5 +10,8 @@ export default defineConfig({
910
timeout: 180_000,
1011
workers: 1,
1112
retries: 0,
12-
use: { trace: "retain-on-failure" },
13+
reporter: [
14+
["line"],
15+
["junit", { outputFile: join(browserEvidenceSuiteDir("jobs"), "junit.xml") }],
16+
],
1317
});

0 commit comments

Comments
 (0)