ext: S1 native-messaging spike (ADR 0003) + WebSocket transport
Spike S1 — run on the target machine through real snap confinement
(apparmor snap.firefox.firefox enforced; web-ext's direct-exec of the inner
binary bypasses it, so runs were forced through `snap run firefox`):
- manifest in ~/.mozilla/native-messaging-hosts/ -> WORKS; host launched
unconfined with real $HOME and real $XDG_RUNTIME_DIR, bound a socket in
the real /run/user/<uid>. Corroborated by the machine's 1Password host.
- ~/snap/firefox/common/.mozilla/native-messaging-hosts/ -> not read
- /usr/lib/mozilla/native-messaging-hosts/ -> not read
- flatpak path -> N/A (snap Firefox)
Decision: WebSocket stays the default; native messaging is an opportunistic
upgrade taken only when its handshake succeeds. docs/05 §4 corrected in this
commit to point the snap manifest at ~/.mozilla and mark /usr/lib as
deb/tarball-only. ADR carries a self-contained reproduction; the scratch
harness has been removed.
transport/ (build order item 1):
- types.ts VeloxTransport interface + error taxonomy
- rpc.ts JSON-RPC id correlation, per-call deadline, AbortSignal
- backoff.ts exponential backoff with jitter
- discovery.ts 52000-52016 scan ordering (last-good port first)
- websocket.ts scan -> session.hello -> auto-pair (token in
storage.local) -> reconnect; -32001 fatal, refused/
rate-limited pairing latches needsPairing (no retry storm);
a mid-handshake drop aborts hello immediately
- native.ts connectNative(); distinguishes "not installed" (fatal,
lets the picker fall through) from a crash (reconnect)
- index.ts createTransport() runtime picker + persisted Options override
Toolchain: package.json / tsconfig (strict) / vitest; webextension-polyfill
mocked. 38 tests, incl. the WS suite against a real loopback ws server.
No manifest.json yet, so CI's extension-lint guard stays a no-op.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// Global test setup: a mock of `webextension-polyfill` with an in-memory
|
||||
// storage.local, so storage.ts and the transport picker run without a browser.
|
||||
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('webextension-polyfill', () => {
|
||||
const store = new Map<string, unknown>();
|
||||
const local = {
|
||||
get: async (keys?: string | string[] | Record<string, unknown> | null) => {
|
||||
if (keys == null) return Object.fromEntries(store);
|
||||
const names =
|
||||
typeof keys === 'string' ? [keys] : Array.isArray(keys) ? keys : Object.keys(keys);
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k of names) if (store.has(k)) out[k] = store.get(k);
|
||||
return out;
|
||||
},
|
||||
set: async (obj: Record<string, unknown>) => {
|
||||
for (const [k, v] of Object.entries(obj)) store.set(k, v);
|
||||
},
|
||||
remove: async (keys: string | string[]) => {
|
||||
for (const k of typeof keys === 'string' ? [keys] : keys) store.delete(k);
|
||||
},
|
||||
clear: async () => {
|
||||
store.clear();
|
||||
},
|
||||
};
|
||||
return {
|
||||
default: {
|
||||
storage: { local },
|
||||
runtime: {
|
||||
getURL: (path = '/') =>
|
||||
`moz-extension://11111111-2222-3333-4444-555555555555${path}`,
|
||||
connectNative: () => {
|
||||
throw new Error('browser.runtime.connectNative was not stubbed for this test');
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const browser = (await import('webextension-polyfill')).default;
|
||||
await browser.storage.local.clear();
|
||||
});
|
||||
Reference in New Issue
Block a user