mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 22:11:13 +00:00
fix(msteams): bound probe token acquisition to request deadline (#106386)
* fix(msteams): bound probe token acquisition to request deadline probeMSTeams() at extensions/msteams/src/probe.ts:75 and :89 awaited tokenProvider.getAccessToken(...) for the Bot Framework and Microsoft Graph token endpoints with no surrounding deadline. The Microsoft Teams SDK does not carry an inherent timeout on these calls, so a stalled Azure AD token endpoint pinned the probe indefinitely. Wrap both awaits with withMSTeamsRequestDeadline (default MSTEAMS_REQUEST_TIMEOUT_MS = 30_000), matching the pattern already used by six other MS Teams call sites: attachments/bot-framework.ts:252, attachments/graph.ts:258, monitor-handler/message-handler.ts:594/654/685/692, attachments/download.ts:167, team-identity.ts:37. The probe was the one missing site. No new helper, no SDK change. The existing outer catch at probe.ts:138 and inner catch at probe.ts:110 convert the timeout into a ProbeMSTeamsResult with ok: false and a structured error field. Added probe.timeout.test.ts: real probeMSTeams() with vi.mock injected never-resolving getBotToken/getGraphToken; asserts the call returns within the 30s bound instead of hanging to the proof budget. * test(msteams): drive probe timeout test with vi.useFakeTimers The original probe.timeout.test.ts waited 90 seconds of wall-clock per focused run (3 stalled cases racing against a real setTimeout budget). Per ClawSweeper P2 (automation), this material deterministic CI cost can slow or time out test shards. Drive the withTimeout race (from @openclaw/fs-safe/dist/timing.js, uses setTimeout + clearTimeout) via vi.useFakeTimers() so each stalled case resolves in milliseconds. Add one new case that spies on withTimeout's timeoutMs argument to assert the production default deadline is exactly MSTEAMS_REQUEST_TIMEOUT_MS = 30_000, so the production contract is not silently weakened by the fake-timer change. Per-case wall-clock: 25ms / 3ms / 2ms / 1ms / 2ms (was: 30s / 30s / 30s / 2ms / n/a). Co-Authored-By: Claude <noreply@anthropic.com> * fix(msteams): bound remaining token acquisition * test(msteams): keep credential fixture unchanged --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
committed by
GitHub
parent
88263e34b4
commit
dc0285366e
64
extensions/msteams/src/graph.timeout.test.ts
Normal file
64
extensions/msteams/src/graph.timeout.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
// Msteams tests cover Graph token request deadlines.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sdkMock = vi.hoisted(() => ({
|
||||
acquire: vi.fn<(scope: string) => Promise<string>>(),
|
||||
}));
|
||||
|
||||
vi.mock("./sdk.js", () => ({
|
||||
createMSTeamsTokenProvider() {
|
||||
return {
|
||||
async getAccessToken(scope: string) {
|
||||
return await sdkMock.acquire(scope);
|
||||
},
|
||||
};
|
||||
},
|
||||
async loadMSTeamsSdkWithAuth() {
|
||||
return { app: {} };
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./token-response.js", () => ({
|
||||
readAccessToken(value: unknown) {
|
||||
return typeof value === "string" ? value : null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./token.js", () => ({
|
||||
async resolveDelegatedAccessToken() {
|
||||
return undefined;
|
||||
},
|
||||
resolveMSTeamsCredentials() {
|
||||
return {
|
||||
type: "secret",
|
||||
appId: "app-id",
|
||||
appPassword: "test-app-password",
|
||||
tenantId: "tenant-id",
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
import { resolveGraphToken } from "./graph.js";
|
||||
import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js";
|
||||
|
||||
describe("resolveGraphToken request deadline", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("bounds stalled SDK token acquisition", async () => {
|
||||
sdkMock.acquire.mockImplementation(() => new Promise<string>(() => {}));
|
||||
vi.useFakeTimers();
|
||||
|
||||
const result = resolveGraphToken({ channels: { msteams: {} } });
|
||||
const rejection = expect(result).rejects.toThrow(
|
||||
`MS Teams Graph token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(MSTEAMS_REQUEST_TIMEOUT_MS);
|
||||
|
||||
await rejection;
|
||||
expect(sdkMock.acquire).toHaveBeenCalledWith("https://graph.microsoft.com");
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MSTEAMS_REQUEST_TIMEOUT_MS,
|
||||
resolveMSTeamsRequestTimeoutMs,
|
||||
type MSTeamsRequestDeadline,
|
||||
withMSTeamsRequestDeadline,
|
||||
} from "./request-timeout.js";
|
||||
import { responseWithRelease } from "./response-with-release.js";
|
||||
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||
@@ -243,7 +244,10 @@ export async function resolveGraphToken(
|
||||
|
||||
const { app } = await loadMSTeamsSdkWithAuth(creds, resolveMSTeamsSdkCloudOptions(msteamsCfg));
|
||||
const tokenProvider = createMSTeamsTokenProvider(app);
|
||||
const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
|
||||
const graphTokenValue = await withMSTeamsRequestDeadline({
|
||||
label: "MS Teams Graph token",
|
||||
work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"),
|
||||
});
|
||||
const accessToken = readAccessToken(graphTokenValue);
|
||||
if (!accessToken) {
|
||||
throw new Error("MS Teams graph token unavailable");
|
||||
|
||||
90
extensions/msteams/src/probe.timeout.test.ts
Normal file
90
extensions/msteams/src/probe.timeout.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// Msteams tests cover probe token request deadlines.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MSTeamsConfig } from "../runtime-api.js";
|
||||
|
||||
const sdkState = vi.hoisted(() => ({
|
||||
stall: null as "bot" | "graph" | null,
|
||||
}));
|
||||
|
||||
vi.mock("@microsoft/teams.apps", () => ({
|
||||
App: class {
|
||||
tokenManager = {
|
||||
async getBotToken() {
|
||||
if (sdkState.stall === "bot") {
|
||||
return await new Promise<never>(() => {});
|
||||
}
|
||||
return { toString: () => "test-token" };
|
||||
},
|
||||
async getGraphToken() {
|
||||
if (sdkState.stall === "graph") {
|
||||
return await new Promise<never>(() => {});
|
||||
}
|
||||
return { toString: () => "test-token" };
|
||||
},
|
||||
};
|
||||
},
|
||||
ExpressAdapter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@microsoft/teams.api", () => ({
|
||||
Client: function Client() {},
|
||||
cloudFromName: () => ({
|
||||
botScope: "https://api.botframework.com/.default",
|
||||
graphScope: "https://graph.microsoft.com/.default",
|
||||
}),
|
||||
}));
|
||||
|
||||
import { probeMSTeams } from "./probe.js";
|
||||
import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js";
|
||||
|
||||
const cfg = {
|
||||
enabled: true,
|
||||
appId: "app-id",
|
||||
appPassword: "test-app-password",
|
||||
tenantId: "tenant-id",
|
||||
} as unknown as MSTeamsConfig;
|
||||
|
||||
describe("probeMSTeams request deadline", () => {
|
||||
beforeEach(() => {
|
||||
sdkState.stall = null;
|
||||
vi.stubEnv("MSTEAMS_APP_ID", "");
|
||||
vi.stubEnv("MSTEAMS_APP_PASSWORD", "");
|
||||
vi.stubEnv("MSTEAMS_TENANT_ID", "");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
stalled: "bot" as const,
|
||||
expected: {
|
||||
ok: false,
|
||||
appId: "app-id",
|
||||
error: `MS Teams Bot Framework probe token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
|
||||
},
|
||||
},
|
||||
{
|
||||
stalled: "graph" as const,
|
||||
expected: {
|
||||
ok: true,
|
||||
appId: "app-id",
|
||||
graph: {
|
||||
ok: false,
|
||||
error: `MS Teams Graph probe token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
|
||||
},
|
||||
},
|
||||
},
|
||||
])("bounds stalled $stalled token acquisition", async ({ stalled, expected }) => {
|
||||
sdkState.stall = stalled;
|
||||
vi.useFakeTimers();
|
||||
|
||||
const result = expect(probeMSTeams(cfg)).resolves.toEqual(expected);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.advanceTimersByTimeAsync(MSTEAMS_REQUEST_TIMEOUT_MS);
|
||||
|
||||
await result;
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../runtime-api.js";
|
||||
import { resolveMSTeamsSdkCloudOptions } from "./cloud.js";
|
||||
import { formatUnknownError } from "./errors.js";
|
||||
import { withMSTeamsRequestDeadline } from "./request-timeout.js";
|
||||
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||
import { readAccessToken } from "./token-response.js";
|
||||
import { loadDelegatedTokens, resolveMSTeamsCredentials } from "./token.js";
|
||||
@@ -72,7 +73,12 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
|
||||
try {
|
||||
const { app } = await loadMSTeamsSdkWithAuth(creds, resolveMSTeamsSdkCloudOptions(cfg));
|
||||
const tokenProvider = createMSTeamsTokenProvider(app);
|
||||
const botTokenValue = await tokenProvider.getAccessToken("https://api.botframework.com");
|
||||
// Token-manager calls can outlive the SDK HTTP timeout, so keep both probe
|
||||
// phases bounded by the shared Teams request deadline.
|
||||
const botTokenValue = await withMSTeamsRequestDeadline({
|
||||
label: "MS Teams Bot Framework probe token",
|
||||
work: () => tokenProvider.getAccessToken("https://api.botframework.com"),
|
||||
});
|
||||
if (!botTokenValue) {
|
||||
throw new Error("Failed to acquire bot token");
|
||||
}
|
||||
@@ -86,7 +92,10 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
|
||||
}
|
||||
| undefined;
|
||||
try {
|
||||
const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
|
||||
const graphTokenValue = await withMSTeamsRequestDeadline({
|
||||
label: "MS Teams Graph probe token",
|
||||
work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"),
|
||||
});
|
||||
const accessToken = readAccessToken(graphTokenValue);
|
||||
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
|
||||
graph = {
|
||||
|
||||
Reference in New Issue
Block a user