mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(line): keep startAccount pending until abort signal to prevent restart loop
monitorLineProvider() registers the webhook HTTP route and returns immediately. Because startAccount() directly returned that resolved promise, the channel supervisor interpreted it as "provider exited" and triggered auto-restart up to 10 times. Await a promise gated on ctx.abortSignal so startAccount stays alive for the full provider lifecycle, matching the contract expected by the channel supervisor. Closes #26478 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
Peter Steinberger
parent
f55238e72a
commit
243e28df4f
@@ -37,6 +37,7 @@ function createStartAccountCtx(params: {
|
||||
token: string;
|
||||
secret: string;
|
||||
runtime: ReturnType<typeof createRuntimeEnv>;
|
||||
abortSignal?: AbortSignal;
|
||||
}): ChannelGatewayContext<ResolvedLineAccount> {
|
||||
const snapshot: ChannelAccountSnapshot = {
|
||||
accountId: "default",
|
||||
@@ -56,7 +57,7 @@ function createStartAccountCtx(params: {
|
||||
},
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: params.runtime,
|
||||
abortSignal: new AbortController().signal,
|
||||
abortSignal: params.abortSignal ?? new AbortController().signal,
|
||||
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
getStatus: () => snapshot,
|
||||
setStatus: vi.fn(),
|
||||
@@ -104,14 +105,19 @@ describe("linePlugin gateway.startAccount", () => {
|
||||
const { runtime, monitorLineProvider } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
await linePlugin.gateway!.startAccount!(
|
||||
const abort = new AbortController();
|
||||
const task = linePlugin.gateway!.startAccount!(
|
||||
createStartAccountCtx({
|
||||
token: "token",
|
||||
secret: "secret",
|
||||
runtime: createRuntimeEnv(),
|
||||
abortSignal: abort.signal,
|
||||
}),
|
||||
);
|
||||
|
||||
// Allow async internals (probeLineBot await) to flush
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
|
||||
expect(monitorLineProvider).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channelAccessToken: "token",
|
||||
@@ -119,5 +125,54 @@ describe("linePlugin gateway.startAccount", () => {
|
||||
accountId: "default",
|
||||
}),
|
||||
);
|
||||
|
||||
abort.abort();
|
||||
await task;
|
||||
});
|
||||
|
||||
it("stays pending until abort signal fires (no premature exit)", async () => {
|
||||
const { runtime, monitorLineProvider } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
const abort = new AbortController();
|
||||
let resolved = false;
|
||||
|
||||
const task = linePlugin.gateway!.startAccount!(
|
||||
createStartAccountCtx({
|
||||
token: "token",
|
||||
secret: "secret",
|
||||
runtime: createRuntimeEnv(),
|
||||
abortSignal: abort.signal,
|
||||
}),
|
||||
).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
// Allow async internals to flush
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
expect(monitorLineProvider).toHaveBeenCalled();
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
abort.abort();
|
||||
await task;
|
||||
expect(resolved).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves immediately when abortSignal is already aborted", async () => {
|
||||
const { runtime } = createRuntime();
|
||||
setLineRuntime(runtime);
|
||||
|
||||
const abort = new AbortController();
|
||||
abort.abort();
|
||||
|
||||
await linePlugin.gateway!.startAccount!(
|
||||
createStartAccountCtx({
|
||||
token: "token",
|
||||
secret: "secret",
|
||||
runtime: createRuntimeEnv(),
|
||||
abortSignal: abort.signal,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -651,7 +651,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
||||
|
||||
ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`);
|
||||
|
||||
return getLineRuntime().channel.line.monitorLineProvider({
|
||||
const monitor = await getLineRuntime().channel.line.monitorLineProvider({
|
||||
channelAccessToken: token,
|
||||
channelSecret: secret,
|
||||
accountId: account.accountId,
|
||||
@@ -660,6 +660,20 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
|
||||
abortSignal: ctx.abortSignal,
|
||||
webhookPath: account.config.webhookPath,
|
||||
});
|
||||
|
||||
// Keep the provider alive until the abort signal fires. Without this,
|
||||
// the startAccount promise resolves immediately after webhook registration
|
||||
// and the channel supervisor treats the provider as "exited", triggering an
|
||||
// auto-restart loop (up to 10 attempts).
|
||||
await new Promise<void>((resolve) => {
|
||||
if (ctx.abortSignal.aborted) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
||||
});
|
||||
|
||||
return monitor;
|
||||
},
|
||||
logoutAccount: async ({ accountId, cfg }) => {
|
||||
const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ?? "";
|
||||
|
||||
Reference in New Issue
Block a user