Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* instantiation block use at first-message time.
*/

import { describe, expect, mock, test } from "bun:test";
import { afterEach, describe, expect, mock, test } from "bun:test";

// ---------------------------------------------------------------------------
// Module mocks for downstream side effects (DB writes, slash resolution,
Expand All @@ -28,9 +28,16 @@ mock.module("../util/logger.js", () => ({
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
}));

/**
* Per-test capability client roster. Set in individual tests to simulate
* a connected macOS client for cross-client drain-path coverage. Reset in
* afterEach so tests don't bleed state.
*/
let mockCapabilityClients: Array<{ clientId: string; actorPrincipalId?: string }> = [];

mock.module("../runtime/assistant-event-hub.js", () => ({
assistantEventHub: {
listClientsByCapability: () => [],
listClientsByCapability: () => mockCapabilityClients,
},
broadcastMessage: () => {},
}));
Expand Down Expand Up @@ -178,6 +185,10 @@ function makeQueuedMessage(opts: {
// ---------------------------------------------------------------------------

describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
afterEach(() => {
mockCapabilityClients = [];
});

test("drainSingleMessage re-adds 'app-control' for macOS-sourced queued message", async () => {
const queue = new MessageQueue();
const ifCtx: TurnInterfaceContext = {
Expand Down Expand Up @@ -288,4 +299,84 @@ describe("drainQueue preactivation re-add for host-proxy interfaces", () => {
expect(ctx.preactivatedSkillIdCalls).not.toContain("computer-use");
expect(ctx.preactivatedSkillIdCalls).not.toContain("app-control");
});

// ── Cross-client drain-path: web source + macOS client connected ──────

test("drainSingleMessage re-adds 'app-control' for web-sourced message when macOS client is connected", async () => {
mockCapabilityClients = [
{ clientId: "macos-client-1", actorPrincipalId: "user-1" },
];
const queue = new MessageQueue();
const ifCtx: TurnInterfaceContext = {
userMessageInterface: "web",
assistantMessageInterface: "web",
};
queue.push(
makeQueuedMessage({ requestId: "req-web-1", turnInterfaceContext: ifCtx }),
);
const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });

await drainQueue(ctx);

// web natively supports neither host_cu nor host_app_control, but the
// connected macOS client provides both via cross-client routing — so
// both skills must be re-preactivated.
expect(ctx.preactivatedSkillIdCalls).toContain("app-control");
expect(ctx.preactivatedSkillIds).toContain("app-control");
expect(ctx.preactivatedSkillIdCalls).toContain("computer-use");
});

test("drainSingleMessage does NOT re-add 'app-control' for web-sourced message when no capable client is connected", async () => {
// mockCapabilityClients remains [] (reset by afterEach from prior test)
const queue = new MessageQueue();
const ifCtx: TurnInterfaceContext = {
userMessageInterface: "web",
assistantMessageInterface: "web",
};
queue.push(
makeQueuedMessage({ requestId: "req-web-2", turnInterfaceContext: ifCtx }),
);
const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });

await drainQueue(ctx);

expect(ctx.preactivatedSkillIdCalls).not.toContain("app-control");
expect(ctx.preactivatedSkillIdCalls).not.toContain("computer-use");
});

test("drainSingleMessage re-adds 'computer-use' for web-sourced message when macOS client is connected", async () => {
mockCapabilityClients = [
{ clientId: "macos-client-1", actorPrincipalId: "user-1" },
];
const queue = new MessageQueue();
const ifCtx: TurnInterfaceContext = {
userMessageInterface: "web",
assistantMessageInterface: "web",
};
queue.push(
makeQueuedMessage({ requestId: "req-web-3", turnInterfaceContext: ifCtx }),
);
const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });

await drainQueue(ctx);

expect(ctx.preactivatedSkillIdCalls).toContain("computer-use");
expect(ctx.preactivatedSkillIds).toContain("computer-use");
});

test("drainSingleMessage does NOT re-add 'computer-use' for web-sourced message when no capable client is connected", async () => {
const queue = new MessageQueue();
const ifCtx: TurnInterfaceContext = {
userMessageInterface: "web",
assistantMessageInterface: "web",
};
queue.push(
makeQueuedMessage({ requestId: "req-web-4", turnInterfaceContext: ifCtx }),
);
const ctx = makeFakeContext({ queue, turnInterfaceContext: ifCtx });

await drainQueue(ctx);

expect(ctx.preactivatedSkillIdCalls).not.toContain("computer-use");
});
});
37 changes: 25 additions & 12 deletions assistant/src/__tests__/host-browser-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ function getPublishedMessages(): unknown[] {
return publishedEvents;
}

/**
* Simulate the HTTP route resolving a host_browser result. Mirrors what
* `resolveHostBrowserResultByRequestId` does after its guards pass: consume
* the pending interaction and invoke `rpcResolve` with the response.
*/
function resolveResult(
requestId: string,
response: { content: string; isError: boolean },
): void {
const interaction = pendingInteractions.resolve(requestId);
interaction?.rpcResolve?.(response);
}

// ── Tests ────────────────────────────────────────────────────────────

describe("HostBrowserProxy", () => {
Expand Down Expand Up @@ -103,7 +116,7 @@ describe("HostBrowserProxy", () => {
const requestId = sent.requestId as string;
expect(pendingInteractions.get(requestId)).toBeDefined();

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });

const result = await resultPromise;
expect(result.content).toBe("ok");
Expand Down Expand Up @@ -131,7 +144,7 @@ describe("HostBrowserProxy", () => {
});
expect(sent.cdpSessionId).toBe("session-abc");

proxy.resolveResult(sent.requestId as string, {
resolveResult(sent.requestId as string, {
content: "Example Domain",
isError: false,
});
Expand All @@ -146,7 +159,7 @@ describe("HostBrowserProxy", () => {
);

const sent = getPublishedMessages()[0] as Record<string, unknown>;
proxy.resolveResult(sent.requestId as string, {
resolveResult(sent.requestId as string, {
content: "Navigation failed",
isError: true,
});
Expand All @@ -168,7 +181,7 @@ describe("HostBrowserProxy", () => {
const requestId = sent.requestId as string;
expect(pendingInteractions.get(requestId)).toBeDefined();

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });

expect(pendingInteractions.get(requestId)).toBeUndefined();
await resultPromise;
Expand Down Expand Up @@ -293,7 +306,7 @@ describe("HostBrowserProxy", () => {

describe("resolve with unknown requestId", () => {
test("silently ignores unknown requestId", () => {
proxy.resolveResult("nonexistent", { content: "stale", isError: false });
resolveResult("nonexistent", { content: "stale", isError: false });
});
});

Expand Down Expand Up @@ -365,7 +378,7 @@ describe("HostBrowserProxy", () => {

const requestId = (getPublishedMessages()[0] as Record<string, unknown>)
.requestId as string;
proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
await resultPromise;

expect(spy.removeCalls).toEqual(["abort"]);
Expand Down Expand Up @@ -438,7 +451,7 @@ describe("HostBrowserProxy", () => {
expect(pending?.targetClientId).toBe("ext-client");
expect(pending?.targetActorPrincipalId).toBe("user-1");

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
const result = await resultPromise;
expect(result.isError).toBe(false);
});
Expand Down Expand Up @@ -499,7 +512,7 @@ describe("HostBrowserProxy", () => {
const pending = pendingInteractions.get(requestId);
expect(pending?.targetClientId).toBe("ext-client");

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
await resultPromise;
});

Expand Down Expand Up @@ -528,7 +541,7 @@ describe("HostBrowserProxy", () => {
expect(pending?.targetClientId).toBe("macos-client");
expect(pending?.targetActorPrincipalId).toBe("user-1");

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
await resultPromise;
});

Expand All @@ -555,7 +568,7 @@ describe("HostBrowserProxy", () => {
expect(pending?.targetClientId).toBe("test-client");
expect(pending?.targetActorPrincipalId).toBeUndefined();

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
await resultPromise;
});

Expand Down Expand Up @@ -629,7 +642,7 @@ describe("HostBrowserProxy", () => {
const pending = pendingInteractions.get(requestId);
expect(pending?.targetClientId).toBe("macos-client");

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
const result = await resultPromise;
expect(result.isError).toBe(false);
});
Expand Down Expand Up @@ -739,7 +752,7 @@ describe("HostBrowserProxy", () => {
const pending = pendingInteractions.get(requestId);
expect(pending?.targetClientId).toBe("ext-client");

proxy.resolveResult(requestId, { content: "ok", isError: false });
resolveResult(requestId, { content: "ok", isError: false });
await resultPromise;
});
});
Expand Down
Loading
Loading