Skip to content

Testing and Debugging

magicelk235 edited this page Jul 22, 2026 · 10 revisions

Testing and Debugging

How the converter is tested, how to run the suite, the manifest corpus behind it, and the field protocol for debugging a converted extension running live in Safari.

Read this first — the suite is a local artifact, not part of the public repo. In .gitignore, under the comment "Tests, per-extension reports, and project notes — kept local, not pushed":

test/
reports/
CLAUDE.md

Only one test file is committed to the public repo: test/content-blocker.test.js — it predates the ignore rule and was never un-tracked. git ls-files test/ returns exactly that one path. The working tree holds ~25 test files and a reports/extension-tests/ folder, but those are local-only developer artifacts. This page documents the real local suite so a maintainer with the working tree can run and extend it — it does not describe what a fresh git clone gets. A clean checkout ships the single content-blocker test.


Running the tests

npm test          # build, then run every test in test/ under node:test
npm run typecheck # tsc --noEmit — types only, no build

npm test is defined as:

"test": "npm run build && node --test test/*.test.js"

Two things follow from that definition:

  1. The build is mandatory and runs first. Every test imports from dist/, not from src/ — e.g. content-blocker.test.js does import { scanExtension } from "../dist/analyze/analyze.js" and verify.test.js does import { parseEnabled } from "../dist/build/verify.js". Running the tests without a fresh build tests stale compiled output (or fails outright if dist/ is missing). npm test guards against this by always building; if you invoke the runner directly, build yourself first. This is the same "always build before running dist/ or the tests" rule that governs the CLI (see Architecture and Build and Install).
  2. test/*.test.js is a shell glob, not a runner feature. Your shell expands it to the file list that node --test receives. The runner is Node's built-in node:test — no Jest, Mocha, Vitest, or other dependency. Tests use node:test's test() and node:assert/strict; several build a temp dir under os.tmpdir(), write a manifest.json, run the real pipeline function against it, and clean up in a finally.

npm run typecheck runs tsc --noEmit and is the fast "did I break the types" check — it does not emit dist/ and does not run any test.


The test suite (local-only)

The working tree carries ~25 test/*.test.js files, ~127 test(...) cases in total. Every one except content-blocker.test.js is gitignored and lives only on the maintainer's machine. Grouped by the subsystem each exercises:

Runtime shim — messaging, actions, injection

File Committed? Covers
action-click-bridge.test.js local chrome.action/browserAction click → popup/handler bridge
action-hotkey.test.js local keyboard-command → action dispatch
action-popup-autowire.test.js local auto-wiring a declared default_popup when the extension expects a click event
sendmessage-relay-scope.test.js local runtime.sendMessage relay targeting / scope correctness
sender-url-normalize.test.js local normalizing sender.url / sender.origin (the UUID-case-mismatch class of bug)
ancestor-origins-guard.test.js local guarding location.ancestorOrigins access on Safari
page-world-inject.test.js local MAIN-world vs isolated-world script injection
color-scheme-inject.test.js local prefers-color-scheme injection into pages
self-page-url-rewrite.test.js local rewriting the extension's own page URLs
offscreen-response.test.js local offscreen-document request/response emulation
native-messaging-handler.test.js local native-messaging host handshake / message handling
xhr-proxy.test.js local the XHR/fetch proxying path
mv2-background-shim.test.js local MV2 background-page shim behavior
webpack-chunk-preregister.test.js local pre-registering webpack chunk loaders so bundles resolve on Safari

Manifest transform & analysis

File Committed? Covers
content-blocker.test.js public (only one) scanExtension firing the content-blocker issue on uBO-shaped MV2 blocking-webRequest manifests
menus-ftp-pattern.test.js local menus/contextMenus URL-pattern handling incl. ftp:
command-shortcut-cap.test.js local capping/validating declared command shortcuts
dynamic-url-strip.test.js local stripping unsupported dynamic-URL constructs
scheme-rewrite.test.js local URL-scheme rewriting in the manifest
empty-content-scripts.test.js local handling an empty/absent content_scripts array
derive-app-name.test.js local deriving the Safari app name from the extension

Build / packaging / CDP

File Committed? Covers
verify.test.js local parseEnabled — reading pluginkit -mv election columns (+/-/!/=/?/blank) to tell if the .appex is enabled
cdp-manifest.test.js local Chrome DevTools Protocol manifest path
cdp-debugger.test.js local CDP debugger integration (largest suite, ~19 cases)

Parsing / codegen primitives

File Committed? Covers
balanced-paren.test.js local balanced-parenthesis / brace scanning used by the code transforms

The single public test, content-blocker.test.js, is representative of the house style: node:test + node:assert/strict, a temp-dir helper that writes a manifest and runs the real dist/ function, no mocking framework. Add a test for any non-trivial logic change — the suite is the regression net for the 50+ real extensions the project converts.


The corpus

The suite is backed by a manifest corpus — a large set of real Chrome extensions the conversion logic is stress-tested against:

npm run corpus   # build, then stress-test conversion against the local manifest corpus

Historically defined as npm run build && node scripts/corpus.mjs. Both the corpus and its inputs are local-only: the real extensions live in a test extensions/ directory that is gitignored ("Local test-extension corpus (large binaries; not source)"), and the scripts/ directory that held corpus.mjs was later removed from the repo (commit 7f971ad "chore: remove scripts/ and docs/ dirs"). So npm run corpus is a maintainer-machine workflow, not something a clone can run. The test/ suite is the durable, checked-in-locally regression net distilled from what the corpus runs surfaced.

Where the tests came from. The suite and CI gate were added in commit 0dd9519 "test: add test suite + CI gate; fix bugs found by corpus run" — the tests exist to lock in bugs the corpus run had just exposed. Coverage was then grown file by file:

  • 6080770defaultBundleId / Safari-registration bundle-id path
  • 78976abapplyDnr error paths + Safari rule-limit warnings
  • 5beb49b — pin deriveChromeId against Chrome's extension-id algorithm
  • 748a9ebextract.ts: CRX3 parsing, folder unwrap, zip-slip guard, error paths
  • 2cc6c34scanExtension: API/URL/file/icon/i18n detections + no-false-positive guard
  • 1156251analyzeManifest: CSP remote-script + MV3 host-misplacement detection
  • 7e8477dapplyDnr: modifyHeaders stripping, missing-file notes, keyless-id fallback
  • b8d8c31 — icon PNG synthesis (was untested)

How the bugs were found — the multi-agent bug-hunt. The project's QA method is a repeated multi-agent audit: many agents read the codebase and cross-reference it against the real-world corpus, and each pass lands as a batch fix. The commit log is full of them — fix: 28 bugs from multi-agent bug hunt (shim semantics, manifest, OAuth, analyze), fix: 13 bugs from multi-agent bug hunt (iOS build, crashes, OAuth, codegen), fix: 22 issues from multi-agent bug hunt (runtime, CLI, analyze accuracy), fix: 8 more shim runtime gaps found via real-world corpus cross-reference, and several deep multi-agent audit rounds. That process is what produced both the bug fixes and the regression tests that pin them.


Live-Safari debug protocol

When a converted extension misbehaves in Safari, the failure is usually a Safari platform quirk the shim hasn't handled, not a plain logic bug — and Safari's error surface is misleading. Follow this protocol (it is the authoritative process from the project's historical CLAUDE.md):

  1. Do not guess from pasted console errors. They are frequently stale or symptoms, not causes. Treat any console dump as a lead to verify, never as the diagnosis. Diagnose from live evidence instead.

  2. Drive diagnostics from the background-page console. The background-page console is reliable; the popup/popover console relay is flaky and will mislead you. Open Safari → Develop → Web Extension Background Pages and work from there.

  3. Use __C2S_DEBUG__ to capture structured state. The shim has a __C2S_DEBUG__ flag at the top of src/runtime/safari-compat-shim.js (var __C2S_DEBUG__ = false;), and diagnostic traces are gated behind it — the tracer reads DBG = (typeof __C2S_DEBUG__ !== "undefined") && __C2S_DEBUG__ and stays silent unless it's flipped on. For a diagnostic build, flip it on and have the shim write structured state to chrome.storage.local, then read that back from the background-page console. Flip __C2S_DEBUG__ OFF and strip the temporary traces before committing. See Runtime Shim for the flag and the traced sections.

  4. Trace the failing flow link by link — not the end symptom. Chain it out: outcome → did the request reach the background port → did the background post a reply → is the privileged flag set → what are the exact string values being compared. Each link narrows the cause to a single fact instead of a vague symptom.

  5. Reproduce the suspected Safari behavior in a Node vm context and prove the fix locally first. Safari's native namespaces (chrome/browser and members like chrome.scripting) are frozen / immutable / exotic in ways a plain object mock will not reveal. Recreate the real conditions in a vm context using Object.freeze, accessor/getter descriptors, live globals, and case-mismatched origins, and confirm the fix there before you reinstall. This is exactly what several shim tests do.

  6. Reinstall cleanly, then verify the new shim actually shipped. A stale install is a common false "the fix didn't work."

    node dist/cli.js --uninstall <App>
    rm -rf <App>_Safari      # stale output dir → ENOTEMPTY on the next build
    node dist/cli.js <input> --install --force --clean

    Then quit and reopen Safari before retesting. Confirm the freshly built shim reached the installed bundle by grepping a marker in the appex's copy — grep <marker> …/Contents/Resources/safari-compat-shim.js. See CLI Reference for --uninstall / --install / --force / --clean (and --doctor for environment checks) and Build and Install for the reinstall/verify flow.

  7. Distinguish shim-fixable from platform limits. Some failures are Safari or extension-internal, not converter bugsWebAuthn / passkey RPID mismatches, WASM-SDK chunk loaders, and webfont CSP refusals. Document them and move on; don't chase them as if the shim could fix them.

  8. After the session, write it up. Record a per-extension report and keep the README.md status table current (see below).


Per-extension reports (local-only)

After a debug session, the outcome is captured as a per-extension report under reports/extension-tests/, following a problem → process → solution → what's left structure. Like test/, reports/ is gitignored — these are local QA notes, not shipped to the public repo.

They were introduced in commit 7bd822d "docs: add per-extension Safari test reports + live-testing debug protocol", which added reports for real extensions — uBlock-Origin.md, Bitwarden.md, Grammarly.md, and an index README.md (e.g. "uBlock (working), Bitwarden (UI works; WASM SDK …)"). They record which extensions convert cleanly, which have platform-limited pieces, and what remains open.


See also

  • Runtime Shimsafari-compat-shim.js, the __C2S_DEBUG__ flag, and the traced sections
  • CLI Reference--uninstall, --install, --force, --clean, --doctor
  • Build and Install — the reinstall cycle and verifying the shim reached the installed .appex
  • Architecture — the pipeline and the build-before-dist/ rule

Clone this wiki locally