import { describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ loadSessionStore: vi.fn().mockReturnValue({ "+1000": { updatedAt: Date.now() - 60_000, verboseLevel: "on", thinkingLevel: "low", inputTokens: 2_000, outputTokens: 3_000, contextTokens: 10_000, model: "pi:opus", sessionId: "abc123", systemSent: true, }, }), resolveMainSessionKey: vi.fn().mockReturnValue("agent:main:main"), resolveStorePath: vi.fn().mockReturnValue("/tmp/sessions.json"), webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(5000), readWebSelfId: vi.fn().mockReturnValue({ e164: "+1999" }), logWebSelfId: vi.fn(), probeGateway: vi.fn().mockResolvedValue({ ok: false, url: "ws://127.0.0.1:18789", connectLatencyMs: null, error: "timeout", close: null, health: null, status: null, presence: null, configSnapshot: null, }), callGateway: vi.fn().mockResolvedValue({}), listAgentsForGateway: vi.fn().mockReturnValue({ defaultId: "main", mainKey: "agent:main:main", scope: "per-sender", agents: [{ id: "main", name: "Main" }], }), runSecurityAudit: vi.fn().mockResolvedValue({ ts: 0, summary: { critical: 1, warn: 1, info: 2 }, findings: [ { checkId: "test.critical", severity: "critical", title: "Test critical finding", detail: "Something is very wrong\nbut on two lines", remediation: "Do the thing", }, { checkId: "test.warn", severity: "warn", title: "Test warning finding", detail: "Something is maybe wrong", }, { checkId: "test.info", severity: "info", title: "Test info finding", detail: "FYI only", }, { checkId: "test.info2", severity: "info", title: "Another info finding", detail: "More FYI", }, ], }), })); vi.mock("../config/sessions.js", () => ({ loadSessionStore: mocks.loadSessionStore, resolveMainSessionKey: mocks.resolveMainSessionKey, resolveStorePath: mocks.resolveStorePath, })); vi.mock("../channels/plugins/index.js", () => ({ listChannelPlugins: () => [ { id: "whatsapp", meta: { id: "whatsapp", label: "WhatsApp", selectionLabel: "WhatsApp", docsPath: "/platforms/whatsapp", blurb: "mock", }, config: { listAccountIds: () => ["default"], resolveAccount: () => ({}), }, status: { buildChannelSummary: async () => ({ linked: true, authAgeMs: 5000 }), }, }, { id: "signal", meta: { 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: { 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, })); vi.mock("../web/session.js", () => ({ webAuthExists: mocks.webAuthExists, getWebAuthAgeMs: mocks.getWebAuthAgeMs, readWebSelfId: mocks.readWebSelfId, logWebSelfId: mocks.logWebSelfId, })); vi.mock("../gateway/probe.js", () => ({ probeGateway: mocks.probeGateway, })); 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("../infra/clawdbot-root.js", () => ({ resolveClawdbotPackageRoot: vi.fn().mockResolvedValue("/tmp/clawdbot"), })); vi.mock("../infra/os-summary.js", () => ({ resolveOsSummary: () => ({ platform: "darwin", arch: "arm64", release: "23.0.0", label: "macos 14.0 (arm64)", }), })); vi.mock("../infra/update-check.js", () => ({ checkUpdateStatus: vi.fn().mockResolvedValue({ root: "/tmp/clawdbot", installKind: "git", packageManager: "pnpm", git: { root: "/tmp/clawdbot", branch: "main", upstream: "origin/main", dirty: false, ahead: 0, behind: 0, fetchOk: true, }, deps: { manager: "pnpm", status: "ok", lockfilePath: "/tmp/clawdbot/pnpm-lock.yaml", markerPath: "/tmp/clawdbot/node_modules/.modules.yaml", }, registry: { latestVersion: "0.0.0" }, }), compareSemverStrings: vi.fn(() => 0), })); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => ({ session: {} }), }; }); vi.mock("../daemon/service.js", () => ({ resolveGatewayService: () => ({ label: "LaunchAgent", loadedText: "loaded", notLoadedText: "not loaded", isLoaded: async () => true, readRuntime: async () => ({ status: "running", pid: 1234 }), readCommand: async () => ({ programArguments: ["node", "dist/entry.js", "gateway"], sourcePath: "/tmp/Library/LaunchAgents/com.clawdbot.gateway.plist", }), }), })); vi.mock("../security/audit.js", () => ({ runSecurityAudit: mocks.runSecurityAudit, })); import { statusCommand } from "./status.js"; const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }; describe("statusCommand", () => { it("prints JSON when requested", async () => { await statusCommand({ json: true }, runtime as never); const payload = JSON.parse((runtime.log as vi.Mock).mock.calls[0][0]); expect(payload.linkChannel.linked).toBe(true); expect(payload.sessions.count).toBe(1); expect(payload.sessions.paths).toContain("/tmp/sessions.json"); expect(payload.sessions.defaults.model).toBeTruthy(); expect(payload.sessions.defaults.contextTokens).toBeGreaterThan(0); expect(payload.sessions.recent[0].percentUsed).toBe(50); expect(payload.sessions.recent[0].remainingTokens).toBe(5000); expect(payload.sessions.recent[0].flags).toContain("verbose:on"); expect(payload.securityAudit.summary.critical).toBe(1); expect(payload.securityAudit.summary.warn).toBe(1); }); it("prints formatted lines otherwise", 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((l) => l.includes("Clawdbot status"))).toBe(true); expect(logs.some((l) => l.includes("Overview"))).toBe(true); expect(logs.some((l) => l.includes("Security audit"))).toBe(true); expect(logs.some((l) => l.includes("Summary:"))).toBe(true); expect(logs.some((l) => l.includes("CRITICAL"))).toBe(true); expect(logs.some((l) => l.includes("Dashboard"))).toBe(true); expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true); expect(logs.some((l) => l.includes("Channels"))).toBe(true); expect(logs.some((l) => l.includes("WhatsApp"))).toBe(true); expect(logs.some((l) => l.includes("Sessions"))).toBe(true); expect(logs.some((l) => l.includes("+1000"))).toBe(true); expect(logs.some((l) => l.includes("50%"))).toBe(true); expect(logs.some((l) => l.includes("LaunchAgent"))).toBe(true); expect(logs.some((l) => l.includes("FAQ:"))).toBe(true); expect(logs.some((l) => l.includes("Troubleshooting:"))).toBe(true); expect(logs.some((l) => l.includes("Next steps:"))).toBe(true); expect(logs.some((l) => l.includes("clawdbot status --all"))).toBe(true); }); it("shows gateway auth when reachable", async () => { const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN; process.env.CLAWDBOT_GATEWAY_TOKEN = "abcd1234"; try { mocks.probeGateway.mockResolvedValueOnce({ ok: true, url: "ws://127.0.0.1:18789", connectLatencyMs: 123, error: null, close: null, health: {}, status: {}, presence: [], configSnapshot: null, }); (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((l) => l.includes("auth token"))).toBe(true); } finally { if (prevToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN; else process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken; } }); it("surfaces channel runtime errors from the gateway", async () => { mocks.probeGateway.mockResolvedValueOnce({ ok: true, url: "ws://127.0.0.1:18789", connectLatencyMs: 10, error: null, close: null, health: {}, status: {}, presence: [], configSnapshot: null, }); mocks.callGateway.mockResolvedValueOnce({ channelAccounts: { signal: [ { accountId: "default", enabled: true, configured: true, running: false, lastError: "signal-cli unreachable", }, ], imessage: [ { accountId: "default", enabled: true, configured: true, running: false, lastError: "imessage permission denied", }, ], }, }); (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.join("\n")).toMatch(/Signal/i); expect(logs.join("\n")).toMatch(/iMessage/i); expect(logs.join("\n")).toMatch(/gateway:/i); expect(logs.join("\n")).toMatch(/WARN/); }); it("includes sessions across agents in JSON output", async () => { const originalAgents = mocks.listAgentsForGateway.getMockImplementation(); const originalResolveStorePath = mocks.resolveStorePath.getMockImplementation(); const originalLoadSessionStore = mocks.loadSessionStore.getMockImplementation(); mocks.listAgentsForGateway.mockReturnValue({ defaultId: "main", mainKey: "agent:main:main", scope: "per-sender", agents: [ { id: "main", name: "Main" }, { id: "ops", name: "Ops" }, ], }); mocks.resolveStorePath.mockImplementation((_store, opts) => opts?.agentId === "ops" ? "/tmp/ops.json" : "/tmp/main.json", ); mocks.loadSessionStore.mockImplementation((storePath) => { if (storePath === "/tmp/ops.json") { return { "agent:ops:main": { updatedAt: Date.now() - 120_000, inputTokens: 1_000, outputTokens: 1_000, contextTokens: 10_000, model: "pi:opus", }, }; } return { "+1000": { updatedAt: Date.now() - 60_000, verboseLevel: "on", thinkingLevel: "low", inputTokens: 2_000, outputTokens: 3_000, contextTokens: 10_000, model: "pi:opus", sessionId: "abc123", systemSent: true, }, }; }); await statusCommand({ json: true }, runtime as never); const payload = JSON.parse((runtime.log as vi.Mock).mock.calls.at(-1)?.[0]); expect(payload.sessions.count).toBe(2); expect(payload.sessions.paths.length).toBe(2); expect( payload.sessions.recent.some((sess: { key?: string }) => sess.key === "agent:ops:main"), ).toBe(true); if (originalAgents) mocks.listAgentsForGateway.mockImplementation(originalAgents); if (originalResolveStorePath) mocks.resolveStorePath.mockImplementation(originalResolveStorePath); if (originalLoadSessionStore) mocks.loadSessionStore.mockImplementation(originalLoadSessionStore); }); });