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.
This commit is contained in:
@@ -3089,6 +3089,232 @@ async function assertA25(): Promise<A25Result> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ─── Plan 02-04 Task 3 — A26 + A27 (strict) + A28 ─────────────────────
|
||||
*
|
||||
* A26 — D-P2-02 + D-P2-03 empirical: meta.json has the 8-field shape
|
||||
* with urls[] (not url:string) and schemaVersion='2'.
|
||||
* A27 — STRICT mode (post DEC-011 Amendment 1): multi-tab URL tracking.
|
||||
* Opens TWO tabs sequentially, activates each, then dispatches
|
||||
* SAVE. Host-side asserts meta.urls contains BOTH URLs (length>=2
|
||||
* REQUIRED; FAILS on length<2).
|
||||
* A28 — REQ-archive-layout strict zip-layout: zip contains EXACTLY the
|
||||
* 5 canonical entries (video/last_30sec.webm, rrweb/session.json,
|
||||
* logs/events.json, screenshot.png, meta.json). Cross-references
|
||||
* REQ-popup-ui + REQ-screenshot-on-export.
|
||||
*
|
||||
* Architecture (per saved memory feedback-no-unilateral-scope-reduction +
|
||||
* Plan 01-13 Approach B): page-side does the orchestration (SAVE dispatch
|
||||
* + tab management); host-side does the zip parsing via JSZip (already
|
||||
* imported in tests/uat/lib/zip.ts; not bundled into the harness page
|
||||
* realm). A26 + A28 page-sides are intentional stubs returning the
|
||||
* assertion name — all zip-inspection work is host-side in the drivers.
|
||||
*
|
||||
* FORBIDDEN_HOOK_STRINGS impact: NONE. A27 uses chrome.tabs.create +
|
||||
* chrome.tabs.update + chrome.tabs.remove (production APIs; `tabs`
|
||||
* permission granted via DEC-011 Amendment 1). Tier-1 inventory stays
|
||||
* at the post-A24/A25 baseline.
|
||||
*
|
||||
* T-02-04-04 mitigation: A27 wraps both tab-cleanup calls in try/catch
|
||||
* with silent-ignore on already-closed (chrome.tabs.remove throws if
|
||||
* the tab id is gone; the test doesn't care about that side effect).
|
||||
*/
|
||||
|
||||
/** SAVE_ARCHIVE dispatch timeout for A27 — matches A24/A25's 15s. */
|
||||
const A27_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||
const A27_SEGMENT_SETTLE_MS = 11_000;
|
||||
/** Wait after chrome.tabs.create for the tab navigation to complete
|
||||
* (the URL field on the tab object only populates once the page
|
||||
* begins loading). Conservative 1500ms per plan spec. */
|
||||
const A27_TAB_NAVIGATION_WAIT_MS = 1_500;
|
||||
/** Wait after chrome.tabs.update({active:true}) for chrome.tabs.onActivated
|
||||
* to fire + tab-url-tracker (Plan 02-03 Task 2) to capture the URL. */
|
||||
const A27_TAB_ACTIVATION_WAIT_MS = 500;
|
||||
|
||||
/** Canonical multi-tab URLs for A27 — public sites with stable URLs,
|
||||
* no PII (per T-02-04-03 disposition). example.com is RFC 2606
|
||||
* reserved; iana.org is the IANA homepage. Both serve plain HTML
|
||||
* reliably under headless Chrome. */
|
||||
const A27_TAB_A_URL = 'https://example.com/';
|
||||
const A27_TAB_B_URL = 'https://www.iana.org/';
|
||||
|
||||
/**
|
||||
* Extended result shape — A27 returns the canonical URLs the host-side
|
||||
* driver needs to assert against meta.urls.
|
||||
*/
|
||||
interface A27Result extends AssertionResult {
|
||||
tabAUrl: string;
|
||||
tabBUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A26 — D-P2-02 + D-P2-03 empirical (page-side stub).
|
||||
*
|
||||
* Returns the assertion name + a sentinel diagnostic. All real work
|
||||
* happens host-side in driveA26 (JSZip-parse the latest zip + assert
|
||||
* meta.json shape). The page-side stub exists purely so the orchestrator's
|
||||
* single-method-per-assertion contract (window.__mokoshHarness.assertA26)
|
||||
* is uniform across all 29 assertions.
|
||||
*
|
||||
* Chaining: A26 reads the zip produced by A25 (host-side driver picks
|
||||
* the most-recently-modified zip in downloadsDir). No new SAVE dispatch.
|
||||
*
|
||||
* @returns Stub AssertionResult with empty checks; driveA26 fills them.
|
||||
*/
|
||||
async function assertA26(): Promise<AssertionResult> {
|
||||
return {
|
||||
passed: true,
|
||||
name: 'A26 — meta.json 8-field shape (D-P2-02 + D-P2-03)',
|
||||
checks: [],
|
||||
diagnostics: [
|
||||
'assertA26 page-side stub; host-side driveA26 inspects latest zip + asserts meta.json shape',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A27 — STRICT mode (post DEC-011 Amendment 1): multi-tab URL tracking.
|
||||
*
|
||||
* Sequence (per plan spec):
|
||||
* 1. setupFreshRecording (clean state)
|
||||
* 2. chrome.tabs.create(TAB_A_URL, active:false) → wait 1500ms for nav
|
||||
* 3. chrome.tabs.update(tabA.id, {active:true}) → wait 500ms for onActivated
|
||||
* 4. chrome.tabs.create(TAB_B_URL, active:false) → wait 1500ms for nav
|
||||
* 5. chrome.tabs.update(tabB.id, {active:true}) → wait 500ms for onActivated
|
||||
* 6. Wait 11s for one segment to land
|
||||
* 7. Dispatch SAVE_ARCHIVE; await ack with success===true
|
||||
* 8. Cleanup: try/catch close both tabs via chrome.tabs.remove
|
||||
*
|
||||
* Host-side driveA27 then loads the produced zip + parses meta.json + asserts:
|
||||
* - meta.urls.length >= 2 (FAIL on length < 2)
|
||||
* - Both TAB_A_URL and TAB_B_URL are in meta.urls (order-flexible)
|
||||
* - No extension-origin sentinels (chrome-extension://)
|
||||
* - No chrome-internal URLs (chrome:// or about:)
|
||||
*
|
||||
* Page-side returns A27.1 (SAVE_ARCHIVE ack) + the canonical URLs so
|
||||
* the host-side driver can compute the strict assertions.
|
||||
*
|
||||
* T-02-04-04 mitigation: cleanup runs in finally; both tabs.remove
|
||||
* calls wrapped in try/catch (silent-ignore on already-closed).
|
||||
*
|
||||
* @returns A27Result with 1 check (SAVE ack) + tabAUrl + tabBUrl for the driver.
|
||||
*/
|
||||
async function assertA27(): Promise<A27Result> {
|
||||
const result: A27Result = {
|
||||
passed: false,
|
||||
name: 'A27 — D-P2-02 STRICT multi-tab urls[] (both URLs REQUIRED post DEC-011 Amendment 1)',
|
||||
checks: [],
|
||||
diagnostics: [],
|
||||
tabAUrl: A27_TAB_A_URL,
|
||||
tabBUrl: A27_TAB_B_URL,
|
||||
};
|
||||
|
||||
let tabAId: number | undefined;
|
||||
let tabBId: number | undefined;
|
||||
|
||||
try {
|
||||
diag(result, 'Step 1: setupFreshRecording (A27 owns its recording)');
|
||||
const setupResp = await setupFreshRecording();
|
||||
if (!setupResp.ok) {
|
||||
throw new Error(
|
||||
`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`,
|
||||
);
|
||||
}
|
||||
diag(result, 'Step 1 OK — REC state established');
|
||||
|
||||
diag(result, `Step 2: chrome.tabs.create(${A27_TAB_A_URL}, active:false)`);
|
||||
const tabA = await chrome.tabs.create({ url: A27_TAB_A_URL, active: false });
|
||||
tabAId = tabA.id;
|
||||
diag(result, `Step 2 result: tabA.id=${tabAId}, tabA.url=${tabA.url ?? '<pending>'}`);
|
||||
await new Promise((r) => setTimeout(r, A27_TAB_NAVIGATION_WAIT_MS));
|
||||
|
||||
if (tabAId !== undefined) {
|
||||
diag(result, `Step 3: chrome.tabs.update(${tabAId}, {active:true})`);
|
||||
await chrome.tabs.update(tabAId, { active: true });
|
||||
await new Promise((r) => setTimeout(r, A27_TAB_ACTIVATION_WAIT_MS));
|
||||
}
|
||||
|
||||
diag(result, `Step 4: chrome.tabs.create(${A27_TAB_B_URL}, active:false)`);
|
||||
const tabB = await chrome.tabs.create({ url: A27_TAB_B_URL, active: false });
|
||||
tabBId = tabB.id;
|
||||
diag(result, `Step 4 result: tabB.id=${tabBId}, tabB.url=${tabB.url ?? '<pending>'}`);
|
||||
await new Promise((r) => setTimeout(r, A27_TAB_NAVIGATION_WAIT_MS));
|
||||
|
||||
if (tabBId !== undefined) {
|
||||
diag(result, `Step 5: chrome.tabs.update(${tabBId}, {active:true})`);
|
||||
await chrome.tabs.update(tabBId, { active: true });
|
||||
await new Promise((r) => setTimeout(r, A27_TAB_ACTIVATION_WAIT_MS));
|
||||
}
|
||||
|
||||
diag(result, `Step 6: settle ${A27_SEGMENT_SETTLE_MS}ms for one segment to land`);
|
||||
await new Promise((r) => setTimeout(r, A27_SEGMENT_SETTLE_MS));
|
||||
|
||||
diag(result, 'Step 7: dispatch SAVE_ARCHIVE');
|
||||
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||
{ type: 'SAVE_ARCHIVE' },
|
||||
A27_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||
'SAVE_ARCHIVE (A27)',
|
||||
);
|
||||
diag(result, `Step 7 result: ${JSON.stringify(ack)}`);
|
||||
|
||||
result.checks.push({
|
||||
name: 'A27.1: SAVE_ARCHIVE ack received with success=true',
|
||||
expected: true,
|
||||
actual: ack.success,
|
||||
passed: ack.success === true,
|
||||
});
|
||||
|
||||
result.passed = result.checks.every((c) => c.passed);
|
||||
} catch (err) {
|
||||
result.error = err instanceof Error ? err.message : String(err);
|
||||
diag(result, `THREW: ${result.error}`);
|
||||
} finally {
|
||||
// T-02-04-04 mitigation: cleanup tabs with silent-ignore on
|
||||
// already-closed. chrome.tabs.remove rejects if the tab id is gone;
|
||||
// we don't care about that side effect for this assertion.
|
||||
if (tabAId !== undefined) {
|
||||
try {
|
||||
await chrome.tabs.remove(tabAId);
|
||||
} catch (rmErr) {
|
||||
diag(result, `(tabA cleanup ignored: ${String(rmErr)})`);
|
||||
}
|
||||
}
|
||||
if (tabBId !== undefined) {
|
||||
try {
|
||||
await chrome.tabs.remove(tabBId);
|
||||
} catch (rmErr) {
|
||||
diag(result, `(tabB cleanup ignored: ${String(rmErr)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A28 — REQ-archive-layout strict zip-layout (page-side stub).
|
||||
*
|
||||
* Returns the assertion name + a sentinel diagnostic. All real work
|
||||
* happens host-side in driveA28 (JSZip enumerates the latest zip's
|
||||
* entries + asserts exactly 5 canonical paths, no extras). The
|
||||
* page-side stub exists purely for orchestrator uniformity.
|
||||
*
|
||||
* Chaining: A28 reads the zip produced by A27 (host-side picks the
|
||||
* most-recently-modified zip in downloadsDir). No new SAVE dispatch.
|
||||
*
|
||||
* @returns Stub AssertionResult with empty checks; driveA28 fills them.
|
||||
*/
|
||||
async function assertA28(): Promise<AssertionResult> {
|
||||
return {
|
||||
passed: true,
|
||||
name: 'A28 — REQ-archive-layout strict (5 entries)',
|
||||
checks: [],
|
||||
diagnostics: [
|
||||
'assertA28 page-side stub; host-side driveA28 enumerates latest zip + asserts 5-entry layout',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
||||
* orchestrator at startup to capture the expected version for A13's
|
||||
@@ -3136,6 +3362,10 @@ declare global {
|
||||
assertA24: () => Promise<AssertionResult>;
|
||||
// Plan 02-04 Task 2 — REQ-archive-export-latency (5s ceiling)
|
||||
assertA25: () => Promise<A25Result>;
|
||||
// Plan 02-04 Task 3 — D-P2-02 + D-P2-03 + REQ-archive-layout
|
||||
assertA26: () => Promise<AssertionResult>;
|
||||
assertA27: () => Promise<A27Result>;
|
||||
assertA28: () => Promise<AssertionResult>;
|
||||
getManifestVersion: () => Promise<string>;
|
||||
};
|
||||
}
|
||||
@@ -3167,6 +3397,9 @@ window.__mokoshHarness = {
|
||||
assertA23,
|
||||
assertA24,
|
||||
assertA25,
|
||||
assertA26,
|
||||
assertA27,
|
||||
assertA28,
|
||||
getManifestVersion,
|
||||
};
|
||||
|
||||
@@ -3175,6 +3408,6 @@ if (statusEl !== null) {
|
||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A28, getManifestVersion} available.';
|
||||
}
|
||||
|
||||
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + getManifestVersion)');
|
||||
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + getManifestVersion)');
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user