ext: pairing-restart test + AMO permission justification

tests/transport/storage.test.ts covers the storage half of "the pairing
token survives a browser restart" (round-trip, unpair-clears, corrupted
override falls back to auto). websocket.test.ts adds the transport half:
a fresh WebSocketTransport instance over the same backing store reuses
the persisted token with no re-pairing, plus pairWithCode/unpair
coverage. "Wrong token rejected and rate-limited" was already covered
(websocket.test.ts's NotPaired/RateLimited cases).

docs/amo-permissions.md is the submission-ready permission justification
for AMO's Notes to Reviewer field, covering every permission in
manifest.json plus what was deliberately not requested and how cookie/
header data is handled.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
This commit is contained in:
2026-09-11 12:25:42 +04:00
co-authored by Claude Sonnet 5
parent 2cb1959bff
commit 25c171f742
5 changed files with 190 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
// browser.storage.local is backed by the real add-on profile on disk, so anything
// written through transport/storage.ts is what "survives a browser restart" means in
// practice — this covers the storage half of that DoD item; websocket.test.ts's
// "survives a browser restart" case covers the transport half (a fresh transport
// instance reusing a persisted token with no re-pairing).
import { beforeEach, describe, expect, it } from 'vitest';
import * as storage from '../../src/background/transport/storage.js';
describe('transport/storage', () => {
beforeEach(async () => {
await browser.storage.local.clear();
});
it('round-trips the pairing token', async () => {
expect(await storage.getToken()).toBeNull();
await storage.setToken('tok-abc');
expect(await storage.getToken()).toBe('tok-abc');
});
it('clears the token on unpair (null)', async () => {
await storage.setToken('tok-abc');
await storage.setToken(null);
expect(await storage.getToken()).toBeNull();
});
it('a fresh read after a simulated restart still sees the persisted token', async () => {
await storage.setToken('tok-survives');
// Nothing here recreates browser.storage.local — that's the point: it is the one
// thing in the extension that outlives the background page's lifetime, restart
// included. A second, independent read call stands in for "the page reloaded".
expect(await storage.getToken()).toBe('tok-survives');
expect(await storage.getToken()).toBe('tok-survives');
});
it('round-trips the transport override, defaulting to auto', async () => {
expect(await storage.getOverride()).toBe('auto');
await storage.setOverride('uds');
expect(await storage.getOverride()).toBe('uds');
await storage.setOverride('ws');
expect(await storage.getOverride()).toBe('ws');
});
it('ignores a corrupted override value and falls back to auto', async () => {
await browser.storage.local.set({ 'velox.transportOverride': 'not-a-transport' });
expect(await storage.getOverride()).toBe('auto');
});
it('round-trips the cached WebSocket port', async () => {
expect(await storage.getCachedWsPort()).toBeNull();
await storage.setCachedWsPort(52003);
expect(await storage.getCachedWsPort()).toBe(52003);
await storage.setCachedWsPort(null);
expect(await storage.getCachedWsPort()).toBeNull();
});
});
@@ -93,6 +93,59 @@ describe('WebSocketTransport', () => {
expect(lastHello?.token).toBe('tok-issued-1');
});
it('the pairing token survives a browser restart: a fresh transport instance over the same storage reuses it, no re-pairing', async () => {
const port = nextPort();
daemon = await FakeDaemon.start({ port, acceptToken: null });
const h = memDeps(); // stands in for browser.storage.local, which outlives the page
transport = makeTransport(port, h);
await transport.connect();
expect(daemon.pairCount).toBe(1);
expect(h.store.token).toBe('tok-issued-1');
transport.disconnect();
// Simulate "the browser restarted": a brand new transport instance, same backing
// store (in reality, the same on-disk profile), no in-memory state carried over.
const restarted = makeTransport(port, h);
await restarted.connect();
try {
expect(restarted.state).toBe('connected');
expect(daemon.pairCount).toBe(1); // still just the one pairing, ever
const lastHello = [...daemon.seen].reverse().find((s) => s.method === 'session.hello');
expect(lastHello?.token).toBe('tok-issued-1');
} finally {
restarted.disconnect();
}
});
it('pairWithCode drops any stale token and pairs fresh with the typed code', async () => {
const port = nextPort();
daemon = await FakeDaemon.start({ port, acceptToken: 'stale' });
const h = memDeps({ token: 'stale' });
transport = makeTransport(port, h);
await transport.pairWithCode('4821');
expect(transport.state).toBe('connected');
expect(daemon.pairCount).toBe(1);
const pairCall = daemon.seen.find((s) => s.method === 'session.pair');
expect((pairCall?.params as { code?: string }).code).toBe('4821');
expect(h.store.token).toBe('tok-issued-1');
});
it('unpair clears the stored token and disconnects', async () => {
const port = nextPort();
daemon = await FakeDaemon.start({ port, acceptToken: 'good-token' });
const h = memDeps({ token: 'good-token' });
transport = makeTransport(port, h);
await transport.connect();
await transport.unpair();
expect(h.store.token).toBeNull();
expect(transport.state).toBe('disconnected');
});
it('with autoPair off, a wrong token surfaces needsPairing and does NOT retry', async () => {
const port = nextPort();
daemon = await FakeDaemon.start({ port, acceptToken: 'the-real-one' });