fix: harden msteams monitor lifecycle edges (#24580) (thanks @chilu18)

This commit is contained in:
Peter Steinberger
2026-03-02 20:31:08 +00:00
parent 243a14fb95
commit b745bc2441
3 changed files with 37 additions and 13 deletions

View File

@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- MSTeams/gateway lifecycle: keep `startAccount` pending until shutdown/abort to stop false auto-restart loops, fail fast on startup bind errors, and add lifecycle regressions for abort + startup failure behavior. (#24580) Thanks @chilu18.
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.

View File

@@ -175,6 +175,24 @@ describe("monitorMSTeamsProvider lifecycle", () => {
);
});
it("returns a shutdown handle when abort signal is not provided", async () => {
const stores = createStores();
const result = await monitorMSTeamsProvider({
cfg: createConfig(0),
runtime: createRuntime(),
conversationStore: stores.conversationStore,
pollStore: stores.pollStore,
});
expect(result).toEqual(
expect.objectContaining({
shutdown: expect.any(Function),
}),
);
await expect(result.shutdown()).resolves.toBeUndefined();
});
it("rejects startup when webhook port is already in use", async () => {
expressControl.mode.value = "error";
await expect(

View File

@@ -277,7 +277,6 @@ export async function monitorMSTeamsProvider(
const httpServer = expressApp.listen(port);
await new Promise<void>((resolve, reject) => {
const onListening = () => {
httpServer.off("error", onError);
log.info(`msteams provider started on port ${port}`);
resolve();
};
@@ -306,24 +305,30 @@ export async function monitorMSTeamsProvider(
});
};
// Handle abort signal
const onAbort = () => {
void shutdown();
};
if (opts.abortSignal) {
if (opts.abortSignal.aborted) {
onAbort();
} else {
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
}
// Some direct callers may invoke monitor without lifecycle wiring.
// Return immediately so they can call shutdown explicitly.
if (!opts.abortSignal) {
return { app: expressApp, shutdown };
}
// Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit.
await new Promise<void>((resolve) => {
const closePromise = new Promise<void>((resolve) => {
httpServer.once("close", () => {
resolve();
});
});
// Handle abort signal
const onAbort = () => {
void shutdown();
};
if (opts.abortSignal.aborted) {
onAbort();
} else {
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
}
// Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit.
await closePromise;
opts.abortSignal?.removeEventListener("abort", onAbort);
return { app: expressApp, shutdown };