test(commands): align slash-command config persistence coverage

This commit is contained in:
Peter Steinberger
2026-03-13 02:51:35 +00:00
parent 7dc447f79f
commit 6b14e6b55b
2 changed files with 161 additions and 125 deletions

View File

@@ -26,7 +26,7 @@ export function buildCommandTestParams(
ctx,
cfg,
isGroup: false,
triggerBodyNormalized: commandBody.trim().toLowerCase(),
triggerBodyNormalized: commandBody.trim(),
commandAuthorized: true,
});

View File

@@ -133,6 +133,31 @@ afterAll(async () => {
await fs.rm(testWorkspaceDir, { recursive: true, force: true });
});
async function withTempConfigPath<T>(
initialConfig: Record<string, unknown>,
run: (configPath: string) => Promise<T>,
): Promise<T> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-commands-config-"));
const configPath = path.join(dir, "openclaw.json");
const previous = process.env.OPENCLAW_CONFIG_PATH;
process.env.OPENCLAW_CONFIG_PATH = configPath;
await fs.writeFile(configPath, JSON.stringify(initialConfig, null, 2), "utf-8");
try {
return await run(configPath);
} finally {
if (previous === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previous;
}
await fs.rm(dir, { recursive: true, force: true });
}
}
async function readJsonFile<T>(filePath: string): Promise<T> {
return JSON.parse(await fs.readFile(filePath, "utf-8")) as T;
}
function buildParams(commandBody: string, cfg: OpenClawConfig, ctxOverrides?: Partial<MsgContext>) {
return buildCommandTestParams(commandBody, cfg, ctxOverrides, { workspaceDir: testWorkspaceDir });
}
@@ -702,13 +727,13 @@ describe("handleCommands /config owner gating", () => {
} as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: { messages: { ackreaction: ":)" } },
parsed: { messages: { ackReaction: ":)" } },
});
const params = buildParams("/config show messages.ackReaction", cfg);
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Config messages.ackreaction");
expect(result.reply?.text).toContain("Config messages.ackReaction");
});
});
@@ -795,7 +820,7 @@ describe("handleCommands /config configWrites gating", () => {
} as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: { messages: { ackreaction: ":)" } },
parsed: { messages: { ackReaction: ":)" } },
});
const params = buildParams("/config show messages.ackReaction", cfg, {
Provider: INTERNAL_MESSAGE_CHANNEL,
@@ -806,10 +831,11 @@ describe("handleCommands /config configWrites gating", () => {
params.command.senderIsOwner = false;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Config messages.ackreaction");
expect(result.reply?.text).toContain("Config messages.ackReaction");
});
it("keeps /config set working for gateway operator.admin clients", async () => {
await withTempConfigPath({ messages: { ackReaction: ":)" } }, async (configPath) => {
const cfg = {
commands: { config: true, text: true },
} as OpenClawConfig;
@@ -830,14 +856,14 @@ describe("handleCommands /config configWrites gating", () => {
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledOnce();
expect(result.reply?.text).toContain("Config updated");
const written = await readJsonFile<OpenClawConfig>(configPath);
expect(written.messages?.ackReaction).toBe(":D");
});
});
it("keeps /config set working for gateway operator.admin on protected account paths", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
const initialConfig = {
channels: {
telegram: {
accounts: {
@@ -845,7 +871,11 @@ describe("handleCommands /config configWrites gating", () => {
},
},
},
},
};
await withTempConfigPath(initialConfig, async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: structuredClone(initialConfig),
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
@@ -874,10 +904,11 @@ describe("handleCommands /config configWrites gating", () => {
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Config updated");
const written = writeConfigFileMock.mock.calls.at(-1)?.[0] as OpenClawConfig;
const written = await readJsonFile<OpenClawConfig>(configPath);
expect(written.channels?.telegram?.accounts?.work?.enabled).toBe(false);
});
});
});
describe("handleCommands /debug owner gating", () => {
it("blocks /debug show from authorized non-owner senders", async () => {
@@ -940,7 +971,7 @@ function buildPolicyParams(
ctx,
cfg,
isGroup: false,
triggerBodyNormalized: commandBody.trim().toLowerCase(),
triggerBodyNormalized: commandBody.trim(),
commandAuthorized: true,
});
@@ -986,6 +1017,11 @@ describe("handleCommands /allowlist", () => {
});
it("adds entries to config and pairing store", async () => {
await withTempConfigPath(
{
channels: { telegram: { allowFrom: ["123"] } },
},
async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
@@ -1009,17 +1045,16 @@ describe("handleCommands /allowlist", () => {
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
channels: { telegram: { allowFrom: ["123", "789"] } },
}),
);
const written = await readJsonFile<OpenClawConfig>(configPath);
expect(written.channels?.telegram?.allowFrom).toEqual(["123", "789"]);
expect(addChannelAllowFromStoreEntryMock).toHaveBeenCalledWith({
channel: "telegram",
entry: "789",
accountId: "default",
});
expect(result.reply?.text).toContain("DM allowlist added");
},
);
});
it("writes store entries to the selected account scope", async () => {
@@ -1151,10 +1186,7 @@ describe("handleCommands /allowlist", () => {
}));
for (const testCase of cases) {
const previousWriteCount = writeConfigFileMock.mock.calls.length;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
const initialConfig = {
channels: {
[testCase.provider]: {
allowFrom: testCase.initialAllowFrom,
@@ -1162,7 +1194,11 @@ describe("handleCommands /allowlist", () => {
configWrites: true,
},
},
},
};
await withTempConfigPath(initialConfig, async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: structuredClone(initialConfig),
});
const cfg = {
@@ -1183,12 +1219,12 @@ describe("handleCommands /allowlist", () => {
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock.mock.calls.length).toBe(previousWriteCount + 1);
const written = writeConfigFileMock.mock.calls.at(-1)?.[0] as OpenClawConfig;
const written = await readJsonFile<OpenClawConfig>(configPath);
const channelConfig = written.channels?.[testCase.provider];
expect(channelConfig?.allowFrom).toEqual(testCase.expectedAllowFrom);
expect(channelConfig?.dm?.allowFrom).toBeUndefined();
expect(result.reply?.text).toContain(`channels.${testCase.provider}.allowFrom`);
});
}
});
});