From ac5f6e7c9df59a466365fa6590ccd26cd5f9c066 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 16:48:28 +0000 Subject: [PATCH] refactor(test): dedupe agent and status command fixtures --- src/commands/agent-via-gateway.e2e.test.ts | 46 +++-- src/commands/agent.e2e.test.ts | 74 +++---- src/commands/agents.identity.e2e.test.ts | 22 +-- src/commands/status.e2e.test.ts | 186 +++++++++--------- src/commands/status.summary.redaction.test.ts | 51 ++--- 5 files changed, 168 insertions(+), 211 deletions(-) diff --git a/src/commands/agent-via-gateway.e2e.test.ts b/src/commands/agent-via-gateway.e2e.test.ts index 6fbf790e19e..0d4ba93455d 100644 --- a/src/commands/agent-via-gateway.e2e.test.ts +++ b/src/commands/agent-via-gateway.e2e.test.ts @@ -57,6 +57,24 @@ async function withTempStore( } } +function mockGatewaySuccessReply(text = "hello") { + vi.mocked(callGateway).mockResolvedValue({ + runId: "idem-1", + status: "ok", + result: { + payloads: [{ text }], + meta: { stub: true }, + }, + }); +} + +function mockLocalAgentReply(text = "local") { + vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => { + rt.log?.(text); + return { payloads: [{ text }], meta: { stub: true } }; + }); +} + beforeEach(() => { vi.clearAllMocks(); }); @@ -64,14 +82,7 @@ beforeEach(() => { describe("agentCliCommand", () => { it("uses a timer-safe max gateway timeout when --timeout is 0", async () => { await withTempStore(async () => { - vi.mocked(callGateway).mockResolvedValue({ - runId: "idem-1", - status: "ok", - result: { - payloads: [{ text: "hello" }], - meta: { stub: true }, - }, - }); + mockGatewaySuccessReply(); await agentCliCommand({ message: "hi", to: "+1555", timeout: "0" }, runtime); @@ -83,14 +94,7 @@ describe("agentCliCommand", () => { it("uses gateway by default", async () => { await withTempStore(async () => { - vi.mocked(callGateway).mockResolvedValue({ - runId: "idem-1", - status: "ok", - result: { - payloads: [{ text: "hello" }], - meta: { stub: true }, - }, - }); + mockGatewaySuccessReply(); await agentCliCommand({ message: "hi", to: "+1555" }, runtime); @@ -103,10 +107,7 @@ describe("agentCliCommand", () => { it("falls back to embedded agent when gateway fails", async () => { await withTempStore(async () => { vi.mocked(callGateway).mockRejectedValue(new Error("gateway not connected")); - vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => { - rt.log?.("local"); - return { payloads: [{ text: "local" }], meta: { stub: true } }; - }); + mockLocalAgentReply(); await agentCliCommand({ message: "hi", to: "+1555" }, runtime); @@ -118,10 +119,7 @@ describe("agentCliCommand", () => { it("skips gateway when --local is set", async () => { await withTempStore(async () => { - vi.mocked(agentCommand).mockImplementationOnce(async (_opts, rt) => { - rt.log?.("local"); - return { payloads: [{ text: "local" }], meta: { stub: true } }; - }); + mockLocalAgentReply(); await agentCliCommand( { diff --git a/src/commands/agent.e2e.test.ts b/src/commands/agent.e2e.test.ts index 81fcbe9c33f..46ce78da9a4 100644 --- a/src/commands/agent.e2e.test.ts +++ b/src/commands/agent.e2e.test.ts @@ -61,6 +61,14 @@ function mockConfig( }); } +function writeSessionStoreSeed( + storePath: string, + sessions: Record>, +) { + fs.mkdirSync(path.dirname(storePath), { recursive: true }); + fs.writeFileSync(storePath, JSON.stringify(sessions, null, 2)); +} + beforeEach(() => { vi.clearAllMocks(); vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ @@ -114,21 +122,13 @@ describe("agentCommand", () => { it("resumes when session-id is provided", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); - fs.mkdirSync(path.dirname(store), { recursive: true }); - fs.writeFileSync( - store, - JSON.stringify( - { - foo: { - sessionId: "session-123", - updatedAt: Date.now(), - systemSent: true, - }, - }, - null, - 2, - ), - ); + writeSessionStoreSeed(store, { + foo: { + sessionId: "session-123", + updatedAt: Date.now(), + systemSent: true, + }, + }); mockConfig(home, store); await agentCommand({ message: "resume me", sessionId: "session-123" }, runtime); @@ -199,22 +199,14 @@ describe("agentCommand", () => { it("uses default fallback list for session model overrides", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); - fs.mkdirSync(path.dirname(store), { recursive: true }); - fs.writeFileSync( - store, - JSON.stringify( - { - "agent:main:subagent:test": { - sessionId: "session-subagent", - updatedAt: Date.now(), - providerOverride: "anthropic", - modelOverride: "claude-opus-4-5", - }, - }, - null, - 2, - ), - ); + writeSessionStoreSeed(store, { + "agent:main:subagent:test": { + sessionId: "session-subagent", + updatedAt: Date.now(), + providerOverride: "anthropic", + modelOverride: "claude-opus-4-5", + }, + }); mockConfig(home, store, { model: { @@ -264,20 +256,12 @@ describe("agentCommand", () => { it("keeps explicit sessionKey even when sessionId exists elsewhere", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); - fs.mkdirSync(path.dirname(store), { recursive: true }); - fs.writeFileSync( - store, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - null, - 2, - ), - ); + writeSessionStoreSeed(store, { + "agent:main:main": { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }); mockConfig(home, store); await agentCommand( diff --git a/src/commands/agents.identity.e2e.test.ts b/src/commands/agents.identity.e2e.test.ts index 9ec2675de13..bebd2e4dc33 100644 --- a/src/commands/agents.identity.e2e.test.ts +++ b/src/commands/agents.identity.e2e.test.ts @@ -43,6 +43,14 @@ function getWrittenMainIdentity() { return written.agents?.list?.find((entry) => entry.id === "main")?.identity; } +async function runIdentityCommandFromWorkspace(workspace: string, fromIdentity = true) { + configMocks.readConfigFileSnapshot.mockResolvedValue({ + ...baseConfigSnapshot, + config: { agents: { list: [{ id: "main", workspace }] } }, + }); + await agentsSetIdentityCommand({ workspace, fromIdentity }, runtime); +} + describe("agents set-identity command", () => { beforeEach(() => { configMocks.readConfigFileSnapshot.mockReset(); @@ -171,12 +179,7 @@ describe("agents set-identity command", () => { const { workspace } = await createIdentityWorkspace(); await writeIdentityFile(workspace, ["- Avatar: avatars/only.png"]); - configMocks.readConfigFileSnapshot.mockResolvedValue({ - ...baseConfigSnapshot, - config: { agents: { list: [{ id: "main", workspace }] } }, - }); - - await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime); + await runIdentityCommandFromWorkspace(workspace); expect(getWrittenMainIdentity()).toEqual({ avatar: "avatars/only.png", @@ -202,12 +205,7 @@ describe("agents set-identity command", () => { it("errors when identity data is missing", async () => { const { workspace } = await createIdentityWorkspace(); - configMocks.readConfigFileSnapshot.mockResolvedValue({ - ...baseConfigSnapshot, - config: { agents: { list: [{ id: "main", workspace }] } }, - }); - - await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime); + await runIdentityCommandFromWorkspace(workspace); expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("No identity data found")); expect(runtime.exit).toHaveBeenCalledWith(1); diff --git a/src/commands/status.e2e.test.ts b/src/commands/status.e2e.test.ts index ae866bbd2ac..55596f881ca 100644 --- a/src/commands/status.e2e.test.ts +++ b/src/commands/status.e2e.test.ts @@ -12,20 +12,79 @@ afterAll(() => { envSnapshot.restore(); }); -const mocks = vi.hoisted(() => ({ - loadSessionStore: vi.fn().mockReturnValue({ +function createDefaultSessionStoreEntry() { + return { + updatedAt: Date.now() - 60_000, + verboseLevel: "on", + thinkingLevel: "low", + inputTokens: 2_000, + outputTokens: 3_000, + totalTokens: 5_000, + contextTokens: 10_000, + model: "pi:opus", + sessionId: "abc123", + systemSent: true, + }; +} + +function createUnknownUsageSessionStore() { + return { "+1000": { updatedAt: Date.now() - 60_000, - verboseLevel: "on", - thinkingLevel: "low", inputTokens: 2_000, outputTokens: 3_000, - totalTokens: 5_000, contextTokens: 10_000, model: "pi:opus", - sessionId: "abc123", - systemSent: true, }, + }; +} + +function createChannelIssueCollector(channel: string) { + return (accounts: Array>) => + accounts + .filter((account) => typeof account.lastError === "string" && account.lastError) + .map((account) => ({ + channel, + accountId: typeof account.accountId === "string" ? account.accountId : "default", + message: `Channel error: ${String(account.lastError)}`, + })); +} + +function createErrorChannelPlugin(params: { id: string; label: string; docsPath: string }) { + return { + id: params.id, + meta: { + id: params.id, + label: params.label, + selectionLabel: params.label, + docsPath: params.docsPath, + blurb: "mock", + }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + }, + status: { + collectStatusIssues: createChannelIssueCollector(params.id), + }, + }; +} + +async function withUnknownUsageStore(run: () => Promise) { + const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation(); + mocks.loadSessionStore.mockReturnValue(createUnknownUsageSessionStore()); + try { + await run(); + } finally { + if (originalLoadSessionStore) { + mocks.loadSessionStore.mockImplementation(originalLoadSessionStore); + } + } +} + +const mocks = vi.hoisted(() => ({ + loadSessionStore: vi.fn().mockReturnValue({ + "+1000": createDefaultSessionStoreEntry(), }), resolveMainSessionKey: vi.fn().mockReturnValue("agent:main:main"), resolveStorePath: vi.fn().mockReturnValue("/tmp/sessions.json"), @@ -148,52 +207,18 @@ vi.mock("../channels/plugins/index.js", () => ({ }, }, { - id: "signal", - meta: { + ...createErrorChannelPlugin({ id: "signal", label: "Signal", - selectionLabel: "Signal", docsPath: "/platforms/signal", - blurb: "mock", - }, - config: { - listAccountIds: () => ["default"], - resolveAccount: () => ({}), - }, - status: { - collectStatusIssues: (accounts: Array>) => - accounts - .filter((account) => typeof account.lastError === "string" && account.lastError) - .map((account) => ({ - channel: "signal", - accountId: typeof account.accountId === "string" ? account.accountId : "default", - message: `Channel error: ${String(account.lastError)}`, - })), - }, + }), }, { - id: "imessage", - meta: { + ...createErrorChannelPlugin({ id: "imessage", label: "iMessage", - selectionLabel: "iMessage", docsPath: "/platforms/mac", - blurb: "mock", - }, - config: { - listAccountIds: () => ["default"], - resolveAccount: () => ({}), - }, - status: { - collectStatusIssues: (accounts: Array>) => - accounts - .filter((account) => typeof account.lastError === "string" && account.lastError) - .map((account) => ({ - channel: "imessage", - accountId: typeof account.accountId === "string" ? account.accountId : "default", - message: `Channel error: ${String(account.lastError)}`, - })), - }, + }), }, ] as unknown, })); @@ -210,9 +235,13 @@ vi.mock("../gateway/call.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, callGateway: mocks.callGateway }; }); -vi.mock("../gateway/session-utils.js", () => ({ - listAgentsForGateway: mocks.listAgentsForGateway, -})); +vi.mock("../gateway/session-utils.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + listAgentsForGateway: mocks.listAgentsForGateway, + }; +}); vi.mock("../infra/openclaw-root.js", () => ({ resolveOpenClawPackageRoot: vi.fn().mockResolvedValue("/tmp/openclaw"), })); @@ -318,52 +347,24 @@ describe("statusCommand", () => { }); it("surfaces unknown usage when totalTokens is missing", async () => { - const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation(); - mocks.loadSessionStore.mockReturnValue({ - "+1000": { - updatedAt: Date.now() - 60_000, - inputTokens: 2_000, - outputTokens: 3_000, - contextTokens: 10_000, - model: "pi:opus", - }, + await withUnknownUsageStore(async () => { + (runtime.log as vi.Mock).mockClear(); + await statusCommand({ json: true }, runtime as never); + const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]); + expect(payload.sessions.recent[0].totalTokens).toBeNull(); + expect(payload.sessions.recent[0].totalTokensFresh).toBe(false); + expect(payload.sessions.recent[0].percentUsed).toBeNull(); + expect(payload.sessions.recent[0].remainingTokens).toBeNull(); }); - - (runtime.log as vi.Mock).mockClear(); - await statusCommand({ json: true }, runtime as never); - const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]); - expect(payload.sessions.recent[0].totalTokens).toBeNull(); - expect(payload.sessions.recent[0].totalTokensFresh).toBe(false); - expect(payload.sessions.recent[0].percentUsed).toBeNull(); - expect(payload.sessions.recent[0].remainingTokens).toBeNull(); - - if (originalLoadSessionStore) { - mocks.loadSessionStore.mockImplementation(originalLoadSessionStore); - } }); it("prints unknown usage in formatted output when totalTokens is missing", async () => { - const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation(); - mocks.loadSessionStore.mockReturnValue({ - "+1000": { - updatedAt: Date.now() - 60_000, - inputTokens: 2_000, - outputTokens: 3_000, - contextTokens: 10_000, - model: "pi:opus", - }, - }); - - try { + await withUnknownUsageStore(async () => { (runtime.log as vi.Mock).mockClear(); await statusCommand({}, runtime as never); const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0])); expect(logs.some((line) => line.includes("unknown/") && line.includes("(?%)"))).toBe(true); - } finally { - if (originalLoadSessionStore) { - mocks.loadSessionStore.mockImplementation(originalLoadSessionStore); - } - } + }); }); it("prints formatted lines otherwise", async () => { @@ -501,18 +502,7 @@ describe("statusCommand", () => { }; } return { - "+1000": { - updatedAt: Date.now() - 60_000, - verboseLevel: "on", - thinkingLevel: "low", - inputTokens: 2_000, - outputTokens: 3_000, - totalTokens: 5_000, - contextTokens: 10_000, - model: "pi:opus", - sessionId: "abc123", - systemSent: true, - }, + "+1000": createDefaultSessionStoreEntry(), }; }); diff --git a/src/commands/status.summary.redaction.test.ts b/src/commands/status.summary.redaction.test.ts index ecb99ae6ecb..9920e63a029 100644 --- a/src/commands/status.summary.redaction.test.ts +++ b/src/commands/status.summary.redaction.test.ts @@ -2,6 +2,23 @@ import { describe, expect, it } from "vitest"; import type { StatusSummary } from "./status.types.js"; import { redactSensitiveStatusSummary } from "./status.summary.js"; +function createRecentSessionRow() { + return { + key: "main", + kind: "direct" as const, + sessionId: "sess-1", + updatedAt: 1, + age: 2, + totalTokens: 3, + totalTokensFresh: true, + remainingTokens: 4, + percentUsed: 5, + model: "gpt-5", + contextTokens: 200_000, + flags: ["id:sess-1"], + }; +} + describe("redactSensitiveStatusSummary", () => { it("removes sensitive session and path details while preserving summary structure", () => { const input: StatusSummary = { @@ -15,43 +32,13 @@ describe("redactSensitiveStatusSummary", () => { paths: ["/tmp/openclaw/sessions.json"], count: 1, defaults: { model: "gpt-5", contextTokens: 200_000 }, - recent: [ - { - key: "main", - kind: "direct", - sessionId: "sess-1", - updatedAt: 1, - age: 2, - totalTokens: 3, - totalTokensFresh: true, - remainingTokens: 4, - percentUsed: 5, - model: "gpt-5", - contextTokens: 200_000, - flags: ["id:sess-1"], - }, - ], + recent: [createRecentSessionRow()], byAgent: [ { agentId: "main", path: "/tmp/openclaw/main-sessions.json", count: 1, - recent: [ - { - key: "main", - kind: "direct", - sessionId: "sess-1", - updatedAt: 1, - age: 2, - totalTokens: 3, - totalTokensFresh: true, - remainingTokens: 4, - percentUsed: 5, - model: "gpt-5", - contextTokens: 200_000, - flags: ["id:sess-1"], - }, - ], + recent: [createRecentSessionRow()], }, ], },