test: streamline auto-reply and tts suites

This commit is contained in:
Peter Steinberger
2026-02-21 21:43:16 +00:00
parent 5d9e7c942c
commit 52ddb6ae18
7 changed files with 1530 additions and 1809 deletions

View File

@@ -165,26 +165,21 @@ describe("handleCommands gating", () => {
expect(result.reply?.text).toContain("elevated is not available");
});
it("blocks /config when disabled", async () => {
it("blocks /config and /debug when disabled", async () => {
const cfg = {
commands: { config: false, debug: false, text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/config show", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/config is disabled");
});
it("blocks /debug when disabled", async () => {
const cfg = {
commands: { config: false, debug: false, text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/debug show", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/debug is disabled");
const cases = [
{ commandBody: "/config show", expectedText: "/config is disabled" },
{ commandBody: "/debug show", expectedText: "/debug is disabled" },
] as const;
for (const testCase of cases) {
const params = buildParams(testCase.commandBody, cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain(testCase.expectedText);
}
});
it("does not enable gated commands from inherited command flags", async () => {
@@ -266,50 +261,29 @@ describe("/approve command", () => {
expect(callGatewayMock).not.toHaveBeenCalled();
});
it("allows gateway clients with approvals scope", async () => {
it("allows gateway clients with approvals or admin scopes", async () => {
const cfg = {
commands: { text: true },
} as OpenClawConfig;
const params = buildParams("/approve abc allow-once", cfg, {
Provider: "webchat",
Surface: "webchat",
GatewayClientScopes: ["operator.approvals"],
});
const scopeCases = [["operator.approvals"], ["operator.admin"]];
for (const scopes of scopeCases) {
callGatewayMock.mockResolvedValueOnce({ ok: true });
const params = buildParams("/approve abc allow-once", cfg, {
Provider: "webchat",
Surface: "webchat",
GatewayClientScopes: scopes,
});
callGatewayMock.mockResolvedValueOnce({ ok: true });
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Exec approval allow-once submitted");
expect(callGatewayMock).toHaveBeenCalledWith(
expect.objectContaining({
method: "exec.approval.resolve",
params: { id: "abc", decision: "allow-once" },
}),
);
});
it("allows gateway clients with admin scope", async () => {
const cfg = {
commands: { text: true },
} as OpenClawConfig;
const params = buildParams("/approve abc allow-once", cfg, {
Provider: "webchat",
Surface: "webchat",
GatewayClientScopes: ["operator.admin"],
});
callGatewayMock.mockResolvedValueOnce({ ok: true });
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Exec approval allow-once submitted");
expect(callGatewayMock).toHaveBeenCalledWith(
expect.objectContaining({
method: "exec.approval.resolve",
params: { id: "abc", decision: "allow-once" },
}),
);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Exec approval allow-once submitted");
expect(callGatewayMock).toHaveBeenLastCalledWith(
expect.objectContaining({
method: "exec.approval.resolve",
params: { id: "abc", decision: "allow-once" },
}),
);
}
});
});
@@ -420,67 +394,76 @@ describe("buildCommandsPaginationKeyboard", () => {
});
describe("parseConfigCommand", () => {
it("parses show/unset", () => {
expect(parseConfigCommand("/config")).toEqual({ action: "show" });
expect(parseConfigCommand("/config show")).toEqual({
action: "show",
path: undefined,
});
expect(parseConfigCommand("/config show foo.bar")).toEqual({
action: "show",
path: "foo.bar",
});
expect(parseConfigCommand("/config get foo.bar")).toEqual({
action: "show",
path: "foo.bar",
});
expect(parseConfigCommand("/config unset foo.bar")).toEqual({
action: "unset",
path: "foo.bar",
});
});
it("parses config/debug command actions and JSON payloads", () => {
const cases: Array<{
parse: (input: string) => unknown;
input: string;
expected: unknown;
}> = [
{ parse: parseConfigCommand, input: "/config", expected: { action: "show" } },
{
parse: parseConfigCommand,
input: "/config show",
expected: { action: "show", path: undefined },
},
{
parse: parseConfigCommand,
input: "/config show foo.bar",
expected: { action: "show", path: "foo.bar" },
},
{
parse: parseConfigCommand,
input: "/config get foo.bar",
expected: { action: "show", path: "foo.bar" },
},
{
parse: parseConfigCommand,
input: "/config unset foo.bar",
expected: { action: "unset", path: "foo.bar" },
},
{
parse: parseConfigCommand,
input: '/config set foo={"a":1}',
expected: { action: "set", path: "foo", value: { a: 1 } },
},
{ parse: parseDebugCommand, input: "/debug", expected: { action: "show" } },
{ parse: parseDebugCommand, input: "/debug show", expected: { action: "show" } },
{ parse: parseDebugCommand, input: "/debug reset", expected: { action: "reset" } },
{
parse: parseDebugCommand,
input: "/debug unset foo.bar",
expected: { action: "unset", path: "foo.bar" },
},
{
parse: parseDebugCommand,
input: '/debug set foo={"a":1}',
expected: { action: "set", path: "foo", value: { a: 1 } },
},
];
it("parses set with JSON", () => {
const cmd = parseConfigCommand('/config set foo={"a":1}');
expect(cmd).toEqual({ action: "set", path: "foo", value: { a: 1 } });
});
});
describe("parseDebugCommand", () => {
it("parses show/reset", () => {
expect(parseDebugCommand("/debug")).toEqual({ action: "show" });
expect(parseDebugCommand("/debug show")).toEqual({ action: "show" });
expect(parseDebugCommand("/debug reset")).toEqual({ action: "reset" });
});
it("parses set with JSON", () => {
const cmd = parseDebugCommand('/debug set foo={"a":1}');
expect(cmd).toEqual({ action: "set", path: "foo", value: { a: 1 } });
});
it("parses unset", () => {
const cmd = parseDebugCommand("/debug unset foo.bar");
expect(cmd).toEqual({ action: "unset", path: "foo.bar" });
for (const testCase of cases) {
expect(testCase.parse(testCase.input)).toEqual(testCase.expected);
}
});
});
describe("extractMessageText", () => {
it("preserves user text that looks like tool call markers", () => {
const message = {
role: "user",
content: "Here [Tool Call: foo (ID: 1)] ok",
};
const result = extractMessageText(message);
expect(result?.text).toContain("[Tool Call: foo (ID: 1)]");
});
it("preserves user markers and sanitizes assistant markers", () => {
const cases = [
{
message: { role: "user", content: "Here [Tool Call: foo (ID: 1)] ok" },
expectedText: "Here [Tool Call: foo (ID: 1)] ok",
},
{
message: { role: "assistant", content: "Here [Tool Call: foo (ID: 1)] ok" },
expectedText: "Here ok",
},
] as const;
it("sanitizes assistant tool call markers", () => {
const message = {
role: "assistant",
content: "Here [Tool Call: foo (ID: 1)] ok",
};
const result = extractMessageText(message);
expect(result?.text).toBe("Here ok");
for (const testCase of cases) {
const result = extractMessageText(testCase.message);
expect(result?.text).toBe(testCase.expectedText);
}
});
});
@@ -498,28 +481,18 @@ describe("handleCommands /config configWrites gating", () => {
});
describe("handleCommands bash alias", () => {
it("routes !poll through the /bash handler", async () => {
resetBashChatCommandForTests();
it("routes !poll and !stop through the /bash handler", async () => {
const cfg = {
commands: { bash: true, text: true },
whatsapp: { allowFrom: ["*"] },
} as OpenClawConfig;
const params = buildParams("!poll", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("No active bash job");
});
it("routes !stop through the /bash handler", async () => {
resetBashChatCommandForTests();
const cfg = {
commands: { bash: true, text: true },
whatsapp: { allowFrom: ["*"] },
} as OpenClawConfig;
const params = buildParams("!stop", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("No active bash job");
for (const aliasCommand of ["!poll", "!stop"]) {
resetBashChatCommandForTests();
const params = buildParams(aliasCommand, cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("No active bash job");
}
});
});
@@ -623,90 +596,66 @@ describe("handleCommands /allowlist", () => {
expect(result.reply?.text).toContain("DM allowlist added");
});
it("removes Slack DM allowlist entries from canonical allowFrom and deletes legacy dm.allowFrom", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
channels: {
slack: {
allowFrom: ["U111", "U222"],
dm: { allowFrom: ["U111", "U222"] },
configWrites: true,
},
},
it("removes DM allowlist entries from canonical allowFrom and deletes legacy dm.allowFrom", async () => {
const cases = [
{
provider: "slack",
removeId: "U111",
initialAllowFrom: ["U111", "U222"],
expectedAllowFrom: ["U222"],
},
});
{
provider: "discord",
removeId: "111",
initialAllowFrom: ["111", "222"],
expectedAllowFrom: ["222"],
},
] as const;
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
const cfg = {
commands: { text: true, config: true },
channels: {
slack: {
allowFrom: ["U111", "U222"],
dm: { allowFrom: ["U111", "U222"] },
configWrites: true,
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,
},
},
},
},
} as OpenClawConfig;
});
const params = buildPolicyParams("/allowlist remove dm U111", cfg, {
Provider: "slack",
Surface: "slack",
});
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledTimes(1);
const written = writeConfigFileMock.mock.calls[0]?.[0] as OpenClawConfig;
expect(written.channels?.slack?.allowFrom).toEqual(["U222"]);
expect(written.channels?.slack?.dm?.allowFrom).toBeUndefined();
expect(result.reply?.text).toContain("channels.slack.allowFrom");
});
it("removes Discord DM allowlist entries from canonical allowFrom and deletes legacy dm.allowFrom", async () => {
readConfigFileSnapshotMock.mockResolvedValueOnce({
valid: true,
parsed: {
const cfg = {
commands: { text: true, config: true },
channels: {
discord: {
allowFrom: ["111", "222"],
dm: { allowFrom: ["111", "222"] },
[testCase.provider]: {
allowFrom: testCase.initialAllowFrom,
dm: { allowFrom: testCase.initialAllowFrom },
configWrites: true,
},
},
},
});
validateConfigObjectWithPluginsMock.mockImplementation((config: unknown) => ({
ok: true,
config,
}));
} as OpenClawConfig;
const cfg = {
commands: { text: true, config: true },
channels: {
discord: {
allowFrom: ["111", "222"],
dm: { allowFrom: ["111", "222"] },
configWrites: true,
},
},
} as OpenClawConfig;
const params = buildPolicyParams(`/allowlist remove dm ${testCase.removeId}`, cfg, {
Provider: testCase.provider,
Surface: testCase.provider,
});
const result = await handleCommands(params);
const params = buildPolicyParams("/allowlist remove dm 111", cfg, {
Provider: "discord",
Surface: "discord",
});
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(writeConfigFileMock).toHaveBeenCalledTimes(1);
const written = writeConfigFileMock.mock.calls[0]?.[0] as OpenClawConfig;
expect(written.channels?.discord?.allowFrom).toEqual(["222"]);
expect(written.channels?.discord?.dm?.allowFrom).toBeUndefined();
expect(result.reply?.text).toContain("channels.discord.allowFrom");
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`);
}
});
});
@@ -736,44 +685,56 @@ describe("/models command", () => {
expect(buttons?.length).toBeGreaterThan(0);
});
it("lists provider models with pagination hints", async () => {
// Use discord surface for text-based output tests
const params = buildPolicyParams("/models anthropic", cfg, { Surface: "discord" });
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Models (anthropic");
expect(result.reply?.text).toContain("page 1/");
expect(result.reply?.text).toContain("anthropic/claude-opus-4-5");
expect(result.reply?.text).toContain("Switch: /model <provider/model>");
expect(result.reply?.text).toContain("All: /models anthropic all");
});
it("handles provider model pagination, all mode, and unknown providers", async () => {
const cases = [
{
name: "lists provider models with pagination hints",
command: "/models anthropic",
includes: [
"Models (anthropic",
"page 1/",
"anthropic/claude-opus-4-5",
"Switch: /model <provider/model>",
"All: /models anthropic all",
],
excludes: [],
},
{
name: "ignores page argument when all flag is present",
command: "/models anthropic 3 all",
includes: ["Models (anthropic", "page 1/1", "anthropic/claude-opus-4-5"],
excludes: ["Page out of range"],
},
{
name: "errors on out-of-range pages",
command: "/models anthropic 4",
includes: ["Page out of range", "valid: 1-"],
excludes: [],
},
{
name: "handles unknown providers",
command: "/models not-a-provider",
includes: ["Unknown provider", "Available providers"],
excludes: [],
},
] as const;
it("ignores page argument when all flag is present", async () => {
// Use discord surface for text-based output tests
const params = buildPolicyParams("/models anthropic 3 all", cfg, { Surface: "discord" });
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Models (anthropic");
expect(result.reply?.text).toContain("page 1/1");
expect(result.reply?.text).toContain("anthropic/claude-opus-4-5");
expect(result.reply?.text).not.toContain("Page out of range");
});
it("errors on out-of-range pages", async () => {
// Use discord surface for text-based output tests
const params = buildPolicyParams("/models anthropic 4", cfg, { Surface: "discord" });
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Page out of range");
expect(result.reply?.text).toContain("valid: 1-");
});
it("handles unknown providers", async () => {
const params = buildPolicyParams("/models not-a-provider", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Unknown provider");
expect(result.reply?.text).toContain("Available providers");
for (const testCase of cases) {
// Use discord surface for deterministic text-based output assertions.
const result = await handleCommands(
buildPolicyParams(testCase.command, cfg, {
Provider: "discord",
Surface: "discord",
}),
);
expect(result.shouldContinue, testCase.name).toBe(false);
for (const expected of testCase.includes) {
expect(result.reply?.text, `${testCase.name}: ${expected}`).toContain(expected);
}
for (const blocked of testCase.excludes ?? []) {
expect(result.reply?.text, `${testCase.name}: !${blocked}`).not.toContain(blocked);
}
}
});
it("lists configured models outside the curated catalog", async () => {
@@ -867,40 +828,33 @@ describe("handleCommands hooks", () => {
});
describe("handleCommands context", () => {
it("returns context help for /context", async () => {
it("returns expected details for /context commands", async () => {
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/context", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/context list");
expect(result.reply?.text).toContain("Inline shortcut");
});
it("returns a per-file breakdown for /context list", async () => {
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/context list", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Injected workspace files:");
expect(result.reply?.text).toContain("AGENTS.md");
});
it("returns a detailed breakdown for /context detail", async () => {
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/context detail", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("Context breakdown (detailed)");
expect(result.reply?.text).toContain("Top tools (schema size):");
const cases = [
{
commandBody: "/context",
expectedText: ["/context list", "Inline shortcut"],
},
{
commandBody: "/context list",
expectedText: ["Injected workspace files:", "AGENTS.md"],
},
{
commandBody: "/context detail",
expectedText: ["Context breakdown (detailed)", "Top tools (schema size):"],
},
] as const;
for (const testCase of cases) {
const params = buildParams(testCase.commandBody, cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
for (const expectedText of testCase.expectedText) {
expect(result.reply?.text).toContain(expectedText);
}
}
});
});
@@ -1039,30 +993,23 @@ describe("handleCommands subagents", () => {
expect(result.reply?.text).not.toContain("Subagents:");
});
it("returns help for unknown subagents action", async () => {
it("returns help/usage for invalid or incomplete subagents commands", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/subagents foo", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/subagents");
});
it("returns usage for subagents info without target", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
const cfg = {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig;
const params = buildParams("/subagents info", cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain("/subagents info");
const cases = [
{ commandBody: "/subagents foo", expectedText: "/subagents" },
{ commandBody: "/subagents info", expectedText: "/subagents info" },
] as const;
for (const testCase of cases) {
const params = buildParams(testCase.commandBody, cfg);
const result = await handleCommands(params);
expect(result.shouldContinue).toBe(false);
expect(result.reply?.text).toContain(testCase.expectedText);
}
});
it("includes subagent count in /status when active", async () => {