test(microsoft-foundry): remove global test bridge (#122881)

* test(microsoft-foundry): remove global test bridge

* test(microsoft-foundry): type boundary fixtures

* perf(ui): keep route transition out of startup
This commit is contained in:
Peter Steinberger
2026-08-12 18:53:44 -07:00
committed by GitHub
parent 3a0cb17739
commit fd0fc80c8c
11 changed files with 205 additions and 185 deletions
-6
View File
@@ -44,12 +44,6 @@ function shouldTestFoundryTextConnection(params: {
);
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.microsoftFoundryTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, { ...api, shouldTestFoundryTextConnection });
}
export const entraIdAuthMethod: ProviderAuthMethod = {
id: "entra-id",
label: "Entra ID (az login)",
+56 -99
View File
@@ -1,6 +1,7 @@
// Microsoft Foundry tests cover index plugin behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { ProviderAuthMethod } from "openclaw/plugin-sdk/core";
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { azLoginDeviceCodeWithOptions, getAccessTokenResultAsync } from "./cli.js";
@@ -22,19 +23,6 @@ import {
requiresFoundryEntraIdClaudeAuth,
usesFoundryResponsesByDefault,
} from "./shared.js";
import { microsoftFoundryTesting } from "./test-support.js";
const {
buildFoundryConnectionTest,
isAnthropicFoundryDeployment,
isValidTenantIdentifier,
resetFoundryRuntimeAuthCaches,
shouldTestFoundryTextConnection,
supportsFoundryImageInput,
supportsFoundryReasoningContent,
supportsFoundryReasoningEffort,
} = microsoftFoundryTesting;
const execFileMock = vi.hoisted(() => vi.fn());
const execFileSyncMock = vi.hoisted(() => vi.fn());
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
@@ -138,6 +126,8 @@ const defaultFoundryModelId = "gpt-5.4";
const defaultFoundryProfileId = "microsoft-foundry:entra";
const defaultFoundryAgentDir = "/tmp/test-agent";
const defaultAzureCliLoginError = "Please run 'az login' to setup account.";
let runtimeAuthTestSequence = 0;
let runtimeAuthTestTenantId = "tenant-0";
function buildFoundryModel(
overrides: Partial<{
@@ -224,7 +214,7 @@ function buildEntraProfileStore(
modelId: "custom-deployment",
modelName: defaultFoundryModelId,
api: "openai-responses",
tenantId: "tenant-id",
tenantId: runtimeAuthTestTenantId,
...overrides,
},
},
@@ -291,7 +281,8 @@ function mockAzureCliLoginFailure(delayMs?: number) {
describe("microsoft-foundry plugin", () => {
beforeEach(() => {
resetFoundryRuntimeAuthCaches();
runtimeAuthTestSequence += 1;
runtimeAuthTestTenantId = `tenant-${runtimeAuthTestSequence}`;
execFileMock.mockReset();
execFileSyncMock.mockReset();
ensureAuthProfileStoreMock.mockReset();
@@ -862,12 +853,6 @@ describe("microsoft-foundry plugin", () => {
);
});
it("accepts tenant domains as valid tenant identifiers", () => {
expect(isValidTenantIdentifier("contoso.onmicrosoft.com")).toBe(true);
expect(isValidTenantIdentifier("00000000-0000-0000-0000-000000000000")).toBe(true);
expect(isValidTenantIdentifier("not a tenant")).toBe(false);
});
it("defaults Azure OpenAI model families to the documented API surfaces", () => {
expect(usesFoundryResponsesByDefault("gpt-5.4")).toBe(true);
expect(usesFoundryResponsesByDefault("gpt-5.2-codex")).toBe(true);
@@ -879,16 +864,6 @@ describe("microsoft-foundry plugin", () => {
expect(requiresFoundryMaxCompletionTokens("gpt-5-chat")).toBe(true);
expect(requiresFoundryMaxCompletionTokens("o3")).toBe(true);
expect(requiresFoundryMaxCompletionTokens("gpt-4o")).toBe(false);
expect(supportsFoundryReasoningEffort("gpt-5.4")).toBe(true);
expect(supportsFoundryReasoningEffort("gpt-5-chat")).toBe(false);
expect(supportsFoundryReasoningEffort("gpt-5.1-chat")).toBe(true);
expect(supportsFoundryReasoningEffort("o3")).toBe(true);
expect(supportsFoundryReasoningEffort("o1-mini")).toBe(false);
expect(supportsFoundryReasoningEffort("MAI-DS-R1")).toBe(false);
expect(supportsFoundryReasoningContent("MAI-DS-R1")).toBe(true);
expect(supportsFoundryImageInput("gpt-5.4")).toBe(true);
expect(supportsFoundryImageInput("gpt-4o")).toBe(true);
expect(supportsFoundryImageInput("MAI-DS-R1")).toBe(false);
expect(isFoundryMaiImageModel("MAI-Image-2.5-Flash")).toBe(true);
expect(isFoundryMaiImageModel("MAI-Image-2e")).toBe(true);
expect(isFoundryMaiImageModel("MAI-DS-R1")).toBe(false);
@@ -940,19 +915,59 @@ describe("microsoft-foundry plugin", () => {
expect(requireFoundryProviderPatch(result).models[0]?.name).toBe("MAI-Image-2.5");
});
it("skips chat connection probes for MAI image deployments", () => {
it("skips chat connection probes for MAI image deployments", async () => {
execFileSyncMock.mockImplementation((_command, args) => {
const azArgs = args as string[];
if (azArgs[0] === "version") {
return "";
}
if (azArgs[0] === "account" && azArgs[1] === "show") {
return JSON.stringify({
name: "Foundry Account",
id: "account-id",
tenantId: "tenant-id",
user: { name: "operator@example.com" },
});
}
if (azArgs[0] === "account" && azArgs[1] === "list") {
return "[]";
}
throw new Error(`unexpected az command: ${azArgs.join(" ")}`);
});
const provider = registerProvider();
const authMethod = provider.auth.find((method: ProviderAuthMethod) => method.id === "entra-id");
if (!authMethod) {
throw new Error("expected Microsoft Foundry Entra auth method");
}
const text = vi
.fn()
.mockResolvedValueOnce("https://example.services.ai.azure.com")
.mockResolvedValueOnce("prod-image");
const select = vi
.fn()
.mockResolvedValueOnce("mai-image")
.mockResolvedValueOnce("MAI-Image-2.5");
const result = await authMethod.run({
config: {},
agentDir: defaultFoundryAgentDir,
prompter: {
confirm: vi.fn(async () => true),
note: vi.fn(async () => undefined),
text,
select,
},
} as never);
expect(
shouldTestFoundryTextConnection({
modelId: "prod-image",
modelNameHint: "MAI-Image-2.5",
execFileSyncMock.mock.calls.some((call) => {
const args = call[1];
return Array.isArray(args) && args[0] === "account" && args[1] === "get-access-token";
}),
).toBe(false);
expect(
shouldTestFoundryTextConnection({
modelId: "prod-chat",
modelNameHint: "gpt-5.4",
}),
).toBe(true);
expect(result.configPatch?.agents?.defaults?.mediaModels?.image).toEqual({
primary: "microsoft-foundry/prod-image",
});
});
it("classifies custom API-key MAI image deployments during manual setup", async () => {
@@ -1273,19 +1288,6 @@ describe("microsoft-foundry plugin", () => {
expect(Object.hasOwn(provider, "headers")).toBe(true);
});
it("uses the minimum supported response token count for GPT-5 connection tests", () => {
const testRequest = buildFoundryConnectionTest({
endpoint: "https://example.services.ai.azure.com",
modelId: "gpt-5.4",
modelNameHint: "gpt-5.4",
api: "openai-responses",
});
expect(testRequest.url).toContain("/responses");
expect(testRequest.body.model).toBe("gpt-5.4");
expect(testRequest.body.max_output_tokens).toBe(16);
});
it("marks Foundry responses models to omit explicit store=false payloads", () => {
const result = buildFoundryAuthResult({
profileId: "microsoft-foundry:entra",
@@ -1855,36 +1857,6 @@ describe("microsoft-foundry plugin", () => {
).toBe("https://example.services.ai.azure.com");
});
it("includes api-version for non GPT-5 chat completion connection tests", () => {
const testRequest = buildFoundryConnectionTest({
endpoint: "https://example.services.ai.azure.com",
modelId: "FW-GLM-5",
modelNameHint: "FW-GLM-5",
api: "openai-completions",
});
expect(testRequest.url).toContain("/chat/completions");
expect(testRequest.body.model).toBe("FW-GLM-5");
expect(testRequest.body.max_tokens).toBe(1);
});
it("builds Anthropic Messages connection tests for Claude deployments", () => {
const testRequest = buildFoundryConnectionTest({
endpoint: "https://example.services.ai.azure.com/openai/v1",
modelId: "prod-fable",
modelNameHint: "claude-fable-5",
api: "anthropic-messages",
});
expect(testRequest.url).toBe("https://example.services.ai.azure.com/anthropic/v1/messages");
expect(testRequest.body).toEqual({
model: "prod-fable",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
thinking: { type: "adaptive" },
});
});
it("returns actionable Azure CLI login errors", async () => {
mockAzureCliLoginFailure();
@@ -2088,21 +2060,6 @@ describe("selectFoundryDeployment", () => {
});
});
describe("isAnthropicFoundryDeployment", () => {
it.each(["claude-opus-4-6", "Claude-Sonnet-4", "claude-3.5-haiku", "CLAUDE-instant"])(
"detects Anthropic model: %s",
(name) => {
expect(isAnthropicFoundryDeployment(name)).toBe(true);
},
);
it.each(["gpt-5.4", "o4-mini", "phi-4", "llama-3", undefined, null, ""])(
"rejects non-Anthropic model: %s",
(name) => {
expect(isAnthropicFoundryDeployment(name)).toBe(false);
},
);
});
describe("azLoginDeviceCodeWithOptions utf-8 chunk boundary", () => {
afterEach(() => {
vi.restoreAllMocks();
@@ -1,8 +1,13 @@
// Microsoft Foundry tests cover bounded connection-test error reads.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as cli from "./cli.js";
import { testFoundryConnection } from "./onboard.js";
import { DEFAULT_API } from "./shared.js";
import { promptTenantId, testFoundryConnection } from "./onboard.js";
import {
ANTHROPIC_MESSAGES_API,
DEFAULT_API,
DEFAULT_GPT5_API,
type FoundryProviderApi,
} from "./shared.js";
const hoisted = vi.hoisted(() => ({
fetchWithSsrFGuard: vi.fn(),
@@ -12,6 +17,59 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: hoisted.fetchWithSsrFGuard,
}));
type FoundryConnectionRequestCase = {
name: string;
endpoint: string;
modelId: string;
modelNameHint: string;
api: FoundryProviderApi;
expectedUrl: string;
expectedBody: Record<string, unknown>;
expectedHeaders: Record<string, string>;
};
const foundryConnectionRequestCases: FoundryConnectionRequestCase[] = [
{
name: "Responses",
endpoint: "https://example.services.ai.azure.com",
modelId: "gpt-5.4",
modelNameHint: "gpt-5.4",
api: DEFAULT_GPT5_API,
expectedUrl: "https://example.services.ai.azure.com/openai/v1/responses",
expectedBody: { model: "gpt-5.4", input: "hi", max_output_tokens: 16 },
expectedHeaders: {},
},
{
name: "Chat Completions",
endpoint: "https://example.services.ai.azure.com",
modelId: "FW-GLM-5",
modelNameHint: "FW-GLM-5",
api: DEFAULT_API,
expectedUrl: "https://example.services.ai.azure.com/openai/v1/chat/completions",
expectedBody: {
model: "FW-GLM-5",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
},
expectedHeaders: {},
},
{
name: "Anthropic Messages",
endpoint: "https://example.services.ai.azure.com/openai/v1",
modelId: "prod-fable",
modelNameHint: "claude-fable-5",
api: ANTHROPIC_MESSAGES_API,
expectedUrl: "https://example.services.ai.azure.com/anthropic/v1/messages",
expectedBody: {
model: "prod-fable",
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
thinking: { type: "adaptive" },
},
expectedHeaders: { "anthropic-version": "2023-06-01" },
},
];
function cancelTrackedResponse(
text: string,
init: ResponseInit,
@@ -44,6 +102,39 @@ describe("testFoundryConnection", () => {
hoisted.fetchWithSsrFGuard.mockReset();
});
it.each(foundryConnectionRequestCases)(
"sends the $name connection request through the guarded transport",
async (testCase) => {
const release = vi.fn(async () => undefined);
hoisted.fetchWithSsrFGuard.mockResolvedValue({
response: new Response(null, { status: 200 }),
release,
});
await testFoundryConnection({
ctx: { prompter: { note: vi.fn() } } as never,
endpoint: testCase.endpoint,
modelId: testCase.modelId,
modelNameHint: testCase.modelNameHint,
api: testCase.api,
});
const request = hoisted.fetchWithSsrFGuard.mock.calls[0]?.[0];
expect(request?.url).toBe(testCase.expectedUrl);
expect(request?.timeoutMs).toBe(15_000);
expect(request?.init?.method).toBe("POST");
expect(request?.init?.body).toBe(JSON.stringify(testCase.expectedBody));
expect(new Headers(request?.init?.headers)).toEqual(
new Headers({
Authorization: "Bearer token",
"Content-Type": "application/json",
...testCase.expectedHeaders,
}),
);
expect(release).toHaveBeenCalledTimes(1);
},
);
it("bounds connection-test error bodies without using response.text()", async () => {
const note = vi.fn();
const tracked = cancelTrackedResponse(`${"foundry failure ".repeat(1024)}tail`, {
@@ -107,3 +198,20 @@ describe("testFoundryConnection", () => {
},
);
});
describe("promptTenantId", () => {
it("validates tenant domains and UUIDs through the prompt boundary", async () => {
const text = vi.fn(async (options: { validate?: (value: string) => string | undefined }) => {
expect(options.validate?.("contoso.onmicrosoft.com")).toBeUndefined();
expect(options.validate?.("00000000-0000-0000-0000-000000000000")).toBeUndefined();
expect(options.validate?.("not a tenant")).toBe("Enter a valid tenant ID or tenant domain");
expect(options.validate?.("")).toBe("Tenant ID is required");
return "contoso.onmicrosoft.com";
});
await expect(promptTenantId({ prompter: { text } } as never, { required: true })).resolves.toBe(
"contoso.onmicrosoft.com",
);
expect(text).toHaveBeenCalledTimes(1);
});
});
-6
View File
@@ -503,12 +503,6 @@ function isValidTenantIdentifier(value: string): boolean {
return isTenantUuid || isTenantDomain;
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.microsoftFoundryTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, { ...api, buildFoundryConnectionTest, isValidTenantIdentifier });
}
export async function promptTenantId(
ctx: ProviderAuthContext,
params?: {
-11
View File
@@ -28,17 +28,6 @@ const cachedTokens = new Map<string, CachedTokenEntry>();
const refreshPromises = new Map<string, Promise<{ apiKey: string; expiresAt: number }>>();
const FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS = 55 * 60 * 1000;
function resetFoundryRuntimeAuthCaches(): void {
cachedTokens.clear();
refreshPromises.clear();
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.microsoftFoundryTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, { ...api, resetFoundryRuntimeAuthCaches });
}
async function refreshEntraToken(params?: {
scope?: string;
subscriptionId?: string;
-12
View File
@@ -295,18 +295,6 @@ function supportsFoundryReasoningEffort(value?: string | null): boolean {
);
}
if (process.env.VITEST === "true") {
const key = Symbol.for("openclaw.microsoftFoundryTestApi");
const api = (Reflect.get(globalThis, key) as Record<string, unknown> | undefined) ?? {};
Reflect.set(globalThis, key, {
...api,
isAnthropicFoundryDeployment,
supportsFoundryImageInput,
supportsFoundryReasoningContent,
supportsFoundryReasoningEffort,
});
}
function resolveFoundryReasoningEfforts(value?: string | null): string[] | undefined {
const normalized = normalizeFoundryModelName(value);
if (!normalized || !supportsFoundryReasoningEffort(normalized)) {
@@ -1,27 +0,0 @@
import type { FoundryProviderApi } from "./shared.js";
type MicrosoftFoundryTestApi = {
buildFoundryConnectionTest: (params: {
endpoint: string;
modelId: string;
modelNameHint?: string | null;
api: FoundryProviderApi;
}) => { url: string; body: Record<string, unknown> };
isAnthropicFoundryDeployment: (value?: string | null) => boolean;
isValidTenantIdentifier: (value: string) => boolean;
resetFoundryRuntimeAuthCaches: () => void;
shouldTestFoundryTextConnection: (params: {
modelId: string;
modelNameHint?: string | null;
}) => boolean;
supportsFoundryImageInput: (value?: string | null) => boolean;
supportsFoundryReasoningContent: (value?: string | null) => boolean;
supportsFoundryReasoningEffort: (value?: string | null) => boolean;
};
const api = Reflect.get(globalThis, Symbol.for("openclaw.microsoftFoundryTestApi"));
if (!api) {
throw new Error("Microsoft Foundry test API is unavailable");
}
export const microsoftFoundryTesting = api as MicrosoftFoundryTestApi;
+4 -13
View File
@@ -44,7 +44,6 @@ import { createNativeChatDrafts } from "./native-bridge.ts";
import { startNativeLinkRouting } from "./native-link-routing.ts";
import { createNativeNotificationsCapability } from "./native-notifications.ts";
import { createApplicationOverlays } from "./overlays.ts";
import { navigateWithRouteTransition } from "./route-transition.ts";
import {
loadSettings,
patchSettings,
@@ -457,19 +456,11 @@ export function bootstrapApplication(
if (!routerStarted) {
pendingRouterStartNavigation = { routeId, location, mode: "push" };
}
// New-session submission awaits this promise so its live progress remains
// visible until the destination route has completed the UI handoff.
return navigateWithRouteTransition({
document,
from: router.getState().matches[0]?.routeId,
to: routeId,
prefersReducedMotion:
globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false,
prepare: () => router.preloadLocation(location, context),
navigate: () => router.navigate(routeId, context, { history: "push" }, location),
}).catch((error: unknown) => {
const navigationPromise = router.navigate(routeId, context, { history: "push" }, location);
void navigationPromise.catch((error: unknown) => {
console.error("[openclaw] route navigation failed", error);
});
return navigationPromise;
};
const context: ApplicationContext<RouteId> = {
basePath,
@@ -508,7 +499,7 @@ export function bootstrapApplication(
});
},
revalidate: (routeId) => router.revalidate(context, routeId),
preload: (routeId) => router.preloadRoute(routeId, context),
preload: (routeId, options) => router.preloadLocation(routeLocation(routeId, options), context),
};
return {
context,
+1 -1
View File
@@ -123,7 +123,7 @@ export type ApplicationContext<TRouteId extends string = string> = {
) => Promise<void>;
readonly replace: (routeId: TRouteId, options?: ApplicationNavigationOptions) => void;
readonly revalidate: (routeId?: TRouteId) => Promise<void>;
readonly preload: (routeId: TRouteId) => Promise<void>;
readonly preload: (routeId: TRouteId, options?: ApplicationNavigationOptions) => Promise<void>;
};
export const applicationContext =
@@ -1,6 +1,7 @@
import type { ReactiveController, ReactiveControllerHost } from "lit";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ApplicationContext } from "../../app/context.ts";
import { CHAT_ROUTE_READY_EVENT } from "../../app/route-transition.ts";
import { buildDraftSessionCreateParams } from "./create-params.ts";
import { DraftGatewayState } from "./draft-gateway-state.ts";
import { DraftPlaceBrowser } from "./draft-place-browser.ts";
@@ -32,12 +33,15 @@ describe("DraftSubmissionFlow", () => {
);
let finishNavigation!: () => void;
const navigateAndWait = vi.fn(
() =>
(_routeId: string, _options?: Parameters<ApplicationContext["navigateAndWait"]>[1]) =>
new Promise<void>((resolve) => {
finishNavigation = resolve;
}),
);
const preload = vi.fn(async () => undefined);
const preload = vi.fn(
async (_routeId: string, _options?: Parameters<ApplicationContext["preload"]>[1]) =>
undefined,
);
const setSessionKey = vi.fn();
const selectAgent = vi.fn();
const client = {
@@ -198,7 +202,12 @@ describe("DraftSubmissionFlow", () => {
await vi.waitFor(() => expect(navigateAndWait).toHaveBeenCalledOnce());
expect(flow.submitting).toBe(true);
expect(preload).toHaveBeenCalledWith("chat", navigateAndWait.mock.calls[0]?.[1]);
expect(preload.mock.invocationCallOrder[0]).toBeLessThan(
navigateAndWait.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
);
finishNavigation();
document.dispatchEvent(new Event(CHAT_ROUTE_READY_EVENT));
await submission;
expect(start).toHaveBeenCalledOnce();
@@ -213,6 +222,6 @@ describe("DraftSubmissionFlow", () => {
expect(createResult).toHaveBeenCalledOnce();
expect(setSessionKey).toHaveBeenCalledWith(start.mock.calls[0]?.[0].recovery.sessionKey);
expect(selectAgent).toHaveBeenCalledWith("cloud");
expect(preload).not.toHaveBeenCalled();
expect(preload).toHaveBeenCalledOnce();
});
});
@@ -1,6 +1,7 @@
import type { SessionsCatalogStartTerminalResult } from "../../../../packages/gateway-protocol/src/index.js";
import { selectApplicationSession } from "../../app/agent-selection.ts";
import type { ApplicationContext } from "../../app/context.ts";
import type { ApplicationContext, ApplicationNavigationOptions } from "../../app/context.ts";
import { navigateWithRouteTransition } from "../../app/route-transition.ts";
import { t } from "../../i18n/index.ts";
import {
readSessionMethodAccess,
@@ -364,6 +365,22 @@ export class DraftSubmissionFlow {
this.callbacks.requestUpdate();
}
private navigateToStartedSession(
context: ApplicationContext,
options: ApplicationNavigationOptions,
): Promise<void> {
// Keep transition code on the lazy new-session path instead of the startup bundle.
return navigateWithRouteTransition({
document,
from: "new-session",
to: "chat",
prefersReducedMotion:
globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false,
prepare: () => context.preload("chat", options),
navigate: () => context.navigateAndWait("chat", options),
}).catch(() => undefined);
}
async submit() {
const context = this.read().context;
if (!context || !this.canSubmit()) {
@@ -528,8 +545,8 @@ export class DraftSubmissionFlow {
sessionKey: result.key,
agentId: submissionAgentId,
});
await context.navigateAndWait(
"chat",
await this.navigateToStartedSession(
context,
sessionNavigationTarget({
context,
face: "chat",
@@ -572,8 +589,8 @@ export class DraftSubmissionFlow {
sessionKey: result.key,
agentId: submissionAgentId,
});
await context.navigateAndWait(
"chat",
await this.navigateToStartedSession(
context,
sessionNavigationTarget({
context,
face: "chat",