feat(02): plans 01-04 — Phase 2 export pipeline closure (Blob URL + meta.urls + schema + harness)
Wave structure (4 plans, 3 waves): - 02-01 (Wave 1 RED): 15 RED tests pinning D-P2-01 (blob: URL contract), D-P2-02 (meta.urls schema + dedup + filter), D-P2-03 (strict 8-field validation + schemaVersion '2' cutover marker). - 02-02 (Wave 2): Offscreen-minted Blob URL pipeline — extends PortMessageType with CREATE/REVOKE messages; SW downloadArchive rewrite (data: → blob: via base64-on-wire to offscreen + URL.createObjectURL + chrome.downloads.onChanged revoke lifecycle). Closes audit P0-6; unblocks >2 MB archives. - 02-03 (Wave 2): meta.urls schema migration + tab-url-tracker module (chrome.tabs.onActivated + onUpdated → deduplicated, filtered, first-seen- ordered string[]); SessionMetadata 7→8 fields with schemaVersion + urls; REQUIREMENTS.md REQ-meta-json-schema amendment. Closes P1 #10. - 02-04 (Wave 3): UAT harness A24+A25+A26+A27 — blob: URL prefix, <5s SAVE→zip latency, meta.json 8-field shape, multi-tab dedup; pre-checkpoint bundle gates per saved memory + operator empirical UAT cycle 1. Tier-1 FORBIDDEN_HOOK_STRINGS inventory stays at 12 (no new hook symbols — chrome.* monkey-patches + JSZip + production APIs only). Locked decisions honored (per 02-CONTEXT.md): - D-P2-01: offscreen-minted Blob URL via existing keepalivePort + base64 wire format (reuses D-12 precedent at src/shared/binary.ts). - D-P2-02: meta.json url:string → urls:string[]; URL filter per CONTEXT.md <specifics> (include https://, chrome-extension://; exclude chrome://, about:, devtools://, file://); dedup + first-seen ordering. - D-P2-03: full scope; 8-field strict schema validation with schemaVersion='2' as the 8th field (planner-resolved tentative pick; revisable by plan-checker). Architectural constraints preserved: - Always-on charter (Plan 01-09 Amendment 3): no finally-block in saveArchive; no clearTabUrlsSeen on SAVE. - Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (no new test-hook symbols). - Never await import(...) in src/background/index.ts (Plan 01-11 SUMMARY). - Pre-checkpoint bundle gates per feedback-pre-checkpoint-bundle-gates.md (run in 02-04 Task 4 before operator surface). Plan validation: gsd-sdk frontmatter.validate + verify.plan-structure GREEN for all 4 plans. ROADMAP updated: Phase 2 Plans list + Goal/Success Criteria block annotated with D-P2-02/D-P2-03 amendments + 5th success criterion (Blob URL + revoke lifecycle for >2 MB archives); Progress table 0/TBD → 0/4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
376
.planning/phases/02-stabilize-export-pipeline/02-01-PLAN.md
Normal file
376
.planning/phases/02-stabilize-export-pipeline/02-01-PLAN.md
Normal file
@@ -0,0 +1,376 @@
|
||||
---
|
||||
phase: 02-stabilize-export-pipeline
|
||||
plan: 01
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- tests/background/blob-url-download.test.ts
|
||||
- tests/background/meta-json-urls-schema.test.ts
|
||||
- tests/build/strict-meta-json-validation.test.ts
|
||||
autonomous: true
|
||||
requirements:
|
||||
- REQ-archive-export-latency
|
||||
- REQ-meta-json-schema
|
||||
tags:
|
||||
- tdd
|
||||
- red-tests
|
||||
- blob-url-migration
|
||||
- meta-json-urls-array
|
||||
- schema-validation
|
||||
- p0-6
|
||||
- p1-10
|
||||
- wave-0
|
||||
must_haves:
|
||||
truths:
|
||||
- "Three RED test files exist and fail under the current HEAD (cc042a5) with concrete failure messages, NOT timeouts or missing imports."
|
||||
- "blob-url-download.test.ts pins downloadArchive() calling chrome.downloads.download with url starting with 'blob:' (not 'data:application/zip;base64,')."
|
||||
- "meta-json-urls-schema.test.ts pins SessionMetadata.urls as string[] and createArchive emitting meta.json with urls (not url)."
|
||||
- "strict-meta-json-validation.test.ts pins ISO-8601 Z-suffix timestamp, non-empty urls[], semver extensionVersion, non-negative totalEvents, exactly 8 fields."
|
||||
- "All 3 files use Vitest describe+it patterns matching existing tests/background/*.test.ts conventions (vi.fn() chrome.* stubs; jsdom or node env per existing patterns)."
|
||||
- "Tier-1 FORBIDDEN_HOOK_STRINGS unit-test gate remains GREEN (12 entries unchanged — this plan adds no test-hook symbols)."
|
||||
- "153/153 baseline vitest count rises to 153 + N (where N = number of new it() blocks; expected ~10-14)."
|
||||
artifacts:
|
||||
- path: "tests/background/blob-url-download.test.ts"
|
||||
provides: "RED pin: downloadArchive calls chrome.downloads.download({ url: blob:..., filename })"
|
||||
min_lines: 80
|
||||
contains: "describe.*blob.*url|chrome.downloads.download"
|
||||
- path: "tests/background/meta-json-urls-schema.test.ts"
|
||||
provides: "RED pin: SessionMetadata.urls: string[] + createArchive emits meta.json.urls"
|
||||
min_lines: 60
|
||||
contains: "urls.*string\\[\\]|meta\\.json"
|
||||
- path: "tests/build/strict-meta-json-validation.test.ts"
|
||||
provides: "RED pin: 8-field schema with ISO-8601 + semver + non-empty urls[] + non-negative totalEvents"
|
||||
min_lines: 80
|
||||
contains: "ISO-8601|semver|urls"
|
||||
key_links:
|
||||
- from: "tests/background/blob-url-download.test.ts"
|
||||
to: "src/background/index.ts:downloadArchive"
|
||||
via: "vi.mocked(chrome.downloads.download) inspection of first-call args[0].url prefix"
|
||||
pattern: "chrome\\.downloads\\.download.*url.*blob:"
|
||||
- from: "tests/background/meta-json-urls-schema.test.ts"
|
||||
to: "src/background/index.ts:createArchive + src/shared/types.ts:SessionMetadata"
|
||||
via: "createArchive() returns Blob; unzip via JSZip; read meta.json; assert .urls is Array, .url is undefined"
|
||||
pattern: "metadata\\.urls|metadata\\.url"
|
||||
- from: "tests/build/strict-meta-json-validation.test.ts"
|
||||
to: "meta.json wire shape (post-Plan 02-03)"
|
||||
via: "regex assertions on timestamp / version / totalEvents + Array.isArray(urls) + Object.keys(meta).length === 8"
|
||||
pattern: "Object\\.keys.*length.*8|Array\\.isArray.*urls"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wave 0 RED gate for Phase 2: three failing test files that pin the locked
|
||||
decisions D-P2-01 (Blob URL download), D-P2-02 (meta.json urls[] migration),
|
||||
and D-P2-03's schema-validation workstream. These tests MUST be RED at the
|
||||
end of this plan and MUST flip GREEN as Plans 02-02 and 02-03 land
|
||||
implementation.
|
||||
|
||||
Purpose: pin the contracts BEFORE any production code changes (TDD discipline
|
||||
matching Plan 01-02 Wave 0 precedent). Without RED tests committed first,
|
||||
nothing forces D-P2-01/D-P2-02/D-P2-03 to be implemented exactly as locked —
|
||||
silent partial implementation could land otherwise.
|
||||
|
||||
Output:
|
||||
- tests/background/blob-url-download.test.ts (RED — pins D-P2-01)
|
||||
- tests/background/meta-json-urls-schema.test.ts (RED — pins D-P2-02)
|
||||
- tests/build/strict-meta-json-validation.test.ts (RED — pins D-P2-03 schema)
|
||||
- vitest count rises by N RED tests (acceptable RED window — closed by Plans 02-02 + 02-03 within the same Phase 2 execution)
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
||||
|
||||
# Source under test
|
||||
@src/background/index.ts
|
||||
@src/shared/types.ts
|
||||
@src/shared/binary.ts
|
||||
|
||||
# Precedent for RED test style (Plan 01-02 Wave 0 + Plan 01-08 webm-remux deps test)
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-08-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts the tests will read from / write expectations against.
|
||||
Embedded so test authors don't go on a codebase scavenger hunt. -->
|
||||
|
||||
From src/shared/types.ts (CURRENT — pre-Plan-02-03):
|
||||
```typescript
|
||||
export interface SessionMetadata {
|
||||
timestamp: string;
|
||||
url: string; // ← D-P2-02 MIGRATES THIS to urls: string[]
|
||||
userAgent: string;
|
||||
extensionVersion: string;
|
||||
videoBufferSeconds: number;
|
||||
logDurationMinutes: number;
|
||||
totalEvents: number;
|
||||
}
|
||||
```
|
||||
|
||||
From src/background/index.ts:695-718 (CURRENT downloadArchive — pre-Plan-02-02):
|
||||
```typescript
|
||||
async function downloadArchive(archiveBlob: Blob) {
|
||||
const filename = `session_report_${dateStr}_${timeStr}.zip`;
|
||||
const base64 = await blobToBase64(archiveBlob);
|
||||
const url = `data:application/zip;base64,${base64}`; // ← D-P2-01 MIGRATES THIS to blob:URL
|
||||
await chrome.downloads.download({ url, filename, saveAs: false });
|
||||
}
|
||||
```
|
||||
|
||||
From src/background/index.ts:608-692 (CURRENT createArchive — pre-Plan-02-03):
|
||||
```typescript
|
||||
async function createArchive(videoBufferResponse, rrwebEvents, userEvents, screenshot): Promise<Blob> {
|
||||
// ... assembles zip ...
|
||||
const metadata: SessionMetadata = {
|
||||
timestamp: new Date().toISOString(),
|
||||
url: new URL(chrome.runtime.getURL('')).origin, // ← D-P2-02 MIGRATES TO urls: string[]
|
||||
userAgent: navigator.userAgent,
|
||||
extensionVersion: manifest.version,
|
||||
videoBufferSeconds: 30,
|
||||
logDurationMinutes: 10,
|
||||
totalEvents: rrwebEvents.length + userEvents.length
|
||||
};
|
||||
zip.file('meta.json', JSON.stringify(metadata, null, 2));
|
||||
const archiveBlob = await zip.generateAsync({ type: 'blob' });
|
||||
return archiveBlob;
|
||||
}
|
||||
```
|
||||
|
||||
Existing test conventions (read these for stub patterns):
|
||||
- `tests/background/save-archive-does-not-stop-recording.test.ts` — chrome.runtime + chrome.downloads stub pattern
|
||||
- `tests/background/webm-remux.test.ts` — JSZip + Blob unzip pattern
|
||||
- `tests/build/no-remote-fonts.test.ts` — post-build dist/ scan pattern (model for strict-meta-json-validation.test.ts STRUCTURE; this plan's variant scans createArchive's output, not dist/)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tdd" tdd="true">
|
||||
<name>Task 1: RED — blob-url-download.test.ts pins D-P2-01</name>
|
||||
<files>tests/background/blob-url-download.test.ts</files>
|
||||
<behavior>
|
||||
- Test 1 ("downloadArchive calls chrome.downloads.download with a blob: URL, NOT a data:application/zip;base64, URL"):
|
||||
stub chrome.downloads.download via vi.fn(); invoke downloadArchive(testBlob); assert
|
||||
mockDownload.mock.calls[0][0].url starts with 'blob:' AND does NOT start with 'data:application/zip;base64,'.
|
||||
- Test 2 ("downloadArchive completes within 5000ms for a 6 MB archive"):
|
||||
build a synthetic 6 MB Blob (above Chrome's ~2 MB data-URL cap that motivates D-P2-01);
|
||||
measure performance.now() before/after downloadArchive; assert delta < 5000.
|
||||
- Test 3 ("URL.revokeObjectURL is scheduled for the minted URL after chrome.downloads.onChanged 'complete' event"):
|
||||
stub chrome.downloads.onChanged.addListener via vi.fn(); invoke downloadArchive(testBlob);
|
||||
simulate the listener callback firing with {state: {current: 'complete'}, id: <downloadId>};
|
||||
assert URL.revokeObjectURL was called with the same url passed to chrome.downloads.download.
|
||||
- All 3 tests MUST fail under current HEAD with `url` starting with `data:application/zip;base64,`
|
||||
(Test 1), exceeding the 5000ms budget for the 6 MB case OR throwing "Network error" if Chrome's
|
||||
data-URL cap rejects (Test 2 — accept either failure mode as the RED signal), and NEVER calling
|
||||
URL.revokeObjectURL because the current path doesn't mint one (Test 3).
|
||||
</behavior>
|
||||
<action>
|
||||
Create `tests/background/blob-url-download.test.ts` per the behavior block above.
|
||||
|
||||
Follow `tests/background/save-archive-does-not-stop-recording.test.ts` for the chrome.* stub pattern
|
||||
(vi.stubGlobal('chrome', { runtime: ..., downloads: { download: vi.fn(), onChanged: { addListener: vi.fn() } } })).
|
||||
Use `import { downloadArchive } from '../../src/background/index'` ONLY IF downloadArchive is exported;
|
||||
if NOT, the test imports an internal-helper-export OR uses the saveArchive path with chrome.downloads.download
|
||||
spy — pick whichever the existing save-archive test uses. Document the choice in a comment block.
|
||||
|
||||
Use jsdom env (default in vitest.config.ts) so URL.createObjectURL + URL.revokeObjectURL are available.
|
||||
|
||||
For Test 2's 6 MB blob: `new Blob([new Uint8Array(6 * 1024 * 1024)], { type: 'application/zip' })`.
|
||||
|
||||
Per D-P2-01 — this test pins downloadArchive's WIRE FORMAT (blob: vs data:), the LATENCY
|
||||
PROPERTY (which only Blob URL achieves for >2 MB archives), and the LIFECYCLE
|
||||
(URL.revokeObjectURL hooked to chrome.downloads.onChanged 'complete'). The implementation
|
||||
in Plan 02-02 will mint the Blob URL inside the offscreen document (D-P2-01) and pass it
|
||||
back to the SW via a new port-bridge message; this test does not encode HOW the URL is
|
||||
minted, only WHAT the SW downloadArchive call site sends to chrome.downloads.download.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/background/blob-url-download.test.ts 2>&1 | grep -E "(FAIL|RED|failed)" | head -5</automated>
|
||||
</verify>
|
||||
<done>
|
||||
File `tests/background/blob-url-download.test.ts` exists, exports 3 it() blocks under a
|
||||
single describe, ALL 3 fail with concrete assertion errors (NOT import errors / NOT timeouts).
|
||||
`npx vitest run tests/background/blob-url-download.test.ts --reporter=verbose` shows
|
||||
3 failed tests with assertion messages identifying the wrong url prefix / latency / missing revoke.
|
||||
Commit message: `test(02-01): RED — pin Blob URL download contract (D-P2-01)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="tdd" tdd="true">
|
||||
<name>Task 2: RED — meta-json-urls-schema.test.ts pins D-P2-02</name>
|
||||
<files>tests/background/meta-json-urls-schema.test.ts</files>
|
||||
<behavior>
|
||||
- Test 1 ("SessionMetadata.urls is string[], SessionMetadata.url does not exist"):
|
||||
`import type { SessionMetadata } from '../../src/shared/types'`; use a `satisfies` check
|
||||
OR a `const _check: SessionMetadata['urls'] extends string[] ? true : false = true` pattern
|
||||
so the test is compile-time-checked AND runtime-introspectable. RED state: TypeScript
|
||||
tsc compile of this test currently fails because `urls` is not on SessionMetadata yet.
|
||||
Wrap the type-level check in a runtime `expect(true).toBe(true)` so the failure mode is
|
||||
"tsc compile failure" → flips GREEN after Plan 02-03 amends types.ts.
|
||||
- Test 2 ("createArchive emits meta.json with `urls: string[]`, not `url: string`"):
|
||||
stub chrome.runtime.getManifest + navigator.userAgent + the offscreen video-buffer fetch
|
||||
(return a minimal 1-segment VideoBufferResponse so remux doesn't blow up — reuse pattern
|
||||
from webm-remux.test.ts); invoke createArchive(); extract meta.json from the returned
|
||||
Blob via JSZip.loadAsync + zip.file('meta.json').async('string') + JSON.parse;
|
||||
assert `Array.isArray(meta.urls) === true` AND `meta.url === undefined`.
|
||||
- Test 3 ("meta.json.urls is deduplicated and ordered first-seen-first"):
|
||||
mock the tab-tracking source (chrome.tabs.query OR a yet-to-be-implemented module helper —
|
||||
see "module seam" note below); inject 3 tab URL events: A, B, A;
|
||||
assert `meta.urls` equals `['A', 'B']` (dedup; A appears once; first-seen order preserved).
|
||||
- Test 4 ("chrome:// and about: URLs are filtered out; chrome-extension:// included"):
|
||||
inject 4 events: 'https://example.com', 'chrome://newtab', 'about:blank',
|
||||
'chrome-extension://abc/popup.html'; assert `meta.urls` equals
|
||||
`['https://example.com', 'chrome-extension://abc/popup.html']` (per CONTEXT.md
|
||||
"URL filter" specifics block).
|
||||
- All 4 tests MUST fail under current HEAD: Test 1 via tsc compile failure (urls missing on type);
|
||||
Test 2 via `meta.url` present + `meta.urls` undefined; Test 3+4 via the tab-tracking module
|
||||
not existing yet (these will throw import errors — acceptable as the RED signal).
|
||||
</behavior>
|
||||
<action>
|
||||
Create `tests/background/meta-json-urls-schema.test.ts` per the behavior block above.
|
||||
|
||||
Module seam for tab tracking: assume Plan 02-03 will introduce a new module (planner-suggested name:
|
||||
`src/background/tab-url-tracker.ts`) exporting `getTabUrlsSeen(): string[]`. The test imports
|
||||
this seam by name; the absence of the module at the current HEAD is the RED signal for Tests 3+4.
|
||||
Document the seam name + expected export in a block comment at the top of the test file so Plan
|
||||
02-03 implements against the same shape.
|
||||
|
||||
For Test 2's createArchive invocation: pattern after `tests/background/webm-remux.test.ts` and
|
||||
`tests/background/save-archive-does-not-stop-recording.test.ts` — stub VideoBufferResponse with
|
||||
a single 1-byte segment (createArchive's remux path needs at least 1 to not throw
|
||||
EmptyVideoBufferError; use a pre-baked fixture Blob OR mock remuxSegments via vi.spyOn).
|
||||
|
||||
Per D-P2-02 — this test pins the SHAPE (urls: string[] not url: string), the DEDUP+ORDER semantics
|
||||
(first-seen-first), and the FILTER (exclude chrome:// + about:; include chrome-extension://) verbatim
|
||||
from CONTEXT.md `<specifics>` block. The implementation in Plan 02-03 implements the tab-tracker
|
||||
+ amends types.ts + amends createArchive's meta.json construction.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/background/meta-json-urls-schema.test.ts 2>&1 | grep -E "(FAIL|RED|failed|TS\d+)" | head -10</automated>
|
||||
</verify>
|
||||
<done>
|
||||
File exists, 4 it() blocks under a single describe. Test 1 fails via tsc compile error on missing
|
||||
`urls` field. Tests 2-4 fail with assertion errors OR import errors (latter for the missing
|
||||
tab-url-tracker module). NONE pass under current HEAD.
|
||||
Commit message: `test(02-01): RED — pin meta.json urls[] schema + dedup/filter semantics (D-P2-02)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="tdd" tdd="true">
|
||||
<name>Task 3: RED — strict-meta-json-validation.test.ts pins D-P2-03 schema</name>
|
||||
<files>tests/build/strict-meta-json-validation.test.ts</files>
|
||||
<behavior>
|
||||
- Test 1 ("meta.json has exactly 8 fields"): Object.keys(meta).length === 8.
|
||||
- Test 2 ("timestamp is ISO-8601 with Z suffix"):
|
||||
`/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(meta.timestamp)`.
|
||||
- Test 3 ("urls is a non-empty string[] of valid URLs"):
|
||||
`Array.isArray(meta.urls) && meta.urls.length >= 1 && meta.urls.every(u => /^(https?|chrome-extension):\/\//.test(u))`.
|
||||
Note: CONTEXT.md `<specifics>` allows empty urls[] for "purely whole-desktop recording without
|
||||
browser-tab interaction" — but D-P2-02 also says "the operator's primary tab at SAVE time should
|
||||
always be in the array if it has a valid URL", and CONTEXT.md `<decisions>` D-P2-03 mandates
|
||||
"urls array non-empty" as a schema validation rule. Resolve in favor of the strict-validation
|
||||
rule from D-P2-03 (non-empty required). Document the resolution inline in the test as a
|
||||
block comment citing both CONTEXT.md sources.
|
||||
- Test 4 ("extensionVersion matches semver"): `/^\d+\.\d+\.\d+$/.test(meta.extensionVersion)`.
|
||||
- Test 5 ("totalEvents is non-negative integer"):
|
||||
`Number.isInteger(meta.totalEvents) && meta.totalEvents >= 0`.
|
||||
- Test 6 ("videoBufferSeconds is exactly 30"): per CON-meta-json-schema verbatim.
|
||||
- Test 7 ("logDurationMinutes is exactly 10"): per CON-meta-json-schema verbatim.
|
||||
- Test 8 ("no extra fields"): Object.keys(meta).every(k => EXPECTED_KEYS.includes(k))
|
||||
where EXPECTED_KEYS = ['timestamp','urls','userAgent','extensionVersion','videoBufferSeconds','logDurationMinutes','totalEvents'] PLUS one of {schemaVersion?, archiveVersion?} ← reserve one slot for the 8th field. The current schema has 7; D-P2-03 says "8 fields exact". DECIDED INLINE: the 8th field is `urls` REPLACING `url` (net +0 → still 7 fields), BUT D-P2-03 explicitly says "8 fields exact". RESOLUTION: read the test as the strict-validation TARGET; the missing 8th field is the SHIPPING DECISION that Plan 02-03 must make and implement (planner suggests `schemaVersion: '2'` to mark the breaking-change cutover; documented as the implementer-choice in Plan 02-03). The RED test file embeds the EXPECTED_KEYS list including `schemaVersion` so Plan 02-03 has zero ambiguity.
|
||||
- All 8 tests MUST fail under current HEAD (the createArchive meta.json shape doesn't match
|
||||
most assertions — it has `url:string`, no `urls`, no `schemaVersion`, and 7 fields).
|
||||
</behavior>
|
||||
<action>
|
||||
Create `tests/build/strict-meta-json-validation.test.ts` per the behavior block above.
|
||||
|
||||
Pattern after `tests/build/no-remote-fonts.test.ts` (a `tests/build/` validator) and
|
||||
`tests/background/save-archive-does-not-stop-recording.test.ts` (createArchive invocation +
|
||||
JSZip extract pattern). The test invokes createArchive() via the same path as Task 2's
|
||||
`meta-json-urls-schema.test.ts`, extracts meta.json, JSON.parses it, and runs all 8 strict
|
||||
schema checks.
|
||||
|
||||
The EXPECTED_KEYS constant at the top of the test:
|
||||
```typescript
|
||||
const EXPECTED_KEYS = [
|
||||
'timestamp', 'urls', 'userAgent', 'extensionVersion',
|
||||
'videoBufferSeconds', 'logDurationMinutes', 'totalEvents', 'schemaVersion',
|
||||
];
|
||||
```
|
||||
The 8th field `schemaVersion` is the planner-suggested marker for the D-P2-02 url→urls
|
||||
breaking-change cutover. Plan 02-03's implementer COMMITS to adding `schemaVersion: '2'`
|
||||
as a constant in src/background/index.ts; the test pins this so Plan 02-03 cannot land
|
||||
a 7-field meta.json that satisfies Tests 1-7 but fails the "exactly 8 fields" + "no extra fields"
|
||||
pair. (Plan-checker may revise this — the 8th field name is a TENTATIVE PLANNER PICK;
|
||||
if Plan 02-03's implementer or Plan 02-04 has a stronger argument for a different 8th
|
||||
field name, this test file becomes the canonical place to amend.)
|
||||
|
||||
Add a block comment at the top citing CONTEXT.md D-P2-03 + REQUIREMENTS.md REQ-meta-json-schema
|
||||
as the contract source, and noting the planner-resolved tension between Tests 3's non-empty
|
||||
rule (per D-P2-03) and the CONTEXT.md `<specifics>` permissive empty-array clause.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run tests/build/strict-meta-json-validation.test.ts 2>&1 | grep -E "(FAIL|RED|failed)" | head -10</automated>
|
||||
</verify>
|
||||
<done>
|
||||
File exists, 8 it() blocks under a single describe. ALL 8 fail (current meta.json has 7 fields
|
||||
+ `url:string` instead of `urls:string[]` + no `schemaVersion`).
|
||||
Commit message: `test(02-01): RED — pin strict 8-field meta.json schema validation (D-P2-03)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| test author → test runtime | Test code controls chrome.* stubs; only the SW/offscreen surfaces under test are exercised |
|
||||
| createArchive output → JSZip parse | meta.json wire shape; D-P2-03 schema validation prevents downstream consumers (UAT harness, future SRV-* upload) from receiving malformed metadata |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-01-01 | Tampering | RED test as silent-skip risk (skip(true) / it.skip pattern slipping in) | mitigate | All 15 tests in this plan use bare `it()` + concrete expect()s; no `describe.skip` or `it.skip`. Plan-checker verifies grep -c "\.skip" $files == 0. |
|
||||
| T-02-01-02 | Repudiation | RED tests committed without RED proof (CI shows GREEN because test imports failed silently) | mitigate | Task verify> blocks run `npx vitest run <file>` and grep for "FAIL" — proves RED state at commit time. Plan 02-02 + 02-03 implementers prove GREEN-flip via same grep with "PASS". |
|
||||
| T-02-01-03 | Information Disclosure | Test creates real `meta.json` containing real `chrome.runtime.id` extension origin URL | accept | Tests run in jsdom + stubbed chrome.runtime.getURL returning fake 'https://test/'. Real origin never reaches test fixtures. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx vitest run tests/background/blob-url-download.test.ts` → 3 RED.
|
||||
- `npx vitest run tests/background/meta-json-urls-schema.test.ts` → 4 RED.
|
||||
- `npx vitest run tests/build/strict-meta-json-validation.test.ts` → 8 RED.
|
||||
- `npm test` → previously-153 GREEN + N RED (15 ± 1 RED from this plan). Acceptable window —
|
||||
Plans 02-02 + 02-03 close all RED before phase closure.
|
||||
- `grep -c '\.skip' tests/background/blob-url-download.test.ts tests/background/meta-json-urls-schema.test.ts tests/build/strict-meta-json-validation.test.ts` → 0.
|
||||
- Tier-1 grep gate: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` GREEN
|
||||
(unchanged at 12 FORBIDDEN_HOOK_STRINGS — this plan touches NO production code, only test fixtures).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. Three RED test files exist with concrete failure messages under current HEAD.
|
||||
2. `meta-json-urls-schema.test.ts` Test 1 fails at the TypeScript-compile layer (proves the type
|
||||
migration is genuinely required, not just a runtime field rename).
|
||||
3. Tier-1 grep gate (12 FORBIDDEN_HOOK_STRINGS) remains GREEN.
|
||||
4. UAT harness (24/24 GREEN baseline) UNCHANGED by this plan (test-only changes).
|
||||
5. Commits land with `test(02-01): RED — ...` prefix matching Plan 01-02 / 01-13 RED-test precedent.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-stabilize-export-pipeline/02-01-SUMMARY.md`
|
||||
documenting:
|
||||
- RED test count delta (153 GREEN + N RED new)
|
||||
- Specific failure-mode-per-test diagnostic strings
|
||||
- Planner-resolved tensions: D-P2-03 "non-empty urls[]" vs CONTEXT.md `<specifics>` permissive
|
||||
empty array (resolved IN FAVOR of strict rule); 8th field name (`schemaVersion` tentative pick).
|
||||
- Forward link: tests flip GREEN in Plans 02-02 + 02-03.
|
||||
</output>
|
||||
Reference in New Issue
Block a user