diff --git a/src/cli/memory-cli.test.ts b/src/cli/memory-cli.test.ts index 4dbe4220d..5821f1c5e 100644 --- a/src/cli/memory-cli.test.ts +++ b/src/cli/memory-cli.test.ts @@ -35,22 +35,42 @@ describe("memory cli", () => { ); } - it("prints vector status when available", async () => { + function makeMemoryStatus(overrides: Record = {}) { + return { + files: 0, + chunks: 0, + dirty: false, + workspaceDir: "/tmp/openclaw", + dbPath: "/tmp/memory.sqlite", + provider: "openai", + model: "text-embedding-3-small", + requestedProvider: "openai", + vector: { enabled: true, available: true }, + ...overrides, + }; + } + + function mockManager(manager: Record) { + getMemorySearchManager.mockResolvedValueOnce({ manager }); + } + + async function runMemoryCli(args: string[]) { const { registerMemoryCli } = await import("./memory-cli.js"); + const program = new Command(); + program.name("test"); + registerMemoryCli(program); + await program.parseAsync(["memory", ...args], { from: "user" }); + } + + it("prints vector status when available", async () => { const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => true), - status: () => ({ + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + status: () => + makeMemoryStatus({ files: 2, chunks: 5, - dirty: false, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", cache: { enabled: true, entries: 123, maxEntries: 50000 }, fts: { enabled: true, available: true }, vector: { @@ -60,15 +80,11 @@ describe("memory cli", () => { dims: 1024, }, }), - close, - }, + close, }); const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status"], { from: "user" }); + await runMemoryCli(["status"]); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024")); @@ -81,36 +97,24 @@ describe("memory cli", () => { }); it("prints vector error when unavailable", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => false), - status: () => ({ - files: 0, - chunks: 0, + mockManager({ + probeVectorAvailability: vi.fn(async () => false), + status: () => + makeMemoryStatus({ dirty: true, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", vector: { enabled: true, available: false, loadError: "load failed", }, }), - close, - }, + close, }); const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status", "--agent", "main"], { from: "user" }); + await runMemoryCli(["status", "--agent", "main"]); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unavailable")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector error: load failed")); @@ -118,34 +122,18 @@ describe("memory cli", () => { }); it("prints embeddings status when deep", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => true), - probeEmbeddingAvailability, - status: () => ({ - files: 1, - chunks: 1, - dirty: false, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", - vector: { enabled: true, available: true }, - }), - close, - }, + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + probeEmbeddingAvailability, + status: () => makeMemoryStatus({ files: 1, chunks: 1 }), + close, }); const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status", "--deep"], { from: "user" }); + await runMemoryCli(["status", "--deep"]); expect(probeEmbeddingAvailability).toHaveBeenCalled(); expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: ready")); @@ -153,63 +141,32 @@ describe("memory cli", () => { }); it("enables verbose logging with --verbose", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { isVerbose } = await import("../globals.js"); const close = vi.fn(async () => {}); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => true), - status: () => ({ - files: 0, - chunks: 0, - dirty: false, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", - vector: { enabled: true, available: true }, - }), - close, - }, + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + status: () => makeMemoryStatus(), + close, }); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status", "--verbose"], { from: "user" }); + await runMemoryCli(["status", "--verbose"]); expect(isVerbose()).toBe(true); }); it("logs close failure after status", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => { throw new Error("close boom"); }); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => true), - status: () => ({ - files: 1, - chunks: 1, - dirty: false, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", - }), - close, - }, + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + status: () => makeMemoryStatus({ files: 1, chunks: 1 }), + close, }); const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status"], { from: "user" }); + await runMemoryCli(["status"]); expect(close).toHaveBeenCalled(); expect(error).toHaveBeenCalledWith( @@ -219,36 +176,20 @@ describe("memory cli", () => { }); it("reindexes on status --index", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const sync = vi.fn(async () => {}); const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - probeVectorAvailability: vi.fn(async () => true), - probeEmbeddingAvailability, - sync, - status: () => ({ - files: 1, - chunks: 1, - dirty: false, - workspaceDir: "/tmp/openclaw", - dbPath: "/tmp/memory.sqlite", - provider: "openai", - model: "text-embedding-3-small", - requestedProvider: "openai", - vector: { enabled: true, available: true }, - }), - close, - }, + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + probeEmbeddingAvailability, + sync, + status: () => makeMemoryStatus({ files: 1, chunks: 1 }), + close, }); vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "status", "--index"], { from: "user" }); + await runMemoryCli(["status", "--index"]); expectCliSync(sync); expect(probeEmbeddingAvailability).toHaveBeenCalled(); @@ -256,22 +197,13 @@ describe("memory cli", () => { }); it("closes manager after index", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const sync = vi.fn(async () => {}); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - sync, - close, - }, - }); + mockManager({ sync, close }); const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "index"], { from: "user" }); + await runMemoryCli(["index"]); expectCliSync(sync); expect(close).toHaveBeenCalled(); @@ -279,26 +211,16 @@ describe("memory cli", () => { }); it("logs qmd index file path and size after index", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const sync = vi.fn(async () => {}); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-cli-qmd-index-")); const dbPath = path.join(tmpDir, "index.sqlite"); await fs.writeFile(dbPath, "sqlite-bytes", "utf-8"); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - sync, - status: () => ({ backend: "qmd", dbPath }), - close, - }, - }); + mockManager({ sync, status: () => ({ backend: "qmd", dbPath }), close }); const log = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "index"], { from: "user" }); + await runMemoryCli(["index"]); expectCliSync(sync); expect(log).toHaveBeenCalledWith(expect.stringContaining("QMD index: ")); @@ -308,26 +230,16 @@ describe("memory cli", () => { }); it("fails index when qmd db file is empty", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const sync = vi.fn(async () => {}); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-cli-qmd-index-")); const dbPath = path.join(tmpDir, "index.sqlite"); await fs.writeFile(dbPath, "", "utf-8"); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - sync, - status: () => ({ backend: "qmd", dbPath }), - close, - }, - }); + mockManager({ sync, status: () => ({ backend: "qmd", dbPath }), close }); const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "index"], { from: "user" }); + await runMemoryCli(["index"]); expectCliSync(sync); expect(error).toHaveBeenCalledWith( @@ -339,24 +251,15 @@ describe("memory cli", () => { }); it("logs close failures without failing the command", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => { throw new Error("close boom"); }); const sync = vi.fn(async () => {}); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - sync, - close, - }, - }); + mockManager({ sync, close }); const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "index"], { from: "user" }); + await runMemoryCli(["index"]); expectCliSync(sync); expect(close).toHaveBeenCalled(); @@ -367,7 +270,6 @@ describe("memory cli", () => { }); it("logs close failure after search", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => { throw new Error("close boom"); @@ -381,18 +283,10 @@ describe("memory cli", () => { snippet: "Hello", }, ]); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - search, - close, - }, - }); + mockManager({ search, close }); const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "search", "hello"], { from: "user" }); + await runMemoryCli(["search", "hello"]); expect(search).toHaveBeenCalled(); expect(close).toHaveBeenCalled(); @@ -403,24 +297,15 @@ describe("memory cli", () => { }); it("closes manager after search error", async () => { - const { registerMemoryCli } = await import("./memory-cli.js"); const { defaultRuntime } = await import("../runtime.js"); const close = vi.fn(async () => {}); const search = vi.fn(async () => { throw new Error("boom"); }); - getMemorySearchManager.mockResolvedValueOnce({ - manager: { - search, - close, - }, - }); + mockManager({ search, close }); const error = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); - const program = new Command(); - program.name("test"); - registerMemoryCli(program); - await program.parseAsync(["memory", "search", "oops"], { from: "user" }); + await runMemoryCli(["search", "oops"]); expect(search).toHaveBeenCalled(); expect(close).toHaveBeenCalled(); diff --git a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts index 6a6f2f74e..1e599be4f 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.e2e.test.ts @@ -35,6 +35,8 @@ afterAll(async () => { const BASE_IMAGE_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII="; +type AgentCommandCall = Record; + function expectChannels(call: Record, channel: string) { expect(call.channel).toBe(channel); expect(call.messageChannel).toBe(channel); @@ -42,6 +44,51 @@ function expectChannels(call: Record, channel: string) { expect(runContext?.messageChannel).toBe(channel); } +async function setTestSessionStore(params: { + entries: Record>; + agentId?: string; +}) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); + testState.sessionStorePath = path.join(dir, "sessions.json"); + await writeSessionStore({ + entries: params.entries, + agentId: params.agentId, + }); +} + +function latestAgentCall(): AgentCommandCall { + return vi.mocked(agentCommand).mock.calls.at(-1)?.[0] as AgentCommandCall; +} + +async function runMainAgentDeliveryWithSession(params: { + entry: Record; + request: Record; + allowFrom?: string[]; +}) { + setRegistry(defaultRegistry); + testState.allowFrom = params.allowFrom ?? ["+1555"]; + try { + await setTestSessionStore({ + entries: { + main: { + ...params.entry, + updatedAt: Date.now(), + }, + }, + }); + const res = await rpcReq(ws, "agent", { + message: "hi", + sessionKey: "main", + deliver: true, + ...params.request, + }); + expect(res.ok).toBe(true); + return latestAgentCall(); + } finally { + testState.allowFrom = undefined; + } +} + const createStubChannelPlugin = (params: { id: ChannelPlugin["id"]; label: string; @@ -125,9 +172,7 @@ describe("gateway server agent", () => { test("agent marks implicit delivery when lastTo is stale", async () => { setRegistry(defaultRegistry); testState.allowFrom = ["+436769770569"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ + await setTestSessionStore({ entries: { main: { sessionId: "sess-main-stale", @@ -146,8 +191,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; + const call = latestAgentCall(); expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliveryTargetMode).toBe("implicit"); @@ -157,9 +201,7 @@ describe("gateway server agent", () => { test("agent forwards sessionKey to agentCommand", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ + await setTestSessionStore({ entries: { "agent:main:subagent:abc": { sessionId: "sess-sub", @@ -174,8 +216,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; + const call = latestAgentCall(); expect(call.sessionKey).toBe("agent:main:subagent:abc"); expect(call.sessionId).toBe("sess-sub"); expectChannels(call, "webchat"); @@ -217,10 +258,7 @@ describe("gateway server agent", () => { test("agent derives sessionKey from agentId", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - testState.agentsConfig = { list: [{ id: "ops" }] }; - await writeSessionStore({ + await setTestSessionStore({ agentId: "ops", entries: { main: { @@ -229,6 +267,7 @@ describe("gateway server agent", () => { }, }, }); + testState.agentsConfig = { list: [{ id: "ops" }] }; const res = await rpcReq(ws, "agent", { message: "hi", agentId: "ops", @@ -236,8 +275,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; + const call = latestAgentCall(); expect(call.sessionKey).toBe("agent:ops:main"); expect(call.sessionId).toBe("sess-ops"); }); @@ -287,144 +325,86 @@ describe("gateway server agent", () => { }); test("agent forwards accountId to agentCommand", async () => { - setRegistry(defaultRegistry); - testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-account", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "default", - }, + const call = await runMainAgentDeliveryWithSession({ + entry: { + sessionId: "sess-main-account", + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "default", + }, + request: { + accountId: "kev", + idempotencyKey: "idem-agent-account", }, }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - deliver: true, - accountId: "kev", - idempotencyKey: "idem-agent-account", - }); - expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.accountId).toBe("kev"); const runContext = call.runContext as { accountId?: string } | undefined; expect(runContext?.accountId).toBe("kev"); - testState.allowFrom = undefined; }); test("agent avoids lastAccountId when explicit to is provided", async () => { - setRegistry(defaultRegistry); - testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-explicit", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "legacy", - }, + const call = await runMainAgentDeliveryWithSession({ + entry: { + sessionId: "sess-main-explicit", + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "legacy", + }, + request: { + to: "+1666", + idempotencyKey: "idem-agent-explicit", }, }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - deliver: true, - to: "+1666", - idempotencyKey: "idem-agent-explicit", - }); - expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1666"); expect(call.accountId).toBeUndefined(); - testState.allowFrom = undefined; }); test("agent keeps explicit accountId when explicit to is provided", async () => { - setRegistry(defaultRegistry); - testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-explicit-account", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "legacy", - }, + const call = await runMainAgentDeliveryWithSession({ + entry: { + sessionId: "sess-main-explicit-account", + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "legacy", + }, + request: { + to: "+1666", + accountId: "primary", + idempotencyKey: "idem-agent-explicit-account", }, }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - deliver: true, - to: "+1666", - accountId: "primary", - idempotencyKey: "idem-agent-explicit-account", - }); - expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1666"); expect(call.accountId).toBe("primary"); - testState.allowFrom = undefined; }); test("agent falls back to lastAccountId for implicit delivery", async () => { - setRegistry(defaultRegistry); - testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-implicit", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "kev", - }, + const call = await runMainAgentDeliveryWithSession({ + entry: { + sessionId: "sess-main-implicit", + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "kev", + }, + request: { + idempotencyKey: "idem-agent-implicit-account", }, }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - deliver: true, - idempotencyKey: "idem-agent-implicit-account", - }); - expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.accountId).toBe("kev"); - testState.allowFrom = undefined; }); test("agent forwards image attachments as images[]", async () => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ + await setTestSessionStore({ entries: { main: { sessionId: "sess-main-images", @@ -446,8 +426,7 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; + const call = latestAgentCall(); expect(call.sessionKey).toBe("agent:main:main"); expectChannels(call, "webchat"); expect(typeof call.message).toBe("string"); @@ -462,175 +441,65 @@ describe("gateway server agent", () => { }); test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => { - setRegistry(defaultRegistry); - testState.allowFrom = ["+1555"]; - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-missing-provider", - updatedAt: Date.now(), - }, + const call = await runMainAgentDeliveryWithSession({ + entry: { + sessionId: "sess-main-missing-provider", + }, + request: { + idempotencyKey: "idem-agent-missing-provider", }, }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - deliver: true, - idempotencyKey: "idem-agent-missing-provider", - }); - expect(res.ok).toBe(true); - - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliver).toBe(true); expect(call.sessionId).toBe("sess-main-missing-provider"); - testState.allowFrom = undefined; }); - test("agent routes main last-channel whatsapp", async () => { - setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main-whatsapp", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - }, - }, - }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - channel: "last", - deliver: true, + test.each([ + { + name: "whatsapp", + sessionId: "sess-main-whatsapp", + lastChannel: "whatsapp", + lastTo: "+1555", idempotencyKey: "idem-agent-last-whatsapp", - }); - expect(res.ok).toBe(true); - - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; - expectChannels(call, "whatsapp"); - expect(call.messageChannel).toBe("whatsapp"); - expect(call.to).toBe("+1555"); - expect(call.deliver).toBe(true); - expect(call.bestEffortDeliver).toBe(true); - expect(call.sessionId).toBe("sess-main-whatsapp"); - }); - - test("agent routes main last-channel telegram", async () => { - setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - lastChannel: "telegram", - lastTo: "123", - }, - }, - }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - channel: "last", - deliver: true, + }, + { + name: "telegram", + sessionId: "sess-main", + lastChannel: "telegram", + lastTo: "123", idempotencyKey: "idem-agent-last", - }); - expect(res.ok).toBe(true); - - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; - expectChannels(call, "telegram"); - expect(call.to).toBe("123"); - expect(call.deliver).toBe(true); - expect(call.bestEffortDeliver).toBe(true); - expect(call.sessionId).toBe("sess-main"); - }); - - test("agent routes main last-channel discord", async () => { - setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-discord", - updatedAt: Date.now(), - lastChannel: "discord", - lastTo: "channel:discord-123", - }, - }, - }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - channel: "last", - deliver: true, + }, + { + name: "discord", + sessionId: "sess-discord", + lastChannel: "discord", + lastTo: "channel:discord-123", idempotencyKey: "idem-agent-last-discord", - }); - expect(res.ok).toBe(true); - - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; - expectChannels(call, "discord"); - expect(call.to).toBe("channel:discord-123"); - expect(call.deliver).toBe(true); - expect(call.bestEffortDeliver).toBe(true); - expect(call.sessionId).toBe("sess-discord"); - }); - - test("agent routes main last-channel slack", async () => { - setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-slack", - updatedAt: Date.now(), - lastChannel: "slack", - lastTo: "channel:slack-123", - }, - }, - }); - const res = await rpcReq(ws, "agent", { - message: "hi", - sessionKey: "main", - channel: "last", - deliver: true, + }, + { + name: "slack", + sessionId: "sess-slack", + lastChannel: "slack", + lastTo: "channel:slack-123", idempotencyKey: "idem-agent-last-slack", - }); - expect(res.ok).toBe(true); - - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; - expectChannels(call, "slack"); - expect(call.to).toBe("channel:slack-123"); - expect(call.deliver).toBe(true); - expect(call.bestEffortDeliver).toBe(true); - expect(call.sessionId).toBe("sess-slack"); - }); - - test("agent routes main last-channel signal", async () => { + }, + { + name: "signal", + sessionId: "sess-signal", + lastChannel: "signal", + lastTo: "+15551234567", + idempotencyKey: "idem-agent-last-signal", + }, + ])("agent routes main last-channel $name", async (tc) => { setRegistry(defaultRegistry); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await writeSessionStore({ + await setTestSessionStore({ entries: { main: { - sessionId: "sess-signal", + sessionId: tc.sessionId, updatedAt: Date.now(), - lastChannel: "signal", - lastTo: "+15551234567", + lastChannel: tc.lastChannel, + lastTo: tc.lastTo, }, }, }); @@ -639,16 +508,15 @@ describe("gateway server agent", () => { sessionKey: "main", channel: "last", deliver: true, - idempotencyKey: "idem-agent-last-signal", + idempotencyKey: tc.idempotencyKey, }); expect(res.ok).toBe(true); - const spy = vi.mocked(agentCommand); - const call = spy.mock.calls.at(-1)?.[0] as Record; - expectChannels(call, "signal"); - expect(call.to).toBe("+15551234567"); + const call = latestAgentCall(); + expectChannels(call, tc.lastChannel); + expect(call.to).toBe(tc.lastTo); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); - expect(call.sessionId).toBe("sess-signal"); + expect(call.sessionId).toBe(tc.sessionId); }); });