From 3ea677f5631b24e7f0d1262bf9c22ffabfa09bc7 Mon Sep 17 00:00:00 2001 From: aaddrick Date: Sun, 3 May 2026 23:02:17 -0400 Subject: [PATCH] =?UTF-8?q?test(harness):=20session=2011=20T21=20dev=20ser?= =?UTF-8?q?ver=20preview=20runtime=20(1=20new=20spec,=2095%=20=E2=86=92=20?= =?UTF-8?q?96%=20coverage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier 2 reframe of the T21 case-doc claim "dev server preview pane starts on Preview → Start". First runtime probe for T21 — no fingerprint sibling shipped (case-doc anchors point at impl-side function names, not user-facing literals). Multi-suffix `waitForEipcChannels` over five case-doc-anchored Launch suffixes (`getConfiguredServices`, `startFromConfig`, `stopServer`, `getAutoVerify`, `capturePreviewScreenshot`) plus dual `invokeEipcChannel` on the case-doc-anchored read-side getters: `getConfiguredServices(cwd)` returns array, `getAutoVerify(cwd)` returns boolean. cwd validator is `typeof cwd === 'string'` only — smoke-tested against the debugger-attached running Claude (session 11 finding); empty / relative / non-existent paths all pass, only null / undefined / object wraps reject. Different shape from T19 / T20: those use `LocalSessions/getAll` as a foundational read-side surrogate because their case-doc anchors are write-side. T21's case-doc anchors include native read-side handlers, so invocation lands on case-doc-anchored handlers directly (mirrors T33c's dual-handler pattern). Mixed-shape dual invocation (one returns array, another returns boolean) is fine — each shape asserted independently. Read-only by design — neither `getConfiguredServices` nor `getAutoVerify` spawns subprocesses, mutates fs, or performs network egress. cwd is `process.cwd()` (the test process's own working directory). Passes on KDE-W in 16.7s (cold) / 5.2s (warm follow-up). Co-Authored-By: Claude --- .../src/runners/T21_runtime.spec.ts | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 tools/test-harness/src/runners/T21_runtime.spec.ts diff --git a/tools/test-harness/src/runners/T21_runtime.spec.ts b/tools/test-harness/src/runners/T21_runtime.spec.ts new file mode 100644 index 0000000..61df8f6 --- /dev/null +++ b/tools/test-harness/src/runners/T21_runtime.spec.ts @@ -0,0 +1,253 @@ +import { test, expect } from '@playwright/test'; +import { launchClaude } from '../lib/electron.js'; +import { createIsolation, type Isolation } from '../lib/isolation.js'; +import { captureSessionEnv } from '../lib/diagnostics.js'; +import { invokeEipcChannel, waitForEipcChannels } from '../lib/eipc.js'; + +// T21 — Dev server preview pane IPC surface registered + Launch read- +// side handlers invocable (Tier 2 reframe of the case-doc claim "Click +// Preview → Start; configured dev server starts, embedded browser +// renders, auto-verify takes screenshots, Stop actually stops"). First +// runtime probe for T21 — no fingerprint sibling shipped; the case-doc +// anchors point at impl-side function names (`setAutoVerify`, +// `parseLaunchJson`, `capturePage`/`captureViaCDP`) plus an MCP tool +// table (`preview_*`), not the user-facing channel names. +// +// Backs T21 in docs/testing/cases/code-tab-workflow.md ("Dev server +// preview pane"). Session 7's per-interface registry walk did not list +// `claude.web/Launch`; session 10 re-ran the probe with an active +// session and saw all 25 invokeHandlers register on the claude.ai +// webContents. Smoke-test against a debugger-attached running Claude +// (session 11) confirmed the wrapper at +// `window['claude.web'].Launch` exposes 30 callable members (25 +// invokeHandlers + 5 `on*` event subscribers + `isAvailable` + +// `activeServersStore`) and that the `cwd` arg validator on the +// read-side getters is `typeof cwd === 'string'` only — no path +// existence check, no absolute-path requirement, empty / relative / +// non-existent strings all pass. +// +// Why both layers — registration AND invocation +// --------------------------------------------- +// Registration of the 5 case-doc-anchored Launch suffixes proves the +// preview pane's IPC surface is wired (start / stop / screenshot / +// auto-verify / configured-services). Invocation of `getConfiguredServices` +// and `getAutoVerify` proves the Launch impl object is reachable +// through the renderer wrapper and returns the documented shapes +// (array of services, boolean auto-verify state). A half-applied +// refactor where the registration block runs but the impl object is +// missing methods would pass registration-only and fail invocation. +// Different shape from T19 / T20 (which use `LocalSessions/getAll` as +// a foundational read-side surrogate because their case-doc anchors +// are write-side); T21's case-doc anchors include native read-side +// handlers, so the invocation is on a case-doc-anchored handler +// directly — same pattern as T33c's dual-handler invocation. +// +// Why these 5 registration suffixes +// --------------------------------- +// The preview pane's user flow per the case-doc steps: +// 1. Configure `.claude/launch.json` (auto-detect populates it) → +// `getConfiguredServices` reads it. +// 2. Click Preview → Start → `startFromConfig` spawns the dev +// server. +// 3. Auto-verify takes screenshots → `capturePreviewScreenshot` + +// `getAutoVerify` reads the `autoVerify: true` flag. +// 4. Stop the server from the dropdown → `stopServer` kills the +// process. +// All five are load-bearing as a unit; partial registration would +// break either "Start" / "Stop" / auto-verify reads / screenshot / +// initial config read. +// +// Why these 2 invocation targets +// ------------------------------ +// Both `getConfiguredServices(cwd) → Array<…>` and +// `getAutoVerify(cwd) → boolean` are pure read-side handlers — no +// process spawn, no fs writes. `getConfiguredServices` reads +// `/.claude/launch.json` and returns an empty array when missing +// (the test's harness CWD has no `.claude/launch.json`, so the +// observed value is `[]`); `getAutoVerify` returns the boolean value +// of the `autoVerify` flag, defaulting to false on a missing config. +// Invoking both gives an array-shape assertion AND a boolean-type +// assertion — strictly stronger than either alone, and the dual- +// invocation cost is negligible (~200ms). +// +// Read-only by design — neither handler spawns subprocesses, mutates +// fs, or performs network egress. The cwd arg is the test process's +// own working directory; no user content is read. +// +// Skip semantics +// -------------- +// `seedFromHost: true` is required — without a signed-in claude.ai, +// the renderer never reaches claude.ai origin and the Launch wrapper +// isn't exposed (mirrors T22b / T31b / T33b / T33c / T35b / T37b / +// T38b / T19 / T20 pattern). + +test.setTimeout(90_000); + +const EXPECTED_SUFFIXES = [ + 'Launch_$_getConfiguredServices', + 'Launch_$_startFromConfig', + 'Launch_$_stopServer', + 'Launch_$_getAutoVerify', + 'Launch_$_capturePreviewScreenshot', +] as const; + +// `cwd` arg shape on Launch read-side handlers: positional string at +// position 0. Validator is `typeof cwd === 'string'` only (smoke-tested +// session 11 against a debugger-attached running Claude — empty, +// relative, and non-existent paths all pass; `null`, `undefined`, and +// object wraps reject). Using `process.cwd()` makes the invocation +// path defensible (the test process is definitely running there) and +// non-sensitive (the harness CWD is the project root, never a user- +// account-scoped path). +const INVOKE_CWD = process.cwd(); + +test('T21 — Dev server preview pane IPC surface + Launch read-sides invocable', async ( + {}, + testInfo, +) => { + testInfo.annotations.push({ type: 'severity', description: 'Should' }); + testInfo.annotations.push({ + type: 'surface', + description: 'Code tab — Preview pane (eipc registration + invocation)', + }); + + await testInfo.attach('session-env', { + body: JSON.stringify(captureSessionEnv(), null, 2), + contentType: 'application/json', + }); + + let isolation: Isolation; + try { + isolation = await createIsolation({ seedFromHost: true }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + test.skip(true, `seedFromHost unavailable: ${msg}`); + return; + } + + const app = await launchClaude({ isolation }); + try { + const ready = await app.waitForReady('userLoaded'); + await testInfo.attach('claude-ai-url', { + body: ready.claudeAiUrl ?? '(no claude.ai webContents observed)', + contentType: 'text/plain', + }); + if (!ready.postLoginUrl) { + test.skip( + true, + 'seeded auth did not reach post-login URL — host config ' + + 'may be stale (signed out, expired session, etc.)', + ); + return; + } + await testInfo.attach('post-login-url', { + body: ready.postLoginUrl, + contentType: 'text/plain', + }); + + const resolved = await waitForEipcChannels( + ready.inspector, + EXPECTED_SUFFIXES, + ); + + // Invoke `getConfiguredServices` — array of configured dev server + // services. The harness CWD has no `.claude/launch.json`, so + // the observed value is `[]`. Service config bodies may include + // user-account-scoped paths (e.g. project workspace paths from + // auto-detect); log length only, never bodies (mirrors T19/T20/ + // T33c/T37b's defensive default). + let servicesShape = 'not-invoked'; + let servicesLength: number | null = null; + const servicesResult = await invokeEipcChannel( + ready.inspector, + 'Launch_$_getConfiguredServices', + [INVOKE_CWD], + ); + if (Array.isArray(servicesResult)) { + servicesShape = `array(length=${servicesResult.length})`; + servicesLength = servicesResult.length; + } else if (servicesResult === null) { + servicesShape = 'null'; + } else { + servicesShape = typeof servicesResult; + } + + // Invoke `getAutoVerify` — boolean. The cwd's launch.json + // `autoVerify` flag defaults to false on missing config. + let autoVerifyShape = 'not-invoked'; + let autoVerifyValue: boolean | null = null; + const autoVerifyResult = await invokeEipcChannel( + ready.inspector, + 'Launch_$_getAutoVerify', + [INVOKE_CWD], + ); + if (typeof autoVerifyResult === 'boolean') { + autoVerifyShape = 'boolean'; + autoVerifyValue = autoVerifyResult; + } else if (autoVerifyResult === null) { + autoVerifyShape = 'null'; + } else { + autoVerifyShape = typeof autoVerifyResult; + } + + const registration: Record = {}; + for (const suffix of EXPECTED_SUFFIXES) { + registration[suffix] = resolved.get(suffix); + } + + await testInfo.attach('t21-runtime', { + body: JSON.stringify( + { + expectedRegistrationSuffixes: EXPECTED_SUFFIXES, + registration, + invocations: [ + { + suffix: 'Launch_$_getConfiguredServices', + args: [INVOKE_CWD], + responseShape: servicesShape, + responseLength: servicesLength, + }, + { + suffix: 'Launch_$_getAutoVerify', + args: [INVOKE_CWD], + responseShape: autoVerifyShape, + responseValue: autoVerifyValue, + }, + ], + }, + null, + 2, + ), + contentType: 'application/json', + }); + + for (const suffix of EXPECTED_SUFFIXES) { + expect( + resolved.get(suffix), + `[T21] eipc channel ending in '${suffix}' is registered on ` + + 'the claude.ai webContents — load-bearing for the dev ' + + 'server preview pane (case-doc anchors index.js:259604 / ' + + ':260015 / :262175)', + ).not.toBeNull(); + } + + expect( + Array.isArray(servicesResult), + `[T21] Launch/getConfiguredServices response is an array ` + + `(got ${servicesShape}) — the preview pane reads the ` + + 'configured dev server list from `/.claude/launch.json`; ' + + 'an array result (empty or non-empty) proves the Launch impl ' + + 'object is reachable through the renderer wrapper', + ).toBe(true); + + expect( + typeof autoVerifyResult, + `[T21] Launch/getAutoVerify response is a boolean ` + + `(got ${autoVerifyShape}) — auto-verify drives the ` + + 'preview-pane screenshot loop; a boolean result proves the ' + + '`autoVerify` flag read path is wired through the Launch impl', + ).toBe('boolean'); + } finally { + await app.close(); + } +});