mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 02:31:24 +00:00
test: make imessage legacy config checks pure
This commit is contained in:
@@ -1,65 +1,13 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AudioSchema, BindingsSchema } from "./zod-schema.agents.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
import {
|
||||
DiscordConfigSchema,
|
||||
IMessageConfigSchema,
|
||||
MSTeamsConfigSchema,
|
||||
SlackConfigSchema,
|
||||
} from "./zod-schema.providers-core.js";
|
||||
|
||||
const { loadConfig, readConfigFileSnapshot, validateConfigObject } =
|
||||
await vi.importActual<typeof import("./config.js")>("./config.js");
|
||||
import { withTempHome } from "./test-helpers.js";
|
||||
|
||||
async function expectLoadRejectionPreservesField(params: {
|
||||
config: unknown;
|
||||
readValue: (parsed: unknown) => unknown;
|
||||
expectedValue: unknown;
|
||||
}) {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(params.config, null, 2), "utf-8");
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
|
||||
expect(snap.valid).toBe(false);
|
||||
expect(snap.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
|
||||
expect(params.readValue(parsed)).toBe(params.expectedValue);
|
||||
});
|
||||
}
|
||||
|
||||
type ConfigSnapshot = Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
|
||||
async function withSnapshotForConfig(
|
||||
config: unknown,
|
||||
run: (params: { snapshot: ConfigSnapshot; parsed: unknown; configPath: string }) => Promise<void>,
|
||||
) {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
|
||||
await run({ snapshot, parsed, configPath });
|
||||
});
|
||||
}
|
||||
|
||||
function expectValidConfigValue(params: {
|
||||
config: unknown;
|
||||
readValue: (config: unknown) => unknown;
|
||||
expectedValue: unknown;
|
||||
}) {
|
||||
const res = validateConfigObject(params.config);
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
throw new Error("expected config to be valid");
|
||||
}
|
||||
expect(params.readValue(res.config)).toBe(params.expectedValue);
|
||||
}
|
||||
|
||||
function expectSchemaConfigValue(params: {
|
||||
schema: { safeParse: (value: unknown) => { success: true; data: unknown } | { success: false } };
|
||||
config: unknown;
|
||||
@@ -74,50 +22,76 @@ function expectSchemaConfigValue(params: {
|
||||
expect(params.readValue(res.data)).toBe(params.expectedValue);
|
||||
}
|
||||
|
||||
function expectInvalidIssuePath(config: unknown, expectedPath: string) {
|
||||
const res = validateConfigObject(config);
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe(expectedPath);
|
||||
function expectSchemaValid(
|
||||
schema: {
|
||||
safeParse: (value: unknown) => { success: true } | { success: false };
|
||||
},
|
||||
config: unknown,
|
||||
) {
|
||||
const res = schema.safeParse(config);
|
||||
expect(res.success).toBe(true);
|
||||
}
|
||||
|
||||
function expectInvalidSchemaIssuePath(params: {
|
||||
schema: {
|
||||
safeParse: (
|
||||
value: unknown,
|
||||
) => { success: true } | { success: false; error: { issues: Array<{ path: PropertyKey[] }> } };
|
||||
};
|
||||
config: unknown;
|
||||
expectedPath: string;
|
||||
}) {
|
||||
const res = params.schema.safeParse(params.config);
|
||||
expect(res.success).toBe(false);
|
||||
if (!res.success) {
|
||||
expect(res.error.issues[0]?.path.join(".")).toBe(params.expectedPath);
|
||||
}
|
||||
}
|
||||
|
||||
function expectSnapshotInvalidRootKey(
|
||||
ctx: { snapshot: ConfigSnapshot; parsed: unknown },
|
||||
key: string,
|
||||
) {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.legacyIssues).toEqual([]);
|
||||
expect(ctx.snapshot.issues[0]?.path).toBe("");
|
||||
expect(ctx.snapshot.issues[0]?.message).toContain(`"${key}"`);
|
||||
expect((ctx.parsed as Record<string, unknown>)[key]).toBeTruthy();
|
||||
function expectOpenClawSchemaInvalidPreservesField(params: {
|
||||
config: unknown;
|
||||
readValue: (parsed: unknown) => unknown;
|
||||
expectedValue: unknown;
|
||||
expectedPath?: string;
|
||||
expectedMessageIncludes?: string;
|
||||
}) {
|
||||
const before = JSON.stringify(params.config);
|
||||
const res = OpenClawSchema.safeParse(params.config);
|
||||
expect(res.success).toBe(false);
|
||||
if (!res.success) {
|
||||
if (params.expectedPath !== undefined) {
|
||||
expect(res.error.issues[0]?.path.join(".")).toBe(params.expectedPath);
|
||||
}
|
||||
if (params.expectedMessageIncludes !== undefined) {
|
||||
expect(res.error.issues[0]?.message).toContain(params.expectedMessageIncludes);
|
||||
}
|
||||
}
|
||||
expect(params.readValue(params.config)).toBe(params.expectedValue);
|
||||
expect(JSON.stringify(params.config)).toBe(before);
|
||||
}
|
||||
|
||||
describe("legacy config detection", () => {
|
||||
it('accepts imessage.dmPolicy="open" with allowFrom "*"', async () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { imessage: { dmPolicy: "open", allowFrom: ["*"] } },
|
||||
it('accepts imessage.dmPolicy="open" with allowFrom "*"', () => {
|
||||
expectSchemaConfigValue({
|
||||
schema: IMessageConfigSchema,
|
||||
config: { dmPolicy: "open", allowFrom: ["*"] },
|
||||
readValue: (config) => (config as { dmPolicy?: string }).dmPolicy,
|
||||
expectedValue: "open",
|
||||
});
|
||||
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", () => {
|
||||
expectValidConfigValue({
|
||||
config: { channels: { imessage: {} } },
|
||||
readValue: (config) =>
|
||||
(config as { channels?: { imessage?: { dmPolicy?: string } } }).channels?.imessage
|
||||
?.dmPolicy,
|
||||
expectSchemaConfigValue({
|
||||
schema: IMessageConfigSchema,
|
||||
config: {},
|
||||
readValue: (config) => (config as { dmPolicy?: string }).dmPolicy,
|
||||
expectedValue: "pairing",
|
||||
});
|
||||
});
|
||||
it("defaults imessage.groupPolicy to allowlist when imessage section exists", () => {
|
||||
expectValidConfigValue({
|
||||
config: { channels: { imessage: {} } },
|
||||
readValue: (config) =>
|
||||
(config as { channels?: { imessage?: { groupPolicy?: string } } }).channels?.imessage
|
||||
?.groupPolicy,
|
||||
expectSchemaConfigValue({
|
||||
schema: IMessageConfigSchema,
|
||||
config: {},
|
||||
readValue: (config) => (config as { groupPolicy?: string }).groupPolicy,
|
||||
expectedValue: "allowlist",
|
||||
});
|
||||
});
|
||||
@@ -146,122 +120,109 @@ describe("legacy config detection", () => {
|
||||
])("defaults: %s", (_name, schema, config, readValue, expectedValue) => {
|
||||
expectSchemaConfigValue({ schema, config, readValue, expectedValue });
|
||||
});
|
||||
it("rejects unsafe executable config values", async () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { imessage: { cliPath: "imsg; rm -rf /" } },
|
||||
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||
it("rejects unsafe executable config values", () => {
|
||||
expectInvalidSchemaIssuePath({
|
||||
schema: IMessageConfigSchema,
|
||||
config: { cliPath: "imsg; rm -rf /" },
|
||||
expectedPath: "cliPath",
|
||||
});
|
||||
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 () => {
|
||||
const res = validateConfigObject({
|
||||
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||
it("accepts tools audio transcription without cli", () => {
|
||||
expectSchemaValid(AudioSchema, {
|
||||
transcription: { command: ["whisper", "--model", "base"] },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
it("accepts path-like executable values with spaces", async () => {
|
||||
const res = validateConfigObject({
|
||||
channels: { imessage: { cliPath: "/Applications/Imsg Tools/imsg" } },
|
||||
audio: {
|
||||
transcription: {
|
||||
command: ["whisper", "--model"],
|
||||
},
|
||||
},
|
||||
it("accepts path-like executable values with spaces", () => {
|
||||
expectSchemaValid(IMessageConfigSchema, {
|
||||
cliPath: "/Applications/Imsg Tools/imsg",
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
'rejects discord.dm.policy="open" without allowFrom "*"',
|
||||
{ channels: { discord: { dm: { policy: "open", allowFrom: ["123"] } } } },
|
||||
"channels.discord.dm.allowFrom",
|
||||
DiscordConfigSchema,
|
||||
{ dm: { policy: "open", allowFrom: ["123"] } },
|
||||
"dm.allowFrom",
|
||||
],
|
||||
[
|
||||
'rejects discord.dmPolicy="open" without allowFrom "*"',
|
||||
{ channels: { discord: { dmPolicy: "open", allowFrom: ["123"] } } },
|
||||
"channels.discord.allowFrom",
|
||||
DiscordConfigSchema,
|
||||
{ dmPolicy: "open", allowFrom: ["123"] },
|
||||
"allowFrom",
|
||||
],
|
||||
[
|
||||
'rejects slack.dm.policy="open" without allowFrom "*"',
|
||||
{ channels: { slack: { dm: { policy: "open", allowFrom: ["U123"] } } } },
|
||||
"channels.slack.dm.allowFrom",
|
||||
SlackConfigSchema,
|
||||
{ dm: { policy: "open", allowFrom: ["U123"] } },
|
||||
"dm.allowFrom",
|
||||
],
|
||||
[
|
||||
'rejects slack.dmPolicy="open" without allowFrom "*"',
|
||||
{ channels: { slack: { dmPolicy: "open", allowFrom: ["U123"] } } },
|
||||
"channels.slack.allowFrom",
|
||||
SlackConfigSchema,
|
||||
{ dmPolicy: "open", allowFrom: ["U123"] },
|
||||
"allowFrom",
|
||||
],
|
||||
])("rejects: %s", (_name, config, expectedPath) => {
|
||||
expectInvalidIssuePath(config, expectedPath);
|
||||
])("rejects: %s", (_name, schema, config, expectedPath) => {
|
||||
expectInvalidSchemaIssuePath({ schema, config, expectedPath });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'accepts discord dm.allowFrom="*" with top-level allowFrom alias',
|
||||
schema: DiscordConfigSchema,
|
||||
config: {
|
||||
channels: { discord: { dm: { policy: "open", allowFrom: ["123"] }, allowFrom: ["*"] } },
|
||||
dm: { policy: "open", allowFrom: ["123"] },
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'accepts slack dm.allowFrom="*" with top-level allowFrom alias',
|
||||
schema: SlackConfigSchema,
|
||||
config: {
|
||||
channels: { slack: { dm: { policy: "open", allowFrom: ["U123"] }, allowFrom: ["*"] } },
|
||||
dm: { policy: "open", allowFrom: ["U123"] },
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
])("$name", ({ config }) => {
|
||||
const res = validateConfigObject(config);
|
||||
expect(res.ok).toBe(true);
|
||||
])("$name", ({ schema, config }) => {
|
||||
expectSchemaValid(schema, config);
|
||||
});
|
||||
it("rejects legacy agent.model string", async () => {
|
||||
const res = validateConfigObject({
|
||||
it("rejects legacy agent.model string", () => {
|
||||
const res = OpenClawSchema.safeParse({
|
||||
agent: { model: "anthropic/claude-opus-4-6" },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(res.issues[0]?.path).toBe("");
|
||||
expect(res.issues[0]?.message).toContain('"agent"');
|
||||
expect(res.success).toBe(false);
|
||||
if (!res.success) {
|
||||
expect(res.error.issues[0]?.path.join(".")).toBe("");
|
||||
expect(res.error.issues[0]?.message).toContain('"agent"');
|
||||
}
|
||||
});
|
||||
it("rejects removed legacy provider sections in snapshot", async () => {
|
||||
await withSnapshotForConfig({ whatsapp: { allowFrom: ["+1555"] } }, async (ctx) => {
|
||||
expectSnapshotInvalidRootKey(ctx, "whatsapp");
|
||||
it("rejects removed legacy provider sections", () => {
|
||||
expectOpenClawSchemaInvalidPreservesField({
|
||||
config: { whatsapp: { allowFrom: ["+1555"] } },
|
||||
readValue: (parsed) =>
|
||||
(parsed as { whatsapp?: { allowFrom?: string[] } }).whatsapp?.allowFrom?.[0],
|
||||
expectedValue: "+1555",
|
||||
expectedPath: "",
|
||||
expectedMessageIncludes: '"whatsapp"',
|
||||
});
|
||||
});
|
||||
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.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",
|
||||
);
|
||||
|
||||
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("preserves claude-cli auth profile mode during validation", () => {
|
||||
const config = {
|
||||
auth: {
|
||||
profiles: {
|
||||
"anthropic:claude-cli": { provider: "anthropic", mode: "token" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = OpenClawSchema.safeParse(config);
|
||||
expect(res.success).toBe(true);
|
||||
if (res.success) {
|
||||
expect(res.data.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||
}
|
||||
expect(config.auth.profiles["anthropic:claude-cli"].mode).toBe("token");
|
||||
});
|
||||
it("rejects bindings[].match.provider on load", async () => {
|
||||
await expectLoadRejectionPreservesField({
|
||||
it("rejects bindings[].match.provider without mutating the source", () => {
|
||||
expectOpenClawSchemaInvalidPreservesField({
|
||||
config: {
|
||||
bindings: [{ agentId: "main", match: { provider: "slack" } }],
|
||||
},
|
||||
@@ -271,8 +232,8 @@ describe("legacy config detection", () => {
|
||||
expectedValue: "slack",
|
||||
});
|
||||
});
|
||||
it("rejects bindings[].match.accountID on load", async () => {
|
||||
await expectLoadRejectionPreservesField({
|
||||
it("rejects bindings[].match.accountID without mutating the source", () => {
|
||||
expectOpenClawSchemaInvalidPreservesField({
|
||||
config: {
|
||||
bindings: [{ agentId: "main", match: { channel: "telegram", accountID: "work" } }],
|
||||
},
|
||||
@@ -282,51 +243,46 @@ describe("legacy config detection", () => {
|
||||
expectedValue: "work",
|
||||
});
|
||||
});
|
||||
it("accepts bindings[].comment on load", () => {
|
||||
expectValidConfigValue({
|
||||
config: {
|
||||
bindings: [{ agentId: "main", comment: "primary route", match: { channel: "telegram" } }],
|
||||
},
|
||||
readValue: (config) =>
|
||||
(config as { bindings?: Array<{ comment?: string }> }).bindings?.[0]?.comment,
|
||||
it("accepts bindings[].comment during validation", () => {
|
||||
expectSchemaConfigValue({
|
||||
schema: BindingsSchema,
|
||||
config: [{ agentId: "main", comment: "primary route", match: { channel: "telegram" } }],
|
||||
readValue: (config) => (config as Array<{ comment?: string }> | undefined)?.[0]?.comment,
|
||||
expectedValue: "primary route",
|
||||
});
|
||||
});
|
||||
it("rejects session.sendPolicy.rules[].match.provider on load", async () => {
|
||||
await withSnapshotForConfig(
|
||||
{
|
||||
it("rejects session.sendPolicy.rules[].match.provider without mutating the source", () => {
|
||||
expectOpenClawSchemaInvalidPreservesField({
|
||||
config: {
|
||||
session: {
|
||||
sendPolicy: {
|
||||
rules: [{ action: "deny", match: { provider: "telegram" } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.issues.length).toBeGreaterThan(0);
|
||||
const parsed = ctx.parsed as {
|
||||
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
|
||||
};
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBe("telegram");
|
||||
},
|
||||
);
|
||||
readValue: (parsed) =>
|
||||
(
|
||||
parsed as {
|
||||
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
|
||||
}
|
||||
).session?.sendPolicy?.rules?.[0]?.match?.provider,
|
||||
expectedValue: "telegram",
|
||||
});
|
||||
});
|
||||
it("rejects messages.queue.byProvider on load", async () => {
|
||||
await withSnapshotForConfig(
|
||||
{ messages: { queue: { byProvider: { whatsapp: "queue" } } } },
|
||||
async (ctx) => {
|
||||
expect(ctx.snapshot.valid).toBe(false);
|
||||
expect(ctx.snapshot.issues.length).toBeGreaterThan(0);
|
||||
|
||||
const parsed = ctx.parsed as {
|
||||
messages?: {
|
||||
queue?: {
|
||||
byProvider?: Record<string, unknown>;
|
||||
it("rejects messages.queue.byProvider without mutating the source", () => {
|
||||
expectOpenClawSchemaInvalidPreservesField({
|
||||
config: { messages: { queue: { byProvider: { whatsapp: "queue" } } } },
|
||||
readValue: (parsed) =>
|
||||
(
|
||||
parsed as {
|
||||
messages?: {
|
||||
queue?: {
|
||||
byProvider?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
|
||||
},
|
||||
);
|
||||
}
|
||||
).messages?.queue?.byProvider?.whatsapp,
|
||||
expectedValue: "queue",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user