test(commands): align slash-command config persistence coverage
This commit is contained in:
@@ -26,7 +26,7 @@ export function buildCommandTestParams(
|
||||
ctx,
|
||||
cfg,
|
||||
isGroup: false,
|
||||
triggerBodyNormalized: commandBody.trim().toLowerCase(),
|
||||
triggerBodyNormalized: commandBody.trim(),
|
||||
commandAuthorized: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user