Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
2 changed files with 190 additions and 1 deletions
Showing only changes of commit 34b36fb58b - Show all commits

View File

@@ -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 };

View File

@@ -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,
};
}