Files
clawdbot/src/config/config.legacy-config-detection.accepts-imessage-dmpolicy.test.ts
2026-01-19 03:39:25 +00:00

400 lines
14 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { withTempHome } from "./test-helpers.js";
describe("legacy config detection", () => {
it('accepts imessage.dmPolicy="open" with allowFrom "*"', async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
channels: { imessage: { dmPolicy: "open", allowFrom: ["*"] } },
});
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.imessage?.dmPolicy).toBe("open");
}
});
it("defaults imessage.dmPolicy to pairing when imessage section exists", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({ channels: { imessage: {} } });
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.imessage?.dmPolicy).toBe("pairing");
}
});
it("defaults imessage.groupPolicy to allowlist when imessage section exists", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({ channels: { imessage: {} } });
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.imessage?.groupPolicy).toBe("allowlist");
}
});
it("defaults discord.groupPolicy to allowlist when discord section exists", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({ channels: { discord: {} } });
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.discord?.groupPolicy).toBe("allowlist");
}
});
it("defaults slack.groupPolicy to allowlist when slack section exists", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({ channels: { slack: {} } });
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.slack?.groupPolicy).toBe("allowlist");
}
});
it("defaults msteams.groupPolicy to allowlist when msteams section exists", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({ channels: { msteams: {} } });
expect(res.ok).toBe(true);
if (res.ok) {
expect(res.config.channels?.msteams?.groupPolicy).toBe("allowlist");
}
});
it("rejects unsafe executable config values", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
channels: { imessage: { cliPath: "imsg; rm -rf /" } },
audio: { transcription: { command: ["whisper", "--model", "base"] } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues.some((i) => i.path === "channels.imessage.cliPath")).toBe(true);
}
});
it("accepts tools audio transcription without cli", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
audio: { transcription: { command: ["whisper", "--model", "base"] } },
});
expect(res.ok).toBe(true);
});
it("accepts path-like executable values with spaces", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
channels: { imessage: { cliPath: "/Applications/Imsg Tools/imsg" } },
audio: {
transcription: {
command: ["whisper", "--model"],
},
},
});
expect(res.ok).toBe(true);
});
it('rejects discord.dm.policy="open" without allowFrom "*"', async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
channels: { discord: { dm: { policy: "open", allowFrom: ["123"] } } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("channels.discord.dm.allowFrom");
}
});
it('rejects slack.dm.policy="open" without allowFrom "*"', async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
channels: { slack: { dm: { policy: "open", allowFrom: ["U123"] } } },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("channels.slack.dm.allowFrom");
}
});
it("rejects legacy agent.model string", async () => {
vi.resetModules();
const { validateConfigObject } = await import("./config.js");
const res = validateConfigObject({
agent: { model: "anthropic/claude-opus-4-5" },
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues.some((i) => i.path === "agent.model")).toBe(true);
}
});
it("migrates telegram.requireMention to channels.telegram.groups.*.requireMention", async () => {
vi.resetModules();
const { migrateLegacyConfig } = await import("./config.js");
const res = migrateLegacyConfig({
telegram: { requireMention: false },
});
expect(res.changes).toContain(
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
);
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
expect(res.config?.channels?.telegram?.requireMention).toBeUndefined();
});
it("migrates legacy model config to agent.models + model lists", async () => {
vi.resetModules();
const { migrateLegacyConfig } = await import("./config.js");
const res = migrateLegacyConfig({
agent: {
model: "anthropic/claude-opus-4-5",
modelFallbacks: ["openai/gpt-4.1-mini"],
imageModel: "openai/gpt-4.1-mini",
imageModelFallbacks: ["anthropic/claude-opus-4-5"],
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
modelAliases: { Opus: "anthropic/claude-opus-4-5" },
},
});
expect(res.config?.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5");
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual(["openai/gpt-4.1-mini"]);
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe("openai/gpt-4.1-mini");
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
"anthropic/claude-opus-4-5",
]);
expect(res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]).toMatchObject({
alias: "Opus",
});
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
expect(res.config?.agent).toBeUndefined();
});
it("flags legacy config in snapshot", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ routing: { allowFrom: ["+15555550123"] } }),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
routing?: { allowFrom?: string[] };
channels?: unknown;
};
expect(parsed.routing?.allowFrom).toEqual(["+15555550123"]);
expect(parsed.channels).toBeUndefined();
});
});
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify(
{
auth: {
profiles: {
"anthropic:claude-cli": { provider: "anthropic", mode: "token" },
},
},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { loadConfig } = await import("./config.js");
const cfg = loadConfig();
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
auth?: { profiles?: Record<string, { mode?: string }> };
};
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
});
});
it("flags legacy provider sections in snapshot", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ whatsapp: { allowFrom: ["+1555"] } }, null, 2),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
channels?: unknown;
whatsapp?: unknown;
};
expect(parsed.channels).toBeUndefined();
expect(parsed.whatsapp).toBeTruthy();
});
});
it("flags routing.allowFrom in snapshot", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ routing: { allowFrom: ["+1666"] } }, null, 2),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
channels?: unknown;
routing?: { allowFrom?: string[] };
};
expect(parsed.channels).toBeUndefined();
expect(parsed.routing?.allowFrom).toEqual(["+1666"]);
});
});
it("rejects bindings[].match.provider on load", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify(
{
bindings: [{ agentId: "main", match: { provider: "slack" } }],
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.issues.length).toBeGreaterThan(0);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
bindings?: Array<{ match?: { provider?: string } }>;
};
expect(parsed.bindings?.[0]?.match?.provider).toBe("slack");
});
});
it("rejects bindings[].match.accountID on load", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify(
{
bindings: [{ agentId: "main", match: { channel: "telegram", accountID: "work" } }],
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.issues.length).toBeGreaterThan(0);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
bindings?: Array<{ match?: { accountID?: string } }>;
};
expect(parsed.bindings?.[0]?.match?.accountID).toBe("work");
});
});
it("rejects session.sendPolicy.rules[].match.provider on load", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify(
{
session: {
sendPolicy: {
rules: [{ action: "deny", match: { provider: "telegram" } }],
},
},
},
null,
2,
),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.issues.length).toBeGreaterThan(0);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
};
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBe("telegram");
});
});
it("rejects messages.queue.byProvider on load", async () => {
await withTempHome(async (home) => {
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ messages: { queue: { byProvider: { whatsapp: "queue" } } } }, null, 2),
"utf-8",
);
vi.resetModules();
const { readConfigFileSnapshot } = await import("./config.js");
const snap = await readConfigFileSnapshot();
expect(snap.valid).toBe(false);
expect(snap.issues.length).toBeGreaterThan(0);
const raw = await fs.readFile(configPath, "utf-8");
const parsed = JSON.parse(raw) as {
messages?: {
queue?: {
byProvider?: Record<string, unknown>;
};
};
};
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
});
});
});