Files
openclaw/extensions/browser/chrome-extension/background.access-mode.test.ts
2026-08-13 15:44:15 -07:00

1071 lines
37 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanupBackgroundHarnesses,
loadBackground,
TEST_RELAY_KEY,
REPLACEMENT_TEST_RELAY_KEY,
sendRuntimeMessage,
} from "./background.test-harness.js";
const RELAY_WATCHDOG_ALARM = "openclaw-relay-watchdog";
describe("relay command authorization", () => {
beforeEach(() => {
vi.resetModules();
});
afterEach(async () => {
await cleanupBackgroundHarnesses();
vi.unstubAllGlobals();
});
it("rejects every authority-bearing command after tab-group revocation", async () => {
const harness = await loadBackground();
const socket = harness.sockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
harness.shareTab(41);
harness.unshareTab(41);
socket.receive({ type: "attach", seq: 1, tabId: 41 });
socket.receive({ type: "cdp", seq: 2, tabId: 41, method: "Runtime.evaluate" });
socket.receive({ type: "closeTab", seq: 3, tabId: 41 });
socket.receive({ type: "activateTab", seq: 4, tabId: 41 });
await vi.waitFor(() => {
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(
frames
.filter((frame) => frame.type === "error")
.map((frame) => frame.seq)
.toSorted((left, right) => left - right),
).toEqual([1, 2, 3, 4]);
});
expect(harness.debuggerAttach).not.toHaveBeenCalled();
expect(harness.debuggerSendCommand).not.toHaveBeenCalled();
expect(harness.tabsRemove).not.toHaveBeenCalled();
expect(harness.tabsUpdate).not.toHaveBeenCalled();
expect(harness.windowsUpdate).not.toHaveBeenCalled();
});
it("controls an eligible ungrouped tab in all mode", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 41, url: "https://example.com/all", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
const hello = socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.find((frame) => frame.type === "hello");
expect(hello.tabs).toContainEqual(
expect.objectContaining({ tabId: 41, url: "https://example.com/all" }),
);
socket.receive({ type: "attach", seq: 20, tabId: 41 });
socket.receive({ type: "cdp", seq: 21, tabId: 41, method: "Runtime.evaluate" });
await vi.waitFor(() => {
expect(harness.debuggerAttach).toHaveBeenCalledWith({ tabId: 41 }, "1.3");
expect(harness.debuggerSendCommand).toHaveBeenCalledWith(
{ tabId: 41 },
"Runtime.evaluate",
{},
);
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({ type: "result", seq: 21, result: {} });
});
});
it("closes the old selected relay before a replacement pairing widens access", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "selected",
},
initialTabs: [{ id: 45, url: "https://example.com/private", groupId: -1 }],
});
const oldSocket = harness.relaySockets[0];
if (!oldSocket) {
throw new Error("expected old relay socket");
}
await harness.authenticate(oldSocket);
await expect(
sendRuntimeMessage(harness, {
type: "pair",
pairingString: `ws://127.0.0.1:18798/extension#${REPLACEMENT_TEST_RELAY_KEY}`,
accessMode: "all",
}),
).resolves.toEqual({ ok: true });
expect(oldSocket.close).toHaveBeenCalledOnce();
const oldFrames = oldSocket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(
oldFrames.some(
(frame) =>
frame.type === "tabs" && frame.tabs?.some((tab: { tabId?: number }) => tab.tabId === 45),
),
).toBe(false);
const replacement = harness.relaySockets.find(
(socket) => socket.url === "ws://127.0.0.1:18798/extension",
);
if (!replacement) {
throw new Error("expected replacement relay socket");
}
await harness.authenticate(replacement);
const replacementHello = replacement.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.find((frame) => frame.type === "hello");
expect(replacementHello.tabs).toContainEqual(expect.objectContaining({ tabId: 45 }));
});
it("cancels a stale lifecycle connection across replacement pairing", async () => {
const harness = await loadBackground();
const oldSocket = harness.relaySockets[0];
if (!oldSocket) {
throw new Error("expected old relay socket");
}
await harness.authenticate(oldSocket);
oldSocket.close();
const releaseConfigRead = harness.deferNextStorageGet();
harness.alarmListener({ name: RELAY_WATCHDOG_ALARM });
const pairing = sendRuntimeMessage(harness, {
type: "pair",
pairingString: `ws://127.0.0.1:18798/extension#${REPLACEMENT_TEST_RELAY_KEY}`,
accessMode: "all",
});
releaseConfigRead();
await expect(pairing).resolves.toEqual({ ok: true });
await vi.waitFor(() => {
expect(
harness.relaySockets.filter((socket) => socket.url === "ws://127.0.0.1:18798/extension"),
).toHaveLength(1);
});
expect(
harness.relaySockets.filter((socket) => socket.url === "ws://127.0.0.1:18797/extension"),
).toHaveLength(1);
});
it("waits for access initialization before changing the stored mode", async () => {
const harness = await loadBackground({
deferTabAccessInitialization: true,
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
});
harness.storageSet.mockClear();
const response = vi.fn();
harness.messageListener({ type: "setAccessMode", accessMode: "selected" }, {}, response);
await Promise.resolve();
await Promise.resolve();
expect(response).not.toHaveBeenCalled();
expect(harness.storageSet).not.toHaveBeenCalled();
harness.releaseTabAccessInitialization();
await vi.waitFor(() => {
expect(response).toHaveBeenCalledWith({ ok: true, accessMode: "selected" });
});
});
it("waits for access initialization before toggling an all-mode tab", async () => {
const harness = await loadBackground({
deferTabAccessInitialization: true,
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 46, url: "https://example.com/pending-init", groupId: -1 }],
});
harness.tabsGet.mockClear();
const response = vi.fn();
harness.messageListener(
{ type: "toggleTabAccess", tabId: 46, accessMode: "all", grant: false },
{},
response,
);
await Promise.resolve();
await Promise.resolve();
expect(response).not.toHaveBeenCalled();
expect(harness.tabsGet).not.toHaveBeenCalled();
expect(harness.tabsGroup).not.toHaveBeenCalled();
harness.releaseTabAccessInitialization();
await vi.waitFor(() => {
expect(response).toHaveBeenCalledWith({ ok: true, accessible: false, denied: true });
});
expect(harness.sessionStorageValues.deniedTabIdsV1).toEqual([46]);
expect(harness.tabsGroup).not.toHaveBeenCalled();
});
it.each([undefined, null, Number.NaN, -1, 1.5, "41"])(
"rejects malformed getTabAccess tab id %s without querying Chrome",
async (tabId) => {
const harness = await loadBackground();
harness.tabsGet.mockClear();
await expect(sendRuntimeMessage(harness, { type: "getTabAccess", tabId })).resolves.toEqual({
accessMode: "selected",
accessible: false,
eligible: false,
denied: false,
});
expect(harness.tabsGet).not.toHaveBeenCalled();
},
);
it.each([
{
accessMode: "all",
label: "restricted",
tab: { id: 51, url: "chrome://settings", groupId: 7 },
},
{
accessMode: "selected",
label: "restricted",
tab: { id: 51, url: "chrome://settings", groupId: 7 },
},
{
accessMode: "all",
label: "incognito",
tab: { id: 52, url: "https://secret.example", incognito: true, groupId: 7 },
},
{
accessMode: "selected",
label: "incognito",
tab: { id: 52, url: "https://secret.example", incognito: true, groupId: 7 },
},
])("rejects an $label tab in $accessMode mode", async ({ accessMode, tab }) => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode,
},
initialTabs: [tab],
});
harness.shareTab(tab.id);
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 22, tabId: tab.id });
await vi.waitFor(() => {
const frame = socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.find((candidate) => candidate.type === "error" && candidate.seq === 22);
expect(frame?.message).toMatch(/restricted|incognito/);
});
expect(harness.debuggerAttach).not.toHaveBeenCalled();
});
it("invalidates an in-flight CDP command when all mode downgrades to selected", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 61, url: "https://example.com/race", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 23, tabId: 61 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalled());
let releaseCommand = () => {};
harness.debuggerSendCommand.mockImplementationOnce(
async () =>
await new Promise<Record<string, never>>((resolve) => {
releaseCommand = () => resolve({});
}),
);
socket.receive({ type: "cdp", seq: 24, tabId: 61, method: "Runtime.evaluate" });
await vi.waitFor(() => expect(harness.debuggerSendCommand).toHaveBeenCalled());
const changingMode = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
releaseCommand();
await expect(changingMode).resolves.toMatchObject({ ok: true, accessMode: "selected" });
await vi.waitFor(() => {
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 61 });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 24,
message: "tab 61 access was revoked",
});
});
});
it("revokes all-mode authority before a queued downgrade reaches the mutation queue", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 204, url: "https://example.com/queued-downgrade", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerEventListener) {
throw new Error("expected relay and debugger event listener");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 40, tabId: 204 });
await vi.waitFor(() =>
expect(harness.debuggerAttach).toHaveBeenCalledWith({ tabId: 204 }, "1.3"),
);
harness.storageSet.mockClear();
socket.send.mockClear();
const releaseOlderMutation = harness.deferNextStorageSet();
const olderMutation = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "all",
});
await vi.waitFor(() => expect(harness.storageSet).toHaveBeenCalledWith({ accessMode: "all" }));
const downgrading = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
await expect(
sendRuntimeMessage(harness, { type: "getTabAccess", tabId: 204 }),
).resolves.toMatchObject({ accessible: false });
harness.debuggerEventListener({ tabId: 204 }, "Runtime.consoleAPICalled", {});
expect(
socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.some((frame) => frame.type === "cdpEvent"),
).toBe(false);
releaseOlderMutation();
await expect(olderMutation).resolves.toEqual({ ok: true, accessMode: "all" });
await expect(downgrading).resolves.toEqual({ ok: true, accessMode: "selected" });
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 204 });
});
it("rejects a stale tab action when a queued mode change executes first", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 205, url: "https://example.com/stale-action", groupId: -1 }],
});
harness.storageSet.mockClear();
const releaseModeStorage = harness.deferNextStorageSet();
const changingMode = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
await vi.waitFor(() => {
expect(harness.storageSet).toHaveBeenCalledWith({ accessMode: "selected" });
});
const staleToggle = sendRuntimeMessage(harness, {
type: "toggleTabAccess",
tabId: 205,
accessMode: "all",
grant: false,
});
releaseModeStorage();
await expect(changingMode).resolves.toEqual({ ok: true, accessMode: "selected" });
await expect(staleToggle).resolves.toEqual({
ok: false,
error: "Browser access mode changed. Refresh and retry.",
});
expect(harness.tabsGroup).not.toHaveBeenCalled();
expect(harness.sessionStorageValues).not.toHaveProperty("deniedTabIdsV1");
});
it("keeps a Selected barrier ahead of a queued All-mode widening", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "selected",
},
initialTabs: [{ id: 206, url: "https://example.com/queued-widening", groupId: -1 }],
});
harness.storageSet.mockClear();
const releaseWideningStorage = harness.deferNextStorageSet();
const widening = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "all",
});
await vi.waitFor(() => {
expect(harness.storageSet).toHaveBeenCalledWith({ accessMode: "all" });
});
const restricting = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
const releaseRestrictingStorage = harness.deferNextStorageSet();
releaseWideningStorage();
await vi.waitFor(() => {
expect(harness.storageSet).toHaveBeenCalledWith({ accessMode: "selected" });
});
await expect(
sendRuntimeMessage(harness, { type: "getTabAccess", tabId: 206 }),
).resolves.toMatchObject({ accessible: false });
releaseRestrictingStorage();
await expect(widening).resolves.toEqual({ ok: true, accessMode: "all" });
await expect(restricting).resolves.toEqual({ ok: true, accessMode: "selected" });
});
it("revalidates a selected survivor that leaves its group during downgrade cleanup", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [
{ id: 211, url: "https://example.com/selected", groupId: 7 },
{ id: 212, url: "https://example.com/unselected", groupId: -1 },
],
});
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerEventListener) {
throw new Error("expected relay and debugger event listener");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 41, tabId: 211 });
socket.receive({ type: "attach", seq: 42, tabId: 212 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalledTimes(2));
let releaseUnselectedDetach = () => {};
const unselectedDetach = new Promise<void>((resolve) => {
releaseUnselectedDetach = resolve;
});
harness.debuggerDetach.mockImplementation(async ({ tabId }: { tabId: number }) => {
if (tabId === 212) {
await unselectedDetach;
}
});
socket.send.mockClear();
const changingMode = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
await vi.waitFor(() => expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 212 }));
harness.unshareTab(211);
harness.tabsUpdatedListener(211, { groupId: -1 });
releaseUnselectedDetach();
await expect(changingMode).resolves.toEqual({ ok: true, accessMode: "selected" });
await vi.waitFor(() => expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 211 }));
harness.debuggerEventListener({ tabId: 211 }, "Runtime.consoleAPICalled", {});
expect(
socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.some((frame) => frame.type === "cdpEvent"),
).toBe(false);
});
it("revokes an ungrouped attach already in flight during all-to-selected downgrade", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 62, url: "https://example.com/attach-race", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
let releaseAttach = () => {};
harness.debuggerAttach.mockImplementationOnce(
async () =>
await new Promise<undefined>((resolve) => {
releaseAttach = () => resolve(undefined);
}),
);
socket.receive({ type: "attach", seq: 25, tabId: 62 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalled());
const changingMode = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
releaseAttach();
await expect(changingMode).resolves.toMatchObject({ ok: true, accessMode: "selected" });
await vi.waitFor(() => {
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 62 });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 25,
message: "tab 62 access was revoked",
});
});
});
it("preserves the proven attach epoch across a deferred target lookup", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 63, url: "https://example.com/target-race", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerEventListener) {
throw new Error("expected relay and debugger event listener");
}
await harness.authenticate(socket);
let releaseTargets = (
_targets: Array<{ id?: string; tabId?: number; attached?: boolean }>,
) => {};
harness.debuggerGetTargets.mockImplementationOnce(
async () =>
await new Promise((resolve) => {
releaseTargets = resolve;
}),
);
socket.receive({ type: "attach", seq: 26, tabId: 63 });
await vi.waitFor(() => {
expect(harness.debuggerAttach).toHaveBeenCalledWith({ tabId: 63 }, "1.3");
expect(harness.debuggerGetTargets).toHaveBeenCalled();
});
const releaseModeStorage = harness.deferNextStorageSet();
releaseTargets([{ id: "target-63", tabId: 63, attached: true }]);
const changingMode = sendRuntimeMessage(harness, {
type: "setAccessMode",
accessMode: "selected",
});
await vi.waitFor(() => {
expect(harness.storageSet).toHaveBeenCalledWith({ accessMode: "selected" });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 26,
message: "tab 63 access was revoked",
});
});
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 63 });
harness.debuggerEventListener({ tabId: 63 }, "Runtime.consoleAPICalled", { value: 1 });
const framesAfterEvent = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(
framesAfterEvent.some(
(frame) => frame.type === "cdpEvent" && frame.method === "Runtime.consoleAPICalled",
),
).toBe(false);
releaseModeStorage();
await expect(changingMode).resolves.toEqual({ ok: true, accessMode: "selected" });
});
it("does not mint an event epoch when a pending attach is selected again", async () => {
const harness = await loadBackground({
initialTabs: [{ id: 64, url: "https://example.com/reselected", groupId: 7 }],
});
harness.shareTab(64);
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerEventListener) {
throw new Error("expected relay and debugger event listener");
}
await harness.authenticate(socket);
let releaseTargets = (
_targets: Array<{ id?: string; tabId?: number; attached?: boolean }>,
) => {};
harness.debuggerGetTargets.mockImplementationOnce(
async () =>
await new Promise((resolve) => {
releaseTargets = resolve;
}),
);
socket.receive({ type: "attach", seq: 27, tabId: 64 });
await vi.waitFor(() => expect(harness.debuggerGetTargets).toHaveBeenCalled());
harness.unshareTab(64);
harness.tabsUpdatedListener(64, { groupId: -1 });
harness.shareTab(64);
harness.tabsUpdatedListener(64, { groupId: 7 });
await new Promise<void>((resolve) => {
setTimeout(resolve, 25);
});
harness.debuggerEventListener({ tabId: 64 }, "Runtime.consoleAPICalled", { value: 1 });
expect(
socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.some((frame) => frame.type === "cdpEvent" && frame.method === "Runtime.consoleAPICalled"),
).toBe(false);
releaseTargets([{ id: "target-64", tabId: 64, attached: true }]);
await vi.waitFor(() => {
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 27,
message: "tab 64 access was revoked",
});
});
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 64 });
});
it.each(["all", "selected"] as const)(
"keeps an unrelated attachment live while toggling one tab in %s mode",
async (accessMode) => {
const groupId = accessMode === "selected" ? 7 : -1;
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode,
},
initialTabs: [
{ id: 201, url: "https://example.com/attached", groupId },
{ id: 202, url: "https://example.com/toggle", groupId },
],
});
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerEventListener) {
throw new Error("expected relay and debugger event listener");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 28, tabId: 201 });
socket.receive({ type: "attach", seq: 29, tabId: 202 });
await vi.waitFor(() => {
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "result",
seq: 28,
result: { targetId: "tab-201" },
});
expect(frames).toContainEqual({
type: "result",
seq: 29,
result: { targetId: "tab-202" },
});
});
socket.send.mockClear();
let releaseMutation = () => {};
if (accessMode === "all") {
releaseMutation = harness.deferNextSessionStorageSet();
harness.sessionStorageSet.mockClear();
} else {
const pendingDetach = new Promise<void>((resolve) => {
releaseMutation = resolve;
});
harness.debuggerDetach.mockImplementation(async ({ tabId }: { tabId: number }) => {
if (tabId === 202) {
await pendingDetach;
}
});
}
const toggling = sendRuntimeMessage(harness, {
type: "toggleTabAccess",
tabId: 202,
accessMode,
grant: false,
});
if (accessMode === "all") {
await vi.waitFor(() => expect(harness.sessionStorageSet).toHaveBeenCalled());
} else {
await vi.waitFor(() => expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 202 }));
}
await expect(
sendRuntimeMessage(harness, { type: "getTabAccess", tabId: 201 }),
).resolves.toMatchObject({ accessible: true });
await expect(
sendRuntimeMessage(harness, { type: "getTabAccess", tabId: 202 }),
).resolves.toMatchObject({ accessible: false });
harness.debuggerEventListener({ tabId: 201 }, "Runtime.consoleAPICalled", {
phase: "during",
});
harness.debuggerEventListener({ tabId: 202 }, "Runtime.consoleAPICalled", {
phase: "during",
});
expect(
socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.filter((frame) => frame.type === "cdpEvent")
.map((frame) => frame.params?.phase),
).toEqual(["during"]);
releaseMutation();
await expect(toggling).resolves.toEqual({
ok: true,
accessible: false,
denied: accessMode === "all",
});
harness.debuggerEventListener({ tabId: 201 }, "Runtime.consoleAPICalled", {
phase: "after",
});
harness.debuggerEventListener({ tabId: 202 }, "Runtime.consoleAPICalled", {
phase: "after",
});
expect(
socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.filter((frame) => frame.type === "cdpEvent")
.map((frame) => frame.params?.phase),
).toEqual(["during", "after"]);
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 202 });
expect(harness.debuggerDetach).not.toHaveBeenCalledWith({ tabId: 201 });
expect(harness.debuggerAttach).toHaveBeenCalledTimes(2);
},
);
it.each(["all", "selected"] as const)(
"republishes restored tab access immediately in %s mode",
async (accessMode) => {
const tabId = 203;
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode,
},
initialTabs: [
{
id: tabId,
url: "https://example.com/restored",
groupId: accessMode === "selected" ? 7 : -1,
},
],
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
await expect(
sendRuntimeMessage(harness, { type: "toggleTabAccess", tabId, accessMode, grant: false }),
).resolves.toMatchObject({ ok: true, accessible: false });
await expect(
sendRuntimeMessage(harness, { type: "toggleTabAccess", tabId, accessMode, grant: true }),
).resolves.toMatchObject({ ok: true, accessible: true, denied: false });
await vi.waitFor(() => {
const refreshes = socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.filter((frame) => frame.type === "tabs");
expect(refreshes.at(-1)?.tabs).toContainEqual(expect.objectContaining({ tabId }));
});
},
);
it("refreshes targets on selected-to-all while keeping session-denied tabs hidden", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "selected",
},
sessionConfig: { deniedTabIdsV1: [72] },
initialTabs: [
{ id: 71, url: "https://example.com/available", groupId: -1 },
{ id: 72, url: "https://example.com/paused", groupId: -1 },
],
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
await expect(
sendRuntimeMessage(harness, { type: "setAccessMode", accessMode: "all" }),
).resolves.toMatchObject({ ok: true, accessMode: "all" });
await vi.waitFor(() => {
const refreshes = socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.filter((frame) => frame.type === "tabs");
expect(refreshes.at(-1)?.tabs).toEqual([expect.objectContaining({ tabId: 71 })]);
});
});
it("keeps detach available as the revocation cleanup command", async () => {
const harness = await loadBackground();
const socket = harness.sockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
harness.unshareTab(41);
socket.receive({ type: "detach", seq: 5, tabId: 41 });
await vi.waitFor(() => {
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 41 });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({ type: "result", seq: 5, result: {} });
});
});
it("allows createTab and groups the new tab before reporting success", async () => {
const harness = await loadBackground();
const socket = harness.sockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
harness.tabsCreate.mockResolvedValueOnce({
id: 42,
url: "https://example.com",
active: true,
windowId: 1,
groupId: -1,
incognito: false,
});
socket.receive({ type: "createTab", seq: 6, url: "https://example.com" });
await vi.waitFor(() => {
expect(harness.tabsGroup).toHaveBeenCalledWith({ tabIds: [42] });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({ type: "result", seq: 6, result: { tabId: 42 } });
});
});
it("invalidates an attach that was in flight when the tab left the group", async () => {
const harness = await loadBackground();
const socket = harness.sockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
harness.shareTab(43);
let releaseAttach = () => {};
harness.debuggerAttach.mockImplementationOnce(
async () =>
await new Promise<undefined>((resolve) => {
releaseAttach = () => resolve(undefined);
}),
);
socket.receive({ type: "attach", seq: 7, tabId: 43 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalledOnce());
harness.unshareTab(43);
harness.tabsUpdatedListener(43, { groupId: -1 });
await Promise.resolve();
releaseAttach();
await vi.waitFor(() => {
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 43 });
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 7,
message: "tab 43 access was revoked",
});
});
});
it("persists Cancel as an all-mode session deny, restores with Allow, and prunes on close", async () => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
},
initialTabs: [{ id: 81, url: "https://example.com/cancel", groupId: -1 }],
});
const socket = harness.relaySockets[0];
if (!socket || !harness.debuggerDetachListener || !harness.tabsRemovedListener) {
throw new Error("expected relay and Chrome lifecycle listeners");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 30, tabId: 81 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalled());
harness.debuggerDetachListener({ tabId: 81 }, "canceled_by_user");
await vi.waitFor(() => {
expect(harness.sessionStorageValues.deniedTabIdsV1).toEqual([81]);
});
socket.receive({ type: "attach", seq: 31, tabId: 81 });
await vi.waitFor(() => {
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 31,
message: "tab 81 is paused for OpenClaw",
});
});
await expect(
sendRuntimeMessage(harness, {
type: "toggleTabAccess",
tabId: 81,
accessMode: "all",
grant: true,
}),
).resolves.toMatchObject({ ok: true, accessible: true, denied: false });
expect(harness.sessionStorageValues).not.toHaveProperty("deniedTabIdsV1");
harness.debuggerDetachListener({ tabId: 81 }, "canceled_by_user");
await vi.waitFor(() => {
expect(harness.sessionStorageValues.deniedTabIdsV1).toEqual([81]);
});
harness.tabsRemovedListener(81);
await vi.waitFor(() => {
expect(harness.sessionStorageValues).not.toHaveProperty("deniedTabIdsV1");
});
});
it("restores a validated Cancel deny after an MV3 worker restart", async () => {
const storedConfig = {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode: "all",
};
const initialTabs = [{ id: 91, url: "https://example.com/reload", groupId: -1 }];
const harness = await loadBackground({
storedConfig,
sessionConfig: { deniedTabIdsV1: [91] },
initialTabs,
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
const hello = socket.send.mock.calls
.map(([raw]) => JSON.parse(raw))
.find((frame) => frame.type === "hello");
expect(hello.tabs).toEqual([]);
socket.receive({ type: "attach", seq: 32, tabId: 91 });
await vi.waitFor(() => {
const frames = socket.send.mock.calls.map(([raw]) => JSON.parse(raw));
expect(frames).toContainEqual({
type: "error",
seq: 32,
message: "tab 91 is paused for OpenClaw",
});
});
});
it.each(["all", "selected"] as const)(
"keeps agent-created tabs in the OpenClaw group in %s mode",
async (accessMode) => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode,
},
});
const socket = harness.relaySockets[0];
if (!socket) {
throw new Error("expected relay socket");
}
await harness.authenticate(socket);
harness.tabsCreate.mockResolvedValueOnce({
id: 101,
url: "https://example.com/created",
active: true,
groupId: -1,
windowId: 1,
incognito: false,
});
socket.receive({ type: "createTab", seq: 33, url: "https://example.com/created" });
await vi.waitFor(() => {
expect(harness.tabsGroup).toHaveBeenCalledWith({ tabIds: [101] });
});
},
);
it.each([
{ accessMode: "all" as const, detached: false },
{ accessMode: "selected" as const, detached: true },
])("revokes on group removal only in $accessMode mode", async ({ accessMode, detached }) => {
const harness = await loadBackground({
storedConfig: {
relayUrl: "ws://127.0.0.1:18797/extension",
token: TEST_RELAY_KEY,
authVersion: 2,
accessMode,
},
initialTabs: [{ id: 111, url: "https://example.com/group", groupId: 7 }],
});
harness.shareTab(111);
const socket = harness.relaySockets[0];
if (!socket || !harness.tabGroupRemovedListener) {
throw new Error("expected relay and tab-group listener");
}
await harness.authenticate(socket);
socket.receive({ type: "attach", seq: 34, tabId: 111 });
await vi.waitFor(() => expect(harness.debuggerAttach).toHaveBeenCalled());
harness.debuggerDetach.mockClear();
harness.unshareTab(111);
harness.tabGroupRemovedListener();
if (detached) {
await vi.waitFor(() => {
expect(harness.debuggerDetach).toHaveBeenCalledWith({ tabId: 111 });
});
} else {
await new Promise<void>((resolve) => {
setTimeout(resolve, 25);
});
expect(harness.debuggerDetach).not.toHaveBeenCalled();
}
});
});