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,
|
driveA29,
|
||||||
// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log)
|
// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log)
|
||||||
driveA30,
|
driveA30,
|
||||||
|
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||||
|
driveA31,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
} from './lib/harness-page-driver';
|
} from './lib/harness-page-driver';
|
||||||
import {
|
import {
|
||||||
@@ -269,7 +271,7 @@ async function assertA0_GrepGate(): Promise<{
|
|||||||
*/
|
*/
|
||||||
async function main(): Promise<number> {
|
async function main(): Promise<number> {
|
||||||
process.stdout.write('\nMokosh Plan 01-13 + 01-14 + 02-04 — UAT harness orchestrator\n');
|
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');
|
process.stdout.write('='.repeat(72) + '\n');
|
||||||
|
|
||||||
// A0 pre-flight (no Chrome launch needed; runs against built dist/).
|
// 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.
|
// of logs/events.json from the just-produced zip.
|
||||||
const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||||
(page) => driveA30(page, handles.downloadsDir);
|
(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<{
|
const drivers: ReadonlyArray<{
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@@ -454,6 +462,19 @@ async function main(): Promise<number> {
|
|||||||
// driveA30 JSZip-parses logs/events.json and asserts presence of
|
// driveA30 JSZip-parses logs/events.json and asserts presence of
|
||||||
// each of the 5 UserEvent.type literal values.
|
// each of the 5 UserEvent.type literal values.
|
||||||
{ name: 'A30', drive: driveA30Wrapped },
|
{ 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 };
|
const buffers = { swConsole: handles.swConsole, offConsole: handles.offConsole };
|
||||||
|
|||||||
@@ -2146,3 +2146,171 @@ export async function driveA30(
|
|||||||
error: pageResult.error,
|
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