mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(whatsapp): resolve configured default account in single-arg setActiveWebListener overload (#53918)
Merged via squash.
Prepared head SHA: ad9be63835
Co-authored-by: yhyatt <10474956+yhyatt@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
This commit is contained in:
@@ -8,6 +8,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- WhatsApp: honor the configured default account when the active listener helper is used without an explicit account id, so named default accounts do not get registered under `default`. (#53918) Thanks @yhyatt.
|
||||
|
||||
## 2026.4.10
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock loadConfig so the single-arg setActiveWebListener overload resolves
|
||||
// the configured default account as "work" (matching the regression test).
|
||||
// All other tests pass explicit accountIds and are unaffected by this mock.
|
||||
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
|
||||
loadConfig: () => ({
|
||||
channels: { whatsapp: { accounts: { work: { enabled: true } }, defaultAccount: "work" } },
|
||||
}),
|
||||
}));
|
||||
|
||||
type ActiveListenerModule = typeof import("./active-listener.js");
|
||||
|
||||
const activeListenerModuleUrl = new URL("./active-listener.ts", import.meta.url).href;
|
||||
@@ -12,18 +21,28 @@ afterEach(async () => {
|
||||
const mod = await importActiveListenerModule(`cleanup-${Date.now()}`);
|
||||
mod.setActiveWebListener(null);
|
||||
mod.setActiveWebListener("work", null);
|
||||
mod.setActiveWebListener("default", null);
|
||||
});
|
||||
|
||||
/** Minimal listener stub */
|
||||
function makeListener() {
|
||||
return {
|
||||
sendMessage: vi.fn(async () => ({ messageId: "msg-1" })),
|
||||
sendPoll: vi.fn(async () => ({ messageId: "poll-1" })),
|
||||
sendReaction: vi.fn(async () => {}),
|
||||
sendComposingTo: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("active WhatsApp listener singleton", () => {
|
||||
it("shares listeners across duplicate module instances", async () => {
|
||||
it("shares listeners across duplicate module instances (bundle-fragmentation fix)", async () => {
|
||||
// Simulates the scenario where two bundled copies of active-listener.ts are loaded
|
||||
// (e.g. channel-web-*.js calls setActiveWebListener, outbound-*.js calls
|
||||
// requireActiveWebListener). Without resolveGlobalSingleton they would each hold
|
||||
// their own Map and the listener would never be found by the outbound path.
|
||||
const first = await importActiveListenerModule(`first-${Date.now()}`);
|
||||
const second = await importActiveListenerModule(`second-${Date.now()}`);
|
||||
const listener = {
|
||||
sendMessage: vi.fn(async () => ({ messageId: "msg-1" })),
|
||||
sendPoll: vi.fn(async () => ({ messageId: "poll-1" })),
|
||||
sendReaction: vi.fn(async () => {}),
|
||||
sendComposingTo: vi.fn(async () => {}),
|
||||
};
|
||||
const listener = makeListener();
|
||||
|
||||
first.setActiveWebListener("work", listener);
|
||||
|
||||
@@ -33,4 +52,74 @@ describe("active WhatsApp listener singleton", () => {
|
||||
listener,
|
||||
});
|
||||
});
|
||||
|
||||
it("single-arg overload registers under configured default account, not always 'default'", async () => {
|
||||
// Regression: setActiveWebListener(listener) used DEFAULT_ACCOUNT_ID ("default")
|
||||
// even when the configured default account is named "work". This caused
|
||||
// requireActiveWebListener("work") to throw while the listener was silently
|
||||
// registered under the wrong key.
|
||||
const mod = await importActiveListenerModule(`named-account-${Date.now()}`);
|
||||
const listener = makeListener();
|
||||
|
||||
// Single-arg call — should resolve accountId from loadConfig() default, which
|
||||
// vitest config maps to "work" (see mock below).
|
||||
mod.setActiveWebListener(listener);
|
||||
|
||||
// "work" must be resolvable — previously this threw
|
||||
expect(mod.requireActiveWebListener("work")).toEqual({
|
||||
accountId: "work",
|
||||
listener,
|
||||
});
|
||||
});
|
||||
|
||||
it("single-arg overload still works when default account is 'default'", async () => {
|
||||
// Backward-compat: configs that rely on the "default" account name must
|
||||
// continue to work after the fix. Use single-arg overload with a temporary
|
||||
// spy that returns "default" as the configured default account.
|
||||
const configRuntime = await import("openclaw/plugin-sdk/config-runtime");
|
||||
const spy = vi.spyOn(configRuntime, "loadConfig").mockReturnValue({
|
||||
channels: {
|
||||
whatsapp: { accounts: { default: { enabled: true } }, defaultAccount: "default" },
|
||||
},
|
||||
} as ReturnType<typeof configRuntime.loadConfig>);
|
||||
|
||||
try {
|
||||
const mod = await importActiveListenerModule(`default-account-${Date.now()}`);
|
||||
const listener = makeListener();
|
||||
|
||||
// Single-arg call — should resolve to "default" via the spy
|
||||
mod.setActiveWebListener(listener);
|
||||
|
||||
expect(mod.requireActiveWebListener("default")).toEqual({
|
||||
accountId: "default",
|
||||
listener,
|
||||
});
|
||||
// The legacy no-arg lookup (undefined → "default") must also work
|
||||
expect(mod.requireActiveWebListener()).toEqual({
|
||||
accountId: "default",
|
||||
listener,
|
||||
});
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("requireActiveWebListener throws a clear error when listener is missing", async () => {
|
||||
const mod = await importActiveListenerModule(`missing-${Date.now()}`);
|
||||
|
||||
expect(() => mod.requireActiveWebListener("work")).toThrowError(
|
||||
/No active WhatsApp Web listener \(account: work\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("setActiveWebListener with null removes the listener", async () => {
|
||||
const mod = await importActiveListenerModule(`remove-${Date.now()}`);
|
||||
const listener = makeListener();
|
||||
|
||||
mod.setActiveWebListener("work", listener);
|
||||
expect(mod.getActiveWebListener("work")).toBe(listener);
|
||||
|
||||
mod.setActiveWebListener("work", null);
|
||||
expect(mod.getActiveWebListener("work")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { PollInput } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
|
||||
import { resolveDefaultWhatsAppAccountId } from "./accounts.js";
|
||||
|
||||
export type ActiveWebSendOptions = {
|
||||
gifPlayback?: boolean;
|
||||
@@ -53,7 +55,7 @@ function setCurrentListener(listener: ActiveWebListener | null): void {
|
||||
}
|
||||
|
||||
export function resolveWebAccountId(accountId?: string | null): string {
|
||||
return (accountId ?? "").trim() || DEFAULT_ACCOUNT_ID;
|
||||
return (accountId ?? "").trim() || resolveDefaultWhatsAppAccountId(loadConfig());
|
||||
}
|
||||
|
||||
export function requireActiveWebListener(accountId?: string | null): {
|
||||
@@ -83,7 +85,10 @@ export function setActiveWebListener(
|
||||
typeof accountIdOrListener === "string"
|
||||
? { accountId: accountIdOrListener, listener: maybeListener ?? null }
|
||||
: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
// Resolve the configured default account name so that callers using the
|
||||
// single-arg overload register under the right key (e.g. "work"), not
|
||||
// always under DEFAULT_ACCOUNT_ID ("default").
|
||||
accountId: resolveDefaultWhatsAppAccountId(loadConfig()),
|
||||
listener: accountIdOrListener ?? null,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user