import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; const configMocks = vi.hoisted(() => ({ readConfigFileSnapshot: vi.fn(), writeConfigFile: vi.fn().mockResolvedValue(undefined), })); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, readConfigFileSnapshot: configMocks.readConfigFileSnapshot, writeConfigFile: configMocks.writeConfigFile, }; }); import { agentsSetIdentityCommand } from "./agents.js"; const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }; const baseSnapshot = { path: "/tmp/clawdbot.json", exists: true, raw: "{}", parsed: {}, valid: true, config: {}, issues: [], legacyIssues: [], }; describe("agents set-identity command", () => { beforeEach(() => { configMocks.readConfigFileSnapshot.mockReset(); configMocks.writeConfigFile.mockClear(); runtime.log.mockClear(); runtime.error.mockClear(); runtime.exit.mockClear(); }); it("sets identity from workspace IDENTITY.md", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-")); const workspace = path.join(root, "work"); await fs.mkdir(workspace, { recursive: true }); await fs.writeFile( path.join(workspace, "IDENTITY.md"), ["- Name: Clawd", "- Creature: helpful sloth", "- Emoji: :)", ""].join("\n"), "utf-8", ); configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot, config: { agents: { list: [ { id: "main", workspace }, { id: "ops", workspace: path.join(root, "ops") }, ], }, }, }); await agentsSetIdentityCommand({ workspace }, runtime); expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1); const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as { agents?: { list?: Array<{ id: string; identity?: Record }> }; }; const main = written.agents?.list?.find((entry) => entry.id === "main"); expect(main?.identity).toEqual({ name: "Clawd", theme: "helpful sloth", emoji: ":)", }); }); it("errors when multiple agents match the same workspace", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-")); const workspace = path.join(root, "shared"); await fs.mkdir(workspace, { recursive: true }); await fs.writeFile(path.join(workspace, "IDENTITY.md"), "- Name: Echo\n", "utf-8"); configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot, config: { agents: { list: [{ id: "main", workspace }, { id: "ops", workspace }] }, }, }); await agentsSetIdentityCommand({ workspace }, runtime); expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Multiple agents match")); expect(runtime.exit).toHaveBeenCalledWith(1); expect(configMocks.writeConfigFile).not.toHaveBeenCalled(); }); it("overrides identity file values with explicit flags", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-")); const workspace = path.join(root, "work"); await fs.mkdir(workspace, { recursive: true }); await fs.writeFile( path.join(workspace, "IDENTITY.md"), ["- Name: Clawd", "- Theme: space lobster", "- Emoji: :)", ""].join("\n"), "utf-8", ); configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot, config: { agents: { list: [{ id: "main", workspace }] } }, }); await agentsSetIdentityCommand( { workspace, fromIdentity: true, name: "Nova", emoji: "🦞" }, runtime, ); const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as { agents?: { list?: Array<{ id: string; identity?: Record }> }; }; const main = written.agents?.list?.find((entry) => entry.id === "main"); expect(main?.identity).toEqual({ name: "Nova", theme: "space lobster", emoji: "🦞", }); }); it("reads identity from an explicit IDENTITY.md path", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-")); const workspace = path.join(root, "work"); const identityPath = path.join(workspace, "IDENTITY.md"); await fs.mkdir(workspace, { recursive: true }); await fs.writeFile( identityPath, ["- **Name:** C-3PO", "- **Creature:** Flustered Protocol Droid", "- **Emoji:** 🤖", ""].join( "\n", ), "utf-8", ); configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot, config: { agents: { list: [{ id: "main" }] } }, }); await agentsSetIdentityCommand({ agent: "main", identityFile: identityPath }, runtime); const written = configMocks.writeConfigFile.mock.calls[0]?.[0] as { agents?: { list?: Array<{ id: string; identity?: Record }> }; }; const main = written.agents?.list?.find((entry) => entry.id === "main"); expect(main?.identity).toEqual({ name: "C-3PO", theme: "Flustered Protocol Droid", emoji: "🤖", }); }); it("errors when identity data is missing", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-identity-")); const workspace = path.join(root, "work"); await fs.mkdir(workspace, { recursive: true }); configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot, config: { agents: { list: [{ id: "main", workspace }] } }, }); await agentsSetIdentityCommand({ workspace, fromIdentity: true }, runtime); expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("No identity data found")); expect(runtime.exit).toHaveBeenCalledWith(1); expect(configMocks.writeConfigFile).not.toHaveBeenCalled(); }); });