mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(slack): retain partial stream participation
This commit is contained in:
committed by
GitHub
parent
a3155ca99a
commit
0d8d75918d
@@ -171,6 +171,10 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/zalo/**"
|
||||
- "docs/channels/zalo.md"
|
||||
"channel: zaloclawbot":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "docs/channels/zaloclawbot.md"
|
||||
"channel: zalouser":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
@@ -1194,5 +1194,9 @@
|
||||
{
|
||||
"source": "cohere",
|
||||
"target": "cohere"
|
||||
},
|
||||
{
|
||||
"source": "Zalo ClawBot",
|
||||
"target": "Zalo ClawBot"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -52,6 +52,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [WhatsApp](/channels/whatsapp) - Most popular; uses Baileys and requires QR pairing.
|
||||
- [Yuanbao](/channels/yuanbao) - Tencent Yuanbao bot (external plugin).
|
||||
- [Zalo](/channels/zalo) - Zalo Bot API; Vietnam's popular messenger (bundled plugin).
|
||||
- [Zalo ClawBot](/channels/zaloclawbot) - Personal Zalo assistant via QR login; owner-bound (external plugin).
|
||||
- [Zalo Personal](/channels/zalouser) - Zalo personal account via QR login (bundled plugin).
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
summary: "Zalo ClawBot channel setup through the external openclaw-zaloclawbot plugin"
|
||||
read_when:
|
||||
- You want a personal Zalo assistant bot with QR-code login
|
||||
- You are installing or troubleshooting the openclaw-zaloclawbot channel plugin
|
||||
title: "Zalo ClawBot"
|
||||
---
|
||||
|
||||
OpenClaw connects to Zalo ClawBot through the catalog-listed external
|
||||
`@zalo-platforms/openclaw-zaloclawbot` plugin. Login uses a Zalo Mini App QR
|
||||
code.
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Plugin Version | OpenClaw Version | npm dist-tag | Status |
|
||||
| -------------- | ---------------- | ------------ | ------------- |
|
||||
| 0.1.x | >=2026.4.10 | `latest` | Active / Beta |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js **>= 22**
|
||||
- [OpenClaw](https://docs.openclaw.ai/install) must be installed (`openclaw` CLI available).
|
||||
- A Zalo account on a mobile device to scan the login QR code.
|
||||
|
||||
## Install with onboard (recommended)
|
||||
|
||||
Run the OpenClaw onboarding wizard and pick **Zalo ClawBot** from the channel menu:
|
||||
|
||||
```bash
|
||||
openclaw onboard
|
||||
```
|
||||
|
||||
The wizard installs the plugin from the official catalog (integrity-verified), renders the login QR right in the terminal, and finishes the channel once you scan it with the Zalo app. No extra commands are needed.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
To add the channel to an already-onboarded gateway, follow these steps:
|
||||
|
||||
### 1. Install the plugin
|
||||
|
||||
```bash
|
||||
openclaw plugins install "@zalo-platforms/openclaw-zaloclawbot@0.1.4"
|
||||
```
|
||||
|
||||
Use the exact pinned version shown above (it matches the official catalog entry), so OpenClaw verifies the package against the catalog integrity hash during install.
|
||||
|
||||
### 2. Enable the plugin in config
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.openclaw-zaloclawbot.enabled true
|
||||
```
|
||||
|
||||
### 3. Generate QR code and log in
|
||||
|
||||
```bash
|
||||
openclaw channels login --channel openclaw-zaloclawbot
|
||||
```
|
||||
|
||||
Scan the terminal-rendered QR code using the Zalo mobile app, accept the Terms of Use inside the Zalo Mini App, and authorize the session.
|
||||
|
||||
### 4. Restart the gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
Unlike the standard developer Zalo channel which requires you to register your own Zalo Official Account (OA) and paste static developer credentials, Zalo ClawBot operates as an **owner-bound personal assistant** using a shared, official infrastructure:
|
||||
|
||||
1. **Secure Onboarding:** The QR code resolves to a secure Zalo Mini App that binds a newly-provisioned, private bot under a shared official OA directly to your Zalo User ID.
|
||||
2. **Owner-Bound Privacy:** By design, the bot is restricted to communicating _only_ with its owner. Messages from other users are dropped at the platform level, making the connection private and secure.
|
||||
3. **Official API path:** The plugin uses Zalo Bot Platform APIs instead of
|
||||
browser or web-session automation.
|
||||
|
||||
## Under the Hood
|
||||
|
||||
The Zalo ClawBot plugin communicates with Zalo APIs via a persistent long-polling message loop. To maintain a clean and lightweight runtime:
|
||||
|
||||
- Long-poll connections utilize the `getUpdates` endpoint.
|
||||
- Webhooks are disabled by default for local desktop/terminal gateway runs.
|
||||
- Messages are processed client-side and mapped directly to your local agent runtime.
|
||||
|
||||
The external plugin manages bot credentials under the OpenClaw state directory.
|
||||
Treat that directory as sensitive and include it in the same access-control and
|
||||
backup policy as the rest of your OpenClaw state.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **QR Login Timeout:** The login token (`zbsk`) expires after 5 minutes for security reasons. If the QR code expires before you scan it, simply rerun the login command to generate a new one.
|
||||
- **Gateway Fails to Load:** Ensure your OpenClaw host version is `2026.4.10` or higher. Older versions do not support the external npm-plugin installation ledger.
|
||||
@@ -316,6 +316,10 @@
|
||||
"source": "/providers/zalo",
|
||||
"destination": "/channels/zalo"
|
||||
},
|
||||
{
|
||||
"source": "/channels/openclaw-zaloclawbot",
|
||||
"destination": "/channels/zaloclawbot"
|
||||
},
|
||||
{
|
||||
"source": "/providers/whatsapp",
|
||||
"destination": "/channels/whatsapp"
|
||||
@@ -1132,6 +1136,7 @@
|
||||
"channels/feishu",
|
||||
"channels/yuanbao",
|
||||
"channels/zalo",
|
||||
"channels/zaloclawbot",
|
||||
"channels/zalouser"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -504,9 +504,10 @@ Legacy aliases still normalize to the canonical bundled ids:
|
||||
sign-in URL. xAI decides which accounts can receive OAuth API tokens, and
|
||||
the consent page may show Grok Build even though OpenClaw does not require
|
||||
the Grok Build app.
|
||||
- `grok-4.20-multi-agent-experimental-beta-0304` is not supported on the
|
||||
normal xAI provider path because it requires a different upstream API
|
||||
surface than the standard OpenClaw xAI transport.
|
||||
- OpenClaw does not currently expose the xAI multi-agent model family. xAI
|
||||
serves these models through the Responses API, but they do not accept the
|
||||
client-side or custom tools used by OpenClaw's shared agent loop. See the
|
||||
[xAI multi-agent limitations](https://docs.x.ai/developers/model-capabilities/text/multi-agent#limitations).
|
||||
- xAI Realtime voice is not registered as an OpenClaw provider yet. It
|
||||
needs a different bidirectional voice session contract than batch STT or
|
||||
streaming transcription.
|
||||
|
||||
@@ -16,6 +16,28 @@ vi.mock("openclaw/plugin-sdk/fetch-runtime", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function cancelTrackedResponse(
|
||||
text: string,
|
||||
init: ResponseInit,
|
||||
): {
|
||||
response: Response;
|
||||
wasCanceled: () => boolean;
|
||||
} {
|
||||
let canceled = false;
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(text));
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(stream, init),
|
||||
wasCanceled: () => canceled,
|
||||
};
|
||||
}
|
||||
|
||||
describe("sendWebhookMessageDiscord proxy support", () => {
|
||||
beforeEach(() => {
|
||||
makeProxyFetchMock.mockReset();
|
||||
@@ -208,4 +230,39 @@ describe("sendWebhookMessageDiscord proxy support", () => {
|
||||
expect(error.rawBody).toEqual({ message: "upstream unavailable" });
|
||||
globalFetchMock.mockRestore();
|
||||
});
|
||||
|
||||
it("bounds webhook error bodies without using response.text()", async () => {
|
||||
const tracked = cancelTrackedResponse(`${"upstream unavailable ".repeat(1024)}tail`, {
|
||||
status: 503,
|
||||
headers: { "content-type": "text/plain" },
|
||||
});
|
||||
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
|
||||
const globalFetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(tracked.response);
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: "Bot test-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const thrown = await sendWebhookMessageDiscord("hello", {
|
||||
cfg,
|
||||
accountId: "default",
|
||||
webhookId: "123",
|
||||
webhookToken: "abc",
|
||||
wait: true,
|
||||
}).then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
expect(thrown).toBeInstanceOf(DiscordError);
|
||||
const error = thrown as DiscordError;
|
||||
expect(error.message).toContain("upstream unavailable");
|
||||
expect(JSON.stringify(error.rawBody)).not.toContain("tail");
|
||||
expect(tracked.wasCanceled()).toBe(true);
|
||||
expect(textSpy).not.toHaveBeenCalled();
|
||||
globalFetchMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Discord plugin module implements send.webhook behavior.
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolveDiscordClientAccountContext } from "./client.js";
|
||||
import {
|
||||
@@ -14,6 +15,8 @@ import { rewriteDiscordKnownMentions } from "./mentions.js";
|
||||
import { createDiscordSendResult } from "./send.receipt.js";
|
||||
import type { DiscordSendResult } from "./send.types.js";
|
||||
|
||||
const DISCORD_WEBHOOK_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
||||
|
||||
type DiscordWebhookSendOpts = {
|
||||
cfg: OpenClawConfig;
|
||||
webhookId: string;
|
||||
@@ -54,7 +57,9 @@ function coerceWebhookErrorBody(raw: string): unknown {
|
||||
}
|
||||
|
||||
async function throwWebhookResponseError(response: Response): Promise<never> {
|
||||
const raw = await response.text().catch(() => "");
|
||||
const raw = await readResponseTextLimited(response, DISCORD_WEBHOOK_ERROR_BODY_LIMIT_BYTES).catch(
|
||||
() => "",
|
||||
);
|
||||
const parsed = coerceWebhookErrorBody(raw);
|
||||
if (response.status === 429) {
|
||||
throw new RateLimitError(response, {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("whatsapp bundled entries", () => {
|
||||
|
||||
it("declares account config as channel-restart reload metadata", () => {
|
||||
expect(whatsappPlugin.reload).toEqual({
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts"],
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
|
||||
noopPrefixes: ["channels.whatsapp"],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -181,7 +181,7 @@ export function createWhatsAppPluginBase(params: {
|
||||
// the broad `channels.whatsapp` noop prefix below otherwise swallows it as a
|
||||
// hot no-op and leaves the account connected until a full restart.
|
||||
reload: {
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts"],
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
|
||||
noopPrefixes: ["channels.whatsapp"],
|
||||
},
|
||||
gatewayMethodDescriptors: [{ name: "web.login.start" }, { name: "web.login.wait" }],
|
||||
|
||||
@@ -31,6 +31,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelCatalog": {
|
||||
"suppressions": [
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-0309",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-latest",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-beta-latest",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-experimental-beta-0304",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-experimental-beta-latest",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
},
|
||||
{
|
||||
"provider": "xai",
|
||||
"model": "grok-4.20-multi-agent-beta-0309",
|
||||
"reason": "OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai."
|
||||
}
|
||||
]
|
||||
},
|
||||
"syntheticAuthRefs": ["xai"],
|
||||
"setup": {
|
||||
"providers": [
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
|
||||
) as {
|
||||
modelCatalog?: {
|
||||
suppressions?: Array<{ provider?: string; model?: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
const XAI_MULTI_AGENT_MODELS = [
|
||||
"grok-4.20-multi-agent-0309",
|
||||
"grok-4.20-multi-agent",
|
||||
"grok-4.20-multi-agent-latest",
|
||||
"grok-4.20-multi-agent-beta-latest",
|
||||
"grok-4.20-multi-agent-experimental-beta-0304",
|
||||
"grok-4.20-multi-agent-experimental-beta-latest",
|
||||
"grok-4.20-multi-agent-beta-0309",
|
||||
] as const;
|
||||
|
||||
describe("xAI plugin manifest", () => {
|
||||
it("suppresses the unsupported multi-agent model aliases", () => {
|
||||
const suppressionRefs = new Set(
|
||||
(manifest.modelCatalog?.suppressions ?? []).map(
|
||||
(suppression) => `${suppression.provider}/${suppression.model}`,
|
||||
),
|
||||
);
|
||||
|
||||
for (const model of XAI_MULTI_AGENT_MODELS) {
|
||||
expect(suppressionRefs).toContain(`xai/${model}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@zalo-platforms/openclaw-zaloclawbot",
|
||||
"description": "OpenClaw Zalo ClawBot channel plugin by the Zalo Platforms team.",
|
||||
"source": "external",
|
||||
"kind": "channel",
|
||||
"openclaw": {
|
||||
"plugin": {
|
||||
"id": "openclaw-zaloclawbot",
|
||||
"label": "Zalo ClawBot"
|
||||
},
|
||||
"channel": {
|
||||
"id": "openclaw-zaloclawbot",
|
||||
"label": "Zalo ClawBot",
|
||||
"selectionLabel": "Zalo ClawBot (QR)",
|
||||
"detailLabel": "Zalo ClawBot",
|
||||
"docsPath": "/channels/zaloclawbot",
|
||||
"docsLabel": "zaloclawbot",
|
||||
"blurb": "Personal Zalo assistant bot via QR-code login — owner-bound, no setup.",
|
||||
"aliases": ["zaloclawbot", "zalo-clawbot"],
|
||||
"order": 82
|
||||
},
|
||||
"channelConfigs": {
|
||||
"openclaw-zaloclawbot": {
|
||||
"label": "Zalo ClawBot",
|
||||
"description": "Personal Zalo assistant — QR-onboarded, owner-bound.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@zalo-platforms/openclaw-zaloclawbot@0.1.4",
|
||||
"defaultChoice": "npm",
|
||||
"expectedIntegrity": "sha512-5IxZriHJYACLLGqkCPPsTP9tas62kXEOFqTFAFMdunAM3SPhIJwVFRp0WvoP/m7L2PX85weD0g8LOtxM93VDYg==",
|
||||
"minHostVersion": ">=2026.4.10"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@openclaw/discord",
|
||||
"description": "OpenClaw Discord channel plugin",
|
||||
|
||||
@@ -61,6 +61,10 @@ vi.mock("../model-suppression.js", () => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isUnsupportedXaiMultiAgentModel(provider?: string, id?: string): boolean {
|
||||
return provider === "xai" && id?.trim().toLowerCase() === "grok-4.20-multi-agent-0309";
|
||||
}
|
||||
|
||||
return {
|
||||
shouldSuppressBuiltInModel: ({
|
||||
provider,
|
||||
@@ -79,6 +83,9 @@ vi.mock("../model-suppression.js", () => {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (isUnsupportedXaiMultiAgentModel(provider, id)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
(provider === "qwen" || provider === "modelstudio") &&
|
||||
id?.trim().toLowerCase() === "qwen3.6-plus" &&
|
||||
@@ -92,7 +99,7 @@ vi.mock("../model-suppression.js", () => {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return isUnsupportedXaiMultiAgentModel(provider, id);
|
||||
},
|
||||
buildSuppressedBuiltInModelError: ({
|
||||
provider,
|
||||
@@ -116,6 +123,9 @@ vi.mock("../model-suppression.js", () => {
|
||||
) {
|
||||
return `Unknown model: ${provider}/gpt-5.3-codex-spark. gpt-5.3-codex-spark is available only through ChatGPT/Codex OAuth. Run \`openclaw models auth login --provider openai\` and use openai/gpt-5.3-codex-spark with that OAuth profile; OpenAI API-key auth cannot use this model.`;
|
||||
}
|
||||
if (isUnsupportedXaiMultiAgentModel(provider, id)) {
|
||||
return "Unknown model: xai/grok-4.20-multi-agent-0309. OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai.";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
@@ -3451,6 +3461,27 @@ describe("resolveModel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not build a configured fallback for unsupported xAI multi-agent models", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
xai: {
|
||||
baseUrl: "https://api.x.ai/v1",
|
||||
api: "openai-completions",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const result = resolveModelForTest("xai", "grok-4.20-multi-agent-0309", "/tmp/agent", cfg);
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe(
|
||||
"Unknown model: xai/grok-4.20-multi-agent-0309. OpenClaw does not currently support xAI multi-agent models; choose another xAI model. See https://docs.openclaw.ai/providers/xai.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects stale openai gpt-5.3-codex-spark discovery rows", () => {
|
||||
mockDiscoveredModel(discoverModels, {
|
||||
provider: "openai",
|
||||
|
||||
@@ -49,3 +49,9 @@ describeChannelCatalogEntryContract({
|
||||
npmSpec: "openclaw-plugin-yuanbao@2.13.1",
|
||||
alias: "yb",
|
||||
});
|
||||
|
||||
describeChannelCatalogEntryContract({
|
||||
channelId: "openclaw-zaloclawbot",
|
||||
npmSpec: "@zalo-platforms/openclaw-zaloclawbot@0.1.4",
|
||||
alias: "zaloclawbot",
|
||||
});
|
||||
|
||||
@@ -160,7 +160,7 @@ describe("buildGatewayReloadPlan", () => {
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
reload: {
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts"],
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
|
||||
noopPrefixes: ["channels.whatsapp"],
|
||||
},
|
||||
};
|
||||
@@ -235,6 +235,14 @@ describe("buildGatewayReloadPlan", () => {
|
||||
expect(plan.noopPaths).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("restarts the WhatsApp channel when selfChatMode changes (configPrefix wins over broad noop prefix)", () => {
|
||||
const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]);
|
||||
expect(plan.restartGateway).toBe(false);
|
||||
expect(plan.restartChannels).toEqual(new Set(["whatsapp"]));
|
||||
expect(plan.hotReasons).toContain("channels.whatsapp.selfChatMode");
|
||||
expect(plan.noopPaths).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("keeps other channels.whatsapp.* changes as hot no-ops", () => {
|
||||
const plan = buildGatewayReloadPlan(["channels.whatsapp.replyToMode"]);
|
||||
expect(plan.restartGateway).toBe(false);
|
||||
|
||||
@@ -5,8 +5,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConfigWriteNotification } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { consumeGatewaySigusr1RestartIntent } from "../infra/restart.js";
|
||||
import {
|
||||
pinActivePluginChannelRegistry,
|
||||
releasePinnedPluginChannelRegistry,
|
||||
} from "../plugins/runtime.js";
|
||||
import { createEmptyRuntimeWebToolsMetadata } from "../secrets/runtime-fast-path.js";
|
||||
import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { diffConfigPaths } from "./config-diff.js";
|
||||
import {
|
||||
buildGatewayReloadPlan,
|
||||
@@ -141,7 +146,13 @@ vi.mock("../agents/agent-bundle-mcp-tools.js", () => ({
|
||||
disposeAllSessionMcpRuntimes: hoisted.disposeAllSessionMcpRuntimes,
|
||||
}));
|
||||
|
||||
function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn() }) {
|
||||
function createReloadHandlersForTest(
|
||||
logReload = { info: vi.fn(), warn: vi.fn() },
|
||||
channels?: {
|
||||
start: (channel: ChannelKind) => Promise<void>;
|
||||
stop: (channel: ChannelKind) => Promise<void>;
|
||||
},
|
||||
) {
|
||||
const cron = { start: vi.fn(async () => {}), stop: vi.fn() };
|
||||
const heartbeatRunner = {
|
||||
stop: vi.fn(),
|
||||
@@ -158,8 +169,8 @@ function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn()
|
||||
channelHealthMonitor: null,
|
||||
}),
|
||||
setState: vi.fn(),
|
||||
startChannel: vi.fn(async () => {}),
|
||||
stopChannel: vi.fn(async () => {}),
|
||||
startChannel: channels?.start ?? vi.fn(async () => {}),
|
||||
stopChannel: channels?.stop ?? vi.fn(async () => {}),
|
||||
stopPostReadySidecars: vi.fn(),
|
||||
reloadPlugins: vi.fn(
|
||||
async (): Promise<GatewayPluginReloadResult> => ({
|
||||
@@ -889,6 +900,42 @@ describe("gateway channel hot reload handlers", () => {
|
||||
}
|
||||
}
|
||||
|
||||
it("restarts WhatsApp when the planner receives a selfChatMode change", async () => {
|
||||
const whatsappPlugin = {
|
||||
...createChannelTestPluginBase({ id: "whatsapp" }),
|
||||
reload: {
|
||||
configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
|
||||
noopPrefixes: ["channels.whatsapp"],
|
||||
},
|
||||
};
|
||||
const registry = createTestRegistry([
|
||||
{ pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" },
|
||||
]);
|
||||
const events: string[] = [];
|
||||
const channels = {
|
||||
stop: vi.fn(async (channel: ChannelKind) => {
|
||||
events.push(`stop:${channel}`);
|
||||
}),
|
||||
start: vi.fn(async (channel: ChannelKind) => {
|
||||
events.push(`start:${channel}`);
|
||||
}),
|
||||
};
|
||||
|
||||
pinActivePluginChannelRegistry(registry);
|
||||
try {
|
||||
const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]);
|
||||
const { applyHotReload } = createReloadHandlersForTest(undefined, channels);
|
||||
|
||||
expect(plan.restartGateway).toBe(false);
|
||||
expect(plan.restartChannels).toEqual(new Set(["whatsapp"]));
|
||||
await withChannelReloadsEnabled(() => applyHotReload(plan, {}));
|
||||
|
||||
expect(events).toEqual(["stop:whatsapp", "start:whatsapp"]);
|
||||
} finally {
|
||||
releasePinnedPluginChannelRegistry(registry);
|
||||
}
|
||||
});
|
||||
|
||||
it("continues restarting later channels after a hot-reload stop failure", async () => {
|
||||
const events: string[] = [];
|
||||
const setState = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user