mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 20:05:19 +00:00
fix(plugins): avoid Signal and Twitch setup regressions
This commit is contained in:
@@ -169,6 +169,21 @@ describe("detectSignalApiMode", () => {
|
||||
expect(result).toBe("native");
|
||||
});
|
||||
|
||||
it("returns container after the native preference grace when native does not respond", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
mockNativeCheck.mockImplementation(() => new Promise(() => {}));
|
||||
mockContainerCheck.mockResolvedValue({ ok: true, status: 200 });
|
||||
|
||||
const result = detectSignalApiMode("http://localhost:8080");
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
await expect(result).resolves.toBe("container");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
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" });
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
const MODE_CACHE_TTL_MS = 30_000;
|
||||
const NATIVE_PREFERENCE_GRACE_MS = 50;
|
||||
|
||||
export type SignalSseEvent = {
|
||||
event?: string;
|
||||
@@ -55,6 +56,19 @@ function resolveAutoProbeTimeoutMs(timeoutMs: number | undefined): number {
|
||||
: DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function waitForNativePreferenceGrace(
|
||||
nativeResultPromise: Promise<{ ok: boolean }>,
|
||||
): Promise<{ ok: boolean }> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => resolve({ ok: false }), NATIVE_PREFERENCE_GRACE_MS);
|
||||
timer.unref?.();
|
||||
nativeResultPromise.then((result) => {
|
||||
clearTimeout(timer);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveAutoApiMode(
|
||||
baseUrl: string,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
@@ -111,21 +125,36 @@ export async function detectSignalApiMode(
|
||||
options: { account?: string; requireContainerReceive?: boolean } = {},
|
||||
): Promise<"native" | "container"> {
|
||||
const containerAccount = options.requireContainerReceive ? options.account?.trim() : undefined;
|
||||
const nativePromise = nativeCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
||||
const containerPromise = containerAccount
|
||||
const nativeResultPromise = nativeCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
||||
const containerResultPromise = containerAccount
|
||||
? containerCheck(baseUrl, timeoutMs, containerAccount).catch(() => ({ ok: false }))
|
||||
: options.requireContainerReceive
|
||||
? Promise.resolve({ ok: false })
|
||||
: containerCheck(baseUrl, timeoutMs).catch(() => ({ ok: false }));
|
||||
|
||||
const [nativeResult, containerResult] = await Promise.all([nativePromise, containerPromise]);
|
||||
if (nativeResult.ok) {
|
||||
return "native";
|
||||
const nativeHealthyPromise = nativeResultPromise.then((result) => {
|
||||
if (result.ok) {
|
||||
return "native" as const;
|
||||
}
|
||||
throw new Error("native not ok");
|
||||
});
|
||||
const containerHealthyPromise = containerResultPromise.then((result) => {
|
||||
if (result.ok) {
|
||||
return "container" as const;
|
||||
}
|
||||
throw new Error("container not ok");
|
||||
});
|
||||
|
||||
try {
|
||||
const firstHealthy = await Promise.any([nativeHealthyPromise, containerHealthyPromise]);
|
||||
if (firstHealthy === "native") {
|
||||
return "native";
|
||||
}
|
||||
const nativeResult = await waitForNativePreferenceGrace(nativeResultPromise);
|
||||
return nativeResult.ok ? "native" : "container";
|
||||
} catch {
|
||||
throw new Error(`Signal API not reachable at ${baseUrl}`);
|
||||
}
|
||||
if (containerResult.ok) {
|
||||
return "container";
|
||||
}
|
||||
throw new Error(`Signal API not reachable at ${baseUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,6 +201,32 @@ describe("TwitchClientManager", () => {
|
||||
expect(client1).toBe(client2);
|
||||
});
|
||||
|
||||
it("waits for disconnect after authentication failure retry events", async () => {
|
||||
mockConnect.mockImplementationOnce(() => {});
|
||||
|
||||
const connection = manager.getClient(testAccount);
|
||||
await Promise.resolve();
|
||||
authFailureHandlers[0]?.("bad token", 1);
|
||||
|
||||
let settled = false;
|
||||
void connection.then(
|
||||
() => {
|
||||
settled = true;
|
||||
},
|
||||
() => {
|
||||
settled = true;
|
||||
},
|
||||
);
|
||||
await Promise.resolve();
|
||||
expect(settled).toBe(false);
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
"Twitch authentication failed for testbot; waiting for retry, disconnect, or timeout: bad token",
|
||||
);
|
||||
|
||||
disconnectHandlers[0]?.(false, new Error("disconnected"));
|
||||
await expect(connection).rejects.toThrow("disconnected");
|
||||
});
|
||||
|
||||
it("should create separate clients for different accounts", async () => {
|
||||
await manager.getClient(testAccount);
|
||||
await manager.getClient(testAccount2);
|
||||
|
||||
Reference in New Issue
Block a user