Skip to content

Commit dc02853

Browse files
wangmiao0668000666claudesteipete
authored
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 <[email protected]> * fix(msteams): bound remaining token acquisition * test(msteams): keep credential fixture unchanged --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent 88263e3 commit dc02853

4 files changed

Lines changed: 170 additions & 3 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Msteams tests cover Graph token request deadlines.
2+
import { afterEach, describe, expect, it, vi } from "vitest";
3+
4+
const sdkMock = vi.hoisted(() => ({
5+
acquire: vi.fn<(scope: string) => Promise<string>>(),
6+
}));
7+
8+
vi.mock("./sdk.js", () => ({
9+
createMSTeamsTokenProvider() {
10+
return {
11+
async getAccessToken(scope: string) {
12+
return await sdkMock.acquire(scope);
13+
},
14+
};
15+
},
16+
async loadMSTeamsSdkWithAuth() {
17+
return { app: {} };
18+
},
19+
}));
20+
21+
vi.mock("./token-response.js", () => ({
22+
readAccessToken(value: unknown) {
23+
return typeof value === "string" ? value : null;
24+
},
25+
}));
26+
27+
vi.mock("./token.js", () => ({
28+
async resolveDelegatedAccessToken() {
29+
return undefined;
30+
},
31+
resolveMSTeamsCredentials() {
32+
return {
33+
type: "secret",
34+
appId: "app-id",
35+
appPassword: "test-app-password",
36+
tenantId: "tenant-id",
37+
};
38+
},
39+
}));
40+
41+
import { resolveGraphToken } from "./graph.js";
42+
import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js";
43+
44+
describe("resolveGraphToken request deadline", () => {
45+
afterEach(() => {
46+
vi.clearAllMocks();
47+
vi.useRealTimers();
48+
});
49+
50+
it("bounds stalled SDK token acquisition", async () => {
51+
sdkMock.acquire.mockImplementation(() => new Promise<string>(() => {}));
52+
vi.useFakeTimers();
53+
54+
const result = resolveGraphToken({ channels: { msteams: {} } });
55+
const rejection = expect(result).rejects.toThrow(
56+
`MS Teams Graph token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
57+
);
58+
await vi.advanceTimersByTimeAsync(0);
59+
await vi.advanceTimersByTimeAsync(MSTEAMS_REQUEST_TIMEOUT_MS);
60+
61+
await rejection;
62+
expect(sdkMock.acquire).toHaveBeenCalledWith("https://graph.microsoft.com");
63+
});
64+
});

extensions/msteams/src/graph.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
MSTEAMS_REQUEST_TIMEOUT_MS,
99
resolveMSTeamsRequestTimeoutMs,
1010
type MSTeamsRequestDeadline,
11+
withMSTeamsRequestDeadline,
1112
} from "./request-timeout.js";
1213
import { responseWithRelease } from "./response-with-release.js";
1314
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
@@ -243,7 +244,10 @@ export async function resolveGraphToken(
243244

244245
const { app } = await loadMSTeamsSdkWithAuth(creds, resolveMSTeamsSdkCloudOptions(msteamsCfg));
245246
const tokenProvider = createMSTeamsTokenProvider(app);
246-
const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
247+
const graphTokenValue = await withMSTeamsRequestDeadline({
248+
label: "MS Teams Graph token",
249+
work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"),
250+
});
247251
const accessToken = readAccessToken(graphTokenValue);
248252
if (!accessToken) {
249253
throw new Error("MS Teams graph token unavailable");
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Msteams tests cover probe token request deadlines.
2+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+
import type { MSTeamsConfig } from "../runtime-api.js";
4+
5+
const sdkState = vi.hoisted(() => ({
6+
stall: null as "bot" | "graph" | null,
7+
}));
8+
9+
vi.mock("@microsoft/teams.apps", () => ({
10+
App: class {
11+
tokenManager = {
12+
async getBotToken() {
13+
if (sdkState.stall === "bot") {
14+
return await new Promise<never>(() => {});
15+
}
16+
return { toString: () => "test-token" };
17+
},
18+
async getGraphToken() {
19+
if (sdkState.stall === "graph") {
20+
return await new Promise<never>(() => {});
21+
}
22+
return { toString: () => "test-token" };
23+
},
24+
};
25+
},
26+
ExpressAdapter: vi.fn(),
27+
}));
28+
29+
vi.mock("@microsoft/teams.api", () => ({
30+
Client: function Client() {},
31+
cloudFromName: () => ({
32+
botScope: "https://api.botframework.com/.default",
33+
graphScope: "https://graph.microsoft.com/.default",
34+
}),
35+
}));
36+
37+
import { probeMSTeams } from "./probe.js";
38+
import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js";
39+
40+
const cfg = {
41+
enabled: true,
42+
appId: "app-id",
43+
appPassword: "test-app-password",
44+
tenantId: "tenant-id",
45+
} as unknown as MSTeamsConfig;
46+
47+
describe("probeMSTeams request deadline", () => {
48+
beforeEach(() => {
49+
sdkState.stall = null;
50+
vi.stubEnv("MSTEAMS_APP_ID", "");
51+
vi.stubEnv("MSTEAMS_APP_PASSWORD", "");
52+
vi.stubEnv("MSTEAMS_TENANT_ID", "");
53+
});
54+
55+
afterEach(() => {
56+
vi.unstubAllEnvs();
57+
vi.useRealTimers();
58+
});
59+
60+
it.each([
61+
{
62+
stalled: "bot" as const,
63+
expected: {
64+
ok: false,
65+
appId: "app-id",
66+
error: `MS Teams Bot Framework probe token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
67+
},
68+
},
69+
{
70+
stalled: "graph" as const,
71+
expected: {
72+
ok: true,
73+
appId: "app-id",
74+
graph: {
75+
ok: false,
76+
error: `MS Teams Graph probe token timed out after ${MSTEAMS_REQUEST_TIMEOUT_MS}ms`,
77+
},
78+
},
79+
},
80+
])("bounds stalled $stalled token acquisition", async ({ stalled, expected }) => {
81+
sdkState.stall = stalled;
82+
vi.useFakeTimers();
83+
84+
const result = expect(probeMSTeams(cfg)).resolves.toEqual(expected);
85+
await vi.advanceTimersByTimeAsync(0);
86+
await vi.advanceTimersByTimeAsync(MSTEAMS_REQUEST_TIMEOUT_MS);
87+
88+
await result;
89+
});
90+
});

extensions/msteams/src/probe.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "../runtime-api.js";
88
import { resolveMSTeamsSdkCloudOptions } from "./cloud.js";
99
import { formatUnknownError } from "./errors.js";
10+
import { withMSTeamsRequestDeadline } from "./request-timeout.js";
1011
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
1112
import { readAccessToken } from "./token-response.js";
1213
import { loadDelegatedTokens, resolveMSTeamsCredentials } from "./token.js";
@@ -72,7 +73,12 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
7273
try {
7374
const { app } = await loadMSTeamsSdkWithAuth(creds, resolveMSTeamsSdkCloudOptions(cfg));
7475
const tokenProvider = createMSTeamsTokenProvider(app);
75-
const botTokenValue = await tokenProvider.getAccessToken("https://api.botframework.com");
76+
// Token-manager calls can outlive the SDK HTTP timeout, so keep both probe
77+
// phases bounded by the shared Teams request deadline.
78+
const botTokenValue = await withMSTeamsRequestDeadline({
79+
label: "MS Teams Bot Framework probe token",
80+
work: () => tokenProvider.getAccessToken("https://api.botframework.com"),
81+
});
7682
if (!botTokenValue) {
7783
throw new Error("Failed to acquire bot token");
7884
}
@@ -86,7 +92,10 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
8692
}
8793
| undefined;
8894
try {
89-
const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
95+
const graphTokenValue = await withMSTeamsRequestDeadline({
96+
label: "MS Teams Graph probe token",
97+
work: () => tokenProvider.getAccessToken("https://graph.microsoft.com"),
98+
});
9099
const accessToken = readAccessToken(graphTokenValue);
91100
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
92101
graph = {

0 commit comments

Comments
 (0)