fix(discord): default preview streaming to off (#120376)

Make Discord progress drafts and activity receipts explicit opt-ins while preserving configured streaming modes and legacy explicit progress migrations. Related: #87704.
This commit is contained in:
Peter Steinberger
2026-08-07 17:18:12 -07:00
committed by GitHub
parent 13e8e1df4a
commit 96a75be170
19 changed files with 139 additions and 152 deletions
-1
View File
@@ -325,7 +325,6 @@ src/acp/control-plane/manager.turn-results.test.ts
src/acp/event-ledger.ts
src/acp/translator.ts
src/agents/acp-spawn-parent-stream.test.ts
src/agents/acp-spawn-parent-stream.ts
src/agents/acp-spawn.test.ts
src/agents/agent-bundle-mcp-runtime.test.ts
src/agents/agent-bundle-mcp-runtime.ts
+2 -2
View File
@@ -700,7 +700,7 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
</Accordion>
<Accordion title="Live stream preview">
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming.mode` takes `off` | `partial` | `block` | `progress` (default when no `streaming`/legacy `streamMode` key is set). `streamMode` is a legacy alias; run `openclaw doctor --fix` to rewrite persisted config to the canonical nested `streaming` shape.
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. Discord preview streaming defaults to `off`; set `channels.discord.streaming.mode` to `partial`, `block`, or `progress` to opt in. `streamMode` is a legacy alias; run `openclaw doctor --fix` to rewrite persisted config to the canonical nested `streaming` shape.
```json5
{
@@ -725,7 +725,7 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
- `block` emits draft-sized chunks; tune size and breakpoints with `streaming.preview.chunk` (`minChars`, `maxChars`, `breakPreference`), clamped to `textChunkLimit`. An explicit non-`off` preview mode overrides inherited `agents.defaults.blockStreamingDefault: "on"`; explicit `streaming.block.enabled: true` overrides the preview. If a turn cannot use previews, inherited block delivery still applies.
- `progress` keeps one editable status draft until final delivery. It shows the agent's latest preamble or narration as a status headline, with the compact tool rows underneath and no generated label.
- Media, error, and explicit-reply finals cancel pending preview edits.
- `streaming.preview.toolProgress` and `streaming.progress.toolProgress` both default to `true` in every mode. Tool rows such as `🛠️ Bash: run tests` or `🔎 Web Search: for "query"` appear without config; set either key to `false` to keep the status headline only.
- `streaming.preview.toolProgress` and `streaming.progress.toolProgress` both default to `true` when preview streaming is active. Tool rows such as `🛠️ Bash: run tests` or `🔎 Web Search: for "query"` need no additional progress config; set either key to `false` to keep the status headline only.
- `streaming.progress.commentary` (default `false`) opts into raw assistant commentary in the temporary progress draft. The default preamble/narration status line is independent of this option. Commentary is cleaned before display, stays transient, and does not change final answer delivery.
- `streaming.progress.maxLineChars` controls the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
+6 -6
View File
@@ -22,11 +22,11 @@ Working...
```
<Note>
Discord and Telegram default to `streaming.mode: "progress"`, so progress
drafts show up there without any config. Set `mode: "partial"` on either to
stream answer text instead. Every other channel defaults to `partial` or
`off`; see [Streaming and chunking](/concepts/streaming#channel-mapping) for
the full per-channel default table.
Discord defaults preview streaming to `off`; set `streaming.mode: "progress"`
to opt in. Telegram defaults to `progress` without additional config. Set
`mode: "partial"` on either to stream answer text instead. See
[Streaming and chunking](/concepts/streaming#channel-mapping) for the full
per-channel default table.
</Note>
## Quick start
@@ -413,7 +413,7 @@ the final answer, except for the label if one is configured.
| Channel | Progress transport | Notes |
| --------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Discord | Send one message, then edit it. | Defaults to `progress` mode; the final answer carries a `-#` activity receipt and the status draft is deleted after the answer lands. |
| Discord | Send one message, then edit it. | `progress` is explicit opt-in; the final answer carries a `-#` activity receipt and the status draft is deleted after the answer lands. |
| Matrix | Send one event, then edit it. | Account-level streaming config controls account-level drafts. |
| Microsoft Teams | Native Teams stream in personal chats. | `streaming.mode: "block"` maps to Teams block delivery instead. |
| Slack | Native stream or editable draft post. | Needs a reply thread target; top-level DMs without one still get draft preview posts and edits. |
+9 -9
View File
@@ -183,16 +183,16 @@ instead of being overwritten in one editable draft.
### Channel mapping
Discord and Telegram default to `progress` when `streaming` is unset; Slack,
Mattermost, and MS Teams default to `partial`.
Discord defaults to `off` when `streaming` is unset, Telegram defaults to
`progress`, and Slack, Mattermost, and MS Teams default to `partial`.
| Channel | `off` | `partial` | `block` | `progress` |
| ---------- | ----- | --------- | ------- | --------------------------------- |
| Telegram | Yes | Yes | Yes | editable progress draft (default) |
| Discord | Yes | Yes | Yes | editable progress draft (default) |
| Slack | Yes | Yes | Yes | Yes |
| Mattermost | Yes | Yes | Yes | Yes |
| MS Teams | Yes | Yes | Yes | native progress stream |
| Channel | `off` | `partial` | `block` | `progress` |
| ---------- | ------------- | --------- | ------- | --------------------------------- |
| Telegram | Yes | Yes | Yes | editable progress draft (default) |
| Discord | Yes (default) | Yes | Yes | editable progress draft (opt-in) |
| Slack | Yes | Yes | Yes | Yes |
| Mattermost | Yes | Yes | Yes | Yes |
| MS Teams | Yes | Yes | Yes | native progress stream |
Preview chunk config (`streaming.preview.chunk.*`, e.g. under
`channels.discord.streaming` or `channels.telegram.streaming`) defaults to
+2 -2
View File
@@ -283,7 +283,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
textChunkLimit: 2000,
suppressEmbeds: true,
streaming: {
mode: "progress", // off | partial | block | progress (Discord default: progress)
mode: "progress", // explicit opt-in; Discord defaults to off
chunkMode: "length", // length | newline
progress: {
label: "auto",
@@ -356,7 +356,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- `channels.discord.voice.reconnectGraceMs` controls how long a disconnected voice session may take to enter reconnect signalling before OpenClaw destroys it (default `15000`).
- Discord voice playback is not interrupted by another user's speaking-start event. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing.
- OpenClaw additionally attempts voice receive recovery by leaving/rejoining a voice session after repeated decrypt failures.
- `channels.discord.streaming` is the canonical stream mode key. Discord defaults to `streaming.mode: "progress"` so tool/work progress appears in one edited preview message; set `streaming.mode: "off"` to disable it. Legacy flat keys (`streamMode`, `chunkMode`, `blockStreaming`, `draftChunk`, `blockStreamingCoalesce`) are no longer read at runtime; run `openclaw doctor --fix` to migrate persisted config.
- `channels.discord.streaming` is the canonical stream mode key. Discord preview streaming defaults to `off`; set `streaming.mode: "progress"` to opt into one edited tool/work progress message, or choose `partial` or `block` for answer previews. Legacy flat keys (`streamMode`, `chunkMode`, `blockStreaming`, `draftChunk`, `blockStreamingCoalesce`) are no longer read at runtime; run `openclaw doctor --fix` to migrate persisted config.
- `channels.discord.autoPresence` maps runtime availability to bot presence (healthy => online, degraded => idle, exhausted => dnd) and allows optional status text overrides.
- `channels.discord.guilds.<id>.presenceEvents` routes human availability arrivals into one configured Discord channel as agent system events. Eligible members must be able to view `channelId`; public threads inherit parent visibility, while private threads additionally require membership or Manage Threads. `users` can further narrow that audience. It seeds current online members from complete `GUILD_CREATE` snapshots, routes observed offline-to-online transitions, and treats a first later online signal for an unseen member as newly available without asserting whether they came online or joined after the snapshot. Guilds above Discord's 75,000-member snapshot limit require an explicit offline update first. Throttling knobs: `reconnectSuppressSeconds` (quiet window after a new Gateway session while guild presence state is rebuilt, default 300, `0` disables) and `burstLimit`/`burstWindowSeconds` (per-guild successfully queued event rate limit, default 8 events per 60s sliding window). Resumed sessions do not start the reconnect suppression window. The existing per-user re-greet cooldown remains eight hours. It requires `channels.discord.intents.presence=true`, the privileged Presence Intent in Discord's Developer Portal, and an enabled agent heartbeat.
- `channels.discord.intents.messageContent` defaults to `true`. Set it to `false` only for mention-only operation when Discord cannot grant the privileged Message Content intent; DMs and explicit bot mentions still carry message content, while other guild messages do not. Keep `requireMention: true` on every configured guild channel in this mode.
+4 -4
View File
@@ -550,10 +550,10 @@ Two ways to start an ACP session:
session as system events. OpenClaw records the full relay history in the
child agent's SQLite state and removes it with the child session. Parent
progress streams show assistant commentary and ACP status progress by default unless
`streaming.progress.commentary=false`. Discord also defaults parent
previews to progress mode when no stream mode is configured. Status
progress still honors `acp.stream.tagVisibility`, so tags such as `plan`
remain hidden unless explicitly enabled.
`streaming.progress.commentary=false`. Discord parent progress requires an
explicit `streaming.mode: "progress"`; unset Discord streaming stays quiet.
Status progress still honors `acp.stream.tagVisibility`, so tags such as
`plan` remain hidden unless explicitly enabled.
</ParamField>
ACP `sessions_spawn` runs use `agents.defaults.subagents.runTimeoutSeconds`
+2 -2
View File
@@ -17,8 +17,8 @@ export const discordChannelConfigUiHints = {
},
nativeCommands: true,
streaming: {
"": 'Unified Discord stream preview mode: "off" | "partial" | "block" | "progress". "progress" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped.',
mode: 'Canonical Discord preview mode: "off" | "partial" | "block" | "progress".',
"": 'Discord preview streaming is off by default. Set mode to "partial", "block", or "progress" to opt in. Run openclaw doctor --fix to migrate legacy keys.',
mode: 'Discord preview mode: "off" | "partial" | "block" | "progress". Default: "off".',
chunkMode:
'Chunking mode for outbound Discord text delivery: "length" (default) or "newline".',
"block.enabled":
@@ -33,11 +33,6 @@ const streamingAliasMigration = defineChannelAliasMigration({
// Runtime mode resolution dropped legacy streamMode reads; the doctor
// resolver keeps them so migration preserves configured intent.
defaultMode: "off",
// Discord previews default to progress only while `streaming` is absent;
// any present object (even without mode) resolves off, so migration pins
// progress when delivery-only aliases create the object with no root
// streaming object to inherit from.
absentObjectDefault: "progress",
includePreviewChunk: true,
},
// Discord's account merge replaces the root streaming object wholesale
+26 -10
View File
@@ -159,10 +159,10 @@ describe("discord doctor", () => {
]);
});
it("pins progress mode when migrating delivery-only aliases", () => {
it("keeps delivery-only aliases mode-free and preserves explicit legacy progress", () => {
const normalize = getDiscordCompatibilityNormalizer();
const result = normalize({
const deliveryOnly = normalize({
cfg: {
channels: {
discord: { blockStreaming: true },
@@ -170,17 +170,33 @@ describe("discord doctor", () => {
} as never,
});
const migrated = result.config.channels?.discord as Record<string, unknown>;
expect(migrated).toEqual({
const deliveryOnlyMigrated = deliveryOnly.config.channels?.discord as Record<string, unknown>;
expect(deliveryOnlyMigrated).toEqual({
streaming: { block: { enabled: true } },
});
expect(resolveDiscordPreviewStreamMode(deliveryOnlyMigrated)).toBe("off");
expect(deliveryOnly.changes).toEqual([
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
]);
const explicitProgress = normalize({
cfg: {
channels: {
discord: { streamMode: "progress", blockStreaming: true },
},
} as never,
});
const explicitProgressMigrated = explicitProgress.config.channels?.discord as Record<
string,
unknown
>;
expect(explicitProgressMigrated).toEqual({
streaming: { mode: "progress", block: { enabled: true } },
});
// Effective preview-mode parity: `streaming` absent resolved to progress
// before migration, so the migrated object must keep progress instead of
// falling to the object-without-mode default (off).
expect(resolveDiscordPreviewStreamMode(migrated)).toBe(resolveDiscordPreviewStreamMode({}));
expect(result.changes).toEqual([
expect(resolveDiscordPreviewStreamMode(explicitProgressMigrated)).toBe("progress");
expect(explicitProgress.changes).toEqual([
"Moved channels.discord.streamMode → channels.discord.streaming.mode (progress).",
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
"Set channels.discord.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
]);
});
@@ -239,12 +239,13 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
});
it("defaults unset Discord preview streaming to progress mode without drafting text-only turns", async () => {
it("keeps unset Discord preview streaming off and delivers the final normally", async () => {
await runSingleChunkFinalScenario({ maxLinesPerMessage: 5 });
expect(getLastDispatchReplyOptions()?.onPartialReply).toBeUndefined();
expect(createDiscordDraftStream).toHaveBeenCalledTimes(1);
expect(createDiscordDraftStream).not.toHaveBeenCalled();
expect(editMessageDiscord).not.toHaveBeenCalled();
expect(deliverDiscordReply).toHaveBeenCalledTimes(1);
expectFreshFinalText("Hello\nWorld");
});
it("does not stream Discord tool progress before the initial delay", async () => {
@@ -258,7 +259,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
const ctx = await createAutomaticSourceDeliveryContext({
discordConfig: { maxLinesPerMessage: 5 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
});
await runProcessDiscordMessage(ctx);
@@ -302,7 +303,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
const ctx = await createAutomaticSourceDeliveryContext({
baseSessionKey: BASE_CHANNEL_ROUTE.sessionKey,
discordConfig: { maxLinesPerMessage: 5 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
route: BASE_CHANNEL_ROUTE,
});
@@ -365,7 +366,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
const ctx = await createAutomaticSourceDeliveryContext({
discordConfig: { maxLinesPerMessage: 5 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
});
await runProcessDiscordMessage(ctx);
@@ -390,7 +391,9 @@ describe("processDiscordMessage draft streaming final delivery", () => {
return { queuedFinal: true, counts: { final: 1, tool: 0, block: 0 } };
});
const ctx = await createAutomaticSourceDeliveryContext();
const ctx = await createAutomaticSourceDeliveryContext({
discordConfig: { streaming: { mode: "progress" } },
});
await runProcessDiscordMessage(ctx);
});
@@ -445,7 +448,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
const ctx = await createAutomaticSourceDeliveryContext({
discordConfig: { maxLinesPerMessage: 5 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
});
await runProcessDiscordMessage(ctx);
@@ -470,7 +473,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
const ctx = await createAutomaticSourceDeliveryContext({
discordConfig: { maxLinesPerMessage: 5 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 5 },
});
await runProcessDiscordMessage(ctx);
@@ -673,6 +676,12 @@ describe("processDiscordMessage draft streaming final delivery", () => {
});
const ctx = await createBaseContext({
discordConfig: {
streaming: {
mode: "progress",
progress: { toolProgress: true },
},
},
cfg: {
channels: {
discord: {
@@ -86,7 +86,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
const ctx = await createAutomaticSourceDeliveryContext({
baseSessionKey: BASE_CHANNEL_ROUTE.sessionKey,
discordConfig: { maxLinesPerMessage: 120 },
discordConfig: { streaming: { mode: "progress" }, maxLinesPerMessage: 120 },
route: BASE_CHANNEL_ROUTE,
});
@@ -450,7 +450,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
expect(firstDispatchParams().replyOptions?.disableBlockStreaming).toBe(true);
});
it("shows the agent status above the tool lines in the default Discord progress draft", async () => {
it("shows the agent status above the tool lines in an opted-in Discord progress draft", async () => {
const elapseProgressDraftStartDelay = useProgressDraftStartDelay();
const draftStream = createMockDraftStreamForTest();
@@ -481,7 +481,7 @@ describe("processDiscordMessage draft streaming recovery", () => {
expect(draftStream.update).toHaveBeenCalledWith(
"Claiming my square footage. Tastefully, but with claws.\n\n🛠️ Exec\n• exec done",
);
// No config, so the implicit label stays hidden under the status headline.
// With no label override, the implicit label stays hidden under the status headline.
expect(String(draftStream.update.mock.calls[0]?.[0])).not.toMatch(/Working/);
expect(draftStream.flush).toHaveBeenCalledTimes(1);
expect(
@@ -9,8 +9,5 @@ export function resolveDiscordPreviewStreamMode(
streaming?: unknown;
} = {},
): StreamingMode {
if (params.streaming === undefined) {
return "progress";
}
return resolveChannelPreviewStreamMode(params, "off");
}
+2 -2
View File
@@ -9,8 +9,8 @@ export function resolveTelegramPreviewStreamMode(
streaming?: unknown;
} = {},
): StreamingMode {
// Telegram defaults to the progress draft like Discord: on tool-heavy turns a
// status draft answers "is it working?", which streamed answer text cannot.
// Telegram defaults to the progress draft: on tool-heavy turns a status draft
// answers "is it working?", which streamed answer text cannot.
// Operators who prefer streamed answer text set `streaming.mode: "partial"`.
return resolveChannelPreviewStreamMode(params, "progress");
}
+7 -7
View File
@@ -580,7 +580,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("relays commentary-phase assistant text in parent progress mode by default", () => {
it("relays commentary-phase assistant text in explicit parent progress mode", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-commentary-default",
parentSessionKey: "agent:main:main",
@@ -588,7 +588,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
agentId: "codex",
cfg: {
channels: {
discord: {},
discord: { streaming: { mode: "progress" } },
},
},
deliveryContext: {
@@ -819,11 +819,11 @@ describe("startAcpSpawnParentStreamRelay", () => {
relay.dispose();
});
it("uses Discord default progress mode for parent commentary", () => {
it("suppresses Discord parent progress commentary when streaming is unset", () => {
const relay = startAcpSpawnParentStreamRelay({
runId: "run-discord-default-progress",
runId: "run-discord-unset-streaming",
parentSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child-discord-default-progress",
childSessionKey: "agent:codex:acp:child-discord-unset-streaming",
agentId: "codex",
cfg: {
channels: {
@@ -840,7 +840,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
});
emitAgentEvent({
runId: "run-discord-default-progress",
runId: "run-discord-unset-streaming",
stream: "item",
data: {
itemId: "preamble-1",
@@ -850,7 +850,7 @@ describe("startAcpSpawnParentStreamRelay", () => {
});
vi.advanceTimersByTime(15);
expect(collectedTexts()).toEqual(["codex: Checking the app-server stream"]);
expect(collectedTexts()).toEqual([]);
relay.dispose();
});
+1 -30
View File
@@ -120,31 +120,6 @@ function mergeStreamingEntry(
};
}
function hasConfiguredPreviewStreamMode(entry: StreamingCompatEntry): boolean {
return asObjectRecord(entry.streaming)?.mode !== undefined;
}
function applyParentPreviewStreamModeDefault(
entry: StreamingCompatEntry,
channelId: string,
): StreamingCompatEntry {
if (channelId !== "discord" || hasConfiguredPreviewStreamMode(entry)) {
return entry;
}
const streaming = asObjectRecord(entry.streaming);
return {
...entry,
streaming: streaming
? {
...streaming,
mode: "progress",
}
: {
mode: "progress",
},
};
}
function resolveParentProgressStreamingEntry(params: {
cfg: OpenClawConfig | undefined;
deliveryContext: DeliveryContext | undefined;
@@ -165,10 +140,7 @@ function resolveParentProgressStreamingEntry(params: {
normalizeAccountId(params.deliveryContext?.accountId),
normalizeAccountId,
);
return applyParentPreviewStreamModeDefault(
mergeStreamingEntry(channelCfg, accountCfg),
channelId,
);
return mergeStreamingEntry(channelCfg, accountCfg);
}
function resolveParentProgressCommentary(params: {
@@ -764,4 +736,3 @@ export type AcpSpawnParentRelayHandle = {
dispose: () => void;
notifyStarted: () => void;
};
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+6 -6
View File
@@ -830,10 +830,10 @@ export function resolveChannelStreamingPreviewToolProgress(
defaultValue = true,
/**
* The channel's resolved stream mode. Only the caller knows it: channels pick
* their own default when `streaming.mode` is unset (Discord and Telegram use
* "progress", Slack and others "partial"), and this helper has no channel
* identity to guess with. Omitting it reads the configured mode and treats
* unset as "partial".
* their own default when `streaming.mode` is unset (Telegram uses "progress",
* Discord uses "off", and Slack uses "partial"), and this helper has no
* channel identity to guess with. Omitting it reads the configured mode and
* treats unset as "partial".
*/
mode?: StreamingMode,
): boolean {
@@ -857,7 +857,7 @@ export function resolveChannelStreamingProgressCommentary(
* resolveChannelStreamingPreviewToolProgress takes one: only the caller knows
* which default applies when `streaming.mode` is unset. Guessing "partial"
* here made `progress.commentary: true` a silent no-op on the progress-draft
* channels, whose own default is "progress".
* channels, such as Telegram, whose own default is "progress".
*/
mode?: StreamingMode,
): boolean {
@@ -871,7 +871,7 @@ export function resolveChannelStreamingProgressCommentary(
}
// Pure toggle: progress-mode gating stays with the caller because channels
// resolve their own default stream mode (Discord defaults to "progress").
// resolve their own default stream mode.
export function resolveChannelStreamingProgressNarration(
entry: StreamingCompatEntry | null | undefined,
defaultValue = true,
+21 -21
View File
@@ -93,19 +93,19 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
});
it("pins the previous default mode when delivery-only aliases create the streaming object", () => {
// Discord previews default to progress only while `streaming` is absent;
// without aliasOnlyMode the migrated object would resolve to off.
// Some channels distinguish an absent streaming object from a mode-free
// object, so aliasOnlyMode preserves that channel-owned default.
const res = normalizeStreaming({
entry: { blockStreaming: true },
pathPrefix: "channels.discord",
pathPrefix: "channels.layered",
resolvedMode: "off",
aliasOnlyMode: "progress",
});
expect(res.entry.streaming).toEqual({ mode: "progress", block: { enabled: true } });
expect(res.changes).toEqual([
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
"Set channels.discord.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
"Set channels.layered.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
]);
});
@@ -125,15 +125,15 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
it("does not apply aliasOnlyMode when a legacy mode source exists", () => {
const res = normalizeStreaming({
entry: { streamMode: "partial", blockStreaming: true },
pathPrefix: "channels.discord",
pathPrefix: "channels.layered",
resolvedMode: "partial",
aliasOnlyMode: "progress",
});
expect(res.entry.streaming).toEqual({ mode: "partial", block: { enabled: true } });
expect(res.changes).toEqual([
"Moved channels.discord.streamMode → channels.discord.streaming.mode (partial).",
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
"Moved channels.layered.streamMode → channels.layered.streaming.mode (partial).",
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
]);
});
@@ -142,14 +142,14 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
// semantics, so pinning a mode would change behavior instead of keeping it.
const res = normalizeStreaming({
entry: { streaming: { chunkMode: "newline" }, blockStreaming: true },
pathPrefix: "channels.discord",
pathPrefix: "channels.layered",
resolvedMode: "off",
aliasOnlyMode: "progress",
});
expect(res.entry.streaming).toEqual({ chunkMode: "newline", block: { enabled: true } });
expect(res.changes).toEqual([
"Moved channels.discord.blockStreaming → channels.discord.streaming.block.enabled.",
"Moved channels.layered.blockStreaming → channels.layered.streaming.block.enabled.",
]);
});
@@ -174,8 +174,8 @@ describe("normalizeCompatibilityConfigValues preview streaming aliases", () => {
});
describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
// Discord-shaped options: object-without-mode default "off", absent default
// "progress", account merge replaces the root streaming object wholesale.
// Synthetic replace-semantics channel: object-without-mode default "off",
// absent default "progress", and account streaming replaces the root object.
function normalizeChannel(
entry: Record<string, unknown>,
options?: { seedAccountStreamingFromRoot?: boolean },
@@ -183,7 +183,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
const changes: string[] = [];
const result = normalizeLegacyChannelAliases({
entry,
pathPrefix: "channels.discord",
pathPrefix: "channels.layered",
changes,
seedAccountStreamingFromRoot: options?.seedAccountStreamingFromRoot ?? true,
resolveStreamingOptions: (value) => ({
@@ -211,8 +211,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
block: { enabled: true },
});
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.blockStreaming → channels.discord.accounts.work.streaming.block.enabled.",
"Set channels.discord.accounts.work.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
"Moved channels.layered.accounts.work.blockStreaming → channels.layered.accounts.work.streaming.block.enabled.",
"Set channels.layered.accounts.work.streaming.mode (progress) to keep the previous default while migrating flat streaming keys.",
]);
});
@@ -231,8 +231,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
});
expect(res.entry.streaming).toEqual({ mode: "block", block: { coalesce: { idleMs: 5 } } });
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.chunkMode → channels.discord.accounts.work.streaming.chunkMode.",
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
"Moved channels.layered.accounts.work.chunkMode → channels.layered.accounts.work.streaming.chunkMode.",
"Copied channels.layered.streaming into channels.layered.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
]);
});
@@ -251,8 +251,8 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
});
expect(streaming.mode).toBeUndefined();
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.blockStreaming → channels.discord.accounts.work.streaming.block.enabled.",
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
"Moved channels.layered.accounts.work.blockStreaming → channels.layered.accounts.work.streaming.block.enabled.",
"Copied channels.layered.streaming into channels.layered.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
]);
});
@@ -309,7 +309,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
mode: "off",
});
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.streaming (boolean) → channels.discord.accounts.work.streaming.mode (off).",
"Moved channels.layered.accounts.work.streaming (boolean) → channels.layered.accounts.work.streaming.mode (off).",
]);
});
@@ -326,7 +326,7 @@ describe("normalizeLegacyChannelAliases account inheritance seeding", () => {
expect(workStreaming(res.entry)).toEqual({ chunkMode: "newline" });
expect(res.changes).toEqual([
"Moved channels.discord.accounts.work.chunkMode → channels.discord.accounts.work.streaming.chunkMode.",
"Moved channels.layered.accounts.work.chunkMode → channels.layered.accounts.work.streaming.chunkMode.",
]);
});
});
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -8,19 +8,19 @@ function cfgWith(channelId: string, entry: Record<string, unknown>): OpenClawCon
}
describe("defineChannelAliasMigration message generation", () => {
it("generates preview-chunk channel messages (discord shape)", () => {
it("generates preview-chunk channel messages with an absent-object default", () => {
const migration = defineChannelAliasMigration({
channelId: "discord",
channelId: "preview",
streaming: { defaultMode: "off", absentObjectDefault: "progress", includePreviewChunk: true },
});
expect(migration.legacyConfigRules.map((rule) => rule.message)).toEqual([
'channels.discord.streamMode, channels.discord.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
'channels.discord.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.discord.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
'channels.preview.streamMode, channels.preview.streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.preview.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
'channels.preview.accounts.<id>.streamMode, streaming (scalar), chunkMode, blockStreaming, draftChunk, and blockStreamingCoalesce are legacy; use channels.preview.accounts.<id>.streaming.{mode,chunkMode,preview.chunk,block.enabled,block.coalesce}. Run "openclaw doctor --fix".',
]);
expect(migration.legacyConfigRules.map((rule) => rule.path)).toEqual([
["channels", "discord"],
["channels", "discord", "accounts"],
["channels", "preview"],
["channels", "preview", "accounts"],
]);
});
@@ -114,9 +114,9 @@ describe("defineChannelAliasMigration rule matching", () => {
});
describe("defineChannelAliasMigration normalizeChannelConfig", () => {
it("migrates root and account aliases with dm normalization", () => {
it("migrates preview root and account aliases with dm normalization", () => {
const migration = defineChannelAliasMigration({
channelId: "discord",
channelId: "preview",
streaming: { defaultMode: "off", absentObjectDefault: "progress", includePreviewChunk: true },
accountStreamingReplacesRoot: true,
dm: { root: true, accounts: true },
@@ -124,7 +124,7 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
const changes: string[] = [];
const result = migration.normalizeChannelConfig({
cfg: cfgWith("discord", {
cfg: cfgWith("preview", {
streamMode: "block",
dm: { policy: "open" },
accounts: { work: { draftChunk: { minChars: 9 } } },
@@ -133,7 +133,7 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
});
expect(result.changes).toBe(changes);
expect((result.config.channels as Record<string, unknown>).discord).toEqual({
expect((result.config.channels as Record<string, unknown>).preview).toEqual({
dmPolicy: "open",
streaming: { mode: "block" },
accounts: {
@@ -143,11 +143,11 @@ describe("defineChannelAliasMigration normalizeChannelConfig", () => {
},
});
expect(changes).toEqual([
"Moved channels.discord.dm.policy → channels.discord.dmPolicy.",
"Removed empty channels.discord.dm after migration.",
"Moved channels.discord.streamMode → channels.discord.streaming.mode (block).",
"Moved channels.discord.accounts.work.draftChunk → channels.discord.accounts.work.streaming.preview.chunk.",
"Copied channels.discord.streaming into channels.discord.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
"Moved channels.preview.dm.policy → channels.preview.dmPolicy.",
"Removed empty channels.preview.dm after migration.",
"Moved channels.preview.streamMode → channels.preview.streaming.mode (block).",
"Moved channels.preview.accounts.work.draftChunk → channels.preview.accounts.work.streaming.preview.chunk.",
"Copied channels.preview.streaming into channels.preview.accounts.work.streaming to keep inherited settings while migrating flat streaming keys.",
]);
});