Skip to content

fix: la extensión vuelve a capturar (B1/B2) + OPFS async + pausa/continuar + tests honestos#2

Open
ctala wants to merge 13 commits into
mainfrom
fix/harness-honesto-y-bugs-criticos
Open

fix: la extensión vuelve a capturar (B1/B2) + OPFS async + pausa/continuar + tests honestos#2
ctala wants to merge 13 commits into
mainfrom
fix/harness-honesto-y-bugs-criticos

Conversation

@ctala

@ctala ctala commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Por qué

El refactor OPFS (v1.3.0→v1.4.2) rompió la captura en Chrome real: el service worker es script clásico y Chrome carga solo background.js, pero el refactor partió la lógica en módulos (opfs-buffer.js, memory-buffer.js) y nunca los cableó (sin importScripts) → self.OpfsBuffer/MemoryBuffer null → captura cero. Los 71 tests pasaban porque el mock pre-inyectaba esos buffers que Chrome nunca inyecta. El verde medía el mock, no producción — de ahí el patrón "arreglo un fix y aparece otro".

La v1.2.x (pre-refactor, background.js autocontenido) sí funcionaba: es la referencia known-good.

Qué arregla (verificado contra el código; file:line en el levantamiento)

Bug Qué era Fix
B1 (crítico) Buffers null en Chrome real → captura cero, DOWNLOAD siempre "No captures" importScripts en el SW + rama worker faltante en el UMD de opfs-buffer.js
B2 (crítico) Filtro legacy de content.js (url.includes(regexCrudo)) descartaba TODA la captura con presets Solo aplica al path sin captureConfig (keyword simple)
B9-guard Re-inyección en cada START duplicaba wrappers de fetch/XHR Guard idempotente window.__ARE_PATCHED__
B24 Versión del PING hardcodeada (1.4.0) vs manifest (1.4.2) Derivada de chrome.runtime.getManifest()

La extensión vuelve a capturar — confirmado en Chromium real (el e2e graba fetch+XHR y descarga un JSONL con datos reales).

Testing sin intervención humana (la red que faltaba)

  • Unit honesto (test/_sw-loader.mjs vía node:vm): carga el SW como lo carga Chrome (solo background.js + importScripts real, sin pre-inyectar globals). test/sw-wiring.test.mjs reproduce B1 en puro Node — rojo antes del fix, verde después. Esa clase de bug ya no pasa desapercibida.
  • E2E (Playwright + Chromium real, channel:chromium + --headless=new): carga la extensión unpacked, graba fetch+XHR, STOP, DOWNLOAD, asserta el JSONL. Fixtures locales que imitan Voyager (sin tocar linkedin.com, sin data privada).
  • CI (.github/workflows/test.yml): unit + e2e headless (xvfb) en cada push; bloquea el build si algo falla.
npm test          → 74/74 unit verde
npm run test:e2e  → 1/1 e2e verde en Chromium real

Además

  • Limpieza de artifacts/drafts commiteados (dist/*.tar.gz, .pr-body-*, screenshots-v2/ duplicado) + .gitignore arreglado.
  • 2 subagentes en .claude/agents/: Chrome MV3 Extension Engineer (el motor) + API Reverse Engineer (el método genérico — Voyager es su primer caso, no su definición).
  • Levantamiento completo: docs/spec/levantamiento-2026-06-24.md (causa raíz + 24 bugs verificados + arquitectura objetivo + plan por fases).

Pendiente (próximas fases, no en este PR)

  • Fase 2: pausa/continuar (cablear restoreFromExisting, ya existe con 0 callers + ADR-0003).
  • Fase 3: LinkedIn Voyager como preset (world:MAIN, document_start, headers de XHR B7, fetch(Request) B8, redacción).

🤖 Generated with Claude Code

Cristian Tala and others added 7 commits June 24, 2026 14:20
- git rm tarballs commiteados (dist/*.tar.gz 1.4M + raíz v1.2.2)
- git rm drafts de proceso .pr-body-*.md (4)
- git rm store-assets/screenshots-v2/ (duplicado exacto de screenshots/)
- .gitignore: agregar *.tar.gz, dist/, .pr-body*, playwright-report/, test-results/

Co-Authored-By: Claude <noreply@anthropic.com>
Investigación multi-agente: la extensión no captura nada en Chrome real
(buffers null en prod, tests verdes contra mock que pre-inyecta deps).
24 bugs verificados, arquitectura objetivo, testing automatizado sin humano.

Co-Authored-By: Claude <noreply@anthropic.com>
…turar (B1)

Causa raíz del 'captura cero en Chrome real': el SW es script clásico
(manifest sin type:module) y Chrome carga SOLO background.js. Los módulos
opfs-buffer/memory-buffer nunca se cargaban → self.OpfsBuffer/MemoryBuffer
null → activeBuffer null → toda captura descartada en silencio. Los 71
tests pasaban porque el mock pre-inyectaba los buffers a globalThis.

- background.js: importScripts('/src/memory-buffer.js','/src/opfs-buffer.js')
  antes del IIFE (guarded; no-op en el harness CJS legacy).
- opfs-buffer.js: el UMD no tenía rama worker → no attachaba a self en el
  SW. Agregada rama self/globalThis (memory-buffer ya la tenía).

Harness honesto (Fase 0): package.json + node:vm loader (test/_sw-loader.mjs)
que carga el SW como Chrome (solo background.js + importScripts real, SIN
pre-inyectar globals) + test/sw-wiring.test.mjs que reproduce B1 en puro
Node. Rojo antes del fix, verde después. 74 tests, 74 pass.

Co-Authored-By: Claude <noreply@anthropic.com>
…p (B9) + versión drift (B24)

- content.js (B2): el filtro legacy de substring corría aun con captureConfig
  estructurado activo. Para presets regex/glob, `filter` es el patrón crudo y
  url.includes(rawRegex) nunca matchea → descartaba TODA la captura. Ahora el
  filtro legacy SOLO aplica al path sin captureConfig (keyword simple).
- injected.js (B9): guard window.__ARE_PATCHED__ — la re-inyección en cada
  START duplicaba wrappers de fetch/XHR → capturas duplicadas. Idempotente.
- content.js (B24): versión del PING derivada del manifest (era '1.4.0'
  hardcodeado vs manifest '1.4.2').

Co-Authored-By: Claude <noreply@anthropic.com>
Capa e2e que carga la extensión unpacked en Chromium real (channel:chromium
+ --headless=new) y valida el flujo completo de captura SIN intervención
humana — la red que faltaba para romper el 'arreglo un fix y aparece otro':

- test/e2e/fixtures-server.mjs: servidor local determinista que imita Voyager
  (/voyager/api/me con x-restli + included[].access_token, /messaging XHR).
  Sin tocar linkedin.com, sin data privada, replicable en CI.
- test/e2e/record-download.spec.mjs: carga extensión → START vía popup→SW →
  dispara fetch+XHR → STOP → DOWNLOAD → asserta el JSONL. Valida B1 (buffers
  cableados en el SW REAL) + B2 (filtro no descarta) end-to-end. VERDE (3.5s).
- scripts/build-dist.mjs: empaqueta dist/unpacked (= lo que ships; chequea
  refs del manifest). scripts/check-version-consistency.mjs: gate anti-drift.
- playwright.config.mjs + .github/workflows/test.yml: unit + e2e headless
  (xvfb) en cada push; bloquea el build si algo falla.

Validado local: unit 74/74 + e2e 1/1 verde.

Co-Authored-By: Claude <noreply@anthropic.com>
…ering

.claude/agents/ (el repo no tenía): dos subagentes anclados al levantamiento
y al código real:
- Chrome MV3 Extension Engineer: el MOTOR (4 contextos, lifecycle SW, OPFS,
  costuras, tests honestos). Custodio de R1/R2/R3 y los 8 key features.
- API Reverse Engineer: el MÉTODO genérico (mapear cualquier API, criterio
  redacción sesión-vs-replay, armar presets). Voyager = primer caso, no su def.

Co-Authored-By: Claude <noreply@anthropic.com>
…sobrevive al restart del SW

Fase 2. Al construir pausa/continuar, un e2e en Chromium real reveló que OPFS
NUNCA funcionó en producción: createSyncAccessHandle() no existe en MV3 service
workers (solo en dedicated workers) → desde v1.4.0 todo corría en
memoria-fallback, el archivo OPFS quedaba en 0 líneas. El mock lo ocultaba.

- opfs-buffer.js: reescrito a la API OPFS ASYNC (createWritable + getFile), que
  sí funciona en el SW. Append sync + flush batcheado por microtask. flush()
  fuerza durabilidad en STOP/PAUSE/lectura.
- background.js: verbos PAUSE/RESUME + restore cablea restoreFromExisting al wake
  del SW (reconstruye contador+dedup desde disco). START trunca (sesión nueva),
  RESUME appendea. Descarga OPFS normalizada al shape canónico _toJsonlLine.
- popup: botones Pausar/Continuar (3 estados), guards lastError (B6), fix B13.
- mocks: createWritable async (antes createSyncAccessHandle — el sync que ocultaba
  el bug). Tests: pausa-resume.test.mjs + e2e sw-restart-resume (CDP stopAllWorkers).
- ADR-0003 (supersede ADR-0002 en el write mechanism). Bump 1.4.2 → 1.5.0.

Validado: unit 78/78 + e2e 2/2 (la grabación sobrevive al teardown del SW).

Co-Authored-By: Claude <noreply@anthropic.com>
@ctala ctala changed the title fix: la extensión vuelve a capturar (regresión OPFS) + harness honesto unit+e2e fix: la extensión vuelve a capturar (B1/B2) + OPFS async + pausa/continuar + tests honestos Jun 24, 2026
@ctala

ctala commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Actualización — Fase 2 (pausa/continuar) + hallazgo OPFS

Construyendo pausa/continuar, un e2e en Chromium real destapó algo grande: OPFS nunca funcionó en producción. createSyncAccessHandle() (la API sync que usaba ADR-0002) no existe en MV3 service workers — solo en dedicated workers. Desde v1.4.0 la extensión corría siempre en memoria-fallback; el archivo OPFS quedaba en 0 líneas. El mock lo ocultaba (implementaba el handle sync). Misma clase de bug que B1.

Fix (ADR-0003, supersede ADR-0002 en el write mechanism): reescrito a la API OPFS async (createWritable + getFile), que sí funciona en el SW. Append sync + flush batcheado por microtask.

Pausa/continuar: verbos PAUSE/RESUME + botones en el popup. restoreFromExisting() (existía con 0 callers) cableado al wake del SW → reconstruye contador+dedup desde disco. START trunca (sesión nueva), RESUME appendea.

Validado: unit 78/78 + e2e 2/2, incluyendo un test que mata el SW con CDP ServiceWorker.stopAllWorkers y verifica que la grabación sobrevive. Bump 1.4.2 → 1.5.0.

Cristian Tala and others added 6 commits June 24, 2026 17:39
… Copy Cookies + B7/B8/B10 + badge contador

Informado por una captura real en LinkedIn (58 reqs):
- FILTRO ARREGLADO: el preset no narrowaba (capturaba todo) porque el popup
  guardaba las patterns como string pero las aplicaba como array → vaciaba el
  filtro. Consolidado a FUENTE ÚNICA: el popup carga capture-config.js y usa sus
  PRESETS + parser canónicos (mata B19). Patterns del preset ya no round-trippean
  por el textarea (origen del bug); textarea = filtros extra opcionales.
- Preset LinkedIn a endpoints REALES 2026: /voyager/api/ + /rsc-action/
  (flagship-web RSC) + /api/graphql; EXCLUDE de telemetría/estáticos (trackO11y,
  sensorCollect, /li/track, static.licdn.com). shouldCapture acepta exclude.
- B10: x-restli-protocol-version ya no se redacta (constante, para replay).
- B7: XHR captura headers (setRequestHeader + getAllResponseHeaders).
- B8: fetch(Request) lee method/headers del Request.
- URLs relativas resueltas a absolutas (injected.js) antes de filtrar.
- Default preset = Generic.
- Badge: contador de requests EN VIVO restaurado (rojo/ámbar, sin parpadeo).

COPY COOKIES (feature nueva): botón en popup que copia las cookies de auth del
sitio incluyendo httpOnly (li_at/JSESSIONID que fetch no puede leer) vía
chrome.cookies, para replay. Permission cookies. NO se guardan en la captura.

Tests: capture-config exclude + preset real; e2e nuevos (filtro narrowea/excluye,
popup fuente única + B10, Copy Cookies httpOnly). Unit 78/78 + e2e 5/5. Bump 1.6.0.

Co-Authored-By: Claude <noreply@anthropic.com>
- Cookies: el botón ahora DESCARGA un .json estructurado (url, host, count,
  cookieHeader listo para curl/Postman, cookies[]) en vez de copiar al
  portapapeles — asset reusable para replay.
- Quitado el formato JSON array legacy v1.2.x: salida siempre JSON-Lines
  (selector removido del popup + rama json-array del SW).
- PRIVACY-POLICY actualizada: declara el permiso cookies (+ unlimitedStorage)
  con justificación read-only/on-demand/local/no-transmitido — requisito de
  revisión del Chrome Web Store al agregar 'cookies'.

Unit 78/78 + e2e 5/5.

Co-Authored-By: Claude <noreply@anthropic.com>
… Store)

La política (PRIVACY-POLICY.md + store-assets/privacy-policy-hosteable.html)
afirmaba 'Does NOT use cookies' / 'no request cookies' — contradice el feature
Download Cookies y causaría rechazo del store review. Corregido: el permiso
cookies se declara con justificación (read-only, on-demand al click del usuario,
guardado local, nunca transmitido) + sección dedicada en ambos docs.

Co-Authored-By: Claude <noreply@anthropic.com>
- README: badge 1.7.0, What It Does preciso (JSONL, no json-array), sección
  Features (pausa/continuar, cookies, redacción, presets, OPFS), puntero a CHANGELOG.
- STORE-LISTING: descripción actualizada (JSONL + features nuevas), output
  format JSONL (no el json-array viejo), justificaciones de permisos cookies +
  unlimitedStorage para la pestaña Privacy practices del Chrome Web Store.

Co-Authored-By: Claude <noreply@anthropic.com>
Captura el diseño acordado de WebSocket/SSE capture (modelo conexión+frames,
JSONL ordenado por connId+seq, auth vía cookies/URL/subprotocolo, scope honesto)
+ principios (100% local, single-purpose, sin backend) + lo descartado con razón
(copy-as-cURL lo cubre DevTools; export Postman/OpenAPI es la diferenciación
real). Alinea el roadmap de STORE-LISTING + puntero en README.

Co-Authored-By: Claude <noreply@anthropic.com>
…tización

La extensión reversea el protocolo; NO implica meter una conexión WS persistente
en el actor de Apify (run-based, mal fit). Aclarado: enviar suele ser HTTP POST
(→ write action en el actor), escuchar realtime → microservicio always-on
(Coolify/n8n/Spark), nunca Apify.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant