diff --git a/src/agents/agents.test.ts b/src/agents/agents.test.ts index 3c1bcbe4b..1f8dbd62f 100644 --- a/src/agents/agents.test.ts +++ b/src/agents/agents.test.ts @@ -10,6 +10,8 @@ describe("pi agent helpers", () => { bodyIndex: 1, isNewSession: true, sessionId: "sess", + provider: "anthropic", + model: "claude-opus-4-5", sendSystemOnce: false, systemSent: false, identityPrefix: "IDENT", @@ -18,6 +20,10 @@ describe("pi agent helpers", () => { expect(built).toContain("-p"); expect(built).toContain("--mode"); expect(built).toContain("json"); + expect(built).toContain("--provider"); + expect(built).toContain("anthropic"); + expect(built).toContain("--model"); + expect(built).toContain("claude-opus-4-5"); expect(built.at(-1)).toContain("IDENT"); const builtNoIdentity = piSpec.buildArgs({ @@ -25,6 +31,8 @@ describe("pi agent helpers", () => { bodyIndex: 1, isNewSession: false, sessionId: "sess", + provider: "anthropic", + model: "claude-opus-4-5", sendSystemOnce: true, systemSent: true, identityPrefix: "IDENT", @@ -33,6 +41,50 @@ describe("pi agent helpers", () => { expect(builtNoIdentity.at(-1)).toBe("hi"); }); + it("injects provider/model for pi invocations only and avoids duplicates", () => { + const base = piSpec.buildArgs({ + argv: ["pi", "hello"], + bodyIndex: 1, + isNewSession: true, + sendSystemOnce: false, + systemSent: false, + format: "json", + }); + expect(base.filter((a) => a === "--provider").length).toBe(1); + expect(base).toContain("anthropic"); + expect(base.filter((a) => a === "--model").length).toBe(1); + expect(base).toContain("claude-opus-4-5"); + + const already = piSpec.buildArgs({ + argv: [ + "pi", + "--provider", + "anthropic", + "--model", + "claude-opus-4-5", + "hi", + ], + bodyIndex: 5, + isNewSession: true, + sendSystemOnce: false, + systemSent: false, + format: "json", + }); + expect(already.filter((a) => a === "--provider").length).toBe(1); + expect(already.filter((a) => a === "--model").length).toBe(1); + + const nonPi = piSpec.buildArgs({ + argv: ["echo", "hi"], + bodyIndex: 1, + isNewSession: true, + sendSystemOnce: false, + systemSent: false, + format: "json", + }); + expect(nonPi).not.toContain("--provider"); + expect(nonPi).not.toContain("--model"); + }); + it("parses final assistant message and preserves usage meta", () => { const stdout = [ '{"type":"message_start","message":{"role":"assistant"}}', diff --git a/src/config/config.test.ts b/src/config/config.test.ts new file mode 100644 index 000000000..ea3ff425f --- /dev/null +++ b/src/config/config.test.ts @@ -0,0 +1,140 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +async function withTempHome(fn: (home: string) => Promise): Promise { + const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-config-")); + const previousHome = process.env.HOME; + process.env.HOME = base; + try { + return await fn(base); + } finally { + process.env.HOME = previousHome; + await fs.rm(base, { recursive: true, force: true }); + } +} + +describe("config identity defaults", () => { + let previousHome: string | undefined; + + beforeEach(() => { + previousHome = process.env.HOME; + }); + + afterEach(() => { + process.env.HOME = previousHome; + }); + + it("derives responsePrefix, mentionPatterns, and sessionIntro when identity is set", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdis"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdis.json"), + JSON.stringify( + { + identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }, + inbound: { + reply: { + mode: "command", + command: ["pi", "--mode", "rpc", "x"], + session: {}, + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.inbound?.responsePrefix).toBe("🦥"); + expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual([ + "\\b@?Samantha\\b", + ]); + expect(cfg.inbound?.reply?.session?.sessionIntro).toContain( + "You are Samantha.", + ); + expect(cfg.inbound?.reply?.session?.sessionIntro).toContain( + "Theme: helpful sloth.", + ); + expect(cfg.inbound?.reply?.session?.sessionIntro).toContain( + "Your emoji is 🦥.", + ); + }); + }); + + it("does not override explicit values", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdis"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdis.json"), + JSON.stringify( + { + identity: { + name: "Samantha Sloth", + theme: "space lobster", + emoji: "🦞", + }, + inbound: { + responsePrefix: "✅", + groupChat: { mentionPatterns: ["@clawd"] }, + reply: { + mode: "command", + command: ["pi", "--mode", "rpc", "x"], + session: { sessionIntro: "Explicit intro" }, + }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.inbound?.responsePrefix).toBe("✅"); + expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual(["@clawd"]); + expect(cfg.inbound?.reply?.session?.sessionIntro).toBe("Explicit intro"); + }); + }); + + it("does not synthesize inbound.reply when it is absent", async () => { + await withTempHome(async (home) => { + const configDir = path.join(home, ".clawdis"); + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + path.join(configDir, "clawdis.json"), + JSON.stringify( + { + identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }, + inbound: {}, + }, + null, + 2, + ), + "utf-8", + ); + + vi.resetModules(); + const { loadConfig } = await import("./config.js"); + const cfg = loadConfig(); + + expect(cfg.inbound?.responsePrefix).toBe("🦥"); + expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual([ + "\\b@?Samantha\\b", + ]); + expect(cfg.inbound?.reply).toBeUndefined(); + }); + }); +});