Files
openclaw/extensions/twitch/src/plugin.lifecycle.test.ts
Eden b67bcd93cc fix(twitch): keep account monitor alive until abort (#81853)
Summary:
- Keep Twitch startAccount alive until abort via runStoppablePassiveMonitor.
- Add lifecycle regression coverage and env-gated live Twitch IRC proof.
- Add changelog credit for #60071 / #81853.

Verification:
- pnpm test extensions/twitch/src/plugin.lifecycle.test.ts extensions/twitch/src/plugin.test.ts extensions/twitch/src/twitch-client.test.ts src/gateway/server-channels.test.ts
- pnpm exec oxfmt --check --threads=1 extensions/twitch/src/plugin.ts extensions/twitch/src/plugin.lifecycle.test.ts extensions/twitch/src/plugin.live.test.ts CHANGELOG.md
- pnpm test:live -- extensions/twitch/src/plugin.live.test.ts (skipped without Twitch live credentials)
- codex-review --mode branch --parallel-tests targeted Twitch/gateway tests
- GitHub checks on aea52056c6 green

Fixes #60071.

Co-authored-by: 許元豪 <146086744+edenfunf@users.noreply.github.com>
2026-05-15 13:47:10 +01:00

87 lines
2.3 KiB
TypeScript

import {
createStartAccountContext,
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "openclaw/plugin-sdk/channel-test-helpers";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { TwitchAccountConfig } from "./types.js";
const hoisted = vi.hoisted(() => ({
monitorTwitchProvider: vi.fn(),
}));
vi.mock("./monitor.js", () => ({
monitorTwitchProvider: hoisted.monitorTwitchProvider,
}));
const { twitchPlugin } = await import("./plugin.js");
type TwitchStartAccount = NonNullable<NonNullable<typeof twitchPlugin.gateway>["startAccount"]>;
function requireStartAccount(): TwitchStartAccount {
const startAccount = twitchPlugin.gateway?.startAccount;
if (!startAccount) {
throw new Error("Expected Twitch gateway startAccount");
}
return startAccount;
}
function buildAccount(): TwitchAccountConfig & { accountId: string } {
return {
accountId: "default",
username: "testbot",
accessToken: "oauth:test-token",
clientId: "test-client-id",
channel: "#testchannel",
enabled: true,
};
}
function mockStartedMonitor() {
const stop = vi.fn();
hoisted.monitorTwitchProvider.mockResolvedValue({ stop });
return stop;
}
function startTwitchAccount(abortSignal?: AbortSignal) {
return requireStartAccount()(
createStartAccountContext({
account: buildAccount(),
abortSignal,
}),
);
}
describe("twitch startAccount lifecycle", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = mockStartedMonitor();
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: requireStartAccount(),
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorTwitchProvider),
isSettled,
abort,
task,
stop,
});
});
it("stops immediately when startAccount receives an already-aborted signal", async () => {
const stop = mockStartedMonitor();
const abort = new AbortController();
abort.abort();
await startTwitchAccount(abort.signal);
expect(hoisted.monitorTwitchProvider).toHaveBeenCalledOnce();
expect(stop).toHaveBeenCalledOnce();
});
});