fix(feishu): honor abortSignal during app registration poll interval (#109909)

* fix(feishu): honor abortSignal during app registration poll interval

* test(feishu): prove poll abort through a real loopback server
This commit is contained in:
chengzhichao-xydt
2026-07-21 12:53:45 +08:00
committed by GitHub
parent a0f03b6f9f
commit 828845a08a
2 changed files with 45 additions and 5 deletions
@@ -242,6 +242,41 @@ describe("Feishu app registration", () => {
await expect(poll).resolves.toEqual({ status: "timeout" });
});
it("stops polling promptly when abortSignal fires during the poll interval", async () => {
let requestCount = 0;
await withRegistrationServer(
(_req, res) => {
requestCount += 1;
writeJson(res, { error: "authorization_pending" });
},
async ({ fetchImpl, lookupFn }) => {
const controller = new AbortController();
const started = Date.now();
const poll = pollAppRegistration({
deviceCode: "device-code",
interval: 30,
expireIn: 600,
abortSignal: controller.signal,
fetchImpl,
lookupFn,
});
// Let the first poll resolve and the loop enter its 30s interval sleep.
await new Promise<void>((resolve) => {
setTimeout(resolve, 200);
});
controller.abort();
await expect(poll).resolves.toEqual({ status: "timeout" });
expect(Date.now() - started).toBeLessThan(10_000);
expect(requestCount).toBe(1);
console.log(
`[feishu pollAppRegistration abort proof] interval=30s aborted_after=200ms elapsed=${Date.now() - started}ms requests=${requestCount} outcome=timeout`,
);
},
);
});
it("prints scan-to-create QR codes with compact terminal rendering", async () => {
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
+10 -5
View File
@@ -1,6 +1,6 @@
// Feishu plugin module implements app registration behavior.
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
import { sleep } from "openclaw/plugin-sdk/runtime-env";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
/**
* Feishu app registration via OAuth device-code flow.
*
@@ -257,7 +257,7 @@ export async function pollAppRegistration(params: {
);
} catch {
// Transient network error — keep polling.
await sleepRegistrationPollInterval(currentInterval);
await sleepRegistrationPollInterval(currentInterval, abortSignal);
continue;
}
@@ -303,7 +303,7 @@ export async function pollAppRegistration(params: {
}
}
await sleepRegistrationPollInterval(currentInterval);
await sleepRegistrationPollInterval(currentInterval, abortSignal);
}
return { status: "timeout" };
@@ -394,9 +394,14 @@ export async function getAppOwnerOpenId(params: {
}
}
function sleepRegistrationPollInterval(intervalSeconds: number): Promise<void> {
async function sleepRegistrationPollInterval(
intervalSeconds: number,
abortSignal?: AbortSignal,
): Promise<void> {
const intervalMs =
finiteSecondsToTimerSafeMilliseconds(intervalSeconds) ??
DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS * 1_000;
return sleep(intervalMs);
// Swallow the abort rejection: the poll loop's `aborted` check owns the exit
// path so PollOutcome stays "timeout" instead of an unhandled rejection.
await sleepWithAbort(intervalMs, abortSignal).catch(() => {});
}