mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 04:16:13 +00:00
fix(plugins): stabilize Twitch and Signal setup
This commit is contained in:
@@ -159,6 +159,16 @@ describe("detectSignalApiMode", () => {
|
||||
expect(result).toBe("native");
|
||||
});
|
||||
|
||||
it("prefers native even when the container probe resolves first", async () => {
|
||||
mockNativeCheck.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ ok: true, status: 200 }), 1)),
|
||||
);
|
||||
mockContainerCheck.mockResolvedValue({ ok: true, status: 200 });
|
||||
|
||||
const result = await detectSignalApiMode("http://localhost:8080");
|
||||
expect(result).toBe("native");
|
||||
});
|
||||
|
||||
it("throws error when neither endpoint responds", async () => {
|
||||
mockNativeCheck.mockResolvedValue({ ok: false, status: null, error: "Connection refused" });
|
||||
mockContainerCheck.mockResolvedValue({ ok: false, status: null, error: "Connection refused" });
|
||||
@@ -530,6 +540,26 @@ describe("streamSignalEvents", () => {
|
||||
"+14259798283",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reuse a cached container mode for no-account receive streams", async () => {
|
||||
setApiMode("auto");
|
||||
mockNativeCheck.mockResolvedValue({ ok: false, status: 404 });
|
||||
mockContainerCheck.mockResolvedValue({ ok: true, status: 200 });
|
||||
|
||||
await expect(signalCheck("http://auto-cache-no-account.local:8080")).resolves.toEqual({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
await expect(
|
||||
streamSignalEvents({
|
||||
baseUrl: "http://auto-cache-no-account.local:8080",
|
||||
onEvent: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow("Signal API not reachable at http://auto-cache-no-account.local:8080");
|
||||
expect(mockStreamContainerEvents).not.toHaveBeenCalled();
|
||||
expect(mockContainerCheck).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchAttachment", () => {
|
||||
|
||||
@@ -65,7 +65,7 @@ async function resolveAutoApiMode(
|
||||
if (
|
||||
cached.mode !== "container" ||
|
||||
!options.requireContainerReceive ||
|
||||
cached.receiveAccount === options.account
|
||||
(Boolean(options.account?.trim()) && cached.receiveAccount === options.account?.trim())
|
||||
) {
|
||||
return cached.mode;
|
||||
}
|
||||
@@ -103,32 +103,29 @@ async function resolveApiModeForOperation(params: {
|
||||
|
||||
/**
|
||||
* Detect which Signal API mode is available by probing endpoints.
|
||||
* First endpoint to respond OK wins.
|
||||
* Native wins when both APIs are healthy because it preserves the richer JSON-RPC contract.
|
||||
*/
|
||||
export async function detectSignalApiMode(
|
||||
baseUrl: string,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
options: { account?: string; requireContainerReceive?: boolean } = {},
|
||||
): Promise<"native" | "container"> {
|
||||
const nativePromise = nativeCheck(baseUrl, timeoutMs).then((r) =>
|
||||
r.ok ? ("native" as const) : Promise.reject(new Error("native not ok")),
|
||||
);
|
||||
const containerAccount = options.requireContainerReceive ? options.account?.trim() : undefined;
|
||||
const nativePromise = nativeCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
||||
const containerPromise = containerAccount
|
||||
? containerCheck(baseUrl, timeoutMs, containerAccount).then((r) =>
|
||||
r.ok ? ("container" as const) : Promise.reject(new Error("container not ok")),
|
||||
)
|
||||
? containerCheck(baseUrl, timeoutMs, containerAccount).catch(() => ({ ok: false }))
|
||||
: options.requireContainerReceive
|
||||
? Promise.reject(new Error("container receive account required"))
|
||||
: containerCheck(baseUrl, timeoutMs).then((r) =>
|
||||
r.ok ? ("container" as const) : Promise.reject(new Error("container not ok")),
|
||||
);
|
||||
? Promise.resolve({ ok: false })
|
||||
: containerCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
||||
|
||||
try {
|
||||
return await Promise.any([nativePromise, containerPromise]);
|
||||
} catch {
|
||||
throw new Error(`Signal API not reachable at ${baseUrl}`);
|
||||
const [nativeResult, containerResult] = await Promise.all([nativePromise, containerPromise]);
|
||||
if (nativeResult.ok) {
|
||||
return "native";
|
||||
}
|
||||
if (containerResult.ok) {
|
||||
return "container";
|
||||
}
|
||||
throw new Error(`Signal API not reachable at ${baseUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -186,6 +186,21 @@ describe("TwitchClientManager", () => {
|
||||
expect(mockConnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("deduplicates concurrent client creation for the same account", async () => {
|
||||
mockConnect.mockImplementationOnce(() => {});
|
||||
|
||||
const first = manager.getClient(testAccount);
|
||||
const second = manager.getClient(testAccount);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(mockConnect).toHaveBeenCalledTimes(1);
|
||||
expect(authSuccessHandlers).toHaveLength(1);
|
||||
authSuccessHandlers[0]?.();
|
||||
|
||||
const [client1, client2] = await Promise.all([first, second]);
|
||||
expect(client1).toBe(client2);
|
||||
});
|
||||
|
||||
it("should create separate clients for different accounts", async () => {
|
||||
await manager.getClient(testAccount);
|
||||
await manager.getClient(testAccount2);
|
||||
|
||||
Reference in New Issue
Block a user