import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, type MockInstance, vi, } from "vitest"; vi.mock("../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn().mockReturnValue(false), runEmbeddedPiAgent: vi.fn(), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import type { ClawdisConfig } from "../config/config.js"; import * as configModule from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import { agentCommand } from "./agent.js"; const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn(() => { throw new Error("exit"); }), }; const configSpy = vi.spyOn(configModule, "loadConfig"); async function withTempHome(fn: (home: string) => Promise): Promise { const base = fs.mkdtempSync(path.join(os.tmpdir(), "clawdis-agent-")); const previousHome = process.env.HOME; process.env.HOME = base; try { return await fn(base); } finally { process.env.HOME = previousHome; fs.rmSync(base, { recursive: true, force: true }); } } function mockConfig( home: string, storePath: string, routingOverrides?: Partial>, agentOverrides?: Partial>, telegramOverrides?: Partial>, ) { configSpy.mockReturnValue({ agent: { model: "anthropic/claude-opus-4-5", workspace: path.join(home, "clawd"), ...agentOverrides, }, session: { store: storePath, mainKey: "main" }, routing: routingOverrides ? { ...routingOverrides } : undefined, telegram: telegramOverrides ? { ...telegramOverrides } : undefined, }); } beforeEach(() => { vi.clearAllMocks(); vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "ok" }], meta: { durationMs: 5, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); }); describe("agentCommand", () => { it("creates a session entry when deriving from --to", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store); await agentCommand({ message: "hello", to: "+1555" }, runtime); const saved = JSON.parse(fs.readFileSync(store, "utf-8")) as Record< string, { sessionId: string } >; const entry = Object.values(saved)[0]; expect(entry.sessionId).toBeTruthy(); }); }); it("persists thinking and verbose overrides", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store); await agentCommand( { message: "hi", to: "+1222", thinking: "high", verbose: "on" }, runtime, ); const saved = JSON.parse(fs.readFileSync(store, "utf-8")) as Record< string, { thinkingLevel?: string; verboseLevel?: string } >; const entry = Object.values(saved)[0]; expect(entry.thinkingLevel).toBe("high"); expect(entry.verboseLevel).toBe("on"); const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; expect(callArgs?.thinkLevel).toBe("high"); expect(callArgs?.verboseLevel).toBe("on"); }); }); 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, ), ); mockConfig(home, store); await agentCommand( { message: "resume me", sessionId: "session-123" }, runtime, ); const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; expect(callArgs?.sessionId).toBe("session-123"); }); }); it("uses provider/model from agent.model", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store, undefined, { model: "openai/gpt-4.1-mini", }); await agentCommand({ message: "hi", to: "+1555" }, runtime); const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; expect(callArgs?.provider).toBe("openai"); expect(callArgs?.model).toBe("gpt-4.1-mini"); }); }); it("prints JSON payload when requested", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ payloads: [{ text: "json-reply", mediaUrl: "http://x.test/a.jpg" }], meta: { durationMs: 42, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); const store = path.join(home, "sessions.json"); mockConfig(home, store); await agentCommand({ message: "hi", to: "+1999", json: true }, runtime); const logged = (runtime.log as MockInstance).mock.calls.at( -1, )?.[0] as string; const parsed = JSON.parse(logged) as { payloads: Array<{ text: string; mediaUrl?: string | null }>; meta: { durationMs: number }; }; expect(parsed.payloads[0].text).toBe("json-reply"); expect(parsed.payloads[0].mediaUrl).toBe("http://x.test/a.jpg"); expect(parsed.meta.durationMs).toBe(42); }); }); it("passes the message through as the agent prompt", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store); await agentCommand({ message: "ping", to: "+1333" }, runtime); const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]; expect(callArgs?.prompt).toBe("ping"); }); }); it("passes telegram token when delivering", async () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store, undefined, undefined, { botToken: "t-1" }); const deps = { sendMessageWhatsApp: vi.fn(), sendMessageTelegram: vi .fn() .mockResolvedValue({ messageId: "t1", chatId: "123" }), sendMessageDiscord: vi.fn(), sendMessageSignal: vi.fn(), sendMessageIMessage: vi.fn(), }; const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { await agentCommand( { message: "hi", to: "123", deliver: true, provider: "telegram", }, runtime, deps, ); expect(deps.sendMessageTelegram).toHaveBeenCalledWith( "123", "ok", expect.objectContaining({ token: "t-1" }), ); } finally { if (prevTelegramToken === undefined) { delete process.env.TELEGRAM_BOT_TOKEN; } else { process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken; } } }); }); });