442 lines
18 KiB
TypeScript
442 lines
18 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
describe("legacy config detection", () => {
|
|
it("rejects routing.allowFrom", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
routing: { allowFrom: ["+15555550123"] },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("routing.allowFrom");
|
|
}
|
|
});
|
|
it("rejects routing.groupChat.requireMention", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
routing: { groupChat: { requireMention: false } },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("routing.groupChat.requireMention");
|
|
}
|
|
});
|
|
it("migrates routing.allowFrom to channels.whatsapp.allowFrom when whatsapp configured", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: { allowFrom: ["+15555550123"] },
|
|
channels: { whatsapp: {} },
|
|
});
|
|
expect(res.changes).toContain("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
|
|
expect(res.config?.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
|
expect(res.config?.routing?.allowFrom).toBeUndefined();
|
|
});
|
|
it("drops routing.allowFrom when whatsapp missing", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: { allowFrom: ["+15555550123"] },
|
|
});
|
|
expect(res.changes).toContain("Removed routing.allowFrom (channels.whatsapp not configured).");
|
|
expect(res.config?.channels?.whatsapp).toBeUndefined();
|
|
expect(res.config?.routing?.allowFrom).toBeUndefined();
|
|
});
|
|
it("migrates routing.groupChat.requireMention to channels whatsapp/telegram/imessage groups when whatsapp configured", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: { groupChat: { requireMention: false } },
|
|
channels: { whatsapp: {} },
|
|
});
|
|
expect(res.changes).toContain(
|
|
'Moved routing.groupChat.requireMention → channels.whatsapp.groups."*".requireMention.',
|
|
);
|
|
expect(res.changes).toContain(
|
|
'Moved routing.groupChat.requireMention → channels.telegram.groups."*".requireMention.',
|
|
);
|
|
expect(res.changes).toContain(
|
|
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
|
);
|
|
expect(res.config?.channels?.whatsapp?.groups?.["*"]?.requireMention).toBe(false);
|
|
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
|
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention).toBe(false);
|
|
expect(res.config?.routing?.groupChat?.requireMention).toBeUndefined();
|
|
});
|
|
it("migrates routing.groupChat.requireMention to telegram/imessage when whatsapp missing", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: { groupChat: { requireMention: false } },
|
|
});
|
|
expect(res.changes).toContain(
|
|
'Moved routing.groupChat.requireMention → channels.telegram.groups."*".requireMention.',
|
|
);
|
|
expect(res.changes).toContain(
|
|
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
|
);
|
|
expect(res.changes).not.toContain(
|
|
'Moved routing.groupChat.requireMention → channels.whatsapp.groups."*".requireMention.',
|
|
);
|
|
expect(res.config?.channels?.whatsapp).toBeUndefined();
|
|
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
|
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention).toBe(false);
|
|
expect(res.config?.routing?.groupChat?.requireMention).toBeUndefined();
|
|
});
|
|
it("migrates routing.groupChat.mentionPatterns to messages.groupChat.mentionPatterns", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: { groupChat: { mentionPatterns: ["@clawd"] } },
|
|
});
|
|
expect(res.changes).toContain(
|
|
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
|
);
|
|
expect(res.config?.messages?.groupChat?.mentionPatterns).toEqual(["@clawd"]);
|
|
expect(res.config?.routing?.groupChat?.mentionPatterns).toBeUndefined();
|
|
});
|
|
it("migrates routing agentToAgent/queue/transcribeAudio to tools/messages/media", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
routing: {
|
|
agentToAgent: { enabled: true, allow: ["main"] },
|
|
queue: { mode: "queue", cap: 3 },
|
|
transcribeAudio: {
|
|
command: ["whisper", "--model", "base"],
|
|
timeoutSeconds: 2,
|
|
},
|
|
},
|
|
});
|
|
expect(res.changes).toContain("Moved routing.agentToAgent → tools.agentToAgent.");
|
|
expect(res.changes).toContain("Moved routing.queue → messages.queue.");
|
|
expect(res.changes).toContain("Moved routing.transcribeAudio → tools.media.audio.models.");
|
|
expect(res.config?.tools?.agentToAgent).toEqual({
|
|
enabled: true,
|
|
allow: ["main"],
|
|
});
|
|
expect(res.config?.messages?.queue).toEqual({
|
|
mode: "queue",
|
|
cap: 3,
|
|
});
|
|
expect(res.config?.tools?.media?.audio).toEqual({
|
|
enabled: true,
|
|
models: [
|
|
{
|
|
command: "whisper",
|
|
type: "cli",
|
|
args: ["--model", "base"],
|
|
timeoutSeconds: 2,
|
|
},
|
|
],
|
|
});
|
|
expect(res.config?.routing).toBeUndefined();
|
|
});
|
|
it("migrates agent config into agents.defaults and tools", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
agent: {
|
|
model: "openai/gpt-5.2",
|
|
tools: { allow: ["sessions.list"], deny: ["danger"] },
|
|
elevated: { enabled: true, allowFrom: { discord: ["user:1"] } },
|
|
bash: { timeoutSec: 12 },
|
|
sandbox: { tools: { allow: ["browser.open"] } },
|
|
subagents: { tools: { deny: ["sandbox"] } },
|
|
},
|
|
});
|
|
expect(res.changes).toContain("Moved agent.tools.allow → tools.allow.");
|
|
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
|
|
expect(res.changes).toContain("Moved agent.elevated → tools.elevated.");
|
|
expect(res.changes).toContain("Moved agent.bash → tools.exec.");
|
|
expect(res.changes).toContain("Moved agent.sandbox.tools → tools.sandbox.tools.");
|
|
expect(res.changes).toContain("Moved agent.subagents.tools → tools.subagents.tools.");
|
|
expect(res.changes).toContain("Moved agent → agents.defaults.");
|
|
expect(res.config?.agents?.defaults?.model).toEqual({
|
|
primary: "openai/gpt-5.2",
|
|
fallbacks: [],
|
|
});
|
|
expect(res.config?.tools?.allow).toEqual(["sessions.list"]);
|
|
expect(res.config?.tools?.deny).toEqual(["danger"]);
|
|
expect(res.config?.tools?.elevated).toEqual({
|
|
enabled: true,
|
|
allowFrom: { discord: ["user:1"] },
|
|
});
|
|
expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 });
|
|
expect(res.config?.tools?.sandbox?.tools).toEqual({
|
|
allow: ["browser.open"],
|
|
});
|
|
expect(res.config?.tools?.subagents?.tools).toEqual({
|
|
deny: ["sandbox"],
|
|
});
|
|
expect((res.config as { agent?: unknown }).agent).toBeUndefined();
|
|
});
|
|
it("migrates tools.bash to tools.exec", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
tools: {
|
|
bash: { timeoutSec: 12 },
|
|
},
|
|
});
|
|
expect(res.changes).toContain("Moved tools.bash → tools.exec.");
|
|
expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 });
|
|
expect((res.config?.tools as { bash?: unknown } | undefined)?.bash).toBeUndefined();
|
|
});
|
|
it("accepts per-agent tools.elevated overrides", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
tools: {
|
|
elevated: {
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
},
|
|
},
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "work",
|
|
workspace: "~/clawd-work",
|
|
tools: {
|
|
elevated: {
|
|
enabled: false,
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config?.agents?.list?.[0]?.tools?.elevated).toEqual({
|
|
enabled: false,
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
});
|
|
}
|
|
});
|
|
it("rejects telegram.requireMention", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
telegram: { requireMention: true },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues.some((issue) => issue.path === "telegram.requireMention")).toBe(true);
|
|
}
|
|
});
|
|
it("rejects gateway.token", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
gateway: { token: "legacy-token" },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("gateway.token");
|
|
}
|
|
});
|
|
it("migrates gateway.token to gateway.auth.token", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
gateway: { token: "legacy-token" },
|
|
});
|
|
expect(res.changes).toContain("Moved gateway.token → gateway.auth.token.");
|
|
expect(res.config?.gateway?.auth?.token).toBe("legacy-token");
|
|
expect(res.config?.gateway?.auth?.mode).toBe("token");
|
|
expect((res.config?.gateway as { token?: string })?.token).toBeUndefined();
|
|
});
|
|
it("keeps gateway.bind tailnet", async () => {
|
|
vi.resetModules();
|
|
const { migrateLegacyConfig, validateConfigObject } = await import("./config.js");
|
|
const res = migrateLegacyConfig({
|
|
gateway: { bind: "tailnet" as const },
|
|
});
|
|
expect(res.changes).not.toContain("Migrated gateway.bind from 'tailnet' to 'auto'.");
|
|
expect(res.config).toBeNull();
|
|
|
|
const validated = validateConfigObject({ gateway: { bind: "tailnet" as const } });
|
|
expect(validated.ok).toBe(true);
|
|
if (validated.ok) {
|
|
expect(validated.config.gateway?.bind).toBe("tailnet");
|
|
}
|
|
});
|
|
it('rejects telegram.dmPolicy="open" without allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: { telegram: { dmPolicy: "open", allowFrom: ["123456789"] } },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("channels.telegram.allowFrom");
|
|
}
|
|
});
|
|
it('accepts telegram.dmPolicy="open" with allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: { telegram: { dmPolicy: "open", allowFrom: ["*"] } },
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.telegram?.dmPolicy).toBe("open");
|
|
}
|
|
});
|
|
it("defaults telegram.dmPolicy to pairing when telegram section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { telegram: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.telegram?.dmPolicy).toBe("pairing");
|
|
}
|
|
});
|
|
it("defaults telegram.groupPolicy to allowlist when telegram section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { telegram: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist");
|
|
}
|
|
});
|
|
it("defaults telegram.streamMode to partial when telegram section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { telegram: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.telegram?.streamMode).toBe("partial");
|
|
}
|
|
});
|
|
it('rejects whatsapp.dmPolicy="open" without allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: {
|
|
whatsapp: { dmPolicy: "open", allowFrom: ["+15555550123"] },
|
|
},
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("channels.whatsapp.allowFrom");
|
|
}
|
|
});
|
|
it('accepts whatsapp.dmPolicy="open" with allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: { whatsapp: { dmPolicy: "open", allowFrom: ["*"] } },
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.whatsapp?.dmPolicy).toBe("open");
|
|
}
|
|
});
|
|
it("defaults whatsapp.dmPolicy to pairing when whatsapp section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { whatsapp: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.whatsapp?.dmPolicy).toBe("pairing");
|
|
}
|
|
});
|
|
it("defaults whatsapp.groupPolicy to allowlist when whatsapp section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { whatsapp: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.whatsapp?.groupPolicy).toBe("allowlist");
|
|
}
|
|
});
|
|
it('rejects signal.dmPolicy="open" without allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: { signal: { dmPolicy: "open", allowFrom: ["+15555550123"] } },
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("channels.signal.allowFrom");
|
|
}
|
|
});
|
|
it('accepts signal.dmPolicy="open" with allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } },
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.signal?.dmPolicy).toBe("open");
|
|
}
|
|
});
|
|
it("defaults signal.dmPolicy to pairing when signal section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { signal: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.signal?.dmPolicy).toBe("pairing");
|
|
}
|
|
});
|
|
it("defaults signal.groupPolicy to allowlist when signal section exists", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({ channels: { signal: {} } });
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.signal?.groupPolicy).toBe("allowlist");
|
|
}
|
|
});
|
|
it("accepts historyLimit overrides per provider and account", async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
messages: { groupChat: { historyLimit: 12 } },
|
|
channels: {
|
|
whatsapp: { historyLimit: 9, accounts: { work: { historyLimit: 4 } } },
|
|
telegram: { historyLimit: 8, accounts: { ops: { historyLimit: 3 } } },
|
|
slack: { historyLimit: 7, accounts: { ops: { historyLimit: 2 } } },
|
|
signal: { historyLimit: 6 },
|
|
imessage: { historyLimit: 5 },
|
|
msteams: { historyLimit: 4 },
|
|
discord: { historyLimit: 3 },
|
|
},
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
if (res.ok) {
|
|
expect(res.config.channels?.whatsapp?.historyLimit).toBe(9);
|
|
expect(res.config.channels?.whatsapp?.accounts?.work?.historyLimit).toBe(4);
|
|
expect(res.config.channels?.telegram?.historyLimit).toBe(8);
|
|
expect(res.config.channels?.telegram?.accounts?.ops?.historyLimit).toBe(3);
|
|
expect(res.config.channels?.slack?.historyLimit).toBe(7);
|
|
expect(res.config.channels?.slack?.accounts?.ops?.historyLimit).toBe(2);
|
|
expect(res.config.channels?.signal?.historyLimit).toBe(6);
|
|
expect(res.config.channels?.imessage?.historyLimit).toBe(5);
|
|
expect(res.config.channels?.msteams?.historyLimit).toBe(4);
|
|
expect(res.config.channels?.discord?.historyLimit).toBe(3);
|
|
}
|
|
});
|
|
it('rejects imessage.dmPolicy="open" without allowFrom "*"', async () => {
|
|
vi.resetModules();
|
|
const { validateConfigObject } = await import("./config.js");
|
|
const res = validateConfigObject({
|
|
channels: {
|
|
imessage: { dmPolicy: "open", allowFrom: ["+15555550123"] },
|
|
},
|
|
});
|
|
expect(res.ok).toBe(false);
|
|
if (!res.ok) {
|
|
expect(res.issues[0]?.path).toBe("channels.imessage.allowFrom");
|
|
}
|
|
});
|
|
});
|