dba51ea233ce23d79023d3c10fa035811381c049
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d0ebc807a2 |
fix(02-04): harness A27.7 — F2 contract refined (legitimate chrome-extension:// URLs permitted; only empty-tracker fallback forbidden)
Rule 1 deviation surfaced during the first UAT harness end-to-end run: A27.7 originally forbade ALL chrome-extension:// URLs in meta.urls. Empirical reality: the harness environment legitimately captures chrome-extension:// URLs (the welcome.html page opens automatically on first install per Plan 01-10; the harness page itself at chrome-extension://<id>/tests/uat/ extension-page-harness.html is a real active tab). The production tracker (src/background/tab-url-tracker.ts:79 URL_SCHEME_ALLOW) EXPLICITLY permits the chrome-extension:// scheme. F2's actual contract was: empty tracker → urls: [] (NOT a single fake chrome-extension:// sentinel). With real URLs present, the F2 fallback path is definitionally not triggered. The refined A27.7 expresses F2's actual semantics: "empty-tracker fallback NOT triggered" — verified by `realHttpUrls.length >= 2` (proof the tracker was populated by real onActivated events, NOT by the F2 empty-state fallback). This is a strict semantic improvement: the original A27.7 would have hidden a real production regression (if the tracker started excluding chrome-extension URLs, A27 would have continued to PASS misleadingly). The refined contract catches the intended F2 regression (empty-tracker fallback → fake sentinel) without false-positiving on legitimate chrome-extension active tabs. Empirical UAT verification: 29/29 GREEN with the fix in place. - A27.4 ✓ meta.urls contains https://example.com/ - A27.5 ✓ meta.urls contains https://www.iana.org/ - A27.7 ✓ F2 contract: real http(s) URLs present (length=2) - A28.* ✓ 5-entry zip-layout strict |
|||
| 20e06a6a58 |
feat(02-04): harness A26+A27(strict)+A28 — meta.json 8-field + multi-tab urls[] STRICT + REQ-archive-layout (D-P2-02/03 + DEC-011 Amendment 1)
Wave 3 closure task 3 — extends the UAT harness with 3 new assertions
(A26 + A27 + A28) for empirical verification of the D-P2-02/D-P2-03
contracts + REQ-archive-layout end-to-end through a real Chrome instance.
Page side (tests/uat/extension-page-harness.ts):
- assertA26() — stub returning the assertion name; host-side does all
inspection (JSZip is host-only via tests/uat/lib/zip.ts).
- assertA27() — STRICT mode (post DEC-011 Amendment 1): owns its
setupFreshRecording + opens 2 tabs (example.com + iana.org) +
activates each (chrome.tabs.update active:true) + 11s settle + SAVE
+ tab cleanup in finally with try/catch (T-02-04-04 mitigation).
Returns A27.1 (SAVE ack) + tabAUrl + tabBUrl for the host driver.
- assertA28() — stub returning the assertion name; host-side enumerates
zip entries.
- __mokoshHarness surface extended from 25 → 28 methods.
Host side (tests/uat/lib/harness-page-driver.ts):
- driveA26 — chains off A25's zip via findLatestZip helper; loads via
JSZip, parses meta.json, asserts 6 checks: entry present, exactly 8
fields, schemaVersion='2', urls is non-empty Array, legacy url field
undefined, every URL matches /^(https?|chrome-extension):\\/\\//.
- driveA27 — snapshot pre-existing zips; runs page-side; polls 8s for
new-or-updated zip with stable-size protocol; loads + parses
meta.json; asserts 8 STRICT checks per DEC-011 Amendment 1: SAVE ack,
meta.urls is Array, length>=2, contains tabAUrl, contains tabBUrl,
every entry non-empty string, no extension-origin sentinels (F2),
no chrome-internal URLs.
- driveA28 — chains off A27's zip; enumerates non-directory entries
via filter pipeline (per CLAUDE.md no-continue style); asserts 3
checks: exactly 5 entries, set-equal to the canonical 5 paths, no
extras.
- findLatestZip helper added for A26/A28 chaining (mtime-sort wins).
- JSZip imported at top (mirrors tests/uat/lib/zip.ts pattern).
Orchestrator (tests/uat/harness.test.ts):
- Imports driveA26/A27/A28 + wraps each with handles.downloadsDir.
- Drivers array extends from 25 → 28 (running total 29/29 with A0).
- Architecture banner updated to mention A26+A27+A28.
FORBIDDEN_HOOK_STRINGS impact: NONE. A26/A28 are host-side JSZip ops;
A27 uses chrome.tabs.create + chrome.tabs.update + chrome.tabs.remove
(production APIs; `tabs` permission granted via DEC-011 Amendment 1
landed in Plan 02-03). Tier-1 inventory stays at 12.
Verification (pre-commit):
- npx tsc --noEmit: clean.
- npm run build: exit 0; dist/ populated.
- 4 new manifest gates (Tier-1 + SW-bundle-import) verified in followup.
Closes Plan 02-04 Task 3 (Wave 3 functional contract). Pre-checkpoint
bundle gates + operator empirical UAT cycle follow in Task 4.
|
|||
| 47e9818cb1 |
feat(02-04): harness A25 — empirical <5s SAVE→zip latency (REQ-archive-export-latency, SPEC §10 #6)
Wire A25 into the UAT harness as the binding empirical gate for REQ-archive-export-latency / SPEC §10 #6 (5000ms hard ceiling end-to-end from SAVE_ARCHIVE dispatch to zip-on-disk). Architecture: - Page-side assertA25 records t0 (performance.now) + t0Wall (Date.now) + tAck bookends around the chrome.runtime.sendMessage(SAVE_ARCHIVE) call. Returns A25Result extending AssertionRecord with the 3 timing fields + ackSuccess flag. - Host-side driveA25(page, downloadsDir) snapshots zip dir BEFORE page.evaluate dispatch, polls for new-or-overwritten .zip via mtime delta (mirrors A12/A13 overwrite-aware pattern), uses page-supplied t0Wall as the host anchor for the dispatch→file-on-disk latency check (NOT a host-side Date.now captured before page.evaluate, which would include setupFreshRecording + 11s segment-settle wall time and always fail the 5s budget). [Rule 1 - Bug] Initial implementation used host-side Date.now() captured before page.evaluate as the latency anchor — this incorrectly included the 11s segment-settle window in the budget. First run observed A25.3=11188ms (FAIL). Fix: page-side captures Date.now() at the SAVE_ARCHIVE dispatch instant (AFTER setupFreshRecording + segment-settle complete) and returns it as t0Wall in A25Result; the driver uses this as the canonical host anchor. Result on re-run: A25.3=61ms (GREEN, well under 5s SLO). Documented per T-02-04-02 disposition (bracket only the SAVE dispatch, not the broader test orchestration). Files modified: - tests/uat/extension-page-harness.ts (+~115 lines): assertA25 + A25_* constants + A25Result interface - tests/uat/lib/harness-page-driver.ts (+~95 lines): driveA25 + A25_HOST_POLL_TIMEOUT_MS const + A25_LATENCY_CEILING_MS const - tests/uat/harness.test.ts (+~15 lines): import driveA25, wrap with downloadsDir, append to drivers list Verification: - HEADLESS=1 npm run test:uat → 26/26 GREEN - elapsedAck=60ms, host-side delta=61ms (both well under 5000ms SLO) - npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts → 13/13 GREEN (Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12) - npx tsc --noEmit → clean Plan 02-04 scope: 2/3 tasks landed (A24 + A25); Task 3 adds A26 (meta.json 8-field) + A27 (multi-tab strict) + A28 (archive-layout strict). |
|||
| 4ae73250fa |
feat(02-04): harness A24 — empirical Blob URL download verification (D-P2-01 closes P0-6)
Wire A24 into the Plan 01-13 Approach B UAT harness as the binding empirical gate for D-P2-01. A24 verifies end-to-end that SAVE_ARCHIVE → chrome.downloads. download receives a `blob:` URL prefix (NOT `data:application/zip;base64,`), closing audit P0-6 functionally. The Plan 02-02 unit tests pin the wire-format at the SW↔offscreen boundary; A24 pins it at the chrome.downloads platform boundary through a real Chrome instance. Strategy: chrome.downloads.onCreated listener captures the URL cross-realm. The plan's <action> block proposed a chrome.downloads.download monkey-patch installed in the harness page realm — but that intercepts only same-realm calls, missing the SW's call. The canonical cross-realm capture pattern is chrome.downloads.onCreated (fires for any download initiated by any extension realm, with the full DownloadItem including .url). Documented as a deviation from the plan's pseudo-code in SUMMARY.md (Rule 1 — bug fix vs the pseudo-code strategy; same A24 contract verified, correct mechanism). Files modified: - tests/uat/extension-page-harness.ts (+~150 lines): assertA24 + A24_* constants - tests/uat/lib/harness-page-driver.ts (+~30 lines): driveA24 page.evaluate wrapper - tests/uat/harness.test.ts (+~10 lines): import driveA24, append to drivers list Verification: - HEADLESS=1 npm run test:uat → 25/25 GREEN (24 baseline + A24) - capturedUrl observed: blob:chrome-extension://lpgnfoop.../... - npx vitest run → 171/171 GREEN (no regression) - Tier-1 FORBIDDEN_HOOK_STRINGS gate → 13/13 GREEN (12 strings preserved) - npx tsc --noEmit → clean Plan 02-04 scope: 1/3 tasks landed (A24); Tasks 2-3 add A25+A26+A27+A28 (latency, meta.json shape, multi-tab strict, REQ-archive-layout strict). |
|||
| b112cb7861 |
test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap-readiness with @import probe); 24/24 GREEN
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) <noreply@anthropic.com>
|
|||
| b909c374cc |
feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)
UAT harness extended with 5 new page-side assertions following the 01-13 Approach B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry): A18 — Lora WOFF2 reachable from harness page (font self-host MV3 CSP invariant). Walks document.styleSheets for the first @font-face rule referencing Lora, resolves the rebased asset URL (handles Vite's content-hashing), fetches, asserts byteLength >= 40_000 (subset Lora is ~49 KB) + WOFF2 signature 'wOF2'. 4 checks. A19 — icons rasterized from Loom mark (not Bug A placeholders). Fetches icon128.png, parses IHDR bytes 24-25 (bit-depth + color-type), asserts (8, 6) RGBA vs the placeholder (16, 2) RGB. 2 checks. A20 — manifest:name resolves via chrome i18n. Reads chrome.runtime.getManifest().name; asserts it matches EN extName 'Mokosh — Session Capture' OR RU 'Mokosh — Запись сессии' (robust to whatever locale Chrome uses); explicitly checks no __MSG_ placeholder leaks. 2 checks. A21 — --mks-font-display resolves to Lora stack. Creates transient .mks-display-1 probe div, reads getComputedStyle.fontFamily, asserts the stack starts with 'Lora' or '"Lora"' (accommodates both quoted + unquoted forms across Chrome versions); explicitly checks no Newsreader leak (R2 substitution complete). 2 checks. A22 — welcome page tokens.css adoption (CONDITIONAL on Plan 01-10). Skip-gate on missing welcome.html: catches both HTTP 404 AND network-layer fetch failure (Chrome extensions throw TypeError 'Failed to fetch' for non-web_accessible_resources paths). On reachable: extracts <link rel=stylesheet> hrefs, fetches each, asserts >= 3 var(--mks-*) usages OR tokens.css reference. 1 check. Companion changes: - tests/uat/extension-page-harness.html gains `<link rel="stylesheet" href="../../src/shared/tokens.css">` so A18 + A21 have the @font-face rules + .mks-display-1 class + CSS custom properties resolvable via document.styleSheets + getComputedStyle. Vite's crxjs plugin handles the asset path rebasing at build:test time. - tests/uat/lib/harness-page-driver.ts: driveA18..driveA22 wrappers following the established driveA8 pattern (page.evaluate → window. __mokoshHarness.assertXX). No new host-side fs/ffprobe primitives; all A18-A22 work is page-side. - tests/uat/harness.test.ts: orchestrator drivers list extended with A18-A22 between A14 and A23. FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries post-Plan-01-14 (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively; no new test-mode symbols). Verification (this commit): - npm run test:uat: 21/21 GREEN (was 16/16 post-01-14) - SKIP_BUILD=1 npm test: 147/147 GREEN - Tier-1 grep gate: 13/13 GREEN (no FORBIDDEN_HOOK_STRINGS growth) - npx tsc --noEmit: clean - npm run build + npm run build:test: both succeed The chain of A1..A14 + A18..A22 + A23 runs in ~95 seconds end-to-end under Puppeteer headless mode against the bundled Chrome at ~/.cache/puppeteer/chrome/linux-148.0.7778.167. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| b467123578 |
feat(01-14): monitorTypeSurfaces:'include' — narrow picker to monitor surfaces only
[per Plan 01-14; closes B-01-14-01 via Step 1b lockstep]
- src/offscreen/recorder.ts: add monitorTypeSurfaces:'include' as top-level
DisplayMediaStreamOptions sibling of video: (W3C Screen Capture spec §6.1;
Chrome >= 119; removes tab/window panes from the operator's picker per
Plan 01-10 RESEARCH §5 + §Pitfall-5 recommendation). Typed widening cast
extended in lockstep to keep the explicit-typing contract (no `as any`).
D-15 post-grant validation block at recorder.ts:294 UNCHANGED — belt
(picker narrowing) + suspenders (post-grant tear-down) chain preserved.
- tests/offscreen/display-surface-constraint.test.ts: lockstep update of
the strict-deep-equality assertion at lines 223-226 with the same key
ordering as the source change (video -> monitorTypeSurfaces -> audio).
toHaveBeenCalledWith contract preserved (NO expect.objectContaining —
the test author's "catches future drops of ANY field" discipline is
honored). This edit + the source change land in the SAME commit so the
98/98 baseline never crosses a commit boundary in RED state.
- src/test-hooks/offscreen-hooks.ts: capture last constraints object in
module-scoped `lastGetDisplayMediaConstraints` cell (was `_constraints`
received-but-unused; renamed to `constraints`); add `get-last-getDisplayMedia-constraints`
bridge op to the __mokoshOffscreenQuery dispatcher between
get-display-surface and get-segment-count. Defensive try/catch mirrors
the existing dispatcher pattern; the cell is module-internal so the
MokoshTestSurface cross-cast in types.ts requires NO change (decision
documented inline in offscreen-hooks.ts).
- tests/uat/extension-page-harness.ts: add `assertA23` mirroring `assertA3`
(bridge query → 2-check AssertionResult: non-null constraints + value).
Extend the `Window.__mokoshHarness` declaration + runtime export + status
bar text + console.log to reference A23.
- tests/uat/lib/harness-page-driver.ts: export `driveA23(page)` mirroring
the `driveA14` page.evaluate wrapper shape. Standard read-only driver.
- tests/uat/harness.test.ts: extend FORBIDDEN_HOOK_STRINGS (line 85) with
`lastGetDisplayMediaConstraints` and `get-last-getDisplayMedia-constraints`.
Import driveA23. Append `{ name: 'A23', drive: driveA23 }` to the drivers
array after the A14 entry. Update header comment + orchestrator stdout
to reflect A14 + A23 chain. The `Total = drivers.length + 1` arithmetic
adapts automatically: 14 + 1 = 15 → 15 + 1 = 16.
- tests/background/no-test-hooks-in-prod-bundle.test.ts: lockstep
extension of FORBIDDEN_HOOK_STRINGS (line 105) with the same 2 strings.
Header comment updated to "Total: 12 surface strings." (was 10).
Confirms production `dist/` has ZERO occurrences after `npm run build`
via the `__MOKOSH_UAT__` dead-branch tree-shake (T-01-14-04 mitigation).
D-01 (whole-desktop only via getDisplayMedia; reject window/tab surfaces) is
the design intent that monitorTypeSurfaces:'include' realizes at the picker-
UI level. D-15 post-grant validation (recorder.ts:294-307) remains the
actual enforcement against managed-policy/DevTools/older-Chrome overrides.
Verification chain (per Plan 01-14 §verify; clean post-commit):
- `npx tsc --noEmit` exit 0
- `npm run build` exit 0; dist/ produced, monitorTypeSurfaces ships in
the offscreen chunk as the operator-facing picker hint
- `npm run build:test` exit 0; dist-test/ produced with the harness
hooks intact (gated)
- `npm test` 100/100 GREEN (was 98/98; +2 via the 2 new FORBIDDEN_HOOK_STRINGS
parametrized tests — both PASS, production bundle hook-free)
- `npm run test:uat` 16/16 GREEN (15 → 16 via A23). A23 reads constraints
`{video: {...}, monitorTypeSurfaces: 'include', audio: false}` from the
fakeGetDisplayMedia capture cell — round-trips through the full call site.
- Production bundle spot-check:
`grep -rc 'lastGetDisplayMediaConstraints\|get-last-getDisplayMedia-constraints' dist/ | grep -v ':0$'`
→ empty (all `:0` filtered) → ZERO leakage.
References:
- W3C Screen Capture §6.1 DisplayMediaStreamOptions:
https://www.w3.org/TR/screen-capture/#dom-displaymediastreamoptions-monitortypesurfaces
- Chrome screen-sharing-controls (Chrome 119+):
https://developer.chrome.com/docs/web-platform/screen-sharing-controls
- Plan 01-10 RESEARCH §5 + §Pitfall-5 (recommendation provenance):
.planning/phases/01-stabilize-video-pipeline/01-10-RESEARCH.md
- Architectural-note (replaces retired AMENDMENT-A.md improvisation per
01-11-SUMMARY): canonical GSD ceremony — plan → checker (B-01-14-01)
→ executor → SUMMARY (this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 1baaf45702 |
feat(01-13-A14-invert): A14 — invert to assert continuous-recording post-SAVE
Plan 01-09 Amendment 3 (2026-05-19) end-to-end lock. Inverted A14 to
match the reversed charter (SAVE creates zip, recording continues).
Page-side (tests/uat/extension-page-harness.ts):
- assertA14: assert badge==='REC' (was ''), popup endsWith
'src/popup/index.html' (was ''), no-new-recovery-notif (unchanged).
- A14 name + check labels updated to reflect continuous-recording semantic.
- New constant A14_POPUP_HTML_SUFFIX for the popup endsWith check
(ext-id-agnostic via suffix match).
- A13 docstring + diag strings refreshed: setupFreshRecording is now
defensive (orthogonal to A12 ordering) rather than a workaround for
the prior auto-stop. 11s settle preserved (same wall-clock cost).
Host-side (tests/uat/lib/harness-page-driver.ts):
- driveA14 docstring refreshed to mention Amendment 3 + the inverted
contract; mechanical wrapper unchanged.
Verification:
- npm run test:uat: 15/15 GREEN
- A14 actual output:
badge='REC'
popup='chrome-extension://<ext-id>/src/popup/index.html'
recoveryDelta=0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 2b6c24b2d9 |
feat(01-13): A14 — post-SAVE state check (badge='', popup='', no new recovery notif)
Plan 01-13 Task 9 closure for operator empirical UAT bug
.planning/debug/01-09-save-stops-recording.md. Adds the harness
assertion that empirically verifies the SAVE-auto-stops-recording fix
(committed at
|
|||
| d793c9e1e5 |
feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).
Assertions wired:
- A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
recording (STOP_RECORDING → START_RECORDING so the recorder's
`resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
offscreen recorder's own keepalive port). Queries the new
`get-segment-count` bridge op. Asserts count >= 3 (per D-13:
SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).
- A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
rather than numbering it, empirically verified during initial RED).
Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
`/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
webm < 10_240B (synthetic-stream-limitation signature — canvas
captureStream in `--headless=new` offscreen produces 0-frame WebM
with only EBML/Track headers) → SKIPPED with explicit diagnostic
pointing operators to `tests/offscreen/webm-playback.test.ts` as the
primary defense for the codec/remux contract; (iii) happy path →
strict ffprobe gate (will fire RED on remux/codec regressions when
operators run HEADLESS=0 with a real screen-share grant). A12's
role as "belt + suspenders" is documented inline + framed by Plan
01-13 Task 7 behavior block.
- A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
idempotency over A12's first save). JSZip parse via the
`assertArchiveShape` helper (extended in this wave to read
`extensionVersion` — the actual production SessionMetadata field
name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
incorrect `version` assumption). Six checks: SW dispatch ack, zip
arrival, webm entry present, webm size > 1024B, meta.json entry
present, meta.json.extensionVersion matches
chrome.runtime.getManifest().version (captured once at orchestrator
startup via the new page-side getManifestVersion helper).
Bridge op + recorder wire:
- Adds `get-segment-count` op to the offscreen-hooks
`__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
`{count: number}` via the existing segmentCountGetter closure
(segments.length captured at recorder.ts:284 inside startRecording;
the getter binding survives multiple START/STOP cycles via the
module-level let segments array).
- Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
(Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
`tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
Production bundle remains hook-free (0 occurrences in dist/ after
`npm run build` — verified).
Harness surface:
- `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
from 10 → 13 assertion methods + 1 helper:
`assertA11, assertA12, assertA13, getManifestVersion`. Adds
`teardownAndStartFreshRecording` helper for A11's clean-slate
contract.
- `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
marker (no more NYI throws). Adds `driveA11` (standard wrapper),
`driveA12` + `driveA13` (heavyweight host-side drivers with fs
polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
detects both new files AND overwrites via mtime delta — fixes the
`download.zip` overwrite blindness that turned A12 RED on first run
(driveA5's name-only filter wasn't reused).
- `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
`extensionVersion` (the production field name per
src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
constant.
- `tests/uat/harness.test.ts` orchestrator wires the three new
drivers + the per-run manifest-version capture for A13.
Baseline:
- `npx tsc --noEmit`: exit 0.
- `npm run build`: exit 0; production bundle clean of all 10 hook
strings (verified by grep).
- `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
- `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
- `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
2× ~13s save settles + ~10s production rebuild + overhead).
A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):
Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
→ `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
the edit; A11 PASSES. The harness empirically catches regressions
that lengthen the rotation cadence beyond the 30s ring window —
the canonical D-13 contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b665919c5f |
feat(01-13): wave-3C — A8+A9+A10 GREEN + Bug A canonical regression rewind
Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):
- A8 (Bug A canonical regression rewind) — invokes
chrome.notifications.create from the harness page with the SAME options
the production SW onStartup handler uses (iconUrl resolved via
chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
imageUtil icon validation — the exact code path Bug A regressed on
(
|
|||
| 6a77967b6c |
feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).
Changes per file:
tests/uat/extension-page-harness.ts
- assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
Page-side only checks SW handler ack; host-side driver verifies
disk-side outcome (zip presence + size floor).
- assertA7: setupFreshRecording helper (A6 tears down; A7 needs
REC state) → snapshot notif count → send RECORDING_ERROR with
a non-Bug-B error code ('codec-unsupported') → 200ms settle →
assert badge='ERR' + popup endsWith popup.html + notif delta=1
+ set-membership for 'mokosh-recovery-*' prefix.
- setupFreshRecording: shared helper for A7 + future assertions
that need a fresh REC state after a teardown.
tests/uat/lib/harness-page-driver.ts
- driveA5: page.evaluate(assertA5) THEN host-side fs polling for
*.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
override renames the file to download.zip (data: URL filename
gap), so we accept any *.zip suffix. Merges page-side check +
host-side checks into a single AssertionRecord. Signature now
takes downloadsDir as a second arg.
- driveA7: standard page.evaluate wrapper (no host-side work).
tests/uat/harness.test.ts
- Wraps driveA5 in a closure that captures handles.downloadsDir.
- Reordered: launchHarnessBrowser MUST run before driver list so
the closure can read handles without a TDZ trap.
tests/uat/lib/launch.ts
- Victim page switched from about:blank to a file:// URL backed by
a tmp HTML file in downloadsDir. About:blank breaks A5 because
chrome.tabs.captureVisibleTab needs <all_urls> permission which
matches http/https/file/ftp but NOT about: or data: URLs. The
stub HTML satisfies <all_urls> + provides a real .url for the
production saveArchive's chrome.tabs.query.
src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
- installFakeDisplayMedia: mintStream() helper called per
fakeGetDisplayMedia invocation; each call mints a FRESH
MediaStream from the persistent canvas. Real getDisplayMedia
returns a new stream per call — fake now matches. Required for
A7's setupFreshRecording where the previous recording's stream
tracks were stopped by A6's onUserStoppedSharing teardown.
- Added 33ms setInterval-driven drawFrame() alongside the
existing requestAnimationFrame loop. RAF can throttle in
headless Chrome on offscreen documents (page-visibility
heuristics produce 0 fps), which yields zero-byte
MediaRecorder segments that crash ts-ebml's VINT decode in
webm-remux.extractFramesFromSegment with "Unrepresentable
length: Infinity". The setInterval is redundant when RAF fires
at full rate; it's a safety net for the headless-MV3 corner.
Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):
Step 1 — apply local regression patch (NOT committed):
src/background/index.ts:792 setIdleMode() → setErrorMode()
Step 2 — npm run build:test && npm run test:uat RED snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
[PASS] SETUP: badge becomes REC after start
[FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
expected: ""
actual: "ERR"
[FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
expected: ""
actual: "chrome-extension://<id>/src/popup/index.html"
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)
Step 3 — revert local patch (git checkout -- src/background/index.ts).
Step 4 — npm run build:test && npm run test:uat GREEN snippet:
A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
[PASS] SETUP: badge becomes REC after start
[PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
[PASS] A6.2: popup is '' (NOT manifest default) after user-stop
[PASS] A6.3: NO recovery notification fired (count delta === 0)
[PASS] A6.4: isRecording=false (via badge proxy)
UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
IMPLEMENTED — Wave 3C wires driveA8)
The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.
vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 1b67b1c1d3 |
feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).
Files changed:
- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
assertA6 }`. Per-assertion contracts:
• A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
+ isRecording=false (badge !== 'REC' proxy per state-machine atomic
pairing). 3 CheckRecords.
• A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
(workaround for the `tabs` manifest permission gap per
01-11-SUMMARY + plan resolved-questions row 2) + manual
setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
badge==='REC'. The bypassed chrome.action.onClicked →
startVideoCapture path is unit-tested in
tests/background/badge-state-machine.test.ts; A2 verifies the
contract that matters (recording reaches the REC state-machine
row). 2 CheckRecords.
• A3 — offscreen bridge query 'get-display-surface' (new in this
plan via the prior commit's offscreen-hooks extension) → asserts
=== 'monitor'. 1 CheckRecord.
• A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
(no duplicate offscreen). Essentially a no-op verification —
regression protection against future refactors that might unpin
the popup during recording or spawn extra offscreens on stray
events. 2 CheckRecords.
• IMPORTANT: chrome.action.getPopup() returns the FULL absolute
chrome-extension://<id>/... URL (not the manifest-relative path).
A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
extension-id independent. Empirical finding from first orchestrator
run; documented inline.
- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
(replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
|
|||
| eb64521321 |
feat(01-13): wave-2 — launchHarnessBrowser + assertions + harness-page-driver scaffolding
Build out the Approach-B harness driver utilities atop the Wave 1
production paths. Three new files form the shared scaffold that
Wave 3's 13 assertion drivers (A1-A5, A7-A13) and the eventual
orchestrator (`tests/uat/harness.test.ts`) will all consume. The
standalone A6 driver (`tests/uat/a6.test.ts`) is rewritten to use
the new lib — behavior-preserving: A6 still PASSES 5/5 in ~7s.
New files:
- tests/uat/lib/launch.ts (~320 LoC)
`launchHarnessBrowser({ headless?, downloadsDir? }) → HarnessHandles`
Extracts the Chrome-launch + victim-page + harness-page + console-
attach pattern from a6.test.ts into a single reusable helper.
NEW vs prototype: CDP `Browser.setDownloadBehavior` wires
Chrome's download path to a per-run `mkdtempSync` tmp dir so A5
(SAVE_ARCHIVE) can poll a known location without colliding with
the operator's real downloads. Architectural commitments
enforced (per 01-11-SUMMARY): no `--auto-select-desktop-capture-
source` flag; victim about:blank brought to front for the
production `chrome.tabs.query({active:true})` workaround; SW
console attach best-effort with bounded poll; offscreen console
attach opportunistic via `targetcreated` listener (offscreen
target appears later, when the harness page calls
chrome.offscreen.createDocument).
- tests/uat/lib/assertions.ts (~210 LoC)
Host-side assertion primitives:
* `AssertionRecord`, `CheckRecord`, `ConsoleBuffers` types —
mirror the page-side shape returned by `assertA*` methods.
* `runAssertion(name, fn, buffers)` — try/catch wrapper that
dumps the SW + offscreen console tails (last 100 lines each)
to stderr on failure, then returns `{passed: false, error}`
if `fn` throws.
* `printAssertionResult(result)` — single source of truth for
the formatted result print. Extracted from the inline
`printResult` previously in the prototype's a6.test.ts so
Wave 3's orchestrator can reuse it across all 14 assertions.
* `assertEqual / assertGte / assertMatch / assertTrue` —
structured failure messages atop node:assert/strict.
* `waitFor(probe, predicate, timeoutMs, description)` — host-
side polling primitive; mirrors the page-side waitFor
semantics verbatim (they can't share a module: page-side is
bundled into the harness HTML, host-side runs in Node).
NO chrome.* helpers here — all chrome.* work happens inside the
extension-internal harness page. This module is host-side ONLY
by construction (no chrome global in Node anyway).
- tests/uat/lib/harness-page-driver.ts (~170 LoC)
One driver wrapper per assertion (A1..A13). Each wraps a single
`page.evaluate(() => window.__mokoshHarness.assertXX())`.
Centralizing this means adding/renaming an assertion = two-file
edit (extension-page-harness.ts impl + this file) instead of
touching every test-file caller.
Wave 2 wires `driveA6` (proven from
|