diff --git a/src/acp/translator.prompt-prefix.test.ts b/src/acp/translator.prompt-prefix.test.ts index 38c186519c0..9d53e3aa103 100644 --- a/src/acp/translator.prompt-prefix.test.ts +++ b/src/acp/translator.prompt-prefix.test.ts @@ -7,7 +7,52 @@ import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; +const TEST_SESSION_ID = "session-1"; +const TEST_SESSION_KEY = "agent:main:main"; +const TEST_PROMPT = { + sessionId: TEST_SESSION_ID, + prompt: [{ type: "text", text: "hello" }], + _meta: {}, +} as unknown as PromptRequest; + describe("acp prompt cwd prefix", () => { + const createStopAfterSendSpy = () => + vi.fn(async (method: string) => { + if (method === "chat.send") { + throw new Error("stop-after-send"); + } + return {}; + }); + + async function runPromptAndCaptureRequest( + options: { + cwd?: string; + prefixCwd?: boolean; + provenanceMode?: "meta" | "meta+receipt"; + } = {}, + ) { + const sessionStore = createInMemorySessionStore(); + sessionStore.createSession({ + sessionId: TEST_SESSION_ID, + sessionKey: TEST_SESSION_KEY, + cwd: options.cwd ?? path.join(os.homedir(), "openclaw-test"), + }); + + const requestSpy = createStopAfterSendSpy(); + const agent = new AcpGatewayAgent( + createAcpConnection(), + createAcpGateway(requestSpy as unknown as GatewayClient["request"]), + { + sessionStore, + prefixCwd: options.prefixCwd, + provenanceMode: options.provenanceMode, + }, + ); + + await expect(agent.prompt(TEST_PROMPT)).rejects.toThrow("stop-after-send"); + return requestSpy; + } + async function runPromptWithCwd(cwd: string) { const pinnedHome = os.homedir(); const previousOpenClawHome = process.env.OPENCLAW_HOME; @@ -15,37 +60,8 @@ describe("acp prompt cwd prefix", () => { delete process.env.OPENCLAW_HOME; process.env.HOME = pinnedHome; - const sessionStore = createInMemorySessionStore(); - sessionStore.createSession({ - sessionId: "session-1", - sessionKey: "agent:main:main", - cwd, - }); - - const requestSpy = vi.fn(async (method: string) => { - if (method === "chat.send") { - throw new Error("stop-after-send"); - } - return {}; - }); - const agent = new AcpGatewayAgent( - createAcpConnection(), - createAcpGateway(requestSpy as unknown as GatewayClient["request"]), - { - sessionStore, - prefixCwd: true, - }, - ); - try { - await expect( - agent.prompt({ - sessionId: "session-1", - prompt: [{ type: "text", text: "hello" }], - _meta: {}, - } as unknown as PromptRequest), - ).rejects.toThrow("stop-after-send"); - return requestSpy; + return await runPromptAndCaptureRequest({ cwd, prefixCwd: true }); } finally { if (previousOpenClawHome === undefined) { delete process.env.OPENCLAW_HOME; @@ -83,42 +99,13 @@ describe("acp prompt cwd prefix", () => { }); it("injects system provenance metadata when enabled", async () => { - const sessionStore = createInMemorySessionStore(); - sessionStore.createSession({ - sessionId: "session-1", - sessionKey: "agent:main:main", - cwd: path.join(os.homedir(), "openclaw-test"), - }); - - const requestSpy = vi.fn(async (method: string) => { - if (method === "chat.send") { - throw new Error("stop-after-send"); - } - return {}; - }); - const agent = new AcpGatewayAgent( - createAcpConnection(), - createAcpGateway(requestSpy as unknown as GatewayClient["request"]), - { - sessionStore, - provenanceMode: "meta", - }, - ); - - await expect( - agent.prompt({ - sessionId: "session-1", - prompt: [{ type: "text", text: "hello" }], - _meta: {}, - } as unknown as PromptRequest), - ).rejects.toThrow("stop-after-send"); - + const requestSpy = await runPromptAndCaptureRequest({ provenanceMode: "meta" }); expect(requestSpy).toHaveBeenCalledWith( "chat.send", expect.objectContaining({ systemInputProvenance: { kind: "external_user", - originSessionId: "session-1", + originSessionId: TEST_SESSION_ID, sourceChannel: "acp", sourceTool: "openclaw_acp", }, @@ -129,42 +116,13 @@ describe("acp prompt cwd prefix", () => { }); it("injects a system provenance receipt when requested", async () => { - const sessionStore = createInMemorySessionStore(); - sessionStore.createSession({ - sessionId: "session-1", - sessionKey: "agent:main:main", - cwd: path.join(os.homedir(), "openclaw-test"), - }); - - const requestSpy = vi.fn(async (method: string) => { - if (method === "chat.send") { - throw new Error("stop-after-send"); - } - return {}; - }); - const agent = new AcpGatewayAgent( - createAcpConnection(), - createAcpGateway(requestSpy as unknown as GatewayClient["request"]), - { - sessionStore, - provenanceMode: "meta+receipt", - }, - ); - - await expect( - agent.prompt({ - sessionId: "session-1", - prompt: [{ type: "text", text: "hello" }], - _meta: {}, - } as unknown as PromptRequest), - ).rejects.toThrow("stop-after-send"); - + const requestSpy = await runPromptAndCaptureRequest({ provenanceMode: "meta+receipt" }); expect(requestSpy).toHaveBeenCalledWith( "chat.send", expect.objectContaining({ systemInputProvenance: { kind: "external_user", - originSessionId: "session-1", + originSessionId: TEST_SESSION_ID, sourceChannel: "acp", sourceTool: "openclaw_acp", }, @@ -182,14 +140,14 @@ describe("acp prompt cwd prefix", () => { expect(requestSpy).toHaveBeenCalledWith( "chat.send", expect.objectContaining({ - systemProvenanceReceipt: expect.stringContaining("originSessionId=session-1"), + systemProvenanceReceipt: expect.stringContaining(`originSessionId=${TEST_SESSION_ID}`), }), { expectFinal: true }, ); expect(requestSpy).toHaveBeenCalledWith( "chat.send", expect.objectContaining({ - systemProvenanceReceipt: expect.stringContaining("targetSession=agent:main:main"), + systemProvenanceReceipt: expect.stringContaining(`targetSession=${TEST_SESSION_KEY}`), }), { expectFinal: true }, ); diff --git a/src/browser/pw-session.get-page-for-targetid.extension-fallback.test.ts b/src/browser/pw-session.get-page-for-targetid.extension-fallback.test.ts index 43f1a6c7e09..8f64b2bf575 100644 --- a/src/browser/pw-session.get-page-for-targetid.extension-fallback.test.ts +++ b/src/browser/pw-session.get-page-for-targetid.extension-fallback.test.ts @@ -12,40 +12,49 @@ afterEach(async () => { await closePlaywrightBrowserConnection().catch(() => {}); }); +function createExtensionFallbackBrowserHarness(options?: { + urls?: string[]; + newCDPSessionError?: string; +}) { + const pageOn = vi.fn(); + const contextOn = vi.fn(); + const browserOn = vi.fn(); + const browserClose = vi.fn(async () => {}); + const newCDPSession = vi.fn(async () => { + throw new Error(options?.newCDPSessionError ?? "Not allowed"); + }); + + const context = { + pages: () => [], + on: contextOn, + newCDPSession, + } as unknown as import("playwright-core").BrowserContext; + + const pages = (options?.urls ?? [undefined]).map( + (url) => + ({ + on: pageOn, + context: () => context, + ...(url ? { url: () => url } : {}), + }) as unknown as import("playwright-core").Page, + ); + (context as unknown as { pages: () => unknown[] }).pages = () => pages; + + const browser = { + contexts: () => [context], + on: browserOn, + close: browserClose, + } as unknown as import("playwright-core").Browser; + + connectOverCdpSpy.mockResolvedValue(browser); + getChromeWebSocketUrlSpy.mockResolvedValue(null); + return { browserClose, newCDPSession, pages }; +} + describe("pw-session getPageForTargetId", () => { it("falls back to the only page when CDP session attachment is blocked (extension relays)", async () => { - connectOverCdpSpy.mockClear(); - getChromeWebSocketUrlSpy.mockClear(); - - const pageOn = vi.fn(); - const contextOn = vi.fn(); - const browserOn = vi.fn(); - const browserClose = vi.fn(async () => {}); - - const context = { - pages: () => [], - on: contextOn, - newCDPSession: vi.fn(async () => { - throw new Error("Not allowed"); - }), - } as unknown as import("playwright-core").BrowserContext; - - const page = { - on: pageOn, - context: () => context, - } as unknown as import("playwright-core").Page; - - // Fill pages() after page exists. - (context as unknown as { pages: () => unknown[] }).pages = () => [page]; - - const browser = { - contexts: () => [context], - on: browserOn, - close: browserClose, - } as unknown as import("playwright-core").Browser; - - connectOverCdpSpy.mockResolvedValue(browser); - getChromeWebSocketUrlSpy.mockResolvedValue(null); + const { browserClose, pages } = createExtensionFallbackBrowserHarness(); + const [page] = pages; const resolved = await getPageForTargetId({ cdpUrl: "http://127.0.0.1:18792", @@ -58,40 +67,9 @@ describe("pw-session getPageForTargetId", () => { }); it("uses the shared HTTP-base normalization when falling back to /json/list for direct WebSocket CDP URLs", async () => { - const pageOn = vi.fn(); - const contextOn = vi.fn(); - const browserOn = vi.fn(); - const browserClose = vi.fn(async () => {}); - - const context = { - pages: () => [], - on: contextOn, - newCDPSession: vi.fn(async () => { - throw new Error("Not allowed"); - }), - } as unknown as import("playwright-core").BrowserContext; - - const pageA = { - on: pageOn, - context: () => context, - url: () => "https://alpha.example", - } as unknown as import("playwright-core").Page; - const pageB = { - on: pageOn, - context: () => context, - url: () => "https://beta.example", - } as unknown as import("playwright-core").Page; - - (context as unknown as { pages: () => unknown[] }).pages = () => [pageA, pageB]; - - const browser = { - contexts: () => [context], - on: browserOn, - close: browserClose, - } as unknown as import("playwright-core").Browser; - - connectOverCdpSpy.mockResolvedValue(browser); - getChromeWebSocketUrlSpy.mockResolvedValue(null); + const [, pageB] = createExtensionFallbackBrowserHarness({ + urls: ["https://alpha.example", "https://beta.example"], + }).pages; const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: true, @@ -117,41 +95,11 @@ describe("pw-session getPageForTargetId", () => { }); it("resolves extension-relay pages from /json/list without probing page CDP sessions first", async () => { - const pageOn = vi.fn(); - const contextOn = vi.fn(); - const browserOn = vi.fn(); - const browserClose = vi.fn(async () => {}); - const newCDPSession = vi.fn(async () => { - throw new Error("Target.attachToBrowserTarget: Not allowed"); + const { newCDPSession, pages } = createExtensionFallbackBrowserHarness({ + urls: ["https://alpha.example", "https://beta.example"], + newCDPSessionError: "Target.attachToBrowserTarget: Not allowed", }); - - const context = { - pages: () => [], - on: contextOn, - newCDPSession, - } as unknown as import("playwright-core").BrowserContext; - - const pageA = { - on: pageOn, - context: () => context, - url: () => "https://alpha.example", - } as unknown as import("playwright-core").Page; - const pageB = { - on: pageOn, - context: () => context, - url: () => "https://beta.example", - } as unknown as import("playwright-core").Page; - - (context as unknown as { pages: () => unknown[] }).pages = () => [pageA, pageB]; - - const browser = { - contexts: () => [context], - on: browserOn, - close: browserClose, - } as unknown as import("playwright-core").Browser; - - connectOverCdpSpy.mockResolvedValue(browser); - getChromeWebSocketUrlSpy.mockResolvedValue(null); + const [, pageB] = pages; const fetchSpy = vi.spyOn(globalThis, "fetch"); fetchSpy diff --git a/src/config/config.talk-validation.test.ts b/src/config/config.talk-validation.test.ts index cb948d75c75..d2fb463613c 100644 --- a/src/config/config.talk-validation.test.ts +++ b/src/config/config.talk-validation.test.ts @@ -8,38 +8,42 @@ describe("talk config validation fail-closed behavior", () => { vi.restoreAllMocks(); }); + async function expectInvalidTalkConfig(config: unknown, messagePattern: RegExp) { + await withTempHomeConfig(config, async () => { + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + let thrown: unknown; + try { + loadConfig(); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(Error); + expect((thrown as { code?: string } | undefined)?.code).toBe("INVALID_CONFIG"); + expect((thrown as Error).message).toMatch(messagePattern); + expect(consoleSpy).toHaveBeenCalled(); + }); + } + it.each([ ["boolean", true], ["string", "1500"], ["float", 1500.5], ])("rejects %s talk.silenceTimeoutMs during config load", async (_label, value) => { - await withTempHomeConfig( + await expectInvalidTalkConfig( { agents: { list: [{ id: "main" }] }, talk: { silenceTimeoutMs: value, }, }, - async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - let thrown: unknown; - try { - loadConfig(); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(Error); - expect((thrown as { code?: string } | undefined)?.code).toBe("INVALID_CONFIG"); - expect((thrown as Error).message).toMatch(/silenceTimeoutMs|talk/i); - expect(consoleSpy).toHaveBeenCalled(); - }, + /silenceTimeoutMs|talk/i, ); }); it("rejects talk.provider when it does not match talk.providers during config load", async () => { - await withTempHomeConfig( + await expectInvalidTalkConfig( { agents: { list: [{ id: "main" }] }, talk: { @@ -51,26 +55,12 @@ describe("talk config validation fail-closed behavior", () => { }, }, }, - async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - let thrown: unknown; - try { - loadConfig(); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(Error); - expect((thrown as { code?: string } | undefined)?.code).toBe("INVALID_CONFIG"); - expect((thrown as Error).message).toMatch(/talk\.provider|talk\.providers|acme/i); - expect(consoleSpy).toHaveBeenCalled(); - }, + /talk\.provider|talk\.providers|acme/i, ); }); it("rejects multi-provider talk config without talk.provider during config load", async () => { - await withTempHomeConfig( + await expectInvalidTalkConfig( { agents: { list: [{ id: "main" }] }, talk: { @@ -84,21 +74,7 @@ describe("talk config validation fail-closed behavior", () => { }, }, }, - async () => { - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - - let thrown: unknown; - try { - loadConfig(); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(Error); - expect((thrown as { code?: string } | undefined)?.code).toBe("INVALID_CONFIG"); - expect((thrown as Error).message).toMatch(/talk\.provider|required/i); - expect(consoleSpy).toHaveBeenCalled(); - }, + /talk\.provider|required/i, ); }); });