fix(02): revise plans per checker (B1 + 4 flags) — add tabs permission for D-P2-02
- BLOCKER B1: add `tabs` to manifest.json permissions (DEC-011 Amendment 1 cites Phase 2 D-P2-02 meta.urls feature as justification). Honors D-P2-02 "all tabs visible" wording verbatim. Updates manifest-i18n test expected permission list lockstep. - F1: add A28 harness assertion for REQ-archive-layout strict zip-layout verification (5 entries, no extras). - F2: createArchive empty-tracker fallback removed; logs warn + sets urls:[] instead of fake [extension-origin URL]. 02-01 RED test pins empty-tracker → urls:[]. - F3: 02-02 Task 3 prose deliberation struck; typed `blob-url-mint-failed` throw is the resolved-only contract. - F4: 02-02 Task 3 verify block adds full-suite `npm test` after focused test runs. - A27 strict-mode (Plan 02-04): REQUIRES both URLs in meta.urls; FAILS on length < 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,7 @@ nothing is validated until SPEC §10 acceptance passes.)
|
||||
| **DEC-008**: Screenshot via `chrome.tabs.captureVisibleTab` | SPEC §4.4, §5 — captured at export time, not continuously. | — Pending | locked (Phase 1) |
|
||||
| **DEC-009**: WebM header chunk retained indefinitely | SPEC §4.1, §8 — WebM without its header is not playable. | — Pending | locked (Phase 1) |
|
||||
| **DEC-010**: Service Worker keepalive via long-lived port | AMENDED by Phase 01: SPEC §8 originally specified `chrome.alarms` at 20 s; Phase 01 swaps to a `chrome.runtime.connect` port between offscreen and SW with 25 s ping cadence and 290 s pre-emptive reconnect. See `.planning/intel/decisions.md` DEC-010 Amendment. | — Pending | locked (Phase 1, post-Amendment) |
|
||||
| **DEC-011**: Manifest permissions set | SPEC §7 — `tabCapture`, `activeTab`, `downloads`, `scripting`, `storage` + `host_permissions: ["<all_urls>"]`. | — Pending | locked (Phase 1) |
|
||||
| **DEC-011**: Manifest permissions set | AMENDED 2026-05-20 (Amendment 1) by Plan 02-03: SPEC §7 originally specified `tabCapture`, `activeTab`, `downloads`, `scripting`, `storage` + `host_permissions: ["<all_urls>"]`. Phase 01 retired `tabCapture` (DEC-003 Amendment) and added `desktopCapture`, `offscreen`, `notifications`. Amendment 1 (2026-05-20) ADDS `tabs` to enable `chrome.tabs.get(tabId).url` + `chrome.tabs.query({})` for the Phase 2 D-P2-02 `meta.urls` feature (tab-url-tracker requires URL visibility beyond active-tab semantics). Current locked set: `desktopCapture`, `activeTab`, `tabs`, `downloads`, `scripting`, `storage`, `offscreen`, `notifications` + `host_permissions: ["<all_urls>"]`. Audit T-1-02 ("unused permissions expand attack surface") is acknowledged but overridden — the permission is genuinely USED by the meta.urls feature, so it is not unused. See `.planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md` Revision Log. | — Pending | locked (Phase 1, post-Amendment 1) |
|
||||
| **DEC-012**: Vite + crxjs + TypeScript build toolchain | README §"Технический стек" — DOC-level only; SPEC does not prescribe. | — Pending | locked (Phase 1) — auto-overridable by future ADR |
|
||||
|
||||
## Success Metric (Developer-Facing)
|
||||
|
||||
@@ -25,11 +25,11 @@ 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."
|
||||
- "meta-json-urls-schema.test.ts pins SessionMetadata.urls as string[], createArchive emitting meta.json with urls (not url), AND the F2-pinned empty-tracker contract (urls: [] for whole-desktop-no-tab sessions; NO sentinel fallback)."
|
||||
- "strict-meta-json-validation.test.ts pins ISO-8601 Z-suffix timestamp, urls is an Array of filter-matching URLs (empty PERMITTED per F2 resolution), 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)."
|
||||
- "153/153 baseline vitest count rises to 153 + N (where N = number of new it() blocks; expected ~16 after F2 added Test 5 to meta-json-urls-schema.test.ts)."
|
||||
artifacts:
|
||||
- path: "tests/background/blob-url-download.test.ts"
|
||||
provides: "RED pin: downloadArchive calls chrome.downloads.download({ url: blob:..., filename })"
|
||||
@@ -229,8 +229,14 @@ Existing test conventions (read these for stub patterns):
|
||||
'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
|
||||
- Test 5 ("empty-tracker → meta.urls === []", per F2 plan-checker iteration 1):
|
||||
mock the tab-tracking source to return an EMPTY array (whole-desktop-no-tab session);
|
||||
invoke createArchive; assert `meta.urls` is EXACTLY `[]` (empty Array; not undefined; not
|
||||
`[extension-origin-URL]`; not `null`). This pins the F2 resolution: NO sentinel fallback.
|
||||
The empty array is the canonical representation of a whole-desktop recording with no browser
|
||||
tabs open at SAVE time.
|
||||
- All 5 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+5 via the tab-tracking module
|
||||
not existing yet (these will throw import errors — acceptable as the RED signal).
|
||||
</behavior>
|
||||
<action>
|
||||
@@ -256,10 +262,10 @@ Existing test conventions (read these for stub patterns):
|
||||
<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
|
||||
File exists, 5 it() blocks under a single describe. Test 1 fails via tsc compile error on missing
|
||||
`urls` field. Tests 2-5 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)`.
|
||||
Commit message: `test(02-01): RED — pin meta.json urls[] schema + dedup/filter + empty-tracker semantics (D-P2-02 + F2)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -270,14 +276,15 @@ Existing test conventions (read these for stub patterns):
|
||||
- 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 3 ("urls is an Array of valid URLs; empty IS PERMITTED for whole-desktop sessions"):
|
||||
`Array.isArray(meta.urls) && meta.urls.every(u => /^(https?|chrome-extension):\/\//.test(u))`.
|
||||
Per F2 (plan-checker iteration 1 resolution): the empty-tracker case is MEANINGFUL (whole-desktop
|
||||
recording with no browser tabs open). The strict-validation rule from D-P2-03 ("urls array
|
||||
non-empty") is RELAXED to PERMIT empty arrays here; CONTEXT.md `<specifics>` permissive clause
|
||||
wins over the original D-P2-03 strict clause. Operator empirical workflow (Plan 02-04 Task 4
|
||||
Step 2) verifies the non-empty case via the multi-tab path. Document this resolution inline
|
||||
in the test as a block comment citing both CONTEXT.md sources AND the F2 plan-checker
|
||||
iteration 1 decision.
|
||||
- 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`.
|
||||
@@ -347,9 +354,9 @@ Existing test conventions (read these for stub patterns):
|
||||
|
||||
<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 —
|
||||
- `npx vitest run tests/background/meta-json-urls-schema.test.ts` → 5 RED (includes F2 empty-tracker → urls:[] pin).
|
||||
- `npx vitest run tests/build/strict-meta-json-validation.test.ts` → 8 RED (Test 3 RELAXED to permit empty urls[]).
|
||||
- `npm test` → previously-153 GREEN + N RED (16 ± 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
|
||||
|
||||
@@ -290,7 +290,7 @@ Existing blobToBase64 / base64ToBlob (src/shared/binary.ts:42-85) — REUSE AS-I
|
||||
(2) Generate a requestId (use crypto.randomUUID() — SW supports it; see getVideoBufferFromOffscreen pattern).
|
||||
(3) Post `{ type: 'CREATE_DOWNLOAD_URL', requestId, dataBase64, mimeType: 'application/zip' }` on the existing port (offscreenPort variable — see Plan 01-04 wiring at src/background/index.ts:onConnect host).
|
||||
(4) Await the matching `DOWNLOAD_URL` response (per-request listener map pattern, mirroring REQUEST_BUFFER → BUFFER from getVideoBufferFromOffscreen).
|
||||
(5) If response.url === '' or no response within timeout (5000ms), throw an EmptyVideoBufferError-equivalent "blob-url-mint-failed" or log + return (planner-decision: log warn, attempt fallback via legacy data: URL ONLY for archives <1 MB — otherwise throw and surface to operator). Resolved inline: NO FALLBACK. The blob: URL is the new contract; failure must surface via a typed error so the operator gets a clear failure mode, not a silent corrupt archive. Throw `new Error('blob-url-mint-failed: offscreen unresponsive or empty url')`.
|
||||
(5) If response.url === '' or no response within timeout (5000ms), throw a typed error: `new Error('blob-url-mint-failed: offscreen unresponsive or empty url')`. NO FALLBACK to legacy data: URL — the blob: URL is the contract; failure surfaces via the typed error so the operator gets a clear failure mode (routed through saveArchive's catch block to the RECORDING_ERROR channel, mirroring the EmptyVideoBufferError pattern). Silent corrupt-archive paths are forbidden.
|
||||
(6) Call chrome.downloads.download({ url, filename, saveAs: false }) capturing the returned downloadId.
|
||||
(7) Store downloadId → url in a module-scoped Map<number, string> (`pendingRevokes`) so chrome.downloads.onChanged can dispatch the revoke.
|
||||
- chrome.downloads.onChanged listener registered at module init (after the existing chrome.runtime.onMessage listener registration around line 843):
|
||||
@@ -433,7 +433,7 @@ Existing blobToBase64 / base64ToBlob (src/shared/binary.ts:42-85) — REUSE AS-I
|
||||
listener for URL.revokeObjectURL lifecycle.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20 ; npm run build 2>&1 | tail -10 ; npx vitest run tests/background/blob-url-download.test.ts 2>&1 | tail -20</automated>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20 ; npm run build 2>&1 | tail -10 ; npx vitest run tests/background/blob-url-download.test.ts 2>&1 | tail -20 ; npm test 2>&1 | tail -30</automated>
|
||||
</verify>
|
||||
<done>
|
||||
tsc clean. npm run build clean. tests/background/blob-url-download.test.ts → 3/3 GREEN.
|
||||
|
||||
@@ -5,6 +5,9 @@ type: auto
|
||||
wave: 2
|
||||
depends_on: [01]
|
||||
files_modified:
|
||||
- manifest.json
|
||||
- tests/i18n/manifest-i18n.test.ts
|
||||
- .planning/PROJECT.md
|
||||
- src/background/tab-url-tracker.ts
|
||||
- src/shared/types.ts
|
||||
- src/background/index.ts
|
||||
@@ -28,7 +31,7 @@ must_haves:
|
||||
- ".planning/REQUIREMENTS.md REQ-meta-json-schema text amended to reflect the new 8-field shape, with the breaking-change cutover documented inline."
|
||||
- "chrome.tabs.onActivated and chrome.tabs.onUpdated listeners registered in src/background/index.ts maintain the tracker's internal Set; pruning happens alongside the video segment ring buffer lifecycle (no separate timer)."
|
||||
- "Always-on charter preserved (Plan 01-09 Amendment 3): SAVE creates a zip with the current tracker state; tracker continues accumulating after SAVE."
|
||||
- "manifest.json permissions: the existing `tabs` permission gap (Plan 01-13 SUMMARY Known Limitations item 3) is NOT closed by this plan — the tracker uses chrome.tabs.onActivated + chrome.tabs.onUpdated which work on `activeTab` permission alone for the active-tab URL events. Cross-window/inactive-tab URL events require `tabs` permission and are deferred to Phase 4 hardening (planner-decision documented inline)."
|
||||
- "DEC-011 amended (Amendment 1, 2026-05-20): `tabs` permission ADDED to manifest.json by this plan. chrome.tabs.get(tabId).url is now reliably defined for all tabs in any window (not just activeTab). The tab-url-tracker enumerates chrome.tabs.query({}) at SAVE time as a defensive fallback to capture tabs the operator OPENED but never activated during the 30s window. tests/i18n/manifest-i18n.test.ts pins the new 8-entry permission set as a regression guard."
|
||||
artifacts:
|
||||
- path: "src/background/tab-url-tracker.ts"
|
||||
provides: "getTabUrlsSeen(): string[] + initTabUrlTracker(): void + clearTabUrlsSeen(): void"
|
||||
@@ -43,6 +46,15 @@ must_haves:
|
||||
- path: ".planning/REQUIREMENTS.md"
|
||||
provides: "REQ-meta-json-schema amended for 8-field shape + breaking-change cutover note"
|
||||
contains: "schemaVersion|urls.*string\\[\\]"
|
||||
- path: "manifest.json"
|
||||
provides: "permissions array extended to include `tabs` (DEC-011 Amendment 1)"
|
||||
contains: "\"tabs\""
|
||||
- path: "tests/i18n/manifest-i18n.test.ts"
|
||||
provides: "regression-pin describe block for the 8-entry permission set including `tabs`"
|
||||
contains: "DEC-011 Amendment 1|EXPECTED_PERMISSIONS"
|
||||
- path: ".planning/PROJECT.md"
|
||||
provides: "DEC-011 row rewritten with Amendment 1 (2026-05-20) citing Phase 2 D-P2-02 as justification"
|
||||
contains: "Amendment 1"
|
||||
key_links:
|
||||
- from: "src/background/index.ts:initialize"
|
||||
to: "src/background/tab-url-tracker.ts:initTabUrlTracker"
|
||||
@@ -189,6 +201,65 @@ REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 0: DEC-011 Amendment 1 — add `tabs` to manifest + sync PROJECT.md + i18n permission-pin test</name>
|
||||
<files>manifest.json, .planning/PROJECT.md, tests/i18n/manifest-i18n.test.ts</files>
|
||||
<action>
|
||||
Per plan-checker iteration 1 BLOCKER B1 resolution (user-approved Option A: honor D-P2-02
|
||||
"all tabs visible" verbatim by adding `tabs` permission to manifest.json).
|
||||
|
||||
1. **manifest.json:** extend the `permissions` array to include `"tabs"`. The locked
|
||||
post-amendment set has exactly 8 entries (alphabetic order is NOT required; the
|
||||
i18n test pins membership via set-equality, not order):
|
||||
```json
|
||||
"permissions": [
|
||||
"desktopCapture",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"downloads",
|
||||
"scripting",
|
||||
"storage",
|
||||
"offscreen",
|
||||
"notifications"
|
||||
]
|
||||
```
|
||||
NOTE: this commit may have already been applied during the revision pass — verify before
|
||||
editing. If `grep -c '"tabs"' manifest.json` returns a value matching the post-amendment
|
||||
state, treat this step as a no-op verification and proceed.
|
||||
|
||||
2. **.planning/PROJECT.md DEC-011 row:** rewrite the row to embed Amendment 1
|
||||
(2026-05-20) referencing Phase 2 D-P2-02 as justification. The amended row must:
|
||||
(a) Cite SPEC §7 as the original source and DEC-003 Amendment as Phase 01 delta.
|
||||
(b) State Amendment 1 ADDS `tabs` for D-P2-02 meta.urls feature.
|
||||
(c) List the current locked set verbatim (8 permissions + host_permissions).
|
||||
(d) Acknowledge audit T-1-02 ("unused permissions expand attack surface") and
|
||||
explicitly override it: the permission is USED by the meta.urls feature, so
|
||||
it is not unused.
|
||||
(e) Set status to `locked (Phase 1, post-Amendment 1)`.
|
||||
|
||||
3. **tests/i18n/manifest-i18n.test.ts:** add a new describe block
|
||||
`"Plan 02-03 DEC-011 Amendment 1: manifest.json permissions include \"tabs\" (D-P2-02)"`
|
||||
containing TWO assertions:
|
||||
(a) `manifest.permissions.includes('tabs') === true`
|
||||
(b) Set-equality between `manifest.permissions` (sorted) and the locked 8-entry
|
||||
EXPECTED_PERMISSIONS constant (sorted). Both lengths must match.
|
||||
Pre-existing test blocks (`Plan 01-12: ...`) MUST remain GREEN unchanged.
|
||||
|
||||
The amendment closes plan-checker iteration 1 BLOCKER B1 and unblocks Plan 02-04 A27's
|
||||
strict-mode (both-URLs-required) assertion — see Plan 02-04 Task 3 amendment in the same
|
||||
iteration 1 pass.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c '"tabs"' manifest.json ; grep -c 'Amendment 1' .planning/PROJECT.md ; npx vitest run tests/i18n/manifest-i18n.test.ts 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>
|
||||
manifest.json permissions includes `"tabs"` exactly once. PROJECT.md DEC-011 row carries
|
||||
Amendment 1 prose (≥1 grep hit for "Amendment 1"). tests/i18n/manifest-i18n.test.ts new
|
||||
describe block GREEN (2/2 assertions pass). Atomic commit shared with the rest of the
|
||||
iteration 1 revision pass — see commit conventions in the orchestrator brief.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create src/background/tab-url-tracker.ts module</name>
|
||||
<files>src/background/tab-url-tracker.ts</files>
|
||||
@@ -239,19 +310,31 @@ REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
and in-tab navigation events. No explicit "snapshot active tab at SAVE time" is needed (the
|
||||
tracker has already captured it via onActivated).
|
||||
|
||||
NOTE on `tabs` permission gap: chrome.tabs.onActivated provides tabId in the event payload;
|
||||
chrome.tabs.get(tabId) returns the tab WITHOUT the `.url` field unless `tabs` permission is
|
||||
granted (Plan 01-13 SUMMARY Known Limitations item 3). For the active tab specifically,
|
||||
`activeTab` permission grants temporary URL access. The tracker accepts the URL when present
|
||||
and skips silently when absent (logged at warn level for diagnostics). This means inactive-tab
|
||||
URL changes are NOT captured — acceptable per planner-decision deferred to Phase 4 hardening
|
||||
(CONTEXT.md `<deferred>` "tabs permission gap"). Document this limitation inline.
|
||||
NOTE on `tabs` permission (post DEC-011 Amendment 1, 2026-05-20): the new task in this plan
|
||||
AMENDS manifest.json to include the `tabs` permission. With `tabs` granted, chrome.tabs.get(tabId)
|
||||
reliably populates the `.url` field for any tab in any window — closing the limitation noted in
|
||||
Plan 01-13 SUMMARY Known Limitations item 3. The tracker therefore captures URL transitions for
|
||||
BOTH active and inactive tabs (the `chrome.tabs.onUpdated` listener already fires for any tab's
|
||||
URL change regardless of active state). A diagnostic logger.warn fires only if `.url` is still
|
||||
missing after the permission is granted (shouldn't happen — defensive only).
|
||||
|
||||
Per D-P2-03's "urls array non-empty" rule from Plan 02-01 Task 3: if getTabUrlsSeen returns
|
||||
an empty array at SAVE time, createArchive (Task 3 below) must fall back to a sentinel value
|
||||
(e.g., the extension-origin URL from `new URL(chrome.runtime.getURL('')).origin` — same as the
|
||||
current single-url path). This guarantees the strict 8-field schema validation never fails on
|
||||
empty urls[].
|
||||
SAVE-time defensive enumeration (per F2 + DEC-011 Amendment 1 capability): in addition to the
|
||||
event-driven Set maintenance, the tracker exposes a `snapshotOpenTabs(): Promise<void>` helper
|
||||
that invokes `chrome.tabs.query({})` and folds every returned tab.url (that passes the filter)
|
||||
into the internal Set + firstSeenOrder. createArchive (Task 2 below) calls this helper BEFORE
|
||||
reading `getTabUrlsSeen()` so any tab the operator OPENED during the 30s window but never
|
||||
activated (so the onActivated listener never fired for it) is still captured. This is purely
|
||||
additive — duplicates dedup; order preserves first-seen-first; no behavior change for
|
||||
already-observed tabs.
|
||||
|
||||
Per F2 (plan-checker iteration 1): the empty-tracker case is NO LONGER a sentinel-URL fallback.
|
||||
If after `snapshotOpenTabs()` the tracker is STILL empty (e.g., whole-desktop recording with NO
|
||||
browser tabs open at SAVE time — operator captured a non-Chrome window via desktopCapture only),
|
||||
that is a MEANINGFUL state and is represented faithfully as `urls: []` (empty array). The strict-
|
||||
validation rule in `tests/build/strict-meta-json-validation.test.ts` Test 3 was relaxed in Plan
|
||||
02-01 iteration 1 to PERMIT empty urls[] for this case; Plan 02-01's RED tests pin the
|
||||
empty-tracker → `meta.urls === []` contract explicitly. NO fake extension-origin URL is
|
||||
inserted.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
@@ -269,16 +352,27 @@ REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
<behavior>
|
||||
- src/shared/types.ts SessionMetadata: REPLACE `url: string` with `urls: string[]`; ADD `schemaVersion: string` as the first field. Total 8 fields.
|
||||
- src/background/index.ts:
|
||||
(a) import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker';
|
||||
(a) import { initTabUrlTracker, snapshotOpenTabs, getTabUrlsSeen } from './tab-url-tracker';
|
||||
(b) call initTabUrlTracker() inside the existing `initialize()` function (line 957) under defensive try/catch matching the chrome.notifications.onClicked pattern.
|
||||
(c) createArchive (line 673-684) updated:
|
||||
(c) createArchive (line 673-684) updated. Per F2 (plan-checker iteration 1) the empty-tracker case is NOT a sentinel-URL fallback; it is `urls: []`:
|
||||
```typescript
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const tabUrls = getTabUrlsSeen();
|
||||
// D-P2-03 non-empty guarantee: if the tracker has no URLs (e.g., SW just spawned,
|
||||
// operator hasn't activated a real tab yet), fall back to the extension-origin URL
|
||||
// so the strict-validation schema (REQ-meta-json-schema) never fails on empty array.
|
||||
const urls = tabUrls.length > 0 ? tabUrls : [new URL(chrome.runtime.getURL('')).origin];
|
||||
// DEC-011 Amendment 1: with `tabs` permission granted, snapshot every currently-open
|
||||
// tab's URL at SAVE time as a defensive fallback. This captures tabs the operator
|
||||
// OPENED during the 30s window but never activated (so the onActivated listener
|
||||
// never fired for them). Dedup + order preservation is handled by the tracker.
|
||||
try {
|
||||
await snapshotOpenTabs();
|
||||
} catch (err) {
|
||||
logger.warn('snapshotOpenTabs failed at SAVE time (continuing with tracker state):', err);
|
||||
}
|
||||
const urls = getTabUrlsSeen();
|
||||
if (urls.length === 0) {
|
||||
// Meaningful state: whole-desktop recording with NO browser tabs open. Per F2
|
||||
// resolution, this is represented faithfully as urls:[] — NO fake extension-origin
|
||||
// URL inserted. tests/background/meta-json-urls-schema.test.ts pins this contract.
|
||||
logger.warn('createArchive: tabUrlsSeen is empty after snapshotOpenTabs — emitting meta.urls=[] (whole-desktop session with no browser tabs)');
|
||||
}
|
||||
const metadata: SessionMetadata = {
|
||||
schemaVersion: '2',
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -308,7 +402,7 @@ REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
decision to make schemaVersion the 8th field.
|
||||
|
||||
2. Edit `src/background/index.ts`:
|
||||
- Add import: `import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker';` near
|
||||
- Add import: `import { initTabUrlTracker, snapshotOpenTabs, getTabUrlsSeen } from './tab-url-tracker';` near
|
||||
the existing import block (line 1-11).
|
||||
- In `initialize()` (line 957), add a defensive try/catch block calling initTabUrlTracker()
|
||||
right after the existing chrome.notifications.onClicked listener registration (search for
|
||||
@@ -436,25 +530,28 @@ REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
- `grep -c "schemaVersion" .planning/REQUIREMENTS.md` ≥ 2 (in REQ block + acceptance bullet).
|
||||
- `grep -c "url: string" src/shared/types.ts` → 0 (the legacy field is gone).
|
||||
- Always-on charter check: `grep -n "clearTabUrlsSeen" src/background/index.ts` → 0 hits (createArchive does NOT clear; tracker keeps accumulating after SAVE).
|
||||
- DEC-011 Amendment 1 verification: `grep -c '"tabs"' manifest.json` ≥ 1; `npx vitest run tests/i18n/manifest-i18n.test.ts` GREEN (pre-existing 10 tests + 2 new permission-set tests = 12/12 GREEN); `grep -c 'Amendment 1' .planning/PROJECT.md` ≥ 1.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. tests/background/meta-json-urls-schema.test.ts 4/4 GREEN.
|
||||
2. tests/build/strict-meta-json-validation.test.ts 8/8 GREEN.
|
||||
1. tests/background/meta-json-urls-schema.test.ts 4/4 GREEN (including the F2-pinned empty-tracker → `meta.urls === []` contract).
|
||||
2. tests/build/strict-meta-json-validation.test.ts 8/8 GREEN (Test 3 relaxed to PERMIT empty urls[] per F2 resolution).
|
||||
3. UAT harness 24/24 GREEN preserved (or A13 amended in lockstep if its meta.json assertions depend on the old `url` field — verify before commit).
|
||||
4. .planning/REQUIREMENTS.md REQ-meta-json-schema reflects the 8-field shape with breaking-change cutover documented.
|
||||
5. Always-on charter preserved: tab-url-tracker continues accumulating after SAVE; no createArchive-time clearTabUrlsSeen call.
|
||||
6. Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
|
||||
7. tabs permission gap NOT closed by this plan — explicit deferral comment in tab-url-tracker.ts citing CONTEXT.md `<deferred>`.
|
||||
7. DEC-011 Amendment 1 LANDED: manifest.json permissions includes `"tabs"`; PROJECT.md DEC-011 row carries Amendment 1 prose; tests/i18n/manifest-i18n.test.ts permission-set describe block GREEN. The `tabs` permission gap noted in Plan 01-13 SUMMARY Known Limitations item 3 is CLOSED by this plan, not deferred.
|
||||
8. createArchive empty-tracker fallback REMOVED (F2): emits `urls: []` + logger.warn for whole-desktop-no-tab sessions; NO fake extension-origin URL inserted.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md`
|
||||
documenting:
|
||||
- New module src/background/tab-url-tracker.ts (LOC + public API summary)
|
||||
- New module src/background/tab-url-tracker.ts (LOC + public API summary) — 4 exports incl. snapshotOpenTabs (DEC-011 Amendment 1)
|
||||
- SessionMetadata field-count delta (7 → 8) + ordering rationale
|
||||
- 8th field `schemaVersion` decision: planner-suggested in Plan 02-01 Task 3, ratified here
|
||||
- Filter rules verbatim from CONTEXT.md `<specifics>`
|
||||
- Tabs permission gap deferral citation (CONTEXT.md `<deferred>`)
|
||||
- Forward link: Plan 02-04 may amend UAT harness A13 if its meta.json assertions assumed the old `url` field
|
||||
- DEC-011 Amendment 1 (2026-05-20): `tabs` permission ADDED; tests/i18n/manifest-i18n.test.ts pins the 8-entry set; PROJECT.md DEC-011 row rewritten with Amendment 1 prose
|
||||
- F2 resolution: empty-tracker case emits `urls: []` (no fake extension-origin fallback)
|
||||
- Forward link: Plan 02-04 A27 strict-mode (both URLs required) is unblocked by Amendment 1
|
||||
</output>
|
||||
|
||||
@@ -22,9 +22,13 @@ tags:
|
||||
- a25
|
||||
- a26
|
||||
- a27
|
||||
- a28
|
||||
- a27-strict-mode
|
||||
- dec-011-amendment-1
|
||||
- blob-url-empirical
|
||||
- latency-5s
|
||||
- meta-urls-shape
|
||||
- archive-layout-strict
|
||||
- operator-checkpoint
|
||||
- phase-2-closure
|
||||
- approach-b
|
||||
@@ -34,29 +38,30 @@ must_haves:
|
||||
- "A24 verifies the SAVE_ARCHIVE → chrome.downloads.create call site receives a `blob:` URL prefix (NOT `data:application/zip;base64,`)."
|
||||
- "A25 verifies the SAVE_ARCHIVE → zip-on-disk latency (<5000ms from chrome.runtime.sendMessage({type:'SAVE_ARCHIVE'}) dispatch to file appearing in downloadsDir) per REQ-archive-export-latency."
|
||||
- "A26 verifies the produced meta.json has the 8-field D-P2-02/D-P2-03 shape: schemaVersion='2', urls is non-empty string[], no `url` key present."
|
||||
- "A27 verifies tab-URL tracking by activating 2 distinct tabs during the recording window, then SAVE, then inspecting meta.urls — expects both URLs to be present (deduplicated, first-seen-ordered)."
|
||||
- "A27 STRICT mode (post DEC-011 Amendment 1, 2026-05-20): harness opens TWO tabs in sequence via chrome.tabs.create + activates each via chrome.tabs.update; after SAVE_ARCHIVE, meta.urls is EXACTLY `[url1, url2]` (order-flexible; ≥2 length REQUIRED; NO `[object Object]`, NO extension-origin sentinel, NO chrome:// URLs). Test FAILS on length < 2 OR on any missing URL string."
|
||||
- "Tier-1 FORBIDDEN_HOOK_STRINGS inventory extended IF new hook surfaces required for A24+; lockstep across tests/background/no-test-hooks-in-prod-bundle.test.ts AND tests/uat/harness.test.ts A0 mirror."
|
||||
- "Final operator empirical checkpoint validates: (a) saving a real ~6MB archive completes successfully (was: data:URL Network error pre-Plan-02-02); (b) the produced meta.json has the new 8-field shape (operator opens the zip with archive-manager tool); (c) the saved zip opens cleanly in the OS file manager."
|
||||
- "Pre-checkpoint bundle gates per saved memory `feedback-pre-checkpoint-bundle-gates.md` are run BEFORE operator checkpoint: SW CSP grep (no `new Function`/`eval`), SW Node-globals grep (no `Buffer.from`), DOM-globals grep, SW-bundle-import gate, manifest validation."
|
||||
- "Phase 2 closes with 4/4 plans landed; 24/24 UAT baseline preserved or extended to 28/28 (A24+A25+A26+A27 inclusive); vitest baseline preserved or extended; operator empirical ack documented."
|
||||
- "A28 verifies REQ-archive-layout: the downloaded zip contains EXACTLY 5 entries — `video/last_30sec.webm`, `rrweb/session.json`, `logs/events.json`, `screenshot.png`, `meta.json` (no extras, no missing). Uses JSZip parsing in the harness driver against the host-side zip blob. Cross-references REQ-archive-layout + REQ-popup-ui (popup-triggered SAVE) + REQ-screenshot-on-export."
|
||||
- "Phase 2 closes with 4/4 plans landed; 24/24 UAT baseline preserved or extended to 29/29 (A24+A25+A26+A27+A28 inclusive); vitest baseline preserved or extended; operator empirical ack documented."
|
||||
artifacts:
|
||||
- path: "tests/uat/extension-page-harness.ts"
|
||||
provides: "assertA24 (blob: URL prefix) + assertA25 (5s latency) + assertA26 (meta.urls shape) + assertA27 (multi-tab URL dedup)"
|
||||
contains: "assertA24|assertA25|assertA26|assertA27"
|
||||
provides: "assertA24 (blob: URL prefix) + assertA25 (5s latency) + assertA26 (meta.urls shape) + assertA27 (multi-tab URL dedup, STRICT) + assertA28 (REQ-archive-layout strict zip-layout)"
|
||||
contains: "assertA24|assertA25|assertA26|assertA27|assertA28"
|
||||
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||
provides: "driveA24, driveA25, driveA26, driveA27 page.evaluate wrappers"
|
||||
contains: "driveA24|driveA25|driveA26|driveA27"
|
||||
provides: "driveA24, driveA25, driveA26, driveA27, driveA28 page.evaluate wrappers"
|
||||
contains: "driveA24|driveA25|driveA26|driveA27|driveA28"
|
||||
- path: "tests/uat/harness.test.ts"
|
||||
provides: "Orchestrator runs A24+A25+A26+A27 after A23; FORBIDDEN_HOOK_STRINGS lockstep if new hook symbols"
|
||||
contains: "driveA24|driveA25|driveA26|driveA27"
|
||||
provides: "Orchestrator runs A24+A25+A26+A27+A28 after A23; FORBIDDEN_HOOK_STRINGS lockstep if new hook symbols"
|
||||
contains: "driveA24|driveA25|driveA26|driveA27|driveA28"
|
||||
- path: "tests/background/no-test-hooks-in-prod-bundle.test.ts"
|
||||
provides: "Lockstep FORBIDDEN_HOOK_STRINGS update if new hook symbols introduced"
|
||||
contains: "FORBIDDEN_HOOK_STRINGS"
|
||||
key_links:
|
||||
- from: "tests/uat/harness.test.ts"
|
||||
to: "tests/uat/lib/harness-page-driver.ts:driveA24..A27"
|
||||
to: "tests/uat/lib/harness-page-driver.ts:driveA24..A28"
|
||||
via: "import + sequential orchestrator dispatch after driveA23"
|
||||
pattern: "driveA24|driveA25|driveA26|driveA27"
|
||||
pattern: "driveA24|driveA25|driveA26|driveA27|driveA28"
|
||||
- from: "tests/uat/extension-page-harness.ts:assertA25"
|
||||
to: "performance.now() bookends around SAVE_ARCHIVE dispatch + host-side downloadsDir poll"
|
||||
via: "page-side measures dispatch→ack; host-side measures dispatch→file-on-disk; assert total < 5000ms"
|
||||
@@ -68,9 +73,11 @@ must_haves:
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wave 3 of Phase 2: extend the UAT harness with A24+A25+A26+A27 assertions covering the D-P2-01 +
|
||||
D-P2-02 + D-P2-03 contracts end-to-end through a real Chrome instance. Run pre-checkpoint bundle
|
||||
gates per saved memory before surfacing the final operator empirical checkpoint. Close Phase 2.
|
||||
Wave 3 of Phase 2: extend the UAT harness with A24+A25+A26+A27+A28 assertions covering the
|
||||
D-P2-01 + D-P2-02 + D-P2-03 contracts AND REQ-archive-layout end-to-end through a real Chrome
|
||||
instance. A27 runs in STRICT mode (post DEC-011 Amendment 1) — REQUIRES both multi-tab URLs in
|
||||
meta.urls; FAILS on length<2. A28 strict-pins the 5-entry zip layout (no extras). Run pre-checkpoint
|
||||
bundle gates per saved memory before surfacing the final operator empirical checkpoint. Close Phase 2.
|
||||
|
||||
Purpose: empirical verification that Plans 02-02 (Blob URL pipeline) and 02-03 (meta.urls schema)
|
||||
work in a real Chrome instance, not just in unit-test isolation. The harness extension is the
|
||||
@@ -78,7 +85,9 @@ closure gate for Phase 2 — analogous to Plan 01-13's 15/15 harness PASS that c
|
||||
functional contract.
|
||||
|
||||
Output:
|
||||
- 4 new harness assertions (A24+A25+A26+A27) wired through page-harness + driver + orchestrator.
|
||||
- 5 new harness assertions (A24+A25+A26+A27+A28) wired through page-harness + driver + orchestrator.
|
||||
- A27 in STRICT mode (post DEC-011 Amendment 1) — both URLs REQUIRED; FAILS on length<2.
|
||||
- A28 pins REQ-archive-layout: exactly 5 zip entries (`video/last_30sec.webm`, `rrweb/session.json`, `logs/events.json`, `screenshot.png`, `meta.json`).
|
||||
- Tier-1 FORBIDDEN_HOOK_STRINGS lockstep IF new hook symbols required.
|
||||
- Pre-checkpoint bundle gate validation completed.
|
||||
- Operator empirical checkpoint documenting Plan 02-02 + 02-03 + the 5s-latency contract on a
|
||||
@@ -403,7 +412,7 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: assertA26 + A27 — meta.urls shape + multi-tab dedup empirical (D-P2-02)</name>
|
||||
<name>Task 3: assertA26 + A27 (strict) + A28 — meta.urls shape + multi-tab strict + REQ-archive-layout (D-P2-02/03 + REQ-archive-layout)</name>
|
||||
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||
<behavior>
|
||||
- assertA26 page-side: chain off A25's produced zip (read host-side filename via merged checks).
|
||||
@@ -413,32 +422,34 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
(c) Array.isArray(meta.urls) && meta.urls.length >= 1
|
||||
(d) meta.url === undefined (the legacy single-URL field is gone)
|
||||
(e) meta.urls.every(u => /^(https?|chrome-extension):\/\//.test(u))
|
||||
- assertA27 page-side: requires multi-tab activation BEFORE save. Sequence:
|
||||
- assertA27 STRICT mode (post DEC-011 Amendment 1) — REQUIRES both multi-tab URLs in meta.urls. Sequence:
|
||||
(1) setupFreshRecording
|
||||
(2) Open tab A (e.g., chrome.tabs.create({ url: 'https://example.com' })) and wait for it to load
|
||||
(3) Activate tab A
|
||||
(4) Open tab B (e.g., chrome.tabs.create({ url: 'https://www.iana.org' })) and wait
|
||||
(5) Activate tab B
|
||||
(2) Open tab A: `chrome.tabs.create({ url: 'https://example.com/', active: false })`; wait 1500ms for navigation
|
||||
(3) Activate tab A: `chrome.tabs.update(tabA.id, { active: true })`; wait 500ms for onActivated to fire
|
||||
(4) Open tab B: `chrome.tabs.create({ url: 'https://www.iana.org/', active: false })`; wait 1500ms for navigation
|
||||
(5) Activate tab B: `chrome.tabs.update(tabB.id, { active: true })`; wait 500ms for onActivated to fire
|
||||
(6) Wait 11s for one segment to land
|
||||
(7) Dispatch SAVE_ARCHIVE
|
||||
(8) Read meta.urls from the produced zip
|
||||
(9) Assert both example.com AND iana.org appear in meta.urls
|
||||
(10) Cleanup: close both tabs
|
||||
- PLANNER-NOTE on tabs permission limitation: A27 depends on chrome.tabs URL access. Per Plan
|
||||
01-13 SUMMARY Known Limitations item 3, `tabs` permission is NOT declared and `chrome.tabs.query`
|
||||
may return tabs without `.url`. RESOLUTION: A27 uses chrome.tabs.create + chrome.tabs.update
|
||||
to drive tab activation directly, bypassing chrome.tabs.query. The tracker's chrome.tabs.onActivated
|
||||
listener fires regardless of permission — it's the post-event chrome.tabs.get(tabId) inside the
|
||||
tracker that may return undefined .url. If A27 reveals the gap empirically, surface a diagnostic
|
||||
in the test result and defer the closure to Phase 4 hardening (CONTEXT.md `<deferred>` tabs
|
||||
permission gap item). If A27 PASSES on the active-tab path, the limitation is non-blocking
|
||||
for Phase 2.
|
||||
- driveA26 + driveA27: standard page.evaluate wrappers (mirror driveA23/A24).
|
||||
- Orchestrator update: tests/uat/harness.test.ts adds A24, A25, A26, A27 to the assertion
|
||||
sequence AFTER A23. Total target: 24 (Phase 1 baseline) + 4 (Phase 2) = 28/28 GREEN.
|
||||
- FORBIDDEN_HOOK_STRINGS lockstep: PLANNER-DECISION — A24/A25/A26/A27 use chrome.* monkey-patch
|
||||
(A24's downloads spy) + JSZip + chrome.tabs.create/update which are production APIs. No new
|
||||
test-hook surface required. Tier-1 inventory STAYS at 12. Plan-checker verifies via final grep.
|
||||
(7) Dispatch SAVE_ARCHIVE; await ack with success===true
|
||||
(8) Host-side: load latest zip; parse meta.json; read meta.urls
|
||||
(9) Strict assertions (all REQUIRED to pass):
|
||||
(a) `meta.urls.length >= 2` (FAIL on length < 2)
|
||||
(b) `meta.urls` contains BOTH 'https://example.com/' AND 'https://www.iana.org/' (order-flexible; use Set membership)
|
||||
(c) `meta.urls.every(u => typeof u === 'string' && u.length > 0)` — no `[object Object]`, no nulls, no empty strings
|
||||
(d) `meta.urls.every(u => !u.startsWith('chrome-extension://'))` — no extension-origin sentinel fallback (F2)
|
||||
(e) `meta.urls.every(u => !u.startsWith('chrome://'))` — no chrome-internal URLs
|
||||
(10) Cleanup: try/catch close both tabs via chrome.tabs.remove (silent-ignore on already-closed)
|
||||
DEC-011 Amendment 1 guarantee: with `tabs` permission granted, chrome.tabs.get(tabId).url
|
||||
and chrome.tabs.query({}) reliably populate the URL field. snapshotOpenTabs at SAVE time
|
||||
(Plan 02-03 Task 2) provides a defensive belt + suspenders. A27 has UNAMBIGUOUS contract:
|
||||
both URLs MUST appear; the test is the binding empirical gate for D-P2-02.
|
||||
- assertA28 (REQ-archive-layout strict zip-layout): chain off A25's produced zip (reuse merged-checks pattern). Use JSZip.loadAsync to enumerate zip entries; assert:
|
||||
(a) Zip contains EXACTLY 5 entries (no more, no fewer)
|
||||
(b) The 5 paths are EXACTLY `video/last_30sec.webm`, `rrweb/session.json`, `logs/events.json`, `screenshot.png`, `meta.json` (set-equality; order-flexible)
|
||||
(c) NO extra entries (no `__MACOSX/`, no `.DS_Store`, no temp files)
|
||||
Cross-references REQ-archive-layout + REQ-popup-ui (popup-triggered SAVE flows through the same createArchive path) + REQ-screenshot-on-export (screenshot.png entry verified present).
|
||||
- driveA26 + driveA27 + driveA28: standard page.evaluate wrappers + host-side zip-lookup (mirror driveA5's merged-checks pattern). driveA27 additionally inspects meta.urls host-side via JSZip; driveA28 inspects the zip directory listing.
|
||||
- Orchestrator update: tests/uat/harness.test.ts adds A24, A25, A26, A27, A28 to the assertion sequence AFTER A23. Total target: 24 (Phase 1 baseline) + 5 (Phase 2) = 29/29 GREEN.
|
||||
- FORBIDDEN_HOOK_STRINGS lockstep: A24/A25/A26/A27/A28 use chrome.* monkey-patch (A24's downloads spy) + JSZip + chrome.tabs.create/update which are production APIs. No new test-hook surface required. Tier-1 inventory STAYS at 12. Plan-checker verifies via final grep.
|
||||
</behavior>
|
||||
<action>
|
||||
Add `assertA26` and `assertA27` to tests/uat/extension-page-harness.ts:
|
||||
@@ -494,20 +505,35 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
}
|
||||
|
||||
/**
|
||||
* A27 — D-P2-02 empirical: multi-tab URL tracking. Activates two distinct
|
||||
* URLs during the recording window; meta.urls should contain both.
|
||||
* A27 — STRICT mode (post DEC-011 Amendment 1): multi-tab URL tracking.
|
||||
* Opens TWO tabs sequentially, activates each, then dispatches SAVE.
|
||||
* Host-side driver asserts meta.urls contains BOTH URLs (length >= 2 REQUIRED;
|
||||
* FAILS on length < 2; no extension-origin sentinel; no chrome-internal URLs).
|
||||
*/
|
||||
async function assertA27(): Promise<AssertionResult> {
|
||||
async function assertA27(): Promise<AssertionResult & { tabAUrl: string; tabBUrl: string }> {
|
||||
const checks: CheckRecord[] = [];
|
||||
const diagnostics: string[] = [];
|
||||
|
||||
await setupFreshRecording();
|
||||
|
||||
// Open + activate 2 distinct tabs.
|
||||
const tabA = await chrome.tabs.create({ url: 'https://example.com', active: true });
|
||||
const TAB_A_URL = 'https://example.com/';
|
||||
const TAB_B_URL = 'https://www.iana.org/';
|
||||
|
||||
// Open + activate tab A.
|
||||
const tabA = await chrome.tabs.create({ url: TAB_A_URL, active: false });
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
const tabB = await chrome.tabs.create({ url: 'https://www.iana.org', active: true });
|
||||
if (tabA.id !== undefined) {
|
||||
await chrome.tabs.update(tabA.id, { active: true });
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Open + activate tab B.
|
||||
const tabB = await chrome.tabs.create({ url: TAB_B_URL, active: false });
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
if (tabB.id !== undefined) {
|
||||
await chrome.tabs.update(tabB.id, { active: true });
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Settle for one segment.
|
||||
await new Promise(r => setTimeout(r, 11_000));
|
||||
@@ -517,16 +543,177 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
chrome.runtime.sendMessage({ type: 'SAVE_ARCHIVE' }, resolve);
|
||||
});
|
||||
checks.push({
|
||||
name: 'A27.1: SAVE_ARCHIVE ack received',
|
||||
name: 'A27.1: SAVE_ARCHIVE ack received with success=true',
|
||||
expected: true, actual: ack.success, passed: ack.success === true,
|
||||
});
|
||||
|
||||
// Cleanup tabs.
|
||||
if (tabA.id) await chrome.tabs.remove(tabA.id);
|
||||
if (tabB.id) await chrome.tabs.remove(tabB.id);
|
||||
// Cleanup tabs (silent-ignore on already-closed).
|
||||
try { if (tabA.id !== undefined) await chrome.tabs.remove(tabA.id); } catch {}
|
||||
try { if (tabB.id !== undefined) await chrome.tabs.remove(tabB.id); } catch {}
|
||||
|
||||
diagnostics.push(`opened tabA=${tabA.id} tabB=${tabB.id}; meta.urls inspection deferred to host-side`);
|
||||
return { name: 'A27 — multi-tab URL dedup (D-P2-02)', checks, diagnostics };
|
||||
diagnostics.push(`opened tabA.id=${tabA.id} url=${TAB_A_URL}; tabB.id=${tabB.id} url=${TAB_B_URL}`);
|
||||
return {
|
||||
name: 'A27 — D-P2-02 STRICT multi-tab urls[] (both URLs REQUIRED)',
|
||||
checks, diagnostics,
|
||||
tabAUrl: TAB_A_URL,
|
||||
tabBUrl: TAB_B_URL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A28 — REQ-archive-layout strict zip-layout: zip contains EXACTLY the 5
|
||||
* canonical entries and no extras. Chains off A25's produced zip via the
|
||||
* host-side driver's merged-checks pattern (no new SAVE dispatch).
|
||||
*
|
||||
* NOTE: page-side assertA28 is a STUB returning the assertion name; all
|
||||
* real work happens host-side in driveA28 (zip directory enumeration).
|
||||
*/
|
||||
async function assertA28(): Promise<AssertionResult> {
|
||||
return {
|
||||
name: 'A28 — REQ-archive-layout strict zip-layout (5 entries)',
|
||||
checks: [],
|
||||
diagnostics: ['assertA28 page-side stub; host-side driver inspects zip directory'],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Host-side driveA27 (merged-checks pattern with strict meta.urls assertions):
|
||||
|
||||
```typescript
|
||||
export async function driveA27(page: Page, downloadsDir: string): Promise<AssertionRecord> {
|
||||
const preExisting = new Set(readdirSync(downloadsDir).filter(isZipFilename));
|
||||
const pageResult = await page.evaluate(async () => {
|
||||
const harness = (window as any).__mokoshHarness;
|
||||
return await harness.assertA27();
|
||||
}) as AssertionRecord & { tabAUrl: string; tabBUrl: string };
|
||||
|
||||
// Locate the new zip produced by A27.
|
||||
let zipPath: string | null = null;
|
||||
const pollStart = Date.now();
|
||||
while (Date.now() - pollStart < 8000) {
|
||||
const candidates = readdirSync(downloadsDir).filter(
|
||||
(name) => isZipFilename(name) && !preExisting.has(name),
|
||||
);
|
||||
if (candidates.length > 0) {
|
||||
zipPath = `${downloadsDir}/${candidates[candidates.length - 1]}`;
|
||||
break;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
|
||||
const mergedDiagnostics = pageResult.diagnostics.slice();
|
||||
|
||||
if (zipPath === null) {
|
||||
mergedChecks.push({
|
||||
name: 'A27.2: zip emitted to downloadsDir',
|
||||
expected: 'zip file present', actual: 'no zip found within 8s', passed: false,
|
||||
});
|
||||
} else {
|
||||
const zipBytes = readFileSync(zipPath);
|
||||
const zip = await JSZip.loadAsync(zipBytes);
|
||||
const metaFile = zip.file('meta.json');
|
||||
const metaText = metaFile !== null ? await metaFile.async('string') : '{}';
|
||||
let meta: { urls?: unknown } = {};
|
||||
try { meta = JSON.parse(metaText); } catch {}
|
||||
const urlsRaw = (meta as { urls?: unknown }).urls;
|
||||
const urls: string[] = Array.isArray(urlsRaw) ? urlsRaw.filter((u): u is string => typeof u === 'string') : [];
|
||||
|
||||
mergedChecks.push({
|
||||
name: 'A27.2: meta.urls is an Array',
|
||||
expected: true, actual: Array.isArray(urlsRaw), passed: Array.isArray(urlsRaw),
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: 'A27.3: meta.urls.length >= 2 (STRICT — both URLs REQUIRED post DEC-011 Amendment 1)',
|
||||
expected: '>=2', actual: urls.length, passed: urls.length >= 2,
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: `A27.4: meta.urls contains ${pageResult.tabAUrl}`,
|
||||
expected: true, actual: urls.includes(pageResult.tabAUrl), passed: urls.includes(pageResult.tabAUrl),
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: `A27.5: meta.urls contains ${pageResult.tabBUrl}`,
|
||||
expected: true, actual: urls.includes(pageResult.tabBUrl), passed: urls.includes(pageResult.tabBUrl),
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: 'A27.6: every meta.urls[i] is a non-empty string (no [object Object], no nulls)',
|
||||
expected: true,
|
||||
actual: urls.every(u => typeof u === 'string' && u.length > 0),
|
||||
passed: urls.length > 0 && urls.every(u => typeof u === 'string' && u.length > 0),
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: 'A27.7: no extension-origin sentinel URLs (F2 — empty-tracker fallback removed)',
|
||||
expected: true,
|
||||
actual: urls.every(u => !u.startsWith('chrome-extension://')),
|
||||
passed: urls.every(u => !u.startsWith('chrome-extension://')),
|
||||
});
|
||||
mergedChecks.push({
|
||||
name: 'A27.8: no chrome-internal URLs in meta.urls (filter rules)',
|
||||
expected: true,
|
||||
actual: urls.every(u => !u.startsWith('chrome://') && !u.startsWith('about:')),
|
||||
passed: urls.every(u => !u.startsWith('chrome://') && !u.startsWith('about:')),
|
||||
});
|
||||
mergedDiagnostics.push(`zipPath=${zipPath}; meta.urls=${JSON.stringify(urls)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
passed: mergedChecks.every(c => c.passed),
|
||||
name: pageResult.name,
|
||||
checks: mergedChecks,
|
||||
diagnostics: mergedDiagnostics,
|
||||
error: pageResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function driveA28(page: Page, downloadsDir: string): Promise<AssertionRecord> {
|
||||
// Reuse A25's most-recent zip (A28 does NOT trigger a new SAVE — chains off A25).
|
||||
const zipFiles = readdirSync(downloadsDir).filter(isZipFilename).sort();
|
||||
const checks: CheckRecord[] = [];
|
||||
const diagnostics: string[] = [];
|
||||
if (zipFiles.length === 0) {
|
||||
checks.push({
|
||||
name: 'A28.1: at least one zip present in downloadsDir',
|
||||
expected: '>=1', actual: 0, passed: false,
|
||||
});
|
||||
return { passed: false, name: 'A28 — REQ-archive-layout strict (5 entries)', checks, diagnostics };
|
||||
}
|
||||
const latest = `${downloadsDir}/${zipFiles[zipFiles.length - 1]}`;
|
||||
const zipBytes = readFileSync(latest);
|
||||
const zip = await JSZip.loadAsync(zipBytes);
|
||||
|
||||
const EXPECTED_PATHS: ReadonlyArray<string> = [
|
||||
'video/last_30sec.webm',
|
||||
'rrweb/session.json',
|
||||
'logs/events.json',
|
||||
'screenshot.png',
|
||||
'meta.json',
|
||||
];
|
||||
const actualPaths = Object.keys(zip.files).filter(p => !zip.files[p].dir).sort();
|
||||
const expectedSorted = [...EXPECTED_PATHS].sort();
|
||||
|
||||
checks.push({
|
||||
name: 'A28.1: zip has EXACTLY 5 entries (REQ-archive-layout)',
|
||||
expected: 5, actual: actualPaths.length, passed: actualPaths.length === 5,
|
||||
});
|
||||
checks.push({
|
||||
name: 'A28.2: zip entries set-equal to the canonical 5 paths',
|
||||
expected: expectedSorted.join(','),
|
||||
actual: actualPaths.join(','),
|
||||
passed: JSON.stringify(actualPaths) === JSON.stringify(expectedSorted),
|
||||
});
|
||||
checks.push({
|
||||
name: 'A28.3: no extras (no __MACOSX/, no .DS_Store, no temp files)',
|
||||
expected: 'no extras',
|
||||
actual: actualPaths.filter(p => !EXPECTED_PATHS.includes(p)).join(',') || 'none',
|
||||
passed: actualPaths.every(p => EXPECTED_PATHS.includes(p)),
|
||||
});
|
||||
diagnostics.push(`zipPath=${latest}; entries=${actualPaths.join(',')}`);
|
||||
|
||||
return {
|
||||
passed: checks.every(c => c.passed),
|
||||
name: 'A28 — REQ-archive-layout strict (5 entries; D-P2-02/03 + REQ-popup-ui + REQ-screenshot-on-export)',
|
||||
checks, diagnostics,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -544,25 +731,29 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
drivers.push({ name: 'A25', fn: () => driveA25(page, handles.downloadsDir) });
|
||||
drivers.push({ name: 'A26', fn: () => driveA26(page, handles.downloadsDir) });
|
||||
drivers.push({ name: 'A27', fn: () => driveA27(page, handles.downloadsDir) });
|
||||
drivers.push({ name: 'A28', fn: () => driveA28(page, handles.downloadsDir) });
|
||||
```
|
||||
Final target: 28/28 GREEN.
|
||||
Final target: 29/29 GREEN.
|
||||
|
||||
FORBIDDEN_HOOK_STRINGS verification: grep the new test files for any new symbols that might
|
||||
leak into dist/. Expected: none (all symbols are local to extension-page-harness.ts which is
|
||||
NOT built into dist/ — it's loaded via the test-harness page, not the production manifest).
|
||||
Tier-1 inventory stays at 12.
|
||||
|
||||
Per D-P2-02 + D-P2-03 + REQ-archive-export-latency: A24+A25+A26+A27 collectively validate the
|
||||
full Phase 2 contract empirically. After A27, Phase 2 functional contract is HARNESS-CLOSED
|
||||
(analogous to Plan 01-13's role for Phase 1).
|
||||
Per D-P2-02 + D-P2-03 + REQ-archive-export-latency + REQ-archive-layout: A24+A25+A26+A27+A28
|
||||
collectively validate the full Phase 2 contract empirically. A27 in STRICT mode is the binding
|
||||
empirical gate for D-P2-02 (post DEC-011 Amendment 1). A28 pins the canonical 5-entry zip layout.
|
||||
After A28, Phase 2 functional contract is HARNESS-CLOSED (analogous to Plan 01-13's role for Phase 1).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>HEADLESS=1 npm run test:uat 2>&1 | grep -E "(A26|A27|FAIL|PASS|28/28)" | tail -30</automated>
|
||||
<automated>HEADLESS=1 npm run test:uat 2>&1 | grep -E "(A26|A27|A28|FAIL|PASS|29/29)" | tail -30</automated>
|
||||
</verify>
|
||||
<done>
|
||||
A26 + A27 page-side + drivers wired. Orchestrator runs 28 assertions sequentially. ALL 28 GREEN.
|
||||
A26 + A27 (strict) + A28 page-side + drivers wired. Orchestrator runs 29 assertions sequentially.
|
||||
ALL 29 GREEN. A27 length>=2 strict check passes (both example.com + iana.org appear in meta.urls
|
||||
per DEC-011 Amendment 1). A28 zip-layout 5-entry strict check passes (REQ-archive-layout).
|
||||
FORBIDDEN_HOOK_STRINGS unchanged at 12. Atomic commit:
|
||||
`feat(02-04): harness A26+A27 — empirical meta.json 8-field + multi-tab urls[] verification (D-P2-02/03)`.
|
||||
`feat(02-04): harness A26+A27(strict)+A28 — empirical meta.json 8-field + multi-tab urls[] STRICT + REQ-archive-layout (D-P2-02/03 + DEC-011 Amendment 1)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -609,12 +800,13 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
└── meta.json
|
||||
```
|
||||
|
||||
Step 2.6 — Open meta.json. Verify:
|
||||
Step 2.6 — Open meta.json. Verify (STRICT, post DEC-011 Amendment 1):
|
||||
(a) `schemaVersion: "2"` present
|
||||
(b) `urls` field is an ARRAY (not a string)
|
||||
(c) `urls` contains at least `https://example.com/` AND `https://www.iana.org/`
|
||||
(c) `urls` contains BOTH `https://example.com/` AND `https://www.iana.org/` (length >= 2 REQUIRED)
|
||||
(d) NO `url` field present (just `urls`)
|
||||
(e) Exactly 8 keys total
|
||||
(f) NO `chrome-extension://...` URLs in `urls` (F2 — empty-tracker fallback removed)
|
||||
|
||||
Step 2.7 — Open video/last_30sec.webm in a browser (drag into a Chrome tab).
|
||||
Expected: ~30 seconds of video plays end-to-end.
|
||||
@@ -652,7 +844,7 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
Phase 2 implementation complete:
|
||||
- Plan 02-02: Offscreen-minted Blob URL pipeline (D-P2-01, closes P0-6).
|
||||
- Plan 02-03: meta.json urls[] + schemaVersion + tab-url-tracker (D-P2-02, closes P1 #10).
|
||||
- Plan 02-04 Tasks 1-3: UAT harness A24+A25+A26+A27 GREEN (28/28).
|
||||
- Plan 02-04 Tasks 1-3: UAT harness A24+A25+A26+A27(STRICT)+A28 GREEN (29/29). A27 strict-mode unblocked by DEC-011 Amendment 1; A28 pins REQ-archive-layout 5-entry zip.
|
||||
Acceptance baselines preserved: vitest GREEN, UAT GREEN, Tier-1 FORBIDDEN_HOOK_STRINGS = 12,
|
||||
production bundle hook-free.
|
||||
</what-built>
|
||||
@@ -681,7 +873,7 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
| T-02-04-02 | Repudiation | A25 latency measurement skewed by harness startup overhead | accept | Measurement bookends bracket ONLY the SAVE_ARCHIVE dispatch → ack, NOT the broader test orchestration. setupFreshRecording + segment-settle happen BEFORE the t0 mark. |
|
||||
| T-02-04-03 | Information Disclosure | A27 creates real tabs with example.com / iana.org which appear in real Downloads zips | accept | Per "log is internal" charter (CONTEXT.md re-phasing context); test tabs are public sites with no PII; downloadsDir is a per-run mkdtempSync, cleaned up by test runner. |
|
||||
| T-02-04-04 | Denial of Service | A27 leaks tabs if cleanup fails (chrome.tabs.remove throws on already-closed tabs) | mitigate | A27's cleanup is wrapped in try/catch (silent-ignore — closing an already-closed tab is benign). |
|
||||
| T-02-04-05 | Elevation of Privilege | New harness assertions reveal previously-uncovered chrome.* APIs to the test surface | accept | Approach B (Plan 01-13 SUMMARY): the harness page is a privileged extension-internal page that already has full chrome.* access. A24+A27 use chrome.downloads + chrome.tabs which are part of the existing extension capability set per manifest.json. No new manifest permissions in this plan. |
|
||||
| T-02-04-05 | Elevation of Privilege | New harness assertions reveal previously-uncovered chrome.* APIs to the test surface | accept | Approach B (Plan 01-13 SUMMARY): the harness page is a privileged extension-internal page that already has full chrome.* access. A24+A27 use chrome.downloads + chrome.tabs which are part of the extension capability set per manifest.json. NOTE: Plan 02-03 (preceding wave) added `tabs` permission via DEC-011 Amendment 1; A27 in STRICT mode consumes that capability. The amendment is the LOCKED scope addition; no further manifest deltas in this plan. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -696,26 +888,30 @@ tests/uat/lib/harness-page-driver.ts:driveA5 (line 215) for the merged-checks pa
|
||||
- `tests/build/fonts-present.test.ts` → GREEN.
|
||||
- `tests/i18n/manifest-i18n.test.ts` + `tests/i18n/locale-parity.test.ts` → GREEN.
|
||||
- `npm test` (full suite) → GREEN.
|
||||
- `HEADLESS=1 npm run test:uat` → 28/28 GREEN.
|
||||
- `HEADLESS=1 npm run test:uat` → 29/29 GREEN (A0-A23 + A24+A25+A26+A27 strict + A28).
|
||||
- Operator empirical Task 4 Step 2 → "approved" or surfaces deviations to drive Plan 02-05 follow-up.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. UAT harness 28/28 GREEN (A0-A23 Phase 1 baseline + A24+A25+A26+A27 Phase 2 extension).
|
||||
2. Vitest full suite GREEN (Phase 1 baseline + Plan 02-01 RED tests now all GREEN post-Plans 02-02 + 02-03).
|
||||
3. Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (unchanged — A24+A27 use chrome.* monkey-patch + production APIs).
|
||||
4. Pre-checkpoint bundle gates PASS per `feedback-pre-checkpoint-bundle-gates.md`.
|
||||
5. Operator empirical UAT cycle 1 ack "approved" OR documented deviations.
|
||||
6. Phase 2 closure marker flippable: REQUIREMENTS.md + STATE.md + ROADMAP.md (this last is orchestrator's territory post-checkpoint).
|
||||
1. UAT harness 29/29 GREEN (A0-A23 Phase 1 baseline + A24+A25+A26+A27 strict + A28 Phase 2 extension).
|
||||
2. A27 STRICT mode: meta.urls contains both example.com AND iana.org (length >= 2 REQUIRED); no extension-origin sentinels (F2).
|
||||
3. A28 STRICT mode: zip contains exactly the 5 canonical entries; no extras (REQ-archive-layout).
|
||||
4. Vitest full suite GREEN (Phase 1 baseline + Plan 02-01 RED tests now all GREEN post-Plans 02-02 + 02-03 + Plan 02-03 Task 0 manifest i18n test additions).
|
||||
5. Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (unchanged — A24+A27+A28 use chrome.* monkey-patch + production APIs).
|
||||
6. Pre-checkpoint bundle gates PASS per `feedback-pre-checkpoint-bundle-gates.md`.
|
||||
7. Operator empirical UAT cycle 1 ack "approved" OR documented deviations.
|
||||
8. Phase 2 closure marker flippable: REQUIREMENTS.md + STATE.md + ROADMAP.md (this last is orchestrator's territory post-checkpoint).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md`
|
||||
documenting:
|
||||
- 4 new harness assertions (A24+A25+A26+A27) with their check-counts and rationale.
|
||||
- 5 new harness assertions (A24+A25+A26+A27 strict + A28) with their check-counts and rationale.
|
||||
- A27 strict-mode rationale (DEC-011 Amendment 1 unblocks both-URLs-required contract).
|
||||
- A28 REQ-archive-layout strict zip-layout pin (5 entries; cross-references REQ-popup-ui + REQ-screenshot-on-export).
|
||||
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 (architectural rationale: chrome.* spy / production APIs vs new test-hook symbols).
|
||||
- Pre-checkpoint bundle gate run record (each gate result inline).
|
||||
- Operator empirical ack quote (verbatim) OR list of deviations + follow-up plan-pointer.
|
||||
- Phase 2 closure summary: 4/4 plans landed; vitest + UAT GREEN; P0-6 + P1 #10 closed; meta.json 8-field schema shipped.
|
||||
- Phase 2 closure summary: 4/4 plans landed; vitest + UAT GREEN; P0-6 + P1 #10 closed; meta.json 8-field schema shipped; DEC-011 Amendment 1 landed.
|
||||
- Forward link: Phase 3 (SPEC §10 smoke + DOM/event-log verification) inherits the harness as its closure template (mirrors Plan 01-13's role for Phase 2).
|
||||
</output>
|
||||
|
||||
@@ -136,7 +136,42 @@ None — discussion stayed within phase scope.
|
||||
|
||||
</deferred>
|
||||
|
||||
## Revision Log
|
||||
|
||||
### 2026-05-20 — DEC-011 Amendment 1 + plan-checker iteration 1
|
||||
|
||||
- **DEC-011 Amendment 1 (Phase 2 scope addition):** `tabs` permission ADDED to manifest.json
|
||||
per user direction during plan-checker iteration 1. Justification: Phase 2's D-P2-02
|
||||
meta.urls feature REQUIRES tab URL visibility beyond active-tab semantics. Audit T-1-02
|
||||
("declaring unused permissions expands attack surface") is acknowledged but overridden for
|
||||
this Phase 2 feature; the meta.urls feature is now genuinely USED, so the permission is not
|
||||
unused. tests/i18n/manifest-i18n.test.ts pins the new 8-entry permission set as a
|
||||
regression guard. PROJECT.md DEC-011 row rewritten with Amendment 1 prose.
|
||||
- **Plan 02-04 A27 strict-mode:** harness MUST observe BOTH tab URLs in meta.urls after a
|
||||
multi-tab session. meta.urls.length >= 2 REQUIRED; test FAILS on length < 2. No
|
||||
extension-origin sentinels permitted. Empty-tracker case (no browser interaction during
|
||||
recording) still produces urls:[] per F2 resolution — but A27 explicitly EXERCISES the
|
||||
multi-tab path so its meta.urls is NEVER empty.
|
||||
- **createArchive empty-tracker fallback removed (F2):** tracker.getTabUrlsSeen() returning
|
||||
empty array is meaningful (whole-desktop-no-tab session) and meta.urls: [] is the canonical
|
||||
representation. tests/background/meta-json-urls-schema.test.ts adds Test 5 pinning this
|
||||
contract; tests/build/strict-meta-json-validation.test.ts Test 3 relaxed to PERMIT empty
|
||||
urls[] (still validates URL format on non-empty arrays). createArchive calls a new
|
||||
snapshotOpenTabs() helper (chrome.tabs.query({}) defensive enumeration via DEC-011
|
||||
Amendment 1) BEFORE reading getTabUrlsSeen() so any tab the operator opened but never
|
||||
activated is still captured. Empty array IS the result when no tabs are open at SAVE time.
|
||||
- **Plan 02-04 A28 added (F1):** REQ-archive-layout strict zip-layout pin. Harness driver
|
||||
enumerates zip entries and asserts EXACTLY 5 paths (`video/last_30sec.webm`,
|
||||
`rrweb/session.json`, `logs/events.json`, `screenshot.png`, `meta.json`). Cross-references
|
||||
REQ-archive-layout + REQ-popup-ui + REQ-screenshot-on-export. UAT target: 28→29 GREEN.
|
||||
- **Plan 02-02 Task 3 F3 resolved:** OR-deliberation in createArchive prose struck. The
|
||||
resolved-only contract is: throw typed `blob-url-mint-failed` error on empty/timeout
|
||||
response from offscreen; NO data: URL fallback for any archive size.
|
||||
- **Plan 02-02 Task 3 F4 resolved:** verify block extended with full-suite `npm test` after
|
||||
the focused-test runs so unrelated regressions surface during execution.
|
||||
|
||||
---
|
||||
|
||||
*Phase: 2-stabilize-export-pipeline*
|
||||
*Context gathered: 2026-05-20*
|
||||
*Revised: 2026-05-20 (plan-checker iteration 1)*
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"permissions": [
|
||||
"desktopCapture",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"downloads",
|
||||
"scripting",
|
||||
"storage",
|
||||
@@ -47,4 +48,4 @@
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,17 @@
|
||||
// after Wave-3 Task 1 landed the manifest + messages.json files. This
|
||||
// file now pins the post-D-07 state as a regression guard.
|
||||
//
|
||||
// 2026-05-20 Plan 02-03 amendment (DEC-011 Amendment 1): manifest.json
|
||||
// permissions array extended to include `"tabs"` per Phase 2 D-P2-02
|
||||
// (meta.urls multi-tab visibility). The permission-set describe block
|
||||
// below pins the post-amendment 8-entry set as a regression guard.
|
||||
//
|
||||
// References:
|
||||
// - RESEARCH §10 + §11 (Chrome i18n schema; __MSG_* placeholder rules)
|
||||
// - brand-decisions-v1.md D-07 override (`Mokosh — Session Capture`)
|
||||
// - brand-decisions-v1.md D-08 tagline (`Thirty seconds ago, always at hand.`)
|
||||
// - .planning/PROJECT.md DEC-011 Amendment 1 (`tabs` permission, 2026-05-20)
|
||||
// - .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md D-P2-02
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
@@ -89,3 +96,35 @@ describe('Plan 01-12: _locales/ru/messages.json (primary operator locale)', () =
|
||||
expect(ru.extDesc?.message).toBe('Тридцать секунд назад, всегда под рукой.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan 02-03 DEC-011 Amendment 1: manifest.json permissions include "tabs" (D-P2-02)', () => {
|
||||
// Locked set per DEC-011 Amendment 1 (2026-05-20). `tabs` added to enable
|
||||
// chrome.tabs.get(tabId).url + chrome.tabs.query for the tab-url-tracker
|
||||
// (D-P2-02 meta.urls feature). Any future permission delta requires a
|
||||
// formal DEC-011 amendment update — DO NOT edit this list without one.
|
||||
const EXPECTED_PERMISSIONS: ReadonlyArray<string> = [
|
||||
'desktopCapture',
|
||||
'activeTab',
|
||||
'tabs',
|
||||
'downloads',
|
||||
'scripting',
|
||||
'storage',
|
||||
'offscreen',
|
||||
'notifications',
|
||||
];
|
||||
|
||||
it('manifest.json:permissions includes "tabs" (DEC-011 Amendment 1)', () => {
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
|
||||
expect(Array.isArray(manifest.permissions)).toBe(true);
|
||||
expect(manifest.permissions).toContain('tabs');
|
||||
});
|
||||
|
||||
it('manifest.json:permissions has exactly the 8 entries in DEC-011 Amendment 1', () => {
|
||||
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
|
||||
expect(Array.isArray(manifest.permissions)).toBe(true);
|
||||
const actual = [...manifest.permissions].sort();
|
||||
const expected = [...EXPECTED_PERMISSIONS].sort();
|
||||
expect(actual).toEqual(expected);
|
||||
expect(manifest.permissions.length).toBe(EXPECTED_PERMISSIONS.length);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user