Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -104,11 +104,13 @@ Requirements for the Phase 1 SPEC. Each maps to one phase in ROADMAP.md.
|
|||||||
- SPEC §10 acceptance criteria: #7.
|
- SPEC §10 acceptance criteria: #7.
|
||||||
|
|
||||||
- [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the
|
- [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the
|
||||||
verbatim schema:
|
verbatim schema (D-P2-02 + D-P2-03 cutover; replaces the 7-field
|
||||||
|
`url: string` shape per audit P1 #10 amendment 2026-05-20):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"schemaVersion": "2",
|
||||||
"timestamp": "2025-05-15T14:32:10Z",
|
"timestamp": "2025-05-15T14:32:10Z",
|
||||||
"url": "https://...",
|
"urls": ["https://example.com/", "https://app.example.com/dashboard"],
|
||||||
"userAgent": "Chrome/...",
|
"userAgent": "Chrome/...",
|
||||||
"extensionVersion": "1.0.0",
|
"extensionVersion": "1.0.0",
|
||||||
"videoBufferSeconds": 30,
|
"videoBufferSeconds": 30,
|
||||||
@@ -116,7 +118,23 @@ Requirements for the Phase 1 SPEC. Each maps to one phase in ROADMAP.md.
|
|||||||
"totalEvents": 143
|
"totalEvents": 143
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
All fields required. Binding: CON-meta-json-schema.
|
All 8 fields required. Acceptance:
|
||||||
|
- `schemaVersion === '2'` (marks the D-P2-02 url→urls cutover; future
|
||||||
|
schema bumps increment)
|
||||||
|
- `timestamp` ISO-8601 with `Z` suffix
|
||||||
|
- `urls` is a `string[]` whose entries each match
|
||||||
|
`/^(https?|chrome-extension):\/\//` (per CONTEXT.md `<specifics>`
|
||||||
|
filter rules — exclude chrome://, about:, devtools://, file://). Empty
|
||||||
|
array IS permitted per F2 (whole-desktop-no-tab session is a
|
||||||
|
meaningful operator state); non-empty arrays validate each entry.
|
||||||
|
- `urls` is deduplicated; ordering is first-seen-first across the
|
||||||
|
rolling recording window
|
||||||
|
- `extensionVersion` matches semver
|
||||||
|
- `totalEvents` is a non-negative integer
|
||||||
|
- exactly 8 keys; no extras
|
||||||
|
Binding: CON-meta-json-schema (this REQ-text supersedes the original
|
||||||
|
CON-meta-json-schema 7-field shape — the original is preserved in the
|
||||||
|
SPEC for provenance; this REQ documents the Phase 2 cutover).
|
||||||
|
|
||||||
### Manifest & Install
|
### Manifest & Install
|
||||||
|
|
||||||
@@ -222,7 +240,7 @@ Which phase covers which requirement. See ROADMAP.md for phase details.
|
|||||||
| REQ-popup-ui | Phase 3 (originally) → **Phase 2** (renumbered) | Pending (largely shipped via Plan 01-09 SAVE-only popup + Plan 01-12 i18n; residual gaps in Phase 2) |
|
| REQ-popup-ui | Phase 3 (originally) → **Phase 2** (renumbered) | Pending (largely shipped via Plan 01-09 SAVE-only popup + Plan 01-12 i18n; residual gaps in Phase 2) |
|
||||||
| REQ-screenshot-on-export | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |
|
| REQ-screenshot-on-export | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |
|
||||||
| REQ-archive-layout | Phase 3 (originally) → **Phase 2** (renumbered) | Pending (substantively shipped via Plans 01-08 webm-remux + JSZip; verification in Phase 2) |
|
| REQ-archive-layout | Phase 3 (originally) → **Phase 2** (renumbered) | Pending (substantively shipped via Plans 01-08 webm-remux + JSZip; verification in Phase 2) |
|
||||||
| REQ-meta-json-schema | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |
|
| REQ-meta-json-schema | Phase 2 | Pending (implementation landed via Plan 02-03; harness validation deferred to Plan 02-04) |
|
||||||
| REQ-archive-export-latency | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |
|
| REQ-archive-export-latency | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |
|
||||||
| REQ-manifest-permissions | Phase 3 (originally) → Phase 1 closure via Plan 01-12 i18n migration | Complete (2026-05-20 — manifest __MSG_*__ + default_locale='en' + 16 i18n keys per locale; permissions DEC-011 baseline unchanged; operator brand-fit ack) |
|
| REQ-manifest-permissions | Phase 3 (originally) → Phase 1 closure via Plan 01-12 i18n migration | Complete (2026-05-20 — manifest __MSG_*__ + default_locale='en' + 16 i18n keys per locale; permissions DEC-011 baseline unchanged; operator brand-fit ack) |
|
||||||
| REQ-install-clean | Phase 4 (originally) → Phase 1 closure via Plan 01-12 design integration | Complete (2026-05-20 — fresh build + load unpacked clean; zero remote-font CSP errors; branded icons rendering; en+ru manifest:name resolution; operator brand-fit ack) |
|
| REQ-install-clean | Phase 4 (originally) → Phase 1 closure via Plan 01-12 design integration | Complete (2026-05-20 — fresh build + load unpacked clean; zero remote-font CSP errors; branded icons rendering; en+ru manifest:name resolution; operator brand-fit ack) |
|
||||||
@@ -245,6 +263,7 @@ RAM-ceiling check.
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Requirements defined: 2026-05-15*
|
*Requirements defined: 2026-05-15*
|
||||||
*Last updated: 2026-05-20 — Plan 01-10 closure (welcome tab; first-install activation; canonical mark + canonical tokens + canonical chrome.i18n welcomeHero; 24/24 UAT GREEN; operator cycle-2 ack "All good"). Plan 01-10 introduced no new functional REQs; it consumed REQ-video-ring-buffer (already Complete via Plan 01-07) by adding the first-install operator-facing activation surface that complements the always-on capture pipeline. Phase 1 final functional plan delivered; final-closure marker flip pending (REQUIREMENTS / ROADMAP / STATE markers + optional /gsd-verify-work 1).*
|
*Updated 2026-05-20 — REQ-meta-json-schema amended for Plan 02-03 (D-P2-02 + D-P2-03 8-field cutover: `url: string` → `urls: string[]`; new `schemaVersion: "2"` field; F2 empty-array permission). Traceability table entry flipped to "Pending (implementation landed via Plan 02-03; harness validation deferred to Plan 02-04)".*
|
||||||
|
*Earlier update: 2026-05-20 — Plan 01-10 closure (welcome tab; first-install activation; canonical mark + canonical tokens + canonical chrome.i18n welcomeHero; 24/24 UAT GREEN; operator cycle-2 ack "All good"). Plan 01-10 introduced no new functional REQs; it consumed REQ-video-ring-buffer (already Complete via Plan 01-07) by adding the first-install operator-facing activation surface that complements the always-on capture pipeline. Phase 1 final functional plan delivered; final-closure marker flip pending (REQUIREMENTS / ROADMAP / STATE markers + optional /gsd-verify-work 1).*
|
||||||
*Earlier update: 2026-05-20 — REQ-install-clean + REQ-manifest-permissions marked Complete on Plan 01-12 closure (design integration + manifest i18n + operator brand-fit ack)*
|
*Earlier update: 2026-05-20 — REQ-install-clean + REQ-manifest-permissions marked Complete on Plan 01-12 closure (design integration + manifest i18n + operator brand-fit ack)*
|
||||||
*Earlier update: 2026-05-15 — REQ-video-ring-buffer marked Complete on Phase 1 (Plan 01-07) closure*
|
*Earlier update: 2026-05-15 — REQ-video-ring-buffer marked Complete on Phase 1 (Plan 01-07) closure*
|
||||||
|
|||||||
264
.planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md
Normal file
264
.planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
---
|
||||||
|
phase: 02-stabilize-export-pipeline
|
||||||
|
plan: 03
|
||||||
|
subsystem: export-pipeline
|
||||||
|
tags:
|
||||||
|
- meta-json-urls-array
|
||||||
|
- tab-url-tracking
|
||||||
|
- schema-amendment
|
||||||
|
- p1-10-fix
|
||||||
|
- d-p2-02
|
||||||
|
- d-p2-03
|
||||||
|
- dec-011-amendment-1
|
||||||
|
- wave-2-green
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 02-stabilize-export-pipeline
|
||||||
|
provides: "Plan 02-01 RED gate (tests/background/meta-json-urls-schema.test.ts pinning SessionMetadata urls field + createArchive meta.urls + tab-url-tracker module seam + F2 empty-tracker contract); tests/build/strict-meta-json-validation.test.ts pinning 8-field exact schema with EXPECTED_KEYS[schemaVersion]. Plan 02-02 Plan 02-02 Blob URL pipeline + extended test helpers for CREATE_DOWNLOAD_URL round-trip simulation (capturedArchiveBytes pattern)."
|
||||||
|
- phase: 01-stabilize-video-pipeline
|
||||||
|
provides: "Logger + chrome.* listener defensive-try/catch pattern (src/background/index.ts:bootstrap)"
|
||||||
|
provides:
|
||||||
|
- "tab-url-tracker module (src/background/tab-url-tracker.ts) — initTabUrlTracker + getTabUrlsSeen + snapshotOpenTabs + clearTabUrlsSeen. Filters via positive-allow regex; deduplicates via Set; preserves first-seen ordering via array. 246 LOC incl. docstrings."
|
||||||
|
- "SessionMetadata 8-field schema (replaces 7-field shape): schemaVersion + timestamp + urls + userAgent + extensionVersion + videoBufferSeconds + logDurationMinutes + totalEvents. JSON.stringify emits in insertion order per ECMA-262."
|
||||||
|
- "meta.urls assembled in createArchive: snapshotOpenTabs() before getTabUrlsSeen() (DEC-011 Amendment 1 capability — captures tabs opened but never activated). Empty array IS the canonical representation of whole-desktop-no-tab sessions (F2)."
|
||||||
|
- "Always-on charter preserved: createArchive does NOT call clearTabUrlsSeen — tracker accumulates across saves (Plan 01-09 Amendment 3 invariant)."
|
||||||
|
- "REQ-meta-json-schema text amended to the new 8-field shape with breaking-change cutover documented; traceability table updated."
|
||||||
|
affects:
|
||||||
|
- "Plan 02-04 (Wave 3 UAT harness extensions — A27 multi-tab strict-mode assertion now unblocked; A28 zip-layout pin orthogonal)"
|
||||||
|
- "Any downstream meta.json consumer (UAT harness, future v2 SRV-* uploads, operator post-mortem tooling) — schema cutover surfaces here, ratified by tests/build/strict-meta-json-validation.test.ts EXPECTED_KEYS"
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "Tier-1-gate-preserving test wiring: when an implementation module wants to expose ergonomic `_resetForTesting` / `_observeForTesting` hooks, the alternative is to drive the registered chrome.* callbacks directly via the test stub's `_callbacks` array. The tracker's `initTabUrlTracker()` registers its listeners on the stub; the test then calls `stub.tabs.onUpdated._callbacks.forEach(cb => cb(...))` to seed observations. Preserves the FORBIDDEN_HOOK_STRINGS inventory at its current size; module state resets via vitest's vi.resetModules() in beforeEach."
|
||||||
|
- "Positive-allow URL filter regex: `/^(https?|chrome-extension):\\/\\//` covers the entire CONTEXT.md `<specifics>` allow-list in one anchor. Replaces the alternative long-form switch over schemes."
|
||||||
|
- "Defensive snapshot-at-SAVE pattern: chrome.tabs.query({}) inside snapshotOpenTabs() folds every currently-open tab.url into the tracker's Set + firstSeenOrder. Dedup gates the array push; tabs already observed via onActivated/onUpdated stay where they are. Captures the operator-opened-but-never-activated tab gap that DEC-011 Amendment 1 enables."
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- "src/background/tab-url-tracker.ts (246 LOC; 4 exports: initTabUrlTracker, getTabUrlsSeen, snapshotOpenTabs, clearTabUrlsSeen)"
|
||||||
|
- ".planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md (this file)"
|
||||||
|
modified:
|
||||||
|
- "src/shared/types.ts (SessionMetadata 7→8 fields: url removed; urls + schemaVersion added; docstring cites D-P2-02 + D-P2-03)"
|
||||||
|
- "src/background/index.ts (import tab-url-tracker; register initTabUrlTracker at module top-level alongside chrome.downloads.onChanged; createArchive metadata block rewritten — 8 fields, snapshotOpenTabs+getTabUrlsSeen, F2 empty-array emission with diagnostic logger.warn)"
|
||||||
|
- "tests/background/meta-json-urls-schema.test.ts (Tests 3+4 rewired to drive chrome.tabs.onUpdated callbacks directly via the chrome stub's _callbacks array; Test 5 simplified — fresh-module-import-on-vi.resetModules gives the empty representation. Rule 3 deviation: preserves Tier-1 gate at 13 entries.)"
|
||||||
|
- ".planning/REQUIREMENTS.md (REQ-meta-json-schema amended for 8-field shape + F2 empty-array permission; traceability table updated; footer line dated 2026-05-20)"
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Tracker module exposes NO `_resetForTesting` / `_observeForTesting` hooks. Reason: vitest's __MOKOSH_UAT__: 'false' define would tree-shake gated test hooks the same way vite.config.ts does, so the conventional `if (__MOKOSH_UAT__)` pattern cannot work for unit tests. Exposing ungated hooks would leak names into the production bundle and grow the Tier-1 FORBIDDEN_HOOK_STRINGS inventory from 13 → 15. The chosen path (test code drives chrome.tabs.onUpdated callbacks directly via the stub's _callbacks array) preserves the 13-entry gate without sacrificing test ergonomics. Plan 02-01 SUMMARY explicitly anticipated this fallback."
|
||||||
|
- "URL filter via positive-allow regex (^(https?|chrome-extension):\\/\\/) rather than switch-over-schemes. Single canonical anchor; easy to grep; matches the EXPECTED regex used by tests/build/strict-meta-json-validation.test.ts URL_SCHEME_ALLOW pin."
|
||||||
|
- "snapshotOpenTabs as a separate async export rather than an inline chrome.tabs.query inside createArchive. Reason: keeps the tracker module self-contained (filter + dedup logic stays in one place); createArchive becomes a pure consumer (await snapshotOpenTabs → getTabUrlsSeen → done); test paths for snapshotOpenTabs become first-class (Plan 02-04 may pin this directly)."
|
||||||
|
- "8th field name `schemaVersion` ratified per Plan 02-01 Task 3 planner pick. Value '2' marks the D-P2-02 url→urls cutover. tests/build/strict-meta-json-validation.test.ts EXPECTED_KEYS held this string since Plan 02-01; no lockstep change needed."
|
||||||
|
- "Tracker initialization registered at module top-level alongside the other chrome.* listeners (chrome.downloads.onChanged + chrome.action.onClicked + chrome.runtime.onStartup + chrome.notifications.onClicked). Reason: matches the canonical listener-registration pattern; defensive try/catch wraps the call; idempotency handled inside the tracker module via the `initialized` flag (so the SW-respawn path that calls initialize() at runtime can't double-register)."
|
||||||
|
- "createArchive does NOT call clearTabUrlsSeen. Always-on charter preserved per Plan 01-09 Amendment 3: SAVE creates a zip with the CURRENT tracker state; tracker continues accumulating across saves. The next save captures any tabs the operator activates AFTER the prior save."
|
||||||
|
- "F2 empty-array emission: createArchive logs a diagnostic warn (`tabUrlsSeen is empty after snapshotOpenTabs — emitting meta.urls=[]`) but still emits the empty array. Whole-desktop-no-tab sessions are meaningful operator state; no fake extension-origin URL is inserted."
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Tier-1-gate-preserving test wiring (see tech-stack.patterns entry above): when the natural test-ergonomic hook would leak into production, drive the underlying chrome.* listeners directly. Generalizes beyond tab-url-tracker — any future module that registers chrome.* listeners can be tested this way."
|
||||||
|
- "Two-step SAVE-time URL capture: passive accumulation via chrome.tabs.onActivated + chrome.tabs.onUpdated listeners (always-on) PLUS active snapshot via chrome.tabs.query({}) at SAVE time (defensive fallback for never-activated tabs). Dedup + ordering invariants gate the combination; the active snapshot is purely additive."
|
||||||
|
|
||||||
|
requirements-completed: []
|
||||||
|
# Per plan-checker output + execution context: REQ-meta-json-schema
|
||||||
|
# implementation lands here, but the requirement is marked Complete by
|
||||||
|
# Plan 02-04 after the UAT harness empirically validates the 8-field
|
||||||
|
# shape end-to-end. The traceability table entry was updated to
|
||||||
|
# "Pending (implementation landed via Plan 02-03; harness validation
|
||||||
|
# deferred to Plan 02-04)" — explicit forward link.
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 7min
|
||||||
|
completed: 2026-05-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 2 Plan 03: meta.json urls[] + schemaVersion (D-P2-02 + D-P2-03)
|
||||||
|
|
||||||
|
**Replaces SessionMetadata.url:string with SessionMetadata.urls:string[]; adds schemaVersion as the 8th field; introduces src/background/tab-url-tracker.ts (4 exports) fed by chrome.tabs.onActivated + onUpdated + a SAVE-time chrome.tabs.query({}) snapshot.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~7 min
|
||||||
|
- **Started:** 2026-05-20T14:02:58Z
|
||||||
|
- **Completed:** 2026-05-20T14:09:52Z
|
||||||
|
- **Tasks:** 4/4 (Task 0 verification + 3 implementation)
|
||||||
|
- **Files created:** 2 (src/background/tab-url-tracker.ts + this SUMMARY)
|
||||||
|
- **Files modified:** 4 (src/shared/types.ts + src/background/index.ts + tests/background/meta-json-urls-schema.test.ts + .planning/REQUIREMENTS.md)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- **Audit P1 #10 closed.** meta.json now captures the operator's multi-tab context across the rolling 30 s recording window — not just the active-at-save tab. `meta.urls` is a deduplicated, first-seen-ordered, filtered Array; F2 empty-array contract preserved for whole-desktop-no-tab sessions.
|
||||||
|
- **8 Plan 02-01 RED tests flipped GREEN** across two files:
|
||||||
|
- tests/background/meta-json-urls-schema.test.ts: Tests 1+2+3+4+5 (SessionMetadata source-text pin; createArchive meta.urls shape; tracker dedup/order; tracker URL filter; tracker empty-state F2 contract).
|
||||||
|
- tests/build/strict-meta-json-validation.test.ts: Tests 1+3+8 (exact 8-field count; urls Array of filter-matching URLs with empty permitted; no extra fields beyond EXPECTED_KEYS).
|
||||||
|
- **D-P2-02 wire contract landed:** SessionMetadata interface (src/shared/types.ts) reshaped from 7 → 8 fields with a docstring citing D-P2-02 + D-P2-03 + Plan 02-01 Task 3 + F2.
|
||||||
|
- **D-P2-03 strict 8-field shape pinned:** createArchive emits schemaVersion: '2' + urls (Array, possibly empty) + the 6 unchanged fields in source-declaration order; JSON.stringify preserves insertion order per ECMA-262.
|
||||||
|
- **DEC-011 Amendment 1 verified in place:** manifest.json includes `"tabs"` (already landed in the plan-checker iteration-1 revision pass via commits `9dcfcf0` + `df8c086`); PROJECT.md DEC-011 row carries Amendment 1 prose; tests/i18n/manifest-i18n.test.ts permission-set describe block 12/12 GREEN.
|
||||||
|
- **Tier-1 FORBIDDEN_HOOK_STRINGS gate preserved at 13 entries.** The tracker module does NOT export `_resetForTesting` / `_observeForTesting` ergonomic hooks; tests drive chrome.tabs.onUpdated callbacks directly. Production bundle scan: zero matches for any test-hook string.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically (--no-verify per parallel-executor mandate):
|
||||||
|
|
||||||
|
1. **Task 0: DEC-011 Amendment 1 verification** — NO-OP this executor; state landed at base commit `d3aa567` from plan-checker iteration-1 revision pass (commits `9dcfcf0` + `df8c086`).
|
||||||
|
2. **Task 1: src/background/tab-url-tracker.ts module + test wiring** — `7beb690` (feat).
|
||||||
|
3. **Task 2: SessionMetadata + createArchive + initialize wiring** — `78031e7` (feat).
|
||||||
|
4. **Task 3: REQUIREMENTS.md REQ-meta-json-schema amendment** — `af03556` (docs).
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### Production source
|
||||||
|
|
||||||
|
- **`src/background/tab-url-tracker.ts`** (new; 246 LOC incl. docstrings)
|
||||||
|
- 4 exports: `initTabUrlTracker(): void`, `getTabUrlsSeen(): string[]`, `snapshotOpenTabs(): Promise<void>`, `clearTabUrlsSeen(): void`.
|
||||||
|
- Module state: `tabUrlsSeen: Set<string>` (O(1) dedup) + `firstSeenOrder: string[]` (append-only order) + `initialized: boolean` (double-registration guard).
|
||||||
|
- URL filter: positive-allow regex `^(https?|chrome-extension):\/\//`.
|
||||||
|
- `initTabUrlTracker` registers chrome.tabs.onActivated + chrome.tabs.onUpdated listeners with defensive try/catch. onActivated uses Promise.resolve over chrome.tabs.get(tabId) for MV3-style async + older-stub compat. onUpdated prefers changeInfo.url, falls back to tab.url; NOT gated on status==='complete' (captures SPA-style routing).
|
||||||
|
- `snapshotOpenTabs` invokes chrome.tabs.query({}); each tab.url that passes the filter is folded into the tracker via the same addUrl gate (idempotent — already-observed tabs stay where they are).
|
||||||
|
- `getTabUrlsSeen` returns a `.slice()` so callers cannot mutate internal state.
|
||||||
|
- `clearTabUrlsSeen` empties both state vars (NOT called by createArchive; reserved for future session-reset).
|
||||||
|
|
||||||
|
- **`src/shared/types.ts`** (+30 lines net incl. docstring)
|
||||||
|
- `SessionMetadata` interface: 7 fields → 8 fields. `url: string` REMOVED; `urls: string[]` + `schemaVersion: string` ADDED. `schemaVersion` is the first field (source-declaration order = JSON emission order per ECMA-262).
|
||||||
|
- Docstring cites D-P2-02 + D-P2-03 + Plan 02-01 Task 3 + F2 empty-array permission.
|
||||||
|
|
||||||
|
- **`src/background/index.ts`** (+~55 lines net)
|
||||||
|
- Import `{ initTabUrlTracker, snapshotOpenTabs, getTabUrlsSeen }` from `./tab-url-tracker` (top-level import block).
|
||||||
|
- `initTabUrlTracker()` registration at module top-level under defensive try/catch, alongside chrome.downloads.onChanged (Plan 02-02 precedent for D-P2-* feature registration).
|
||||||
|
- `createArchive`: metadata block rewritten. New flow:
|
||||||
|
1. `await snapshotOpenTabs()` inside its own try/catch (continue on failure with whatever state the tracker already had).
|
||||||
|
2. `const urls = getTabUrlsSeen()`.
|
||||||
|
3. If `urls.length === 0` → diagnostic `logger.warn` for F2 whole-desktop-no-tab sessions; emission proceeds verbatim.
|
||||||
|
4. Metadata literal: 8 fields in source order — `schemaVersion: '2'`, `timestamp`, `urls`, `userAgent`, `extensionVersion`, `videoBufferSeconds: 30`, `logDurationMinutes: 10`, `totalEvents`.
|
||||||
|
- No changes to PortMessageType variants from Plan 02-02 (CREATE_DOWNLOAD_URL / DOWNLOAD_URL / REVOKE_DOWNLOAD_URL stay intact).
|
||||||
|
- No changes to downloadArchive or chrome.downloads.onChanged from Plan 02-02.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- **`tests/background/meta-json-urls-schema.test.ts`** (Tests 3 + 4 + 5 rewired)
|
||||||
|
- **Test 3 (dedup/order):** Now imports tracker + calls `initTabUrlTracker()` on a chrome stub, then dispatches synthetic chrome.tabs.onUpdated events via `stub.tabs.onUpdated._callbacks.forEach(cb => cb(...))`. The first 'A' establishes order; 'B' appends; the repeated 'A' is deduplicated.
|
||||||
|
- **Test 4 (URL filter):** Same driver pattern; emits 4 events (https://, chrome://, about:, chrome-extension://) and asserts only https + chrome-extension:// pass through.
|
||||||
|
- **Test 5 (F2 empty-tracker):** Imports tracker on fresh module graph (vi.resetModules in beforeEach), fires no events, asserts `getTabUrlsSeen()` returns `[]`. No hook calls needed.
|
||||||
|
- The `_resetForTesting` / `_observeForTesting` skeletons + the `expect.fail` dynamic-import-missing-module RED paths are deleted (the module exists now; the RED gate is closed).
|
||||||
|
|
||||||
|
### Planning / Requirements
|
||||||
|
|
||||||
|
- **`.planning/REQUIREMENTS.md`** (REQ-meta-json-schema block + traceability table + footer)
|
||||||
|
- REQ block: 8-field schema verbatim with `schemaVersion: '2'`, ISO-8601 timestamp, `urls` filter regex + F2 empty-array permission, dedup + order rules, semver, non-negative integer, exactly-8-keys, binding note preserving original CON-meta-json-schema 7-field provenance.
|
||||||
|
- Traceability table: "Phase 3 (originally) → **Phase 2** (renumbered) | Pending" → "Phase 2 | Pending (implementation landed via Plan 02-03; harness validation deferred to Plan 02-04)".
|
||||||
|
- Footer line dated 2026-05-20 with the REQ-meta-json-schema amendment citation; prior Plan 01-10 closure entry preserved as "Earlier update".
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
See `key-decisions` in the frontmatter (7 decisions covering test-hook gate preservation, URL filter regex shape, snapshotOpenTabs as separate export, schemaVersion field-name ratification, top-level listener registration, no-clear-on-save always-on charter, F2 empty-array diagnostic warn).
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] tab-url-tracker test ergonomics required choice between two API shapes**
|
||||||
|
|
||||||
|
- **Found during:** Task 1 design (pre-implementation analysis of the Plan 02-01 RED test contract).
|
||||||
|
- **Issue:** Plan 02-01 RED Tests 3+4+5 in tests/background/meta-json-urls-schema.test.ts use optional `t._resetForTesting()` + `t._observeForTesting(url)` hooks via `typeof === 'function'` guards. If absent, Tests 3+4 wedge in their current text because:
|
||||||
|
* Test 3 expects `getTabUrlsSeen()` to equal `['https://a.example.com', 'https://b.example.com']` (with no observations seeded — i.e., the test's `if` branches are skipped — `getTabUrlsSeen()` returns `[]` → assertion fails).
|
||||||
|
* Test 4 expects `getTabUrlsSeen()` to equal `['https://example.com', 'chrome-extension://abc/popup.html']` (same failure mode).
|
||||||
|
* Test 5 (F2 empty case) is unaffected — already asserts `getTabUrlsSeen() === []`.
|
||||||
|
- **Architecture choices considered:**
|
||||||
|
* **(A) Expose `_resetForTesting` + `_observeForTesting` as ungated module exports.** Pros: minimal test-code change. Cons: leaks two new strings into the production bundle, growing the Tier-1 FORBIDDEN_HOOK_STRINGS inventory from 13 → 15. Also requires lockstep tests/uat/harness.test.ts edits (per the Plan 01-14 lockstep-change pattern). Plan execution_context addendum explicitly notes this option but suggests preserving 13 entries if possible.
|
||||||
|
* **(B) Expose hooks gated by `__MOKOSH_UAT__` Vite-define-token.** Pros: tree-shakes from production builds (same pattern as src/test-hooks/offscreen-hooks.ts). Cons: vitest.config.ts sets `__MOKOSH_UAT__: 'false'` for the unit-test run too (Plan 01-11 SUMMARY rationale: vitest defaults to MODE='test' but the unit suite uses vi.fn() stubs and would clobber). Gated hooks would tree-shake out of the unit-test runtime AS WELL AS production — so the tests would still fail. Verified by reading vitest.config.ts line 15.
|
||||||
|
* **(C) Drive chrome.tabs.onUpdated callbacks directly via the chrome stub's `_callbacks` array.** Pros: preserves Tier-1 gate at 13 entries; production bundle stays test-hook-clean; test code becomes more honest (exercises the SAME contract the production tracker listens for). Cons: each test must build a chrome stub + install on globalThis.chrome + import the tracker + call `initTabUrlTracker()` + fire synthetic events. ~6 extra lines per test.
|
||||||
|
- **Fix (Option C — Plan 02-01 SUMMARY-anticipated fallback):** Tests 3+4 in tests/background/meta-json-urls-schema.test.ts rewired to drive chrome.tabs.onUpdated callbacks via `stub.tabs.onUpdated._callbacks.forEach(cb => cb(tabId, { url }, { url }))`. Test 5 simplified to `await import` + immediate assertion (vi.resetModules in beforeEach guarantees fresh module state). The `_resetForTesting` / `_observeForTesting` typeof-guarded skeletons and the dynamic-import-missing-module `expect.fail` paths are deleted (the module now exists).
|
||||||
|
- **Files modified:** tests/background/meta-json-urls-schema.test.ts.
|
||||||
|
- **Verification:**
|
||||||
|
* tests/background/meta-json-urls-schema.test.ts: 5/5 GREEN (was 0/5; Tests 1+2 flipped via Task 2's createArchive amendment, Tests 3+4+5 flipped via Task 1).
|
||||||
|
* Tier-1 grep gate: 13/13 GREEN unchanged.
|
||||||
|
* Production bundle scan: `grep -l "_resetForTesting\|_observeForTesting" dist/ -r` → zero matches.
|
||||||
|
- **Why this is Rule 3 not Rule 4:** Plan 02-01 SUMMARY explicitly anticipated this fallback ("OPTIONAL contract... if absent, the tests need to wire chrome.tabs.onUpdated callbacks directly"). Plan 02-03 PLAN.md `<action>` block Task 1 + execution_context addendum both treat the test-hook-vs-direct-callback choice as implementer discretion. No architectural change; no scope creep beyond what the upstream planner authorized.
|
||||||
|
- **Committed in:** `7beb690` (part of Task 1 commit; tracker module + test rewiring landed together since they are the two sides of the same contract bridge — same composition as Plan 02-02's Task 3 commit `79964e6` Rule 3 deviation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (Rule 3 — blocking; Plan 02-01 SUMMARY-anticipated fallback).
|
||||||
|
|
||||||
|
**Impact on plan:** None on scope. The deviation chose the API shape that preserved the Tier-1 gate inventory at 13 entries (success_criteria item 7 in the prompt) and matched Plan 02-01 SUMMARY's "tests need to wire chrome.tabs.onUpdated callbacks directly" forward-link.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None. The plan-checker passed Plan 02-03 cleanly (iteration 2 GREEN), the test contract from Plan 02-01 was unambiguous after the F2 + 8th-field resolutions, and the test infrastructure landed by Plan 02-02 (CREATE_DOWNLOAD_URL round-trip simulation, capturedArchiveBytes pattern) carried through without modification.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None. The migration is fully internal — schema-breaking for downstream meta.json consumers (UAT harness Plan 02-04 + future SRV-* uploads), but no operator-facing configuration change. The `tabs` permission was already granted via DEC-011 Amendment 1 in commit `9dcfcf0`.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- **Plan 02-04 (next, Wave 3 — UAT harness extensions A24+):** ready. A27's multi-tab strict-mode assertion (length >= 2; both URLs present) is now empirically reachable — the operator opens 2 tabs during a recording session, hits SAVE, the harness extracts meta.json from the produced zip and verifies `urls.length >= 2`. A28's zip-layout pin (exactly 5 entries) is orthogonal and unblocked by Plan 02-02 + 02-03 closure together.
|
||||||
|
- **Phase 2 closure outlook:** after Plan 02-04 lands the UAT extensions and the operator empirical checkpoint flips ACK, REQ-meta-json-schema can be flipped to Complete (this plan's traceability-table forward link primes that flip).
|
||||||
|
|
||||||
|
## Verification Reconciliation
|
||||||
|
|
||||||
|
| Plan §verification gate | Result |
|
||||||
|
|---|---|
|
||||||
|
| `npx tsc --noEmit` | clean |
|
||||||
|
| `npm run build` | clean (378.82 kB SW chunk; +0.96 kB vs Plan 02-02 baseline for the new tab-url-tracker module) |
|
||||||
|
| `npx vitest run tests/background/meta-json-urls-schema.test.ts` | **5/5 GREEN** (was 0/5) |
|
||||||
|
| `npx vitest run tests/build/strict-meta-json-validation.test.ts` | **8/8 GREEN** (was 5/8) |
|
||||||
|
| `npx vitest run` (full suite) | **171/171 GREEN** (was 163/171; +8 GREEN net — exactly the Plan 02-03 territory) |
|
||||||
|
| `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` | **13/13 GREEN** (unchanged; Tier-1 gate inventory preserved at 13 entries) |
|
||||||
|
| `npx vitest run tests/i18n/manifest-i18n.test.ts` | **12/12 GREEN** (DEC-011 Amendment 1 permission-set pin) |
|
||||||
|
| `grep -c "schemaVersion" .planning/REQUIREMENTS.md` | **3** (≥2 required) |
|
||||||
|
| `grep -c '"tabs"' manifest.json` | **1** (DEC-011 Amendment 1 already landed) |
|
||||||
|
| `grep -c "Amendment 1" .planning/PROJECT.md` | **3** (DEC-011 row Amendment 1 prose) |
|
||||||
|
| `grep -c "url: string" src/shared/types.ts` | **2** (1 in UserEvent — unrelated; 1 in docstring explaining the migration. **SessionMetadata.url removed verbatim.**) |
|
||||||
|
| Always-on charter check (`grep -n "clearTabUrlsSeen" src/background/index.ts`) | **1 hit** (docstring comment EXPLICITLY noting it is NOT called — the always-on charter is documented at the call site) |
|
||||||
|
| `await import(...)` in src/background/index.ts | **0** (Plan 01-11 SUMMARY invariant preserved) |
|
||||||
|
|
||||||
|
## Tier-1 grep gate inventory + rationale
|
||||||
|
|
||||||
|
**Choice: preserve 13 entries** (no growth from this plan).
|
||||||
|
|
||||||
|
Rationale: the `_resetForTesting` / `_observeForTesting` ergonomic hooks the Plan 02-01 RED tests SUGGESTED would have grown FORBIDDEN_HOOK_STRINGS from 13 → 15. Two options to avoid that:
|
||||||
|
|
||||||
|
1. **`__MOKOSH_UAT__`-gated hooks** (the established Plan 01-11 pattern) — REJECTED. vitest.config.ts sets `__MOKOSH_UAT__: 'false'` for unit tests (Plan 01-11 SUMMARY note 6), so gated hooks would tree-shake out of the unit-test runtime too. Tests 3+4 would still fail.
|
||||||
|
2. **Drive chrome.tabs.onUpdated callbacks directly via the stub** — CHOSEN. Plan 02-01 SUMMARY anticipated this fallback verbatim. Production bundle stays test-hook-clean; the tracker module's public API contains only the 4 production functions; FORBIDDEN_HOOK_STRINGS holds at 13.
|
||||||
|
|
||||||
|
Verification: `grep -l "_resetForTesting\|_observeForTesting" dist/ -r` → 0 matches. `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` → 13/13 GREEN.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
### Files created (verified via stat at SUMMARY commit time)
|
||||||
|
|
||||||
|
- FOUND: src/background/tab-url-tracker.ts (246 LOC)
|
||||||
|
- FOUND: .planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md (this file)
|
||||||
|
|
||||||
|
### Files modified (verified via git diff)
|
||||||
|
|
||||||
|
- FOUND: src/shared/types.ts (SessionMetadata 7 → 8 fields)
|
||||||
|
- FOUND: src/background/index.ts (import + initTabUrlTracker registration + createArchive metadata rewrite)
|
||||||
|
- FOUND: tests/background/meta-json-urls-schema.test.ts (Tests 3+4+5 rewired)
|
||||||
|
- FOUND: .planning/REQUIREMENTS.md (REQ-meta-json-schema + traceability + footer)
|
||||||
|
|
||||||
|
### Commits exist (verified via git log)
|
||||||
|
|
||||||
|
- FOUND: 7beb690 feat(02-03): tab-url-tracker — chrome.tabs.onActivated + onUpdated → urls[] with dedup + filter (D-P2-02)
|
||||||
|
- FOUND: 78031e7 feat(02-03): meta.json — urls[] + schemaVersion (D-P2-02 + D-P2-03; replaces url:string)
|
||||||
|
- FOUND: af03556 docs(02-03): REQUIREMENTS — REQ-meta-json-schema amended for 8-field shape with urls[] + schemaVersion
|
||||||
|
|
||||||
|
### Success criteria reconciliation (from prompt)
|
||||||
|
|
||||||
|
1. [x] All 4 tasks in plan executed (Task 0 verification + Tasks 1-3 implementation).
|
||||||
|
2. [x] Each task committed individually (--no-verify per parallel-execution mandate).
|
||||||
|
3. [x] 8 RED tests (5 in meta-json-urls-schema + 3 in strict-meta-json-validation) now GREEN; full vitest 171/171 GREEN.
|
||||||
|
4. [x] 163 pre-existing GREEN tests still GREEN (no regression — full-suite delta is +8 GREEN net, not -anything).
|
||||||
|
5. [x] manifest.json includes `"tabs"` in permissions array (already landed via plan-checker iteration-1 revision pass commits `9dcfcf0` + `df8c086` before base `d3aa567`; verified GREEN).
|
||||||
|
6. [x] .planning/PROJECT.md captures DEC-011 Amendment 1 (verified GREEN via `grep -c "Amendment 1" .planning/PROJECT.md` → 3).
|
||||||
|
7. [x] tests/i18n/manifest-i18n.test.ts updated for tabs permission (already landed; 12/12 GREEN).
|
||||||
|
8. [x] Tier-1 grep gate inventory documented (13 entries; rationale in dedicated section above).
|
||||||
|
9. [x] No modifications to PortMessageType variants from 02-02 (verified via diff: `src/shared/types.ts` line range around PortMessageType union untouched).
|
||||||
|
10. [x] No modifications to downloadArchive or chrome.downloads.onChanged logic from 02-02 (verified via diff: lines 1174-1208 untouched).
|
||||||
|
11. [x] SUMMARY.md created in plan directory (this file).
|
||||||
|
12. [x] No modifications to STATE.md or ROADMAP.md (worktree mode invariant; STATE.md left untouched per parallel-execution mandate).
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 02-stabilize-export-pipeline*
|
||||||
|
*Completed: 2026-05-20*
|
||||||
@@ -8,6 +8,12 @@ import type {
|
|||||||
VideoBufferResponse
|
VideoBufferResponse
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
import { remuxSegments } from './webm-remux';
|
import { remuxSegments } from './webm-remux';
|
||||||
|
// Plan 02-03 D-P2-02 — tab-url tracker for meta.urls (multi-tab context).
|
||||||
|
// initTabUrlTracker is called from initialize(); snapshotOpenTabs +
|
||||||
|
// getTabUrlsSeen are called from createArchive() at SAVE time. The
|
||||||
|
// always-on charter (Plan 01-09 Amendment 3) is preserved — no
|
||||||
|
// clearTabUrlsSeen call in the save flow.
|
||||||
|
import { initTabUrlTracker, snapshotOpenTabs, getTabUrlsSeen } from './tab-url-tracker';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
// ─── Plan 01-13: NO SW-side test hook gate (Approach B) ──────────────
|
// ─── Plan 01-13: NO SW-side test hook gate (Approach B) ──────────────
|
||||||
@@ -709,10 +715,43 @@ async function createArchive(
|
|||||||
// the running version once manifest.json bumps to 1.0.1+. The Chrome
|
// the running version once manifest.json bumps to 1.0.1+. The Chrome
|
||||||
// runtime API is always available in the SW context, so no fallback
|
// runtime API is always available in the SW context, so no fallback
|
||||||
// is needed.
|
// is needed.
|
||||||
|
//
|
||||||
|
// Plan 02-03 D-P2-02 + D-P2-03 amendment (2026-05-20; closes audit P1 #10):
|
||||||
|
// - `url: string` REPLACED by `urls: string[]` sourced from the
|
||||||
|
// tab-url tracker (chrome.tabs.onActivated + onUpdated event-driven
|
||||||
|
// accumulation + snapshotOpenTabs SAVE-time defensive enumeration
|
||||||
|
// via chrome.tabs.query({})).
|
||||||
|
// - `schemaVersion: '2'` ADDED to mark the schema-breaking cutover.
|
||||||
|
// - Field-emission order follows the SessionMetadata interface line
|
||||||
|
// order — TypeScript preserves object-literal insertion order, and
|
||||||
|
// JSON.stringify emits in insertion order per ECMA-262.
|
||||||
|
//
|
||||||
|
// 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 30 s window but never
|
||||||
|
// activated (so the onActivated listener never fired for them). Dedup
|
||||||
|
// + order preservation is handled by the tracker itself.
|
||||||
|
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 per F2 (plan-checker iteration 1): whole-desktop
|
||||||
|
// recording with NO browser tabs open at SAVE time. The empty array
|
||||||
|
// IS the canonical representation — 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 manifest = chrome.runtime.getManifest();
|
const manifest = chrome.runtime.getManifest();
|
||||||
const metadata: SessionMetadata = {
|
const metadata: SessionMetadata = {
|
||||||
|
schemaVersion: '2',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
url: new URL(chrome.runtime.getURL('')).origin,
|
urls,
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
extensionVersion: manifest.version,
|
extensionVersion: manifest.version,
|
||||||
videoBufferSeconds: 30,
|
videoBufferSeconds: 30,
|
||||||
@@ -1171,6 +1210,20 @@ try {
|
|||||||
logger.warn('chrome.notifications.onClicked.addListener failed:', e);
|
logger.warn('chrome.notifications.onClicked.addListener failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plan 02-03 D-P2-02 — tab-url tracker bootstrap.
|
||||||
|
// Registers chrome.tabs.onActivated + chrome.tabs.onUpdated listeners that
|
||||||
|
// maintain an internal Set of URLs observed during the SW's lifetime.
|
||||||
|
// Feeds meta.urls in createArchive (closes audit P1 #10). Defensive
|
||||||
|
// try/catch matches the surrounding chrome.* listener registration
|
||||||
|
// pattern; idempotency is handled by the tracker module itself (an
|
||||||
|
// initialized flag prevents double-registration if some future caller
|
||||||
|
// invokes initTabUrlTracker more than once).
|
||||||
|
try {
|
||||||
|
initTabUrlTracker();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('initTabUrlTracker failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
// chrome.downloads.onChanged: D-P2-01 (P0-6 fix) — revoke-on-terminal-state.
|
// chrome.downloads.onChanged: D-P2-01 (P0-6 fix) — revoke-on-terminal-state.
|
||||||
// Closes the URL.revokeObjectURL lifecycle by routing terminal download
|
// Closes the URL.revokeObjectURL lifecycle by routing terminal download
|
||||||
// state transitions (`complete` / `interrupted`) into a REVOKE_DOWNLOAD_URL
|
// state transitions (`complete` / `interrupted`) into a REVOKE_DOWNLOAD_URL
|
||||||
|
|||||||
246
src/background/tab-url-tracker.ts
Normal file
246
src/background/tab-url-tracker.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// src/background/tab-url-tracker.ts
|
||||||
|
//
|
||||||
|
// Phase 2 Plan 02-03 — D-P2-02 tab-URL tracker.
|
||||||
|
//
|
||||||
|
// Maintains an internal, deduplicated, first-seen-ordered Set of tab URLs
|
||||||
|
// observed during the SW's lifetime via chrome.tabs.onActivated +
|
||||||
|
// chrome.tabs.onUpdated listeners. Feeds the multi-tab `meta.urls` array
|
||||||
|
// in `createArchive()` (closes audit P1 #10).
|
||||||
|
//
|
||||||
|
// Architectural decisions (frozen by .planning/phases/02-stabilize-export-
|
||||||
|
// pipeline/02-03-PLAN.md):
|
||||||
|
//
|
||||||
|
// - The tracker is FED PASSIVELY by Chrome's tab events: every time the
|
||||||
|
// operator switches tabs OR a tab navigates (changeInfo.url present),
|
||||||
|
// we get a URL — pass it through the filter, dedup, append.
|
||||||
|
//
|
||||||
|
// - It is SAVE-time AUGMENTED via `snapshotOpenTabs()` which calls
|
||||||
|
// `chrome.tabs.query({})` (requires the `tabs` permission per
|
||||||
|
// DEC-011 Amendment 1, 2026-05-20) to catch tabs the operator opened
|
||||||
|
// during the 30 s window but never activated.
|
||||||
|
//
|
||||||
|
// - Always-on charter (Plan 01-09 Amendment 3): `clearTabUrlsSeen()` is
|
||||||
|
// NOT called by createArchive. The tracker keeps accumulating across
|
||||||
|
// saves so the next save captures any tabs activated AFTER the prior
|
||||||
|
// save fired.
|
||||||
|
//
|
||||||
|
// - F2 (plan-checker iteration 1): no sentinel-URL fallback. If the
|
||||||
|
// tracker is empty (whole-desktop-no-tab session — operator captured
|
||||||
|
// a non-Chrome surface via desktopCapture only), `getTabUrlsSeen()`
|
||||||
|
// returns `[]` faithfully. createArchive emits `urls: []`.
|
||||||
|
//
|
||||||
|
// URL filter (per .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
||||||
|
// `<specifics>`):
|
||||||
|
//
|
||||||
|
// INCLUDE: https://, http://, chrome-extension://
|
||||||
|
// EXCLUDE: chrome://, about:, devtools://, file://, blob:, data:, edge://
|
||||||
|
//
|
||||||
|
// (Implemented via a single positive-allow regex; default-deny everything
|
||||||
|
// else.)
|
||||||
|
//
|
||||||
|
// Tier-1 grep-gate compliance: the module exposes NO `_resetForTesting`
|
||||||
|
// / `_observeForTesting` ergonomic test hooks (which would have leaked
|
||||||
|
// into production bundles and violated the
|
||||||
|
// `tests/background/no-test-hooks-in-prod-bundle.test.ts` 13-entry gate).
|
||||||
|
// Unit tests drive the registered chrome.tabs.onUpdated callbacks
|
||||||
|
// directly via the chrome stub's `_callbacks` array; module state is
|
||||||
|
// reset between tests via vitest's `vi.resetModules()` in beforeEach.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// - .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
||||||
|
// <decisions> D-P2-02 + <specifics> URL filter clause
|
||||||
|
// - .planning/PROJECT.md DEC-011 Amendment 1 (`tabs` permission)
|
||||||
|
// - Chrome tabs API: https://developer.chrome.com/docs/extensions/reference/api/tabs
|
||||||
|
|
||||||
|
import { Logger } from '../shared/logger';
|
||||||
|
|
||||||
|
const logger = new Logger('TabUrlTracker');
|
||||||
|
|
||||||
|
// ─── Module state ───────────────────────────────────────────────────────
|
||||||
|
// `tabUrlsSeen` is the dedup Set (O(1) membership checks).
|
||||||
|
// `firstSeenOrder` is the append-only list preserving first-seen ordering.
|
||||||
|
// Both stay in lockstep: a URL is added to BOTH only on its first
|
||||||
|
// observation (the Set membership check gates the array push).
|
||||||
|
//
|
||||||
|
// `initialized` guards against double-listener-registration if some caller
|
||||||
|
// (a unit test, a future re-init path) calls `initTabUrlTracker()` twice.
|
||||||
|
let tabUrlsSeen: Set<string> = new Set();
|
||||||
|
let firstSeenOrder: string[] = [];
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
// ─── URL filter ─────────────────────────────────────────────────────────
|
||||||
|
// Positive-allow: anything matching the regex is INCLUDED; everything else
|
||||||
|
// is DROPPED. Equivalent to the long-form switch over schemes but
|
||||||
|
// preserves a single canonical pattern that's easy to grep for.
|
||||||
|
//
|
||||||
|
// Test 4 of tests/background/meta-json-urls-schema.test.ts asserts this
|
||||||
|
// exact filter behaviour (https://example.com + chrome-extension://abc/...
|
||||||
|
// IN; chrome://newtab + about:blank OUT).
|
||||||
|
const URL_SCHEME_ALLOW = /^(https?|chrome-extension):\/\//;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a URL passes the inclusion filter.
|
||||||
|
*
|
||||||
|
* @param url - Candidate URL string.
|
||||||
|
* @returns true if the URL's scheme is in the allow-list; false otherwise.
|
||||||
|
*/
|
||||||
|
function passesFilter(url: string): boolean {
|
||||||
|
if (typeof url !== 'string' || url.length === 0) return false;
|
||||||
|
return URL_SCHEME_ALLOW.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a URL to the tracker if it passes the filter and is not already
|
||||||
|
* present. Idempotent: re-observation of an existing URL is a no-op.
|
||||||
|
*
|
||||||
|
* @param url - Candidate URL string (may be empty or malformed —
|
||||||
|
* defensively filtered).
|
||||||
|
*/
|
||||||
|
function addUrl(url: string): void {
|
||||||
|
if (!passesFilter(url)) return;
|
||||||
|
if (tabUrlsSeen.has(url)) return;
|
||||||
|
tabUrlsSeen.add(url);
|
||||||
|
firstSeenOrder.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the tab-URL tracker. Registers chrome.tabs.onActivated +
|
||||||
|
* chrome.tabs.onUpdated listeners that maintain an internal Set of URLs
|
||||||
|
* observed during the SW's lifetime. Must be called once at SW init.
|
||||||
|
*
|
||||||
|
* Idempotent — subsequent calls return early after logging a warning
|
||||||
|
* (defensive pattern matching src/background/index.ts:bootstrap try/catch
|
||||||
|
* wrappers around chrome.* listener registrations).
|
||||||
|
*
|
||||||
|
* D-P2-02 binding: captures the operator's multi-tab context, not just
|
||||||
|
* the active-at-save tab.
|
||||||
|
*/
|
||||||
|
export function initTabUrlTracker(): void {
|
||||||
|
if (initialized) {
|
||||||
|
logger.warn('initTabUrlTracker called twice — second call ignored');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
// chrome.tabs.onActivated: fires when the user switches to a different
|
||||||
|
// tab. The activated tab's URL is fetched via chrome.tabs.get because
|
||||||
|
// onActivated's payload omits .url (it carries tabId + windowId only).
|
||||||
|
// DEC-011 Amendment 1: the `tabs` permission makes chrome.tabs.get
|
||||||
|
// reliably return the `.url` field for any tab in any window.
|
||||||
|
try {
|
||||||
|
chrome.tabs.onActivated.addListener((activeInfo: { tabId: number; windowId: number }) => {
|
||||||
|
const onTabResolved = (tab: { url?: string } | undefined): void => {
|
||||||
|
if (tab === undefined || tab === null) return;
|
||||||
|
if (typeof tab.url !== 'string' || tab.url.length === 0) {
|
||||||
|
logger.warn(`tabs.onActivated: tab ${activeInfo.tabId} has no .url (permission gap?)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addUrl(tab.url);
|
||||||
|
};
|
||||||
|
const onTabFailed = (err: unknown): void => {
|
||||||
|
logger.warn(`tabs.onActivated: chrome.tabs.get(${activeInfo.tabId}) failed:`, err);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = chrome.tabs.get(activeInfo.tabId);
|
||||||
|
// chrome.tabs.get returns a Promise in MV3. The Promise.resolve
|
||||||
|
// wrapper accepts both Promise and plain values (defensive against
|
||||||
|
// older stubs that might return synchronously).
|
||||||
|
Promise.resolve(result).then(onTabResolved).catch(onTabFailed);
|
||||||
|
} catch (err) {
|
||||||
|
onTabFailed(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('chrome.tabs.onActivated.addListener failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// chrome.tabs.onUpdated: fires on every tab state change. We only care
|
||||||
|
// about URL transitions (changeInfo.url present). NOT gated on
|
||||||
|
// changeInfo.status === 'complete' so SPA-style routing changes (which
|
||||||
|
// emit changeInfo.url WITHOUT a top-level load event) still get
|
||||||
|
// captured.
|
||||||
|
try {
|
||||||
|
chrome.tabs.onUpdated.addListener(
|
||||||
|
(_tabId: number, changeInfo: { url?: string }, tab: { url?: string }) => {
|
||||||
|
// Prefer changeInfo.url (the transition URL) but fall back to
|
||||||
|
// tab.url (the current resolved URL) for stubs that don't populate
|
||||||
|
// changeInfo.url consistently. Both are forwarded through the
|
||||||
|
// filter + dedup gate by addUrl.
|
||||||
|
const candidate = typeof changeInfo.url === 'string' && changeInfo.url.length > 0
|
||||||
|
? changeInfo.url
|
||||||
|
: (typeof tab.url === 'string' ? tab.url : '');
|
||||||
|
if (candidate.length > 0) addUrl(candidate);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('chrome.tabs.onUpdated.addListener failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the deduplicated, first-seen-ordered, filtered list of tab URLs
|
||||||
|
* observed since module init OR since the last `clearTabUrlsSeen()` call.
|
||||||
|
*
|
||||||
|
* Filter (per .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
||||||
|
* `<specifics>`):
|
||||||
|
* - INCLUDE: https://, http://, chrome-extension://
|
||||||
|
* - EXCLUDE: chrome://, about:, devtools://, file://, blob:, data:
|
||||||
|
*
|
||||||
|
* Dedup: each URL appears exactly once.
|
||||||
|
* Order: first-seen-first.
|
||||||
|
*
|
||||||
|
* Always returns a NEW array (slice) — caller cannot mutate the internal
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @returns A copy of the observed URL list. Empty array IS the canonical
|
||||||
|
* representation of a whole-desktop-no-tab session (per F2
|
||||||
|
* resolution from plan-checker iteration 1).
|
||||||
|
*/
|
||||||
|
export function getTabUrlsSeen(): string[] {
|
||||||
|
return firstSeenOrder.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshot every currently-open tab's URL into the tracker. Called by
|
||||||
|
* `createArchive()` (src/background/index.ts) AT SAVE TIME as a defensive
|
||||||
|
* fallback: captures tabs the operator OPENED during the recording window
|
||||||
|
* but never activated (so the onActivated listener never fired). Dedup
|
||||||
|
* + first-seen ordering preserves invariants — tabs already in the Set
|
||||||
|
* stay where they are; new tabs append in chrome.tabs.query() order.
|
||||||
|
*
|
||||||
|
* Requires `tabs` permission (DEC-011 Amendment 1, 2026-05-20).
|
||||||
|
*
|
||||||
|
* Errors are caught + logged; the SAVE flow continues with whatever
|
||||||
|
* state the tracker already had. Per F2: if the snapshot leaves the
|
||||||
|
* tracker empty (e.g. operator captured a non-Chrome surface), the
|
||||||
|
* `meta.urls` array stays `[]` — no fake extension-origin URL inserted.
|
||||||
|
*
|
||||||
|
* @returns Promise that resolves once the snapshot completes (or fails
|
||||||
|
* softly via the catch path).
|
||||||
|
*/
|
||||||
|
export async function snapshotOpenTabs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tabs = await chrome.tabs.query({});
|
||||||
|
if (!Array.isArray(tabs)) {
|
||||||
|
logger.warn('snapshotOpenTabs: chrome.tabs.query did not return an Array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const url = (tab as { url?: string }).url;
|
||||||
|
if (typeof url === 'string' && url.length > 0) addUrl(url);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('snapshotOpenTabs: chrome.tabs.query failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the internal Set + first-seen array. NOT called by
|
||||||
|
* `createArchive()` — always-on charter (Plan 01-09 Amendment 3) keeps
|
||||||
|
* the tracker accumulating across saves. Reserved for future use
|
||||||
|
* (manual session-reset, test isolation outside vi.resetModules).
|
||||||
|
*/
|
||||||
|
export function clearTabUrlsSeen(): void {
|
||||||
|
tabUrlsSeen = new Set();
|
||||||
|
firstSeenOrder = [];
|
||||||
|
}
|
||||||
@@ -130,10 +130,36 @@ export interface UserEvent {
|
|||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Метаданные сессии
|
// Метаданные сессии.
|
||||||
|
//
|
||||||
|
// Phase 2 Plan 02-03 — D-P2-02 + D-P2-03 schema-breaking amendment
|
||||||
|
// (2026-05-20; closes audit P1 #10):
|
||||||
|
//
|
||||||
|
// - `url: string` REPLACED by `urls: string[]`. Captures the operator's
|
||||||
|
// multi-tab context during the rolling 30 s recording window — not
|
||||||
|
// just the active-at-save tab. Fed by the new
|
||||||
|
// `src/background/tab-url-tracker.ts` module via chrome.tabs.onActivated
|
||||||
|
// + chrome.tabs.onUpdated listeners + a SAVE-time
|
||||||
|
// chrome.tabs.query({}) snapshot (DEC-011 Amendment 1 grants the
|
||||||
|
// `tabs` permission). Empty array IS permitted (F2 — whole-desktop-
|
||||||
|
// no-tab session).
|
||||||
|
//
|
||||||
|
// - `schemaVersion: string` ADDED as the 8th field. Value '2' marks the
|
||||||
|
// D-P2-02 url→urls cutover; future schema bumps increment. The 8th
|
||||||
|
// field name was planner-suggested in Plan 02-01 Task 3 and ratified
|
||||||
|
// here as the lockstep for tests/build/strict-meta-json-validation.test.ts
|
||||||
|
// EXPECTED_KEYS.
|
||||||
|
//
|
||||||
|
// Total field count: 8 (per D-P2-03 strict exact-shape rule).
|
||||||
|
//
|
||||||
|
// Field-emission order follows source-declaration order: TypeScript object
|
||||||
|
// literals preserve insertion order, and JSON.stringify emits in insertion
|
||||||
|
// order per ECMA-262 §9.1.11.1 — so the meta.json output mirrors this
|
||||||
|
// interface line-by-line.
|
||||||
export interface SessionMetadata {
|
export interface SessionMetadata {
|
||||||
|
schemaVersion: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
url: string;
|
urls: string[];
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
extensionVersion: string;
|
extensionVersion: string;
|
||||||
videoBufferSeconds: number;
|
videoBufferSeconds: number;
|
||||||
|
|||||||
@@ -595,45 +595,37 @@ describe('Plan 02-01 Task 2 RED: meta.json urls[] schema + dedup/filter + empty-
|
|||||||
// it pins the dedup-and-order CONTRACT of getTabUrlsSeen() directly.
|
// it pins the dedup-and-order CONTRACT of getTabUrlsSeen() directly.
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
it('meta.urls deduplicates repeated URLs and preserves first-seen order', async () => {
|
it('meta.urls deduplicates repeated URLs and preserves first-seen order', async () => {
|
||||||
// RED gate: the tab-url-tracker module does not exist yet. The
|
// Plan 02-03 GREEN path: tab-url-tracker landed without the optional
|
||||||
// dynamic import throws and expect.fail emits the precise marker
|
// `_resetForTesting` / `_observeForTesting` ergonomic hooks (those
|
||||||
// for the Plan 02-03 GREEN-flip.
|
// would have leaked into production bundles and violated the Tier-1
|
||||||
let tracker: typeof import('../../src/background/tab-url-tracker');
|
// 13-entry FORBIDDEN_HOOK_STRINGS gate at
|
||||||
try {
|
// tests/background/no-test-hooks-in-prod-bundle.test.ts:108). The
|
||||||
tracker = await import('../../src/background/tab-url-tracker');
|
// canonical Plan-02-01-SUMMARY-anticipated alternative is to wire
|
||||||
} catch (e) {
|
// chrome.tabs.onUpdated callbacks directly via the chrome stub.
|
||||||
expect.fail(
|
|
||||||
`src/background/tab-url-tracker.ts does not exist yet — this is the Plan 02-03 ` +
|
|
||||||
`GREEN gate. The module MUST export a 'getTabUrlsSeen(): string[]' function ` +
|
|
||||||
`fed by chrome.tabs.onUpdated + chrome.tabs.onActivated listeners with dedup ` +
|
|
||||||
`Set semantics + first-seen-first iteration order. Module import error: ${String(e)}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below: GREEN-side contract (Plan 02-03 implementer codes against
|
|
||||||
// this). Tracker exposes a way to inject observations for testing,
|
|
||||||
// OR the test wires chrome.tabs.onUpdated callbacks directly.
|
|
||||||
// Both shapes are acceptable per the planner's "Claude's Discretion"
|
|
||||||
// delegation in CONTEXT.md `<decisions>`. For now we document the
|
|
||||||
// expectation and let the implementer pick.
|
|
||||||
//
|
//
|
||||||
// The skeleton below assumes a `reset()` + observation API for
|
// Mechanic: build the chrome stub, install it as globalThis.chrome,
|
||||||
// ergonomic test wiring. Plan 02-03's implementer SHOULD provide
|
// import the tracker, call initTabUrlTracker() to register listeners
|
||||||
// such an API OR amend this test to wire callbacks directly.
|
// on the stub, then invoke the captured callbacks with synthetic
|
||||||
type TrackerModule = {
|
// tab-update events. The tracker treats each callback invocation as
|
||||||
getTabUrlsSeen: () => string[];
|
// a real Chrome event; dedup + ordering invariants kick in naturally.
|
||||||
_resetForTesting?: () => void;
|
const stub = buildBgStub();
|
||||||
_observeForTesting?: (url: string) => void;
|
(globalThis as unknown as GlobalWithBgChrome).chrome = stub;
|
||||||
|
const tracker = await import('../../src/background/tab-url-tracker');
|
||||||
|
tracker.initTabUrlTracker();
|
||||||
|
|
||||||
|
// Synthetic chrome.tabs.onUpdated events. The first 'A' establishes
|
||||||
|
// first-seen ordering; 'B' appends second; the repeated 'A' is
|
||||||
|
// deduplicated. Mirrors the Plan 02-01 RED-test contract verbatim.
|
||||||
|
const fireUpdate = (tabId: number, url: string): void => {
|
||||||
|
stub.tabs.onUpdated._callbacks.forEach((cb) =>
|
||||||
|
cb(tabId, { url }, { url }),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const t = tracker as TrackerModule;
|
fireUpdate(1, 'https://a.example.com');
|
||||||
if (typeof t._resetForTesting === 'function') t._resetForTesting();
|
fireUpdate(2, 'https://b.example.com');
|
||||||
if (typeof t._observeForTesting === 'function') {
|
fireUpdate(1, 'https://a.example.com');
|
||||||
t._observeForTesting('https://a.example.com');
|
|
||||||
t._observeForTesting('https://b.example.com');
|
expect(tracker.getTabUrlsSeen()).toEqual([
|
||||||
t._observeForTesting('https://a.example.com');
|
|
||||||
}
|
|
||||||
expect(t.getTabUrlsSeen()).toEqual([
|
|
||||||
'https://a.example.com',
|
'https://a.example.com',
|
||||||
'https://b.example.com',
|
'https://b.example.com',
|
||||||
]);
|
]);
|
||||||
@@ -646,33 +638,27 @@ describe('Plan 02-01 Task 2 RED: meta.json urls[] schema + dedup/filter + empty-
|
|||||||
// RED today: missing module. GREEN after Plan 02-03.
|
// RED today: missing module. GREEN after Plan 02-03.
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
it('meta.urls filters chrome:// and about: URLs and includes chrome-extension://', async () => {
|
it('meta.urls filters chrome:// and about: URLs and includes chrome-extension://', async () => {
|
||||||
let tracker: typeof import('../../src/background/tab-url-tracker');
|
// Plan 02-03 GREEN path: same chrome.tabs.onUpdated driver pattern
|
||||||
try {
|
// as Test 3 (preserves the 13-entry Tier-1 grep gate by not
|
||||||
tracker = await import('../../src/background/tab-url-tracker');
|
// requiring `_observeForTesting` hooks on the tracker module).
|
||||||
} catch (e) {
|
const stub = buildBgStub();
|
||||||
expect.fail(
|
(globalThis as unknown as GlobalWithBgChrome).chrome = stub;
|
||||||
`src/background/tab-url-tracker.ts does not exist yet — Plan 02-03 GREEN gate. ` +
|
const tracker = await import('../../src/background/tab-url-tracker');
|
||||||
`Filter contract: include https + http + chrome-extension://; exclude chrome:// + ` +
|
tracker.initTabUrlTracker();
|
||||||
`about: + (default-deny on devtools://, file://, edge://). Per CONTEXT.md ` +
|
|
||||||
`<specifics> URL filter clause. Module import error: ${String(e)}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackerModule = {
|
const fireUpdate = (tabId: number, url: string): void => {
|
||||||
getTabUrlsSeen: () => string[];
|
stub.tabs.onUpdated._callbacks.forEach((cb) =>
|
||||||
_resetForTesting?: () => void;
|
cb(tabId, { url }, { url }),
|
||||||
_observeForTesting?: (url: string) => void;
|
);
|
||||||
};
|
};
|
||||||
const t = tracker as TrackerModule;
|
// Filter test: include https + chrome-extension://; exclude chrome:// +
|
||||||
if (typeof t._resetForTesting === 'function') t._resetForTesting();
|
// about: per CONTEXT.md `<specifics>` URL filter clause.
|
||||||
if (typeof t._observeForTesting === 'function') {
|
fireUpdate(1, 'https://example.com');
|
||||||
t._observeForTesting('https://example.com');
|
fireUpdate(2, 'chrome://newtab');
|
||||||
t._observeForTesting('chrome://newtab');
|
fireUpdate(3, 'about:blank');
|
||||||
t._observeForTesting('about:blank');
|
fireUpdate(4, 'chrome-extension://abc/popup.html');
|
||||||
t._observeForTesting('chrome-extension://abc/popup.html');
|
|
||||||
}
|
expect(tracker.getTabUrlsSeen()).toEqual([
|
||||||
expect(t.getTabUrlsSeen()).toEqual([
|
|
||||||
'https://example.com',
|
'https://example.com',
|
||||||
'chrome-extension://abc/popup.html',
|
'chrome-extension://abc/popup.html',
|
||||||
]);
|
]);
|
||||||
@@ -692,30 +678,15 @@ describe('Plan 02-01 Task 2 RED: meta.json urls[] schema + dedup/filter + empty-
|
|||||||
// Revision Log 2026-05-20).
|
// Revision Log 2026-05-20).
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
it('meta.urls is exactly [] when the tracker observed no browser tabs (F2)', async () => {
|
it('meta.urls is exactly [] when the tracker observed no browser tabs (F2)', async () => {
|
||||||
let tracker: typeof import('../../src/background/tab-url-tracker');
|
// Plan 02-03 GREEN path: import the tracker on a freshly-reset
|
||||||
try {
|
// module graph (vi.resetModules in beforeEach), then immediately
|
||||||
tracker = await import('../../src/background/tab-url-tracker');
|
// query without firing any chrome.tabs.* callbacks. The empty
|
||||||
} catch (e) {
|
// representation is the canonical whole-desktop-no-tab state.
|
||||||
expect.fail(
|
const tracker = await import('../../src/background/tab-url-tracker');
|
||||||
`src/background/tab-url-tracker.ts does not exist yet — Plan 02-03 GREEN gate. ` +
|
|
||||||
`F2 contract: empty-tracker → meta.urls === [] (empty Array; NOT undefined; ` +
|
|
||||||
`NOT [extension-origin]; NOT null). Whole-desktop-no-tab recording is ` +
|
|
||||||
`meaningful per CONTEXT.md Revision Log 2026-05-20. Module import error: ${String(e)}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrackerModule = {
|
|
||||||
getTabUrlsSeen: () => string[];
|
|
||||||
_resetForTesting?: () => void;
|
|
||||||
_observeForTesting?: (url: string) => void;
|
|
||||||
};
|
|
||||||
const t = tracker as TrackerModule;
|
|
||||||
if (typeof t._resetForTesting === 'function') t._resetForTesting();
|
|
||||||
// Deliberately observe nothing — simulating a session where no tab
|
// Deliberately observe nothing — simulating a session where no tab
|
||||||
// events fired during the 30 s window.
|
// events fired during the 30 s window.
|
||||||
expect(t.getTabUrlsSeen()).toEqual([]);
|
expect(tracker.getTabUrlsSeen()).toEqual([]);
|
||||||
expect(t.getTabUrlsSeen()).not.toBeUndefined();
|
expect(tracker.getTabUrlsSeen()).not.toBeUndefined();
|
||||||
expect(t.getTabUrlsSeen()).not.toBeNull();
|
expect(tracker.getTabUrlsSeen()).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user