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,76 +831,82 @@ 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 () => {
const cfg = {
commands: { config: true, text: true },
} as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: { messages: { ackReaction: ":)" } },
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
const params = buildParams('/config set messages.ackReaction=":D"', cfg, {
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
GatewayClientScopes: ["operator.write", "operator.admin"],
});
params.command.channel = INTERNAL_MESSAGE_CHANNEL;
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledOnce();
expect(result.reply?.text).toContain("Config updated");
});
it("keeps /config set working for gateway operator.admin on protected account paths", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
channels: {
telegram: {
accounts: {
work: { enabled: true, configWrites: false },
},
},
},
},
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
const params = buildParams(
"/config set channels.telegram.accounts.work.enabled=false",
{
await withTempConfigPath({ messages: { ackReaction: ":)" } }, async (configPath) => {
const cfg = {
commands: { config: true, text: true },
channels: {
telegram: {
accounts: {
work: { enabled: true, configWrites: false },
},
},
},
} as OpenClawConfig,
{
} as OpenClawConfig;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: { messages: { ackReaction: ":)" } },
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
const params = buildParams('/config set messages.ackReaction=":D"', cfg, {
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
GatewayClientScopes: ["operator.write", "operator.admin"],
});
params.command.channel = INTERNAL_MESSAGE_CHANNEL;
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
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 () => {
const initialConfig = {
channels: {
telegram: {
accounts: {
work: { enabled: true, configWrites: false },
},
},
},
);
params.command.channel = INTERNAL_MESSAGE_CHANNEL;
params.command.senderIsOwner = true;
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;
expect(written.channels?.telegram?.accounts?.work?.enabled).toBe(false);
};
await withTempConfigPath(initialConfig, async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: structuredClone(initialConfig),
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
const params = buildParams(
"/config set channels.telegram.accounts.work.enabled=false",
{
commands: { config: true, text: true },
channels: {
telegram: {
accounts: {
work: { enabled: true, configWrites: false },
},
},
},
} as OpenClawConfig,
{
Provider: INTERNAL_MESSAGE_CHANNEL,
Surface: INTERNAL_MESSAGE_CHANNEL,
GatewayClientScopes: ["operator.write", "operator.admin"],
},
);
params.command.channel = INTERNAL_MESSAGE_CHANNEL;
params.command.senderIsOwner = true;
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Config updated");
const written = await readJsonFile<OpenClawConfig>(configPath);
expect(written.channels?.telegram?.accounts?.work?.enabled).toBe(false);
});
});
});
@@ -940,7 +971,7 @@ function buildPolicyParams(
ctx,
cfg,
isGroup: false,
triggerBodyNormalized: commandBody.trim().toLowerCase(),
triggerBodyNormalized: commandBody.trim(),
commandAuthorized: true,
});
@@ -986,40 +1017,44 @@ describe("handleCommands /allowlist", () => {
});
it("adds entries to config and pairing store", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
await withTempConfigPath(
{
channels: { telegram: { allowFrom: ["123"] } },
},
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
addChannelAllowFromStoreEntryMock.mockResolvedValueOnce({
changed: true,
allowFrom: ["123", "789"],
});
async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
channels: { telegram: { allowFrom: ["123"] } },
},
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
addChannelAllowFromStoreEntryMock.mockResolvedValueOnce({
changed: true,
allowFrom: ["123", "789"],
});
const cfg = {
commands: { text: true, config: true },
channels: { telegram: { allowFrom: ["123"] } },
} as OpenClawConfig;
const params = buildPolicyParams("/allowlist add dm 789", cfg);
const result = await handleCommands(params);
const cfg = {
commands: { text: true, config: true },
channels: { telegram: { allowFrom: ["123"] } },
} as OpenClawConfig;
const params = buildPolicyParams("/allowlist add dm 789", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledWith(
expect.objectContaining({
channels: { telegram: { allowFrom: ["123", "789"] } },
}),
expect(result.shouldContinue).toBe(false);
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");
},
);
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,22 +1186,7 @@ describe("handleCommands /allowlist", () => {
}));
for (const testCase of cases) {
const previousWriteCount = writeConfigFileMock.mock.calls.length;
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
channels: {
[testCase.provider]: {
allowFrom: testCase.initialAllowFrom,
dm: { allowFrom: testCase.initialAllowFrom },
configWrites: true,
},
},
},
});
const cfg = {
commands: { text: true, config: true },
const initialConfig = {
channels: {
[testCase.provider]: {
allowFrom: testCase.initialAllowFrom,
@@ -1174,21 +1194,37 @@ describe("handleCommands /allowlist", () => {
configWrites: true,
},
},
} as OpenClawConfig;
};
await withTempConfigPath(initialConfig, async (configPath) => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: structuredClone(initialConfig),
});
const params = buildPolicyParams(`/allowlist remove dm ${testCase.removeId}`, cfg, {
Provider: testCase.provider,
Surface: testCase.provider,
const cfg = {
commands: { text: true, config: true },
channels: {
[testCase.provider]: {
allowFrom: testCase.initialAllowFrom,
dm: { allowFrom: testCase.initialAllowFrom },
configWrites: true,
},
},
} as OpenClawConfig;
const params = buildPolicyParams(`/allowlist remove dm ${testCase.removeId}`, cfg, {
Provider: testCase.provider,
Surface: testCase.provider,
});
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
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`);
});
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 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`);
}
});
});