diff --git a/extensions/feishu/src/outbound-tool-trace-sanitize.test.ts b/extensions/feishu/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 460e8f1c372f..000000000000
--- a/extensions/feishu/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// Feishu outbound must strip assistant internal tool-trace scaffolding, matching
-// the sibling channel fixes tracked under #90684 (Slack / Signal / Matrix /
-// Telegram / Google Chat / QQBot / IRC / SMS). sanitizeAssistantVisibleText
-// keeps markdown formatting suitable for Feishu card rendering.
-import { describe, expect, it } from "vitest";
-import { feishuPlugin } from "./channel.js";
-
-describe("feishu outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(feishuPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe("Done.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(feishuPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe(text);
- });
-});
diff --git a/extensions/irc/src/channel.test.ts b/extensions/irc/src/channel.test.ts
index 05f8e0abe1b5..832ed01ec049 100644
--- a/extensions/irc/src/channel.test.ts
+++ b/extensions/irc/src/channel.test.ts
@@ -10,17 +10,3 @@ describe("irc outbound chunking", () => {
expect(ircOutboundBaseAdapter.textChunkLimit).toBe(350);
});
});
-
-describe("irc outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(ircOutboundBaseAdapter.sanitizeText({ text })).toBe("Done.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(ircOutboundBaseAdapter.sanitizeText({ text })).toBe(text);
- });
-});
diff --git a/extensions/matrix/src/outbound-tool-trace-sanitize.test.ts b/extensions/matrix/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 5bf41b2a7d9e..000000000000
--- a/extensions/matrix/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// Matrix outbound must strip assistant internal tool-trace scaffolding, matching
-// the sibling channel fixes tracked under #90684 (Telegram #95774 / Google Chat
-// #95084 / IRC #97214). The hook runs before the markdown->HTML render, so a
-// single sanitize cleans both the plain body and the formatted_body.
-import { describe, expect, it } from "vitest";
-import { matrixPlugin } from "./channel.js";
-
-describe("matrix outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(matrixPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe("Done.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(matrixPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe(text);
- });
-});
diff --git a/extensions/nextcloud-talk/src/outbound-tool-trace-sanitize.test.ts b/extensions/nextcloud-talk/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 5b0067694a03..000000000000
--- a/extensions/nextcloud-talk/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// Nextcloud Talk outbound must strip assistant internal tool-trace scaffolding
-// before delivery, matching the shared channel sanitizer contract.
-import { describe, expect, it } from "vitest";
-import { nextcloudTalkPlugin } from "./channel.js";
-
-function sanitizeOutboundText(text: string): string {
- const sanitizeText = nextcloudTalkPlugin.outbound?.sanitizeText;
- if (!sanitizeText) {
- throw new Error("Expected Nextcloud Talk outbound sanitizeText hook");
- }
- return sanitizeText({ text, payload: { text } });
-}
-
-describe("nextcloud-talk outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
- expect(sanitizeOutboundText(text)).toBe("Done.");
- });
-
- it("strips XML tool-call scaffolding leaked into assistant text", () => {
- const text = '{"name":"exec"}Meeting notes sent.';
- expect(sanitizeOutboundText(text)).toBe("Meeting notes sent.");
- });
-
- it("strips multiline tool-response scaffolding leaked into assistant text", () => {
- const text = [
- "Checking now.",
- "",
- 'Searching for: "agenda"',
- "",
- "Meeting notes sent.",
- ].join("\n");
- expect(sanitizeOutboundText(text)).toBe("Checking now.\n\nMeeting notes sent.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The agenda has 3 open action items.";
- expect(sanitizeOutboundText(text)).toBe(text);
- });
-
- it("preserves internal trace examples inside fenced code", () => {
- const text = ["Example:", "```", "â ď¸ đ ď¸ `search repos (agent)` failed", "```"].join("\n");
- expect(sanitizeOutboundText(text)).toBe(text);
- });
-});
diff --git a/extensions/signal/src/outbound-tool-trace-sanitize.test.ts b/extensions/signal/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 54e0bcfbcc3c..000000000000
--- a/extensions/signal/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Signal outbound must strip assistant internal tool-trace scaffolding, matching
-// the sibling channel fixes tracked under #90684 (Telegram #95774 / Google Chat
-// #95084 / IRC #97214). Signal is plaintext-only, so leaked traces are verbatim.
-import { describe, expect, it } from "vitest";
-import { signalPlugin } from "./channel.js";
-
-describe("signal outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(signalPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe("Done.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(signalPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe(text);
- });
-});
diff --git a/extensions/slack/src/outbound-tool-trace-sanitize.test.ts b/extensions/slack/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 681ff785433e..000000000000
--- a/extensions/slack/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Slack outbound must strip assistant internal tool-trace scaffolding, matching
-// the sibling channel fixes tracked under #90684 (Telegram #95774 / Google Chat
-// #95084 / IRC #97214). sanitizeAssistantVisibleText keeps mrkdwn formatting.
-import { describe, expect, it } from "vitest";
-import { slackPlugin } from "./channel.js";
-
-describe("slack outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(slackPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe("Done.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(slackPlugin.outbound?.sanitizeText?.({ text, payload: { text } })).toBe(text);
- });
-});
diff --git a/extensions/twitch/src/outbound-tool-trace-sanitize.test.ts b/extensions/twitch/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index 2da0b3640812..000000000000
--- a/extensions/twitch/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// Twitch outbound must strip assistant internal tool-trace scaffolding before
-// delivery (#90684). The hook runs in core delivery before chunk planning, so
-// the 500-char Twitch chunker only ever sees sanitized text.
-import { describe, expect, it } from "vitest";
-import { twitchPlugin } from "./plugin.js";
-
-function sanitizeOutboundText(text: string): string {
- const sanitizeText = twitchPlugin.outbound?.sanitizeText;
- if (!sanitizeText) {
- throw new Error("Expected Twitch outbound sanitizeText hook");
- }
- return sanitizeText({ text, payload: { text } });
-}
-
-describe("twitch outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- const text = "Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed";
-
- expect(sanitizeOutboundText(text)).toBe("Done.");
- });
-
- it("strips XML tool-call scaffolding leaked into assistant text", () => {
- const text = '{"name":"exec"}Stream is live.';
-
- expect(sanitizeOutboundText(text)).toBe("Stream is live.");
- });
-
- it("strips multiline tool-response scaffolding leaked into assistant text", () => {
- const text = [
- "Checking now.",
- "",
- 'Searching for: "stream status"',
- "",
- "Stream is live.",
- ].join("\n");
-
- expect(sanitizeOutboundText(text)).toBe("Checking now.\n\nStream is live.");
- });
-
- it("preserves ordinary assistant prose while sanitizing", () => {
- const text = "The pipeline has 3 open deals.";
-
- expect(sanitizeOutboundText(text)).toBe(text);
- });
-});
diff --git a/extensions/zalo/src/outbound-tool-trace-sanitize.test.ts b/extensions/zalo/src/outbound-tool-trace-sanitize.test.ts
deleted file mode 100644
index a1de1235b2c5..000000000000
--- a/extensions/zalo/src/outbound-tool-trace-sanitize.test.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { zaloPlugin } from "./channel.js";
-
-function sanitizeOutboundText(text: string): string {
- const sanitizeText = zaloPlugin.outbound?.sanitizeText;
- if (!sanitizeText) {
- throw new Error("Expected Zalo outbound sanitizeText hook");
- }
- return sanitizeText({ text, payload: { text } });
-}
-
-describe("zalo outbound sanitizeText", () => {
- it("strips internal tool-trace banners before outbound delivery", () => {
- expect(sanitizeOutboundText("Done.\nâ ď¸ đ ď¸ `search repos (agent)` failed")).toBe("Done.");
- });
-
- it("strips XML tool-call scaffolding leaked into assistant text", () => {
- expect(sanitizeOutboundText('{"name":"exec"}Message sent.')).toBe(
- "Message sent.",
- );
- });
-
- it("preserves ordinary assistant prose", () => {
- const text = "The group has 5 active members.";
- expect(sanitizeOutboundText(text)).toBe(text);
- });
-
- it("preserves literal tool-call examples inside fenced code", () => {
- const text = ["```xml", '{"name":"exec"}', "```"].join("\n");
- expect(sanitizeOutboundText(text)).toBe(text);
- });
-
- it("returns empty text when the payload contains only an internal trace", () => {
- expect(sanitizeOutboundText("â ď¸ đ ď¸ `search repos (agent)` failed")).toBe("");
- });
-});
diff --git a/packages/memory-host-sdk/src/host/memory-schema.test.ts b/packages/memory-host-sdk/src/host/memory-schema.test.ts
index 7e4506ed5245..3173d6c3280d 100644
--- a/packages/memory-host-sdk/src/host/memory-schema.test.ts
+++ b/packages/memory-host-sdk/src/host/memory-schema.test.ts
@@ -183,79 +183,6 @@ describe("memory index schema", () => {
}
});
- it("does not import a legacy sidecar memory database during schema startup", () => {
- const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-memory-sidecar-"));
- const legacyPath = path.join(rootDir, "memory", "main.sqlite");
- const agentPath = path.join(rootDir, "agents", "main", "agent", "openclaw-agent.sqlite");
- fs.mkdirSync(path.dirname(legacyPath), { recursive: true });
- fs.mkdirSync(path.dirname(agentPath), { recursive: true });
- const legacyDb = new DatabaseSync(legacyPath);
- try {
- legacyDb.exec(`
- CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
- CREATE TABLE files (
- path TEXT PRIMARY KEY,
- source TEXT NOT NULL DEFAULT 'memory',
- hash TEXT NOT NULL,
- mtime INTEGER NOT NULL,
- size INTEGER NOT NULL
- );
- CREATE TABLE chunks (
- id TEXT PRIMARY KEY,
- path TEXT NOT NULL,
- source TEXT NOT NULL DEFAULT 'memory',
- start_line INTEGER NOT NULL,
- end_line INTEGER NOT NULL,
- hash TEXT NOT NULL,
- model TEXT NOT NULL,
- text TEXT NOT NULL,
- embedding TEXT NOT NULL,
- updated_at INTEGER NOT NULL
- );
- CREATE TABLE embedding_cache (
- provider TEXT NOT NULL,
- model TEXT NOT NULL,
- provider_key TEXT NOT NULL,
- hash TEXT NOT NULL,
- embedding TEXT NOT NULL,
- dims INTEGER,
- updated_at INTEGER NOT NULL,
- PRIMARY KEY (provider, model, provider_key, hash)
- );
- INSERT INTO meta VALUES ('memory_index_meta_v1', '{"vectorDims":3}');
- INSERT INTO files VALUES ('MEMORY.md', 'memory', 'file-hash', 10, 20);
- INSERT INTO chunks VALUES (
- 'chunk-1', 'MEMORY.md', 'memory', 1, 2, 'chunk-hash', 'embed-model',
- 'remember this', '[1,0,0]', 30
- );
- INSERT INTO embedding_cache VALUES (
- 'openai', 'embed-model', 'key', 'chunk-hash', '[1,0,0]', 3, 40
- );
- `);
- } finally {
- legacyDb.close();
- }
-
- const db = new DatabaseSync(agentPath);
- try {
- const result = ensureMemoryIndexSchema({
- db,
- cacheEnabled: true,
- ftsEnabled: true,
- });
-
- expect(result.ftsAvailable).toBe(true);
- expect(db.prepare("SELECT * FROM memory_index_sources").all()).toEqual([]);
- expect(db.prepare("SELECT id, text FROM memory_index_chunks").all()).toEqual([]);
- expect(db.prepare("SELECT id, text FROM memory_index_chunks_fts").all()).toEqual([]);
- expect(db.prepare("SELECT provider, hash FROM memory_embedding_cache").all()).toEqual([]);
- expect(fs.existsSync(legacyPath)).toBe(true);
- } finally {
- db.close();
- fs.rmSync(rootDir, { recursive: true, force: true });
- }
- });
-
it("stores source records with the same path in separate sources", () => {
const db = new DatabaseSync(":memory:");
try {
diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts
index b4085e0590e0..98dea2181a20 100644
--- a/src/auto-reply/reply/followup-runner.test.ts
+++ b/src/auto-reply/reply/followup-runner.test.ts
@@ -3389,7 +3389,18 @@ describe("createFollowupRunner progress forwarding", () => {
expect(routeReplyMock).not.toHaveBeenCalled();
});
- it("delivers queued fast auto progress for non-room-event message-tool-only turns", async () => {
+ it.each([
+ [
+ "delivers queued fast auto progress for non-room-event message-tool-only turns",
+ "user_request",
+ true,
+ ],
+ [
+ "suppresses queued fast auto progress for room-event message-tool-only turns",
+ "room_event",
+ false,
+ ],
+ ] as const)("%s", async (_name, currentInboundEventKind, shouldDeliverProgress) => {
vi.useFakeTimers();
vi.setSystemTime(1_000);
const realAgentEvents = await vi.importActual(
@@ -3426,7 +3437,7 @@ describe("createFollowupRunner progress forwarding", () => {
await runner(
createQueuedRun({
- currentInboundEventKind: "user_request",
+ currentInboundEventKind,
originatingChannel: "discord",
originatingTo: "channel:C1",
originatingAccountId: "acct-1",
@@ -3445,6 +3456,10 @@ describe("createFollowupRunner progress forwarding", () => {
}),
);
+ if (!shouldDeliverProgress) {
+ expect(routeReplyMock).not.toHaveBeenCalled();
+ return;
+ }
expect(routeReplyMock).toHaveBeenCalledWith(
expect.objectContaining({
channel: "discord",
@@ -3461,65 +3476,6 @@ describe("createFollowupRunner progress forwarding", () => {
);
});
- it("suppresses queued fast auto progress for room-event message-tool-only turns", async () => {
- vi.useFakeTimers();
- vi.setSystemTime(1_000);
- const realAgentEvents = await vi.importActual(
- "../../infra/agent-events.js",
- );
- const runtimeConfig: OpenClawConfig = {
- agents: {
- defaults: {
- models: {
- "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } },
- },
- },
- },
- };
- runCliAgentMock.mockImplementationOnce((params: { runId?: string }) => {
- realAgentEvents.emitAgentEvent({
- runId: params.runId ?? "run-fast-followup",
- stream: "tool",
- data: { phase: "start", name: "bash", toolCallId: "call-1" },
- });
- vi.setSystemTime(7_100);
- realAgentEvents.emitAgentEvent({
- runId: params.runId ?? "run-fast-followup",
- stream: "tool",
- data: { phase: "result", name: "bash", toolCallId: "call-1" },
- });
- return { payloads: [], meta: { agentMeta: {} } };
- });
- const runner = createFollowupRunner({
- typing: createMockTypingController(),
- typingMode: "instant",
- defaultModel: "anthropic/claude-opus-4-7",
- });
-
- await runner(
- createQueuedRun({
- currentInboundEventKind: "room_event",
- originatingChannel: "discord",
- originatingTo: "channel:C1",
- originatingAccountId: "acct-1",
- originatingThreadId: "thread-1",
- run: {
- config: runtimeConfig,
- messageProvider: "discord",
- provider: "anthropic",
- model: "claude-opus-4-7",
- sourceReplyDeliveryMode: "message_tool_only",
- fastMode: "auto",
- fastModeOverride: true,
- fastModeAutoOnSeconds: 5,
- fastModeAutoOnSecondsOverride: true,
- },
- }),
- );
-
- expect(routeReplyMock).not.toHaveBeenCalled();
- });
-
it("drains fire-and-forget queued tool progress before final delivery", async () => {
const queued = createQueuedRun({
originatingChannel: "discord",
@@ -3677,24 +3633,11 @@ describe("createFollowupRunner progress forwarding", () => {
);
});
- it("forwards queued Codex command tool results as command output completion", async () => {
- const onCommandOutput = vi.fn(async () => {});
- const queued = createQueuedRun({
- originatingChannel: "discord",
- originatingTo: "channel:C1",
- originatingAccountId: "acct-1",
- originatingThreadId: "thread-1",
- run: {
- messageProvider: "discord",
- verboseLevel: "on",
- },
- });
-
- runEmbeddedAgentMock.mockImplementationOnce(
- async (args: {
- onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
- }) => {
- await args.onAgentEvent?.({
+ it.each([
+ [
+ "forwards queued Codex command tool results as command output completion",
+ [
+ {
stream: "tool",
data: {
phase: "result",
@@ -3702,57 +3645,28 @@ describe("createFollowupRunner progress forwarding", () => {
toolCallId: "queued-exec",
name: "exec",
status: "completed",
- result: {
- exitCode: 0,
- durationMs: 24,
- },
+ result: { exitCode: 0, durationMs: 24 },
},
- });
- return { payloads: [{ text: "final reply" }], meta: { agentMeta: {} } };
+ },
+ ],
+ {
+ itemId: "command:queued-exec",
+ phase: "end",
+ title: undefined,
+ toolCallId: "queued-exec",
+ name: "exec",
+ output: undefined,
+ status: "completed",
+ exitCode: 0,
+ durationMs: 24,
+ cwd: undefined,
},
- );
-
- const runner = createFollowupRunner({
- opts: { onCommandOutput },
- typing: createMockTypingController(),
- typingMode: "instant",
- defaultModel: "claude",
- });
-
- await runner(queued);
-
- expect(onCommandOutput).toHaveBeenCalledWith({
- itemId: "command:queued-exec",
- phase: "end",
- title: undefined,
- toolCallId: "queued-exec",
- name: "exec",
- output: undefined,
- status: "completed",
- exitCode: 0,
- durationMs: 24,
- cwd: undefined,
- });
- });
-
- it("marks queued Codex command tool result errors as failed command output", async () => {
- const onCommandOutput = vi.fn(async () => {});
- const queued = createQueuedRun({
- originatingChannel: "discord",
- originatingTo: "channel:C1",
- originatingAccountId: "acct-1",
- originatingThreadId: "thread-1",
- run: {
- messageProvider: "discord",
- verboseLevel: "on",
- },
- });
-
- runEmbeddedAgentMock.mockImplementationOnce(
- async (args: {
- onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
- }) => {
- await args.onAgentEvent?.({
+ true,
+ ],
+ [
+ "marks queued Codex command tool result errors as failed command output",
+ [
+ {
stream: "tool",
data: {
phase: "result",
@@ -3760,53 +3674,23 @@ describe("createFollowupRunner progress forwarding", () => {
toolCallId: "queued-exec",
name: "exec",
isError: true,
- result: {
- content: [{ type: "text", text: "command failed" }],
- },
+ result: { content: [{ type: "text", text: "command failed" }] },
},
- });
- return { payloads: [{ text: "final reply" }], meta: { agentMeta: {} } };
- },
- );
-
- const runner = createFollowupRunner({
- opts: { onCommandOutput },
- typing: createMockTypingController(),
- typingMode: "instant",
- defaultModel: "claude",
- });
-
- await runner(queued);
-
- expect(onCommandOutput).toHaveBeenCalledWith(
- expect.objectContaining({
+ },
+ ],
+ {
itemId: "command:queued-exec",
phase: "end",
toolCallId: "queued-exec",
name: "exec",
status: "failed",
- }),
- );
- });
-
- it("does not synthesize queued command output from bare exec tool results", async () => {
- const onCommandOutput = vi.fn(async () => {});
- const queued = createQueuedRun({
- originatingChannel: "discord",
- originatingTo: "channel:C1",
- originatingAccountId: "acct-1",
- originatingThreadId: "thread-1",
- run: {
- messageProvider: "discord",
- verboseLevel: "on",
},
- });
-
- runEmbeddedAgentMock.mockImplementationOnce(
- async (args: {
- onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
- }) => {
- await args.onAgentEvent?.({
+ false,
+ ],
+ [
+ "does not synthesize queued command output from bare exec tool results",
+ [
+ {
stream: "tool",
data: {
phase: "result",
@@ -3814,8 +3698,8 @@ describe("createFollowupRunner progress forwarding", () => {
toolCallId: "queued-exec",
isError: false,
},
- });
- await args.onAgentEvent?.({
+ },
+ {
stream: "command_output",
data: {
itemId: "command:queued-exec",
@@ -3826,7 +3710,31 @@ describe("createFollowupRunner progress forwarding", () => {
status: "completed",
exitCode: 0,
},
- });
+ },
+ ],
+ { itemId: "command:queued-exec", phase: "end", status: "completed" },
+ false,
+ ],
+ ] as const)("%s", async (_name, events, expectedCommandOutput, expectExactCall) => {
+ const onCommandOutput = vi.fn(async () => {});
+ const queued = createQueuedRun({
+ originatingChannel: "discord",
+ originatingTo: "channel:C1",
+ originatingAccountId: "acct-1",
+ originatingThreadId: "thread-1",
+ run: {
+ messageProvider: "discord",
+ verboseLevel: "on",
+ },
+ });
+
+ runEmbeddedAgentMock.mockImplementationOnce(
+ async (args: {
+ onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
+ }) => {
+ for (const event of events) {
+ await args.onAgentEvent?.(event);
+ }
return { payloads: [{ text: "final reply" }], meta: { agentMeta: {} } };
},
);
@@ -3841,13 +3749,11 @@ describe("createFollowupRunner progress forwarding", () => {
await runner(queued);
expect(onCommandOutput).toHaveBeenCalledTimes(1);
- expect(onCommandOutput).toHaveBeenCalledWith(
- expect.objectContaining({
- itemId: "command:queued-exec",
- phase: "end",
- status: "completed",
- }),
- );
+ if (expectExactCall) {
+ expect(onCommandOutput).toHaveBeenCalledWith(expectedCommandOutput);
+ return;
+ }
+ expect(onCommandOutput).toHaveBeenCalledWith(expect.objectContaining(expectedCommandOutput));
});
it("suppresses queued follow-up progress when verbose progress is disabled", async () => {
@@ -4146,8 +4052,23 @@ describe("createFollowupRunner progress forwarding", () => {
expect(onCommandOutput).toHaveBeenCalledTimes(1);
});
- it("keeps queued tool-error fallbacks when the channel declines failed progress", async () => {
- const onCommandOutput = vi.fn(async () => false as const);
+ it.each([
+ [
+ "keeps queued tool-error fallbacks when the channel declines failed progress",
+ "on",
+ "declines",
+ ],
+ [
+ "keeps queued full-verbose tool-error fallbacks available after failed progress",
+ "full",
+ "accepts",
+ ],
+ ["keeps queued tool-error fallbacks when failed progress has no callback", "on", "missing"],
+ ] as const)("%s", async (_name, verboseLevel, callbackMode) => {
+ const onCommandOutput =
+ callbackMode === "missing"
+ ? undefined
+ : vi.fn(async () => (callbackMode === "declines" ? (false as const) : undefined));
let completedAfterEvent = false;
runEmbeddedAgentMock.mockImplementationOnce(
@@ -4173,7 +4094,7 @@ describe("createFollowupRunner progress forwarding", () => {
);
const runner = createFollowupRunner({
- opts: { onCommandOutput },
+ opts: onCommandOutput ? { onCommandOutput } : undefined,
typing: createMockTypingController(),
typingMode: "instant",
defaultModel: "claude",
@@ -4184,98 +4105,17 @@ describe("createFollowupRunner progress forwarding", () => {
run: {
messageProvider: "discord",
sourceReplyDeliveryMode: "message_tool_only",
- verboseLevel: "on",
+ verboseLevel,
},
}),
);
- expect(onCommandOutput).toHaveBeenCalledTimes(1);
+ if (onCommandOutput) {
+ expect(onCommandOutput).toHaveBeenCalledTimes(1);
+ }
expect(completedAfterEvent).toBe(true);
});
- it("keeps queued full-verbose tool-error fallbacks available after failed progress", async () => {
- const onCommandOutput = vi.fn(async () => {});
-
- runEmbeddedAgentMock.mockImplementationOnce(
- async (args: {
- onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
- suppressToolErrorWarnings?: boolean | (() => boolean | undefined);
- }) => {
- const shouldSuppress = args.suppressToolErrorWarnings as () => boolean | undefined;
- expect(shouldSuppress()).toBeUndefined();
- await args.onAgentEvent?.({
- stream: "command_output",
- data: {
- phase: "end",
- name: "exec",
- status: "failed",
- exitCode: 1,
- },
- });
- expect(shouldSuppress()).toBeUndefined();
- return { payloads: [], meta: { agentMeta: {} } };
- },
- );
-
- const runner = createFollowupRunner({
- opts: { onCommandOutput },
- typing: createMockTypingController(),
- typingMode: "instant",
- defaultModel: "claude",
- });
-
- await runner(
- createQueuedRun({
- run: {
- messageProvider: "discord",
- sourceReplyDeliveryMode: "message_tool_only",
- verboseLevel: "full",
- },
- }),
- );
-
- expect(onCommandOutput).toHaveBeenCalledTimes(1);
- });
-
- it("keeps queued tool-error fallbacks when failed progress has no callback", async () => {
- runEmbeddedAgentMock.mockImplementationOnce(
- async (args: {
- onAgentEvent?: (evt: { stream: string; data: Record }) => Promise;
- suppressToolErrorWarnings?: boolean | (() => boolean | undefined);
- }) => {
- const shouldSuppress = args.suppressToolErrorWarnings as () => boolean | undefined;
- expect(shouldSuppress()).toBeUndefined();
- await args.onAgentEvent?.({
- stream: "command_output",
- data: {
- phase: "end",
- name: "exec",
- status: "failed",
- exitCode: 1,
- },
- });
- expect(shouldSuppress()).toBeUndefined();
- return { payloads: [], meta: { agentMeta: {} } };
- },
- );
-
- const runner = createFollowupRunner({
- typing: createMockTypingController(),
- typingMode: "instant",
- defaultModel: "claude",
- });
-
- await runner(
- createQueuedRun({
- run: {
- messageProvider: "discord",
- sourceReplyDeliveryMode: "message_tool_only",
- verboseLevel: "on",
- },
- }),
- );
- });
-
it("uses current session verbose state for queued follow-up progress", async () => {
const sessionEntry: SessionEntry = {
sessionId: "session",
@@ -5119,136 +4959,104 @@ describe("createFollowupRunner messaging delivery and dedupe", () => {
persistSpy.mockRestore();
});
- it("appends configured responseUsage footers during followup delivery", async () => {
- const sessionKey = "main";
- const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
- const cfg = {
- messages: {
- responseUsage: "tokens",
- },
- } as OpenClawConfig;
+ it.each([
+ [
+ "appends configured responseUsage footers during followup delivery",
+ "main",
+ "tokens",
+ undefined,
+ undefined,
+ ["hello world!", "Usage:", "out"],
+ undefined,
+ undefined,
+ ],
+ [
+ "renders full responseUsage followup footers without exposing the session key",
+ "discord:channel:user",
+ "full",
+ undefined,
+ "model={model.display_name} tokens={usage.input_tokens|num}/{usage.output_tokens|num}",
+ ["hello world!", "model=claude-opus-4-6 tokens=1.0k/50"],
+ "discord:channel:user",
+ undefined,
+ ],
+ [
+ "keeps explicit responseUsage off during followup delivery",
+ "main",
+ "tokens",
+ "off",
+ undefined,
+ [],
+ undefined,
+ "hello world!",
+ ],
+ ] as const)(
+ "%s",
+ async (
+ _name,
+ sessionKey,
+ configuredResponseUsage,
+ sessionResponseUsage,
+ usageTemplateText,
+ expectedFragments,
+ excludedText,
+ exactText,
+ ) => {
+ const sessionEntry: SessionEntry = {
+ sessionId: "session",
+ updatedAt: Date.now(),
+ ...(sessionResponseUsage ? { responseUsage: sessionResponseUsage } : {}),
+ };
+ const cfg = {
+ messages: {
+ responseUsage: configuredResponseUsage,
+ ...(usageTemplateText
+ ? {
+ usageTemplate: {
+ output: { default: [{ text: usageTemplateText }] },
+ },
+ }
+ : {}),
+ },
+ } as OpenClawConfig;
- const { onBlockReply } = await runMessagingCase({
- agentResult: {
- payloads: [{ text: "hello world!" }],
- meta: {
- agentMeta: {
- usage: { input: 1_000, output: 50 },
- model: "claude-opus-4-6",
- provider: "anthropic",
+ const { onBlockReply } = await runMessagingCase({
+ agentResult: {
+ payloads: [{ text: "hello world!" }],
+ meta: {
+ agentMeta: {
+ usage: { input: 1_000, output: 50 },
+ model: "claude-opus-4-6",
+ provider: "anthropic",
+ },
},
},
- },
- runnerOverrides: {
- sessionEntry,
- sessionStore: { [sessionKey]: sessionEntry },
- sessionKey,
- },
- queued: createQueuedRun({
- run: {
- config: cfg,
- messageProvider: "discord",
+ runnerOverrides: {
+ sessionEntry,
+ sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
},
- }),
- });
-
- const payload = requireMockCallArg(onBlockReply, 0);
- expect(payload.text).toContain("hello world!");
- expect(payload.text).toContain("Usage:");
- expect(payload.text).toContain("out");
- });
-
- it("renders full responseUsage followup footers without exposing the session key", async () => {
- const sessionKey = "discord:channel:user";
- const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() };
- const cfg = {
- messages: {
- responseUsage: "full",
- usageTemplate: {
- output: {
- default: [
- {
- text: "model={model.display_name} tokens={usage.input_tokens|num}/{usage.output_tokens|num}",
- },
- ],
+ queued: createQueuedRun({
+ run: {
+ config: cfg,
+ messageProvider: "discord",
+ sessionKey,
},
- },
- },
- } as OpenClawConfig;
+ }),
+ });
- const { onBlockReply } = await runMessagingCase({
- agentResult: {
- payloads: [{ text: "hello world!" }],
- meta: {
- agentMeta: {
- usage: { input: 1_000, output: 50 },
- model: "claude-opus-4-6",
- provider: "anthropic",
- },
- },
- },
- runnerOverrides: {
- sessionEntry,
- sessionStore: { [sessionKey]: sessionEntry },
- sessionKey,
- },
- queued: createQueuedRun({
- run: {
- config: cfg,
- messageProvider: "discord",
- sessionKey,
- },
- }),
- });
-
- const payload = requireMockCallArg(onBlockReply, 0);
- expect(payload.text).toContain("hello world!");
- expect(payload.text).toContain("model=claude-opus-4-6 tokens=1.0k/50");
- expect(payload.text).not.toContain(sessionKey);
- });
-
- it("keeps explicit responseUsage off during followup delivery", async () => {
- const sessionKey = "main";
- const sessionEntry: SessionEntry = {
- sessionId: "session",
- updatedAt: Date.now(),
- responseUsage: "off",
- };
- const cfg = {
- messages: {
- responseUsage: "tokens",
- },
- } as OpenClawConfig;
-
- const { onBlockReply } = await runMessagingCase({
- agentResult: {
- payloads: [{ text: "hello world!" }],
- meta: {
- agentMeta: {
- usage: { input: 1_000, output: 50 },
- model: "claude-opus-4-6",
- provider: "anthropic",
- },
- },
- },
- runnerOverrides: {
- sessionEntry,
- sessionStore: { [sessionKey]: sessionEntry },
- sessionKey,
- },
- queued: createQueuedRun({
- run: {
- config: cfg,
- messageProvider: "discord",
- sessionKey,
- },
- }),
- });
-
- const payload = requireMockCallArg(onBlockReply, 0);
- expect(payload.text).toBe("hello world!");
- });
+ const payload = requireMockCallArg(onBlockReply, 0);
+ for (const fragment of expectedFragments) {
+ expect(payload.text).toContain(fragment);
+ }
+ if (excludedText) {
+ expect(payload.text).not.toContain(excludedText);
+ }
+ if (exactText) {
+ expect(payload.text).toBe(exactText);
+ }
+ },
+ );
it("uses providerUsed for snapshot freshness when agent metadata overrides the run provider", async () => {
const storePath = "/tmp/openclaw-followup-usage-provider.json";
diff --git a/src/channels/plugins/contracts/plugin-shape.contract.test.ts b/src/channels/plugins/contracts/plugin-shape.contract.test.ts
index d27f7ec7404d..6dec08195454 100644
--- a/src/channels/plugins/contracts/plugin-shape.contract.test.ts
+++ b/src/channels/plugins/contracts/plugin-shape.contract.test.ts
@@ -14,18 +14,37 @@
// slash commands through gateway HTTP routes).
// - blockStreaming=true does not imply a streaming adapter (coalesce tuning
// is optional).
-import { beforeAll, describe, expect, it } from "vitest";
+import { beforeAll, describe, expect, it, vi } from "vitest";
import { listBundledPackageChannelMetadata } from "../../../plugins/bundled-package-channel-metadata.js";
import {
getBundledChannelPluginAsync,
listBundledChannelPluginIds,
} from "./test-helpers/bundled-channel-plugin-loader.js";
+const sanitizeAssistantVisibleTextMock = vi.hoisted(() =>
+ vi.fn((text: string) => `shared-sanitizer:${text}`),
+);
+
+vi.mock("openclaw/plugin-sdk/text-chunking", async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual, sanitizeAssistantVisibleText: sanitizeAssistantVisibleTextMock };
+});
+
const CHAT_TYPES = new Set(["direct", "group", "channel", "thread"]);
const bundledChannelPluginIds = listBundledChannelPluginIds();
const packageMetadataById = new Map(
listBundledPackageChannelMetadata().map((channel) => [channel.id, channel]),
);
+const SHARED_SANITIZER_CHANNEL_IDS = [
+ "nextcloud-talk",
+ "zalo",
+ "irc",
+ "feishu",
+ "signal",
+ "twitch",
+ "matrix",
+ "slack",
+] as const;
describe("bundled channel plugin shape coherence", () => {
const plugins = new Map>>();
@@ -40,6 +59,22 @@ describe("bundled channel plugin shape coherence", () => {
expect(bundledChannelPluginIds.length).toBeGreaterThan(0);
});
+ it.each(SHARED_SANITIZER_CHANNEL_IDS)(
+ "%s wires outbound sanitizeText through the shared sanitizer",
+ (id) => {
+ const sanitizeText = plugins.get(id)?.outbound?.sanitizeText;
+ if (!sanitizeText) {
+ throw new Error(`Missing outbound sanitizeText hook for ${id}`);
+ }
+ const text = `visible:${id}`;
+
+ sanitizeAssistantVisibleTextMock.mockClear();
+
+ expect(sanitizeText({ text, payload: { text } })).toBe(`shared-sanitizer:${text}`);
+ expect(sanitizeAssistantVisibleTextMock).toHaveBeenCalledExactlyOnceWith(text);
+ },
+ );
+
describe.each(bundledChannelPluginIds)("%s", (id) => {
it("keeps plugin identity aligned with the catalog id", () => {
const plugin = plugins.get(id);
diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts
index 356dc339bdef..067eeab9fcb4 100644
--- a/src/cli/update-cli.test.ts
+++ b/src/cli/update-cli.test.ts
@@ -1451,7 +1451,6 @@ describe("update-cli", () => {
await updateCommand({ yes: true, restart: false });
- expect(installCompletion).toHaveBeenCalledWith("zsh", true, "openclaw");
const logOutput = getLogOutput();
expect(logOutput).toContain("Shell completion refresh failed: EACCES: permission denied");
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
@@ -5014,7 +5013,6 @@ describe("update-cli", () => {
await updateCommand({ yes: true, restart: false });
- expect(nodeVersionSatisfiesEngine).toHaveBeenCalledWith("22.18.0", ">=22.19.0");
expect(packageInstallCommandCall()).toBeUndefined();
expect(serviceStop).not.toHaveBeenCalled();
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
@@ -5198,14 +5196,6 @@ describe("update-cli", () => {
await updateCommand({ yes: true });
- expect(nodeVersionSatisfiesEngine).toHaveBeenCalledWith("24.14.0", ">=24.15.0 <25");
- expect(nodeVersionSatisfiesEngine).toHaveBeenCalledWith("24.15.0", ">=24.15.0 <25");
- expect(doctorCommandCall()?.[0][0]).toBe(process.execPath);
- expect(spawnCall()?.[0]).toBe(process.execPath);
- const serviceInstallCall = commandCalls().find(
- ([argv]) => argv[2] === "gateway" && argv[3] === "install",
- );
- expect(serviceInstallCall?.[0][0]).toBe(process.execPath);
const logs = getLogOutput();
expect(logs).toContain(`Managed gateway service Node (${serviceNode}) cannot run`);
expect(logs).toContain(`Using current Node (${process.execPath})`);