

























@@ -4,17 +4,27 @@ import {
44createTestWizardPrompter,
55} from "openclaw/plugin-sdk/plugin-test-runtime";
66import type { OAuthCredential } from "openclaw/plugin-sdk/provider-auth";
7-import { afterEach, describe, expect, it, vi } from "vitest";
7+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8+9+const waitForLocalOAuthCallbackMock = vi.hoisted(() => vi.fn());
10+11+vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
12+waitForLocalOAuthCallback: waitForLocalOAuthCallbackMock,
13+}));
14+815import {
916buildXaiOAuthAuthorizationCodeTokenBody,
1017buildXaiOAuthAuthorizeUrl,
1118fetchXaiOAuthDiscovery,
1219isTrustedXaiOAuthEndpoint,
1320loginXaiDeviceCode,
21+loginXaiOAuth,
1422refreshXaiOAuthCredential,
1523XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST,
24+XAI_OAUTH_CALLBACK_HOST,
1625XAI_OAUTH_CALLBACK_PORT,
1726XAI_OAUTH_CLIENT_ID,
27+XAI_OAUTH_DISCOVERY_URL,
1828XAI_OAUTH_REDIRECT_URI,
1929XAI_OAUTH_SCOPE,
2030} from "./xai-oauth.js";
@@ -40,7 +50,42 @@ function requireStringBody(init: RequestInit | undefined): string {
4050return init.body;
4151}
425253+function requestUrl(input: RequestInfo | URL): string {
54+if (typeof input === "string") {
55+return input;
56+}
57+if (input instanceof URL) {
58+return input.href;
59+}
60+return input.url;
61+}
62+63+function stubSuccessfulXaiOAuthNetwork(): void {
64+const fetchImpl = vi.fn<typeof fetch>(async (url, init) => {
65+if (requestUrl(url) === XAI_OAUTH_DISCOVERY_URL) {
66+return jsonResponse({
67+authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
68+token_endpoint: "https://auth.x.ai/oauth2/token",
69+});
70+}
71+72+expect(requestUrl(url)).toBe("https://auth.x.ai/oauth2/token");
73+expect(init?.method).toBe("POST");
74+expect(requireStringBody(init)).toContain("code=AUTHCODE");
75+return jsonResponse({
76+access_token: "access-token",
77+refresh_token: "refresh-token",
78+expires_in: 3600,
79+});
80+});
81+vi.stubGlobal("fetch", fetchImpl);
82+}
83+4384describe("xAI OAuth", () => {
85+beforeEach(() => {
86+waitForLocalOAuthCallbackMock.mockReset();
87+});
88+4489afterEach(() => {
4590vi.unstubAllGlobals();
4691vi.unstubAllEnvs();
@@ -163,7 +208,85 @@ describe("xAI OAuth", () => {
163208expect(refreshed.access).toBe("access-2");
164209expect(refreshed.refresh).toBe("refresh-1");
165210expect(refreshed.expires).toBe(121_000);
166-vi.unstubAllEnvs();
211+});
212+213+it("prints the authorize URL through plain prompter output so terminal link detection keeps it whole", async () => {
214+waitForLocalOAuthCallbackMock.mockResolvedValue({ code: "AUTHCODE", state: "state-1" });
215+stubSuccessfulXaiOAuthNetwork();
216+217+const progress = { update: vi.fn(), stop: vi.fn() };
218+const note = vi.fn<(message: string, title?: string) => Promise<void>>(async () => undefined);
219+const plain = vi.fn<(message: string) => Promise<void>>(async () => undefined);
220+const openUrl = vi.fn<(url: string) => Promise<void>>(async () => undefined);
221+const runtimeLog = vi.fn<(message: string) => void>();
222+const ctx = {
223+config: {},
224+isRemote: true,
225+ openUrl,
226+prompter: {
227+ note,
228+ plain,
229+progress: vi.fn(() => progress),
230+},
231+runtime: {
232+log: runtimeLog,
233+error: vi.fn(),
234+exit: vi.fn(),
235+},
236+oauth: { createVpsAwareHandlers: vi.fn() },
237+} as unknown as ProviderAuthContext;
238+239+await loginXaiOAuth(ctx);
240+241+expect(openUrl).not.toHaveBeenCalled();
242+const noteMessage = note.mock.calls[0]?.[0] ?? "";
243+expect(noteMessage).toContain("Open this xAI OAuth URL in your browser:");
244+expect(noteMessage).toContain(
245+`ssh -N -L ${XAI_OAUTH_CALLBACK_PORT}:${XAI_OAUTH_CALLBACK_HOST}:${XAI_OAUTH_CALLBACK_PORT} <host>`,
246+);
247+expect(noteMessage).not.toContain("https://auth.x.ai/oauth2/authorize");
248+249+const plainOutput = plain.mock.calls[0]?.[0] ?? "";
250+expect(plainOutput.trim()).toMatch(/^https:\/\/auth\.x\.ai\/oauth2\/authorize\?/);
251+expect(plainOutput).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`);
252+expect(plainOutput).toContain("code_challenge=");
253+expect(runtimeLog).not.toHaveBeenCalled();
254+expect(progress.stop).toHaveBeenCalledWith("xAI OAuth complete");
255+});
256+257+it("keeps the authorize URL visible for prompters without plain output", async () => {
258+waitForLocalOAuthCallbackMock.mockResolvedValue({ code: "AUTHCODE", state: "state-1" });
259+stubSuccessfulXaiOAuthNetwork();
260+261+const progress = { update: vi.fn(), stop: vi.fn() };
262+const note = vi.fn<(message: string, title?: string) => Promise<void>>(async () => undefined);
263+const openUrl = vi.fn<(url: string) => Promise<void>>(async () => undefined);
264+const runtimeLog = vi.fn<(message: string) => void>();
265+const ctx = {
266+config: {},
267+isRemote: false,
268+ openUrl,
269+prompter: {
270+ note,
271+progress: vi.fn(() => progress),
272+},
273+runtime: {
274+log: runtimeLog,
275+error: vi.fn(),
276+exit: vi.fn(),
277+},
278+oauth: { createVpsAwareHandlers: vi.fn() },
279+} as unknown as ProviderAuthContext;
280+281+await loginXaiOAuth(ctx);
282+283+const authorizeUrl = openUrl.mock.calls[0]?.[0] ?? "";
284+const noteMessage = note.mock.calls[0]?.[0] ?? "";
285+expect(authorizeUrl).toContain("https://auth.x.ai/oauth2/authorize?");
286+expect(noteMessage).toContain("Open this xAI OAuth URL in your browser:");
287+expect(noteMessage).not.toContain(authorizeUrl);
288+expect(runtimeLog.mock.calls[0]?.[0] ?? "").toContain(authorizeUrl);
289+expect(progress.stop).toHaveBeenCalledWith("xAI OAuth complete");
167290});
168291169292it("logs in with xAI device code without a localhost callback", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。