























@@ -1,3 +1,6 @@
1+import type { ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
2+import type { OAuthCredential } from "openclaw/plugin-sdk/provider-auth";
3+import { createRuntimeEnv, createTestWizardPrompter } from "openclaw/plugin-sdk/testing";
14import { afterEach, describe, expect, it, vi } from "vitest";
25import {
36buildXaiOAuthAuthorizationCodeTokenBody,
@@ -97,34 +100,29 @@ describe("xAI OAuth", () => {
9710098101it("validates discovered endpoints before using them", async () => {
99102vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
100-const fetchImpl = vi.fn(async () =>
103+const fetchImpl = vi.fn<typeof fetch>(async () =>
101104jsonResponse({
102105authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
103-device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
104106token_endpoint: "https://auth.x.ai/oauth2/token",
105107}),
106-) as unknown as typeof fetch;
108+);
107109108110await expect(fetchXaiOAuthDiscovery({ fetchImpl })).resolves.toEqual({
109111authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
110-deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
111112tokenEndpoint: "https://auth.x.ai/oauth2/token",
112113});
113114114-const discoveryInit = (fetchImpl as unknown as ReturnType<typeof vi.fn>).mock.calls.at(
115-0,
116-)?.[1] as RequestInit | undefined;
115+const discoveryInit = fetchImpl.mock.calls.at(0)?.[1];
117116const discoveryHeaders = new Headers(discoveryInit?.headers ?? {});
118117expect(discoveryHeaders.get("user-agent")).toBe("openclaw/2026.3.22");
119118vi.unstubAllEnvs();
120119121-const poisonedFetch = vi.fn(async () =>
120+const poisonedFetch = vi.fn<typeof fetch>(async () =>
122121jsonResponse({
123122authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
124-device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
125123token_endpoint: "https://evil.test/oauth2/token",
126124}),
127-) as unknown as typeof fetch;
125+);
128126129127await expect(fetchXaiOAuthDiscovery({ fetchImpl: poisonedFetch })).rejects.toThrow(
130128"untrusted token endpoint",
@@ -133,10 +131,10 @@ describe("xAI OAuth", () => {
133131134132it("refreshes with the cached token endpoint and preserves refresh fallback", async () => {
135133vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
136-const fetchImpl = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
134+const fetchImpl = vi.fn<typeof fetch>(async (_url, init) => {
137135expect(init?.method).toBe("POST");
138136expect(typeof init?.body).toBe("string");
139-const body = init?.body as string;
137+const body = requireStringBody(init);
140138expect(body).toContain("grant_type=refresh_token");
141139expect(body).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`);
142140expect(body).toContain("refresh_token=refresh-1");
@@ -146,19 +144,17 @@ describe("xAI OAuth", () => {
146144access_token: "access-2",
147145expires_in: 120,
148146});
149-}) as unknown as typeof fetch;
147+});
150148151-const refreshed = await refreshXaiOAuthCredential(
152-{
153-type: "oauth",
154-provider: "xai",
155-access: "access-1",
156-refresh: "refresh-1",
157-expires: 100,
158-tokenEndpoint: "https://auth.x.ai/oauth2/token",
159-} as unknown as Parameters<typeof refreshXaiOAuthCredential>[0],
160-{ fetchImpl, now: () => 1_000 },
161-);
149+const credential = {
150+type: "oauth",
151+provider: "xai",
152+access: "access-1",
153+refresh: "refresh-1",
154+expires: 100,
155+tokenEndpoint: "https://auth.x.ai/oauth2/token",
156+} satisfies OAuthCredential & { tokenEndpoint: string };
157+const refreshed = await refreshXaiOAuthCredential(credential, { fetchImpl, now: () => 1_000 });
162158163159expect(fetchImpl).toHaveBeenCalledWith("https://auth.x.ai/oauth2/token", expect.any(Object));
164160expect(refreshed.access).toBe("access-2");
@@ -205,28 +201,30 @@ describe("xAI OAuth", () => {
205201}),
206202);
207203vi.stubGlobal("fetch", fetchImpl);
208-const ctx = {
204+const note = vi.fn(async () => {});
205+const openUrl = vi.fn(async () => {});
206+const runtime = createRuntimeEnv();
207+const ctx: ProviderAuthContext = {
209208config: {},
210209isRemote: true,
211-openUrl: vi.fn(async () => {}),
212-prompter: {
210+ openUrl,
211+prompter: createTestWizardPrompter({
213212progress: vi.fn(() => progress),
214-note: vi.fn(async () => {}),
215-},
216-runtime: {
217-log: vi.fn(),
213+ note,
214+}),
215+ runtime,
216+oauth: {
217+createVpsAwareHandlers: () => {
218+throw new Error("unexpected VPS OAuth handler request");
219+},
218220},
219-oauth: {},
220221};
221222222-const result = await loginXaiDeviceCode(ctx as never);
223+const result = await loginXaiDeviceCode(ctx);
223224224-expect(ctx.openUrl).not.toHaveBeenCalled();
225-expect(ctx.prompter.note).toHaveBeenCalledWith(
226-expect.stringContaining("ABCD-1234"),
227-"xAI device code",
228-);
229-const remoteLog = ctx.runtime.log.mock.calls[0]?.[0];
225+expect(openUrl).not.toHaveBeenCalled();
226+expect(note).toHaveBeenCalledWith(expect.stringContaining("ABCD-1234"), "xAI device code");
227+const remoteLog = runtime.log.mock.calls[0]?.[0];
230228expect(remoteLog).toContain("https://accounts.x.ai/oauth2/device");
231229expect(remoteLog).not.toContain("ABCD-1234");
232230const deviceRequest = fetchImpl.mock.calls[1]?.[1];
@@ -243,8 +241,7 @@ describe("xAI OAuth", () => {
243241);
244242expect(tokenBody).toContain("device_code=device-code-1");
245243246-const credential = result.profiles[0]?.credential as Record<string, unknown> | undefined;
247-expect(credential).toMatchObject({
244+expect(result.profiles[0]?.credential).toMatchObject({
248245type: "oauth",
249246provider: "xai",
250247refresh: "refresh-1",
@@ -255,8 +252,8 @@ describe("xAI OAuth", () => {
255252issuer: "https://auth.x.ai",
256253authFlow: "device-code",
257254accountId: "acct-1",
255+access: expect.any(String),
258256});
259-expect(credential?.access).toEqual(expect.any(String));
260257expect(progress.update).toHaveBeenCalledWith("Waiting for xAI device authorization...");
261258expect(progress.stop).toHaveBeenCalledWith("xAI device code complete");
262259});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。