feat(03-03): Task 2 — driveA31 + orchestrator wiring (A31 password-filter PARTIAL)
- Append driveA31 to tests/uat/lib/harness-page-driver.ts after driveA30:
- Reuses UserEvent type (Plan 03-02 import already present).
- 3-phase pattern: page.evaluate → findLatestZip → JSZip
logs/events.json parse + filter-pipeline grep for sentinel absence
+ control-sentinel presence.
- 3 host-side checks: A31.2 (eventsContainingSentinel.length === 0),
A31.3 (eventsTargetingPassword.length === 0), A31.4
(eventsContainingControl.length >= 1; defense-in-depth proves
the listener is alive so A31.2/A31.3 absences mean the filter
fired rather than a tautological "no events at all" pass).
- Standard guard checks A31.0 (zip present) + A31.0a (events.json
entry exists) + A31.0b (JSON.parse success) gate before A31.2..A31.4
per Plan 02-04 / Plan 03-01 / Plan 03-02 driveA26/A29/A30 precedent.
- Filter-pipeline form preserved (no `continue`) per CLAUDE.md
Control Flow §.
- Wire orchestrator in tests/uat/harness.test.ts:
- Add `driveA31,` to import block after `driveA30,`.
- Add `driveA31Wrapped` const after `driveA30Wrapped`.
- Add `{ name: 'A31', drive: driveA31Wrapped }` entry to drivers
array after the A30 entry with explanatory banner comment
citing the cs-injection-world precedent + the defense-in-depth
A31.4 control check.
- Append `, A31` to the orchestrator banner string.
Acceptance grep gates (post-commit):
- grep -c 'driveA31' tests/uat/lib/harness-page-driver.ts returns 2
- grep -c 'driveA31' tests/uat/harness.test.ts returns 6
- grep -c 'secret-do-not-log-123' tests/uat/lib/harness-page-driver.ts returns 1
- tsc --noEmit exit 0
A29 flake disclosure (per Plan 03-02 SUMMARY "Issues Encountered"):
- During Plan 03-03 empirical verification of A31, the pre-existing
A29 flakiness documented in 03-02-SUMMARY.md surfaced: A29 chains
off incidental zip-mtime ordering against prior assertions' zips,
so when A29's own (empty chrome-extension:// SAVE) zip mtime ties
with a prior real-content zip, findLatestZip non-deterministically
returns the prior zip with rrweb events from iana.org/example.com.
- 3 base runs (HEAD=de398347, no Plan 03-03 changes): 2/3 PASS,
1/3 FAIL — confirms PRE-EXISTING flake, NOT a Plan 03-03 regression.
- Per CLAUDE.md SCOPE BOUNDARY ("Only auto-fix issues DIRECTLY caused
by the current task's changes") + Plan 03-02 SUMMARY's explicit
recommendation ("Plan 03-05's VERIFICATION.md aggregator + a
Phase 4 hardening pass can pick it up"): A29 flake is OUT OF SCOPE
for Plan 03-03. Documented in SUMMARY as deferred item.
This commit is contained in:
@@ -101,6 +101,8 @@ import {
|
||||
driveA29,
|
||||
// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log)
|
||||
driveA30,
|
||||
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||
driveA31,
|
||||
getManifestVersion,
|
||||
} from './lib/harness-page-driver';
|
||||
import {
|
||||
@@ -269,7 +271,7 @@ async function assertA0_GrepGate(): Promise<{
|
||||
*/
|
||||
async function main(): Promise<number> {
|
||||
process.stdout.write('\nMokosh Plan 01-13 + 01-14 + 02-04 — UAT harness orchestrator\n');
|
||||
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30)\n');
|
||||
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31)\n');
|
||||
process.stdout.write('='.repeat(72) + '\n');
|
||||
|
||||
// A0 pre-flight (no Chrome launch needed; runs against built dist/).
|
||||
@@ -345,6 +347,12 @@ async function main(): Promise<number> {
|
||||
// of logs/events.json from the just-produced zip.
|
||||
const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||
(page) => driveA30(page, handles.downloadsDir);
|
||||
// Plan 03-03 — driveA31 needs downloadsDir for host-side JSZip
|
||||
// negative-assertion against logs/events.json (sentinel absence +
|
||||
// password-selector-target absence) + control-sentinel presence
|
||||
// (defense-in-depth A31.4).
|
||||
const driveA31Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||
(page) => driveA31(page, handles.downloadsDir);
|
||||
|
||||
const drivers: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
@@ -454,6 +462,19 @@ async function main(): Promise<number> {
|
||||
// driveA30 JSZip-parses logs/events.json and asserts presence of
|
||||
// each of the 5 UserEvent.type literal values.
|
||||
{ name: 'A30', drive: driveA30Wrapped },
|
||||
// Plan 03-03 A31: password-filter PARTIAL (SPEC §10 #8 PARTIAL per
|
||||
// D-P3-02). Negative-assertion: opens a fresh https://example.com
|
||||
// probe tab (Plan 03-02 cs-injection-world precedent), injects a
|
||||
// synthetic <input type="password"> + a control <input type="text">
|
||||
// via chrome.scripting.executeScript ISOLATED-world, types the
|
||||
// sentinels, settles, SAVEs while the probe tab is active, finally-
|
||||
// cleanup. Host-side driveA31 inspects logs/events.json and asserts
|
||||
// sentinel value absence + password-selector-target absence (proves
|
||||
// src/content/index.ts:82 filter fired) + control-sentinel presence
|
||||
// (defense-in-depth: proves the listener is alive so A31.2/A31.3
|
||||
// mean the filter actually fired rather than the trivial "no
|
||||
// events at all" tautology).
|
||||
{ name: 'A31', drive: driveA31Wrapped },
|
||||
];
|
||||
|
||||
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
||||
|
||||
@@ -2146,3 +2146,171 @@ export async function driveA30(
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
/* ─── Plan 03-03 — driveA31 (password-filter PARTIAL host-side) ─────── */
|
||||
|
||||
/** Fixed test sentinel — same value as page-side A31_PASSWORD_SENTINEL.
|
||||
* Negative-assertion driver searches events.json for its absence. */
|
||||
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
|
||||
/** Control sentinel — must be PRESENT in logs/events.json (A31.4
|
||||
* defense-in-depth: proves the production setupInputLogging listener
|
||||
* is alive, so A31.2/A31.3 absence checks are not vacuously
|
||||
* satisfied — they actually mean the line-82 filter fired). */
|
||||
const A31_CONTROL_SENTINEL = 'control-event-must-be-logged-a31';
|
||||
/** Selector the production getSelector returns for #probe-password. */
|
||||
const A31_PASSWORD_SELECTOR = '#probe-password';
|
||||
|
||||
/**
|
||||
* Drive A31 (Plan 03-03 — SPEC §10 #8 PARTIAL per D-P3-02).
|
||||
*
|
||||
* Page-side assertA31 opened a fresh https://example.com probe tab,
|
||||
* injected a `<input type="password" id="probe-password">` element +
|
||||
* a `<input type="text" id="probe-control">` element, typed the
|
||||
* corresponding sentinels in the content-script's ISOLATED world,
|
||||
* settled, SAVEd, finally-cleanup the tab. Host-side asserts:
|
||||
* - the password SENTINEL is ABSENT from any UserEvent.value field
|
||||
* (proves the line-82 filter early-returned before addUserEvent)
|
||||
* - no UserEvent has target === '#probe-password' (proves the same
|
||||
* filter via the orthogonal selector path)
|
||||
* - at least one UserEvent contains the control sentinel
|
||||
* (defense-in-depth: proves the listener IS alive, so the
|
||||
* absences A31.2/A31.3 actually mean the filter fired rather
|
||||
* than the trivial "no events captured" tautology)
|
||||
*
|
||||
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
|
||||
*
|
||||
* Checks (4 visible + guards):
|
||||
* - A31.1: SAVE_ARCHIVE ack (page-side)
|
||||
* - A31.0a: logs/events.json entry exists in zip
|
||||
* - A31.0b: logs/events.json parses as JSON
|
||||
* - A31.2: 0 events contain SENTINEL in .value field
|
||||
* - A31.3: 0 events have target === '#probe-password'
|
||||
* - A31.4: >=1 event contains the control sentinel
|
||||
*
|
||||
* @param page - The harness page from `launchHarnessBrowser`.
|
||||
* @param downloadsDir - Absolute path to the per-run downloads directory.
|
||||
* @returns AssertionRecord with the merged checks.
|
||||
*/
|
||||
export async function driveA31(
|
||||
page: Page,
|
||||
downloadsDir: string,
|
||||
): Promise<AssertionRecord> {
|
||||
// Phase 1 — page-side orchestration + SAVE.
|
||||
const pageResult = await page.evaluate(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
|
||||
const harness = (window as any).__mokoshHarness;
|
||||
const r: AssertionRecord = await harness.assertA31();
|
||||
return r;
|
||||
}) as AssertionRecord;
|
||||
|
||||
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
|
||||
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
|
||||
|
||||
// Phase 2 — locate the produced zip.
|
||||
const zipPath = findLatestZip(downloadsDir);
|
||||
if (zipPath === null) {
|
||||
mergedChecks.push({
|
||||
name: 'A31.0: at least one zip present in downloadsDir',
|
||||
expected: '>=1 zip',
|
||||
actual: 'no zip in downloadsDir',
|
||||
passed: false,
|
||||
});
|
||||
return {
|
||||
passed: false,
|
||||
name: pageResult.name,
|
||||
checks: mergedChecks,
|
||||
diagnostics: mergedDiagnostics,
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
mergedDiagnostics.push(`A31 zipPath=${zipPath}`);
|
||||
|
||||
// Phase 3 — load + inspect logs/events.json.
|
||||
const zipBytes = readFileSync(zipPath);
|
||||
const zip = await JSZip.loadAsync(zipBytes);
|
||||
const eventsFile = zip.file('logs/events.json');
|
||||
mergedChecks.push({
|
||||
name: 'A31.0a: logs/events.json entry exists in zip',
|
||||
expected: true,
|
||||
actual: eventsFile !== null,
|
||||
passed: eventsFile !== null,
|
||||
});
|
||||
|
||||
if (eventsFile === null) {
|
||||
return {
|
||||
passed: false,
|
||||
name: pageResult.name,
|
||||
checks: mergedChecks,
|
||||
diagnostics: mergedDiagnostics,
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
const eventsRaw = await eventsFile.async('string');
|
||||
let userEvents: UserEvent[] = [];
|
||||
let parseErr: string | null = null;
|
||||
try {
|
||||
userEvents = JSON.parse(eventsRaw) as UserEvent[];
|
||||
} catch (err) {
|
||||
parseErr = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
if (parseErr !== null) {
|
||||
mergedChecks.push({
|
||||
name: 'A31.0b: logs/events.json parses as JSON',
|
||||
expected: 'JSON.parse success',
|
||||
actual: `<error: ${parseErr}>`,
|
||||
passed: false,
|
||||
});
|
||||
return {
|
||||
passed: false,
|
||||
name: pageResult.name,
|
||||
checks: mergedChecks,
|
||||
diagnostics: mergedDiagnostics,
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
// Filter-pipeline form per CLAUDE.md Control Flow §.
|
||||
const eventsContainingSentinel = userEvents.filter(
|
||||
(e) => typeof e.value === 'string' && e.value.includes(A31_PASSWORD_SENTINEL),
|
||||
);
|
||||
const eventsTargetingPassword = userEvents.filter(
|
||||
(e) => e.target === A31_PASSWORD_SELECTOR,
|
||||
);
|
||||
const eventsContainingControl = userEvents.filter(
|
||||
(e) => typeof e.value === 'string' && e.value.includes(A31_CONTROL_SENTINEL),
|
||||
);
|
||||
mergedDiagnostics.push(`A31 userEvents.length=${userEvents.length}`);
|
||||
mergedDiagnostics.push(
|
||||
`A31 sentinel-containing count=${eventsContainingSentinel.length}, password-targeting count=${eventsTargetingPassword.length}, control-containing count=${eventsContainingControl.length}`,
|
||||
);
|
||||
|
||||
mergedChecks.push({
|
||||
name: 'A31.2: 0 UserEvent entries contain the SENTINEL in their .value field (proves src/content/index.ts:82 filter fired)',
|
||||
expected: 0,
|
||||
actual: eventsContainingSentinel.length,
|
||||
passed: eventsContainingSentinel.length === 0,
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: `A31.3: 0 UserEvent entries have target === '${A31_PASSWORD_SELECTOR}' (filter early-returns BEFORE addUserEvent)`,
|
||||
expected: 0,
|
||||
actual: eventsTargetingPassword.length,
|
||||
passed: eventsTargetingPassword.length === 0,
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: 'A31.4: >=1 UserEvent entry contains the CONTROL sentinel (defense-in-depth: proves the listener is alive, so A31.2/A31.3 absences mean the filter fired — not "no events at all")',
|
||||
expected: '>=1 control',
|
||||
actual: eventsContainingControl.length,
|
||||
passed: eventsContainingControl.length >= 1,
|
||||
});
|
||||
|
||||
const mergedPassed = mergedChecks.every((c) => c.passed);
|
||||
return {
|
||||
passed: mergedPassed,
|
||||
name: pageResult.name,
|
||||
checks: mergedChecks,
|
||||
diagnostics: mergedDiagnostics,
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user