From b112cb7861ea21944fbc48fbc2fa05580b2a8ffe Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 20 May 2026 09:41:10 +0200 Subject: [PATCH] =?UTF-8?q?test(01-10):=20wave-3=20task-4=20=E2=80=94=20ha?= =?UTF-8?q?rness=20A15+A16+A17=20(onboarding=20flag=20observability=20+=20?= =?UTF-8?q?no-re-open=20settle=20+=20design-swap-readiness=20with=20@impor?= =?UTF-8?q?t=20probe);=2024/24=20GREEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 01-10 Wave 3: extends the UAT harness with three new page-side assertions covering the onboarding contract + the canonical-tokens design-swap-readiness invariant. UAT baseline 21 → 24 GREEN. tests/uat/extension-page-harness.ts (page-side): - assertA15 — chrome.storage.local 'onboarding-completed' === true + 'installed-at' is number. Verifies SW's openWelcomeIfFirstInstall side-effects. - assertA16 — 2s settle window; chrome.tabs.query welcome-tab count delta === 0. Verifies flag-gating across SW respawns. - assertA17 — 7 sub-checks covering: welcome.html parse + .welcome-hero + >=7 mokosh-keyed attrs + welcome.css canonical @import literal OR inlined --mks-* evidence + (zero hex OR canonical resolved) + >=5 var(--mks-*) refs + bundled JS preserves populate plumbing + getComputedStyle --mks-rec → rgb(178, 84, 61) (canonical D-04 Loom). - window.__mokoshHarness surface extended with the three new methods; type declaration + assignment + page-ready status text updated. tests/uat/lib/harness-page-driver.ts (host-side): - driveA15, driveA16, driveA17 — standard page.evaluate wrappers matching driveA14 / driveA18..A22 idiom. driveA16 dominates the new wall-clock budget (~2.1s for the settle window). tests/uat/harness.test.ts (orchestrator): - Drivers array interleaves A15/A16/A17 AFTER A14 + BEFORE A18. A22's skip-gate no longer triggers (Plan 01-10 lands welcome.html; A22 now exercises the substantive token-usage path). - FORBIDDEN_HOOK_STRINGS unchanged at 12 entries (A15-A17 use only chrome.tabs.query / chrome.storage.local.get / fetch / DOMParser / getComputedStyle — all production-API surfaces). DEVIATION (Rule 1 — auto-fix bug in plan-supplied check): The plan's A17.6 spec used literal substring checks 'COPY[' and 'chrome.i18n.getMessage(' which fail against minified production output. Vite/Rollup terser renames `COPY` → `f` (local variable mangling) and welcome.ts's source uses optional chaining `chrome?.i18n?.getMessage?.(` which doesn't match the verbatim literal. Replaced with two minification-survivable witnesses: 1. 'welcome.page.title' — literal Object.freeze key (terser preserves object-literal keys verbatim). 2. 'i18n' + 'getMessage' + 'welcomeHero' substring conjunction — chrome global + property access + fallback key literal; all three survive minification regardless of optional-chaining insertion or rename. Both witnesses prove the populate plumbing survives the build (the ground-truth contract A17.6 enforces). The relaxed contract is semantically equivalent — neither substring is load-bearing on its own; both witness the same underlying invariant. Verify (all GREEN): - npm run test:uat: 24/24 assertions passed (A0 grep gate + A1..A14 + A15..A17 + A18..A22 + A23). - npx tsc --noEmit: clean. - npm run build:test: clean; dist-test/assets/welcome-wB0e_R_n.js bundled; harness page bundle includes new asserts. - SKIP_BUILD=1 npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: 13/13 GREEN (Tier-1 grep gate; FORBIDDEN_HOOK_STRINGS at 12). - Full vitest baseline preserved: 137 ex-grep-gate + 13 grep-gate = 150 GREEN (Plan 01-10 target). A17.7 canonical proof: getComputedStyle.color = 'rgb(178, 84, 61)' — the @import '../shared/tokens.css' directive resolves through to the canonical D-04 Loom palette --mks-madder-600 = #b2543d at runtime, as the empirical proof Plan 01-12 must_have #9 path-B contract demands. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/uat/extension-page-harness.ts | 364 ++++++++++++++++++++++++++- tests/uat/harness.test.ts | 22 +- tests/uat/lib/harness-page-driver.ts | 59 +++++ 3 files changed, 442 insertions(+), 3 deletions(-) diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index 14c42cf..15566c8 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -1903,6 +1903,359 @@ async function assertA14(): Promise { return result; } +/* ─── Plan 01-10 Wave 3 — A15 + A16 + A17 (onboarding + design-swap-readiness) ─── + * + * Plan 01-10 D-17-onboarding harness extensions: + * A15 — onboarding flag observability: chrome.storage.local + * 'onboarding-completed' === true AND 'installed-at' is a + * number (SW's openWelcomeIfFirstInstall side-effects observed + * from extension-internal context with full chrome.* privilege). + * A16 — subsequent-install does NOT re-open welcome tab: snapshot + * chrome.tabs.query before a 2-second settle window; assert no + * new welcome.html tabs appear (flag-gating contract: the SW's + * onInstalled handler running again across SW respawns must + * NOT spawn additional welcome tabs once the flag is set). + * A17 — design-swap-readiness invariant (EXTENDED per Plan 01-12 + * path-B revision): welcome.html parses + .welcome-hero slot + * exists + ≥7 mokosh-keyed attrs + welcome.css carries the + * canonical @import directive (or inlined evidence) + zero + * #hex literals in welcome.css source-file region + ≥5 + * var(--mks-*) references + bundled welcome JS contains COPY[ + * OR chrome.i18n.getMessage('welcomeHero pattern + A17.7's + * getComputedStyle probe resolves --mks-rec to a non-default + * value (canonical rgb(178, 84, 61) = #b2543d). + * + * Each assertion uses ONLY production chrome.* APIs + fetch + DOMParser + * + getComputedStyle — NO new test-mode symbols are introduced (Tier-1 + * FORBIDDEN_HOOK_STRINGS stays at 12 post-01-14). + */ + +/** + * A15 — onboarding flag observability. Read-only inspection of + * chrome.storage.local for the two keys Plan 01-10 Wave 2's + * openWelcomeIfFirstInstall sets on first install: + * - 'onboarding-completed' === true + * - 'installed-at' === (Date.now() at install) + * + * The flag-gating contract this verifies is: the SW's onInstalled + * handler ran with reason='install' AT LEAST ONCE this session AND + * the helper completed both chrome.storage.local.set + chrome.tabs.create + * before A15 fires. The harness page is opened as a normal tab during + * `launchHarnessBrowser` AFTER Chrome has loaded the extension, so by + * the time A15 fires the chrome.runtime.onInstalled.addListener has + * already fired with reason='install' (first-install path) and the + * helper has had ample time to complete its three awaited calls. + * + * Mirrors `assertA22` style: bridge-free read of public chrome.storage.local. + * + * @returns Structured result with 2 checks (flag === true + installed-at number). + */ +async function assertA15(): Promise { + const result: AssertionResult = { + passed: false, + name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)", + checks: [], + diagnostics: [], + }; + + try { + diag(result, "Step 1: chrome.storage.local.get(['onboarding-completed', 'installed-at'])"); + const stored = await chrome.storage.local.get([ + 'onboarding-completed', + 'installed-at', + ]); + diag(result, `Step 1 result: ${JSON.stringify(stored)}`); + + result.checks.push({ + name: "A15.1: chrome.storage.local 'onboarding-completed' === true (set by openWelcomeIfFirstInstall on first install)", + expected: true, + actual: stored['onboarding-completed'], + passed: stored['onboarding-completed'] === true, + }); + result.checks.push({ + name: "A15.2: chrome.storage.local 'installed-at' is a number (Date.now() recorded at install)", + expected: 'number', + actual: typeof stored['installed-at'], + passed: typeof stored['installed-at'] === 'number', + }); + + result.passed = result.checks.every((c) => c.passed); + } catch (err) { + result.error = err instanceof Error ? err.message : String(err); + diag(result, `THREW: ${result.error}`); + } + + return result; +} + +/** A16 — subsequent-install settle window (welcome tab does NOT spontaneously + * reappear). Snapshot before/after a 2-second window; the delta MUST be 0 + * because the openWelcomeIfFirstInstall helper's flag-gating prevents + * re-fire even if the SW spawns multiple onInstalled events across + * respawn cycles. 2 s is a generous over-budget; the SW's storage.get + * + storage.set + tabs.create round-trip is sub-100ms. */ +const A16_SETTLE_MS = 2_000; +const A16_WELCOME_URL_SUFFIX = 'src/welcome/welcome.html'; + +/** + * A16 — subsequent-install does NOT re-open welcome tab. Snapshot + * chrome.tabs.query({}) for welcome.html tab URLs before a settle + * window, then again after, and assert no NEW welcome tabs appeared. + * + * Implementation note: chrome.tabs.query without filters returns all + * tabs across all windows. We filter to URLs ending with the + * '/src/welcome/welcome.html' suffix (extension-id-agnostic). + * + * @returns Structured result with 1 check (delta === 0). + */ +async function assertA16(): Promise { + const result: AssertionResult = { + passed: false, + name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle window: no new welcome.html tabs appear)', + checks: [], + diagnostics: [], + }; + + try { + diag(result, 'Step 1: snapshot chrome.tabs.query({}) — count welcome.html tabs BEFORE settle'); + const tabsBefore = await chrome.tabs.query({}); + const beforeCount = tabsBefore.filter( + (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX), + ).length; + diag(result, `Step 1 result: ${beforeCount} welcome tab(s) before settle`); + + diag(result, `Step 2: settle ${A16_SETTLE_MS}ms (sufficient for any spurious onInstalled re-fire to flush)`); + await new Promise((r) => setTimeout(r, A16_SETTLE_MS)); + + diag(result, 'Step 3: re-snapshot chrome.tabs.query({}) — count welcome.html tabs AFTER settle'); + const tabsAfter = await chrome.tabs.query({}); + const afterCount = tabsAfter.filter( + (t) => typeof t.url === 'string' && t.url.endsWith(A16_WELCOME_URL_SUFFIX), + ).length; + diag(result, `Step 3 result: ${afterCount} welcome tab(s) after settle (delta=${afterCount - beforeCount})`); + + const delta = afterCount - beforeCount; + result.checks.push({ + name: 'A16.1: welcome-tab count delta over 2s settle === 0 (onInstalled flag-gating works)', + expected: 0, + actual: delta, + passed: delta === 0, + }); + + result.passed = result.checks.every((c) => c.passed); + } catch (err) { + result.error = err instanceof Error ? err.message : String(err); + diag(result, `THREW: ${result.error}`); + } + + return result; +} + +/** A17 invariants — design-swap-readiness contract from Plan 01-10 + * must_have #9. Each constant captures one numeric threshold used in + * the A17 sub-checks. + * + * A17.3: zero hex literals in welcome.css source-file region — relaxed + * per post-01-12 revision: if Vite inlines @import contents, hex + * literals from the canonical token values may surface in the BUILT + * artifact. Pass criteria becomes (no hex OR canonical-tokens-resolved). + * A17.4: ≥ 5 var(--mks-*) references. + * A17.5: @import '../shared/tokens.css' literal in welcome.css OR + * inlined evidence via '--mks-rec' literal in the compiled bundle. + * A17.6: bundled JS contains 'COPY[' OR 'chrome.i18n.getMessage(' + * with welcomeHero substring (either pattern proves populate plumbing). + * A17.7: getComputedStyle probe — --mks-rec resolves to canonical + * rgb(178, 84, 61). The harness page s tokens.css directly (Plan + * 01-12 Wave 6); the probe creates a transient div, applies the var, + * reads computed color. + */ +const A17_MIN_KEYED_ATTRS = 7; +const A17_MIN_VAR_USES = 5; +const A17_CANONICAL_REC_RGB = 'rgb(178, 84, 61)'; + +/** + * A17 — design-swap-readiness invariant. Multi-step assertion verifying + * welcome.html + welcome.css + the bundled welcome JS chunk all hold + * the contracts that Plan 01-10's path-B (canonical tokens import) + + * chrome.i18n adoption depend on. + * + * Reaches the bundled JS via parsing the