Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32eb8eb8ee |
Vendored
+8
@@ -41,6 +41,7 @@ async function ensureAttached(tabId, aggressiveRetry = false) {
|
||||
const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2;
|
||||
const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500;
|
||||
let lastError = "";
|
||||
const preservedNetworkCapture = networkCaptures.get(tabId);
|
||||
for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) {
|
||||
try {
|
||||
try {
|
||||
@@ -85,6 +86,13 @@ async function ensureAttached(tabId, aggressiveRetry = false) {
|
||||
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
||||
} catch {
|
||||
}
|
||||
if (preservedNetworkCapture) {
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, "Network.enable");
|
||||
networkCaptures.set(tabId, preservedNetworkCapture);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
async function evaluate(tabId, expression, aggressiveRetry = false) {
|
||||
const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2;
|
||||
|
||||
@@ -393,3 +393,75 @@ describe('cdp download waits', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cdp network capture survives forced re-attach', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function createReattachMock() {
|
||||
const onDetachListeners: Array<(source: { tabId?: number }) => void> = [];
|
||||
let failNextHealthCheck = false;
|
||||
let networkEnableCount = 0;
|
||||
const debuggerApi = {
|
||||
attach: vi.fn(async () => {}),
|
||||
detach: vi.fn(async ({ tabId }: { tabId?: number }) => {
|
||||
// Chrome fires onDetach whenever the debugger detaches from a tab.
|
||||
for (const fn of onDetachListeners) fn({ tabId });
|
||||
}),
|
||||
sendCommand: vi.fn(async (_target: unknown, method: string, params?: any) => {
|
||||
if (method === 'Runtime.evaluate' && params?.expression === '1') {
|
||||
if (failNextHealthCheck) {
|
||||
failNextHealthCheck = false;
|
||||
throw new Error('Inspected target navigated or closed');
|
||||
}
|
||||
return { result: { value: '1' } };
|
||||
}
|
||||
if (method === 'Network.enable') {
|
||||
networkEnableCount += 1;
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
onDetach: { addListener: vi.fn((fn: (s: { tabId?: number }) => void) => { onDetachListeners.push(fn); }) },
|
||||
onEvent: { addListener: vi.fn() },
|
||||
};
|
||||
const tabs = {
|
||||
get: vi.fn(async () => ({ id: 1, windowId: 1, url: 'https://x.com/home' })),
|
||||
onRemoved: { addListener: vi.fn() },
|
||||
onUpdated: { addListener: vi.fn() },
|
||||
};
|
||||
return {
|
||||
chrome: { tabs, debugger: debuggerApi, scripting: {}, runtime: { id: 'opencli-test' } },
|
||||
debuggerApi,
|
||||
failNextHealthCheck: () => { failNextHealthCheck = true; },
|
||||
networkEnableCount: () => networkEnableCount,
|
||||
};
|
||||
}
|
||||
|
||||
it('preserves armed network capture and re-enables Network across a forced re-attach', async () => {
|
||||
const mock = createReattachMock();
|
||||
vi.stubGlobal('chrome', mock.chrome);
|
||||
|
||||
const mod = await import('./cdp');
|
||||
// Wire the onDetach handler that wipes networkCaptures on detach.
|
||||
mod.registerListeners();
|
||||
|
||||
await mod.startNetworkCapture(1);
|
||||
expect(mod.hasActiveNetworkCapture(1)).toBe(true);
|
||||
const enablesAfterStart = mock.networkEnableCount();
|
||||
|
||||
// The next ensureAttached health-check throws, forcing a detach + re-attach.
|
||||
// The detach fires onDetach (which deletes the capture) and disables the
|
||||
// Network domain — the capture must be restored, not silently dropped.
|
||||
mock.failNextHealthCheck();
|
||||
await mod.ensureAttached(1);
|
||||
|
||||
expect(mod.hasActiveNetworkCapture(1)).toBe(true);
|
||||
expect(mock.networkEnableCount()).toBeGreaterThan(enablesAfterStart);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,6 +101,15 @@ export async function ensureAttached(tabId: number, aggressiveRetry: boolean = f
|
||||
const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500;
|
||||
let lastError = '';
|
||||
|
||||
// The forced detach below fires chrome.debugger.onDetach, whose handler wipes
|
||||
// this tab's armed network-capture state; detaching also disables the CDP
|
||||
// Network domain. Snapshot the capture so we can restore it after a successful
|
||||
// re-attach instead of silently dropping in-flight capture — otherwise any
|
||||
// non-navigate command that triggers a re-attach (a stale-attach health-check
|
||||
// failure during SPA navigation or third-party debugger interference) leaves
|
||||
// network-capture-read returning [] even though requests fired.
|
||||
const preservedNetworkCapture = networkCaptures.get(tabId);
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) {
|
||||
try {
|
||||
// Force detach first to clear any stale state from other extensions
|
||||
@@ -153,6 +162,21 @@ export async function ensureAttached(tabId: number, aggressiveRetry: boolean = f
|
||||
} catch {
|
||||
// Some pages may not need explicit enable
|
||||
}
|
||||
|
||||
// Restore network capture that the re-attach (detach + onDetach) tore down.
|
||||
// The detach always disables the CDP Network domain, so re-enable it and put
|
||||
// the accumulated capture state back unconditionally. Done last (after the
|
||||
// awaits above) so it wins over the onDetach handler's delete, which fires
|
||||
// while those awaits yield to the event loop.
|
||||
if (preservedNetworkCapture) {
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Network.enable');
|
||||
networkCaptures.set(tabId, preservedNetworkCapture);
|
||||
} catch {
|
||||
// Leave capture cleared rather than arm a half-attached Network domain;
|
||||
// the next start-capture re-arms cleanly.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function evaluate(tabId: number, expression: string, aggressiveRetry: boolean = false): Promise<unknown> {
|
||||
|
||||
Reference in New Issue
Block a user