


















@@ -1,4 +1,11 @@
1-import { describe, expect, it, vi } from "vitest";
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, describe, expect, it, vi } from "vitest";
5+import {
6+clearRuntimeAuthProfileStoreSnapshots,
7+ensureAuthProfileStore,
8+} from "../../src/agents/auth-profiles.js";
29import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js";
310411const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
@@ -12,6 +19,19 @@ vi.mock("./register.runtime.js", () => ({
12191320import plugin from "./index.js";
142122+const tempDirs: string[] = [];
23+24+afterEach(async () => {
25+clearRuntimeAuthProfileStoreSnapshots();
26+await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
27+});
28+29+async function createAgentDir() {
30+const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-github-copilot-test-"));
31+tempDirs.push(dir);
32+return dir;
33+}
34+1535function _registerProvider() {
1636return registerProviderWithPluginConfig({});
1737}
@@ -116,4 +136,234 @@ describe("github-copilot plugin", () => {
116136},
117137});
118138});
139+140+it("stores GitHub Copilot token from non-interactive onboarding", async () => {
141+const provider = registerProviderWithPluginConfig({});
142+const method = provider.auth[0];
143+const agentDir = await createAgentDir();
144+const runtime = { error: vi.fn(), exit: vi.fn() };
145+146+const result = await method.runNonInteractive({
147+authChoice: "github-copilot",
148+config: {},
149+baseConfig: {},
150+opts: { githubCopilotToken: "ghu_test\r\n123" },
151+ runtime,
152+ agentDir,
153+resolveApiKey: vi.fn(async () => ({
154+key: "ghu_test123",
155+source: "flag" as const,
156+})),
157+toApiKeyCredential: vi.fn(),
158+});
159+160+expect(runtime.error).not.toHaveBeenCalled();
161+expect(result?.auth?.profiles?.["github-copilot:github"]).toEqual({
162+provider: "github-copilot",
163+mode: "token",
164+});
165+expect(result?.agents?.defaults?.model).toEqual({
166+primary: "github-copilot/claude-opus-4.7",
167+});
168+expect(result?.agents?.defaults?.models?.["github-copilot/claude-opus-4.7"]).toEqual({});
169+170+const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
171+expect(profile).toEqual({
172+type: "token",
173+provider: "github-copilot",
174+token: "ghu_test123",
175+});
176+});
177+178+it("stores env-backed token refs for non-interactive onboarding ref mode", async () => {
179+const provider = registerProviderWithPluginConfig({});
180+const method = provider.auth[0];
181+const agentDir = await createAgentDir();
182+const runtime = { error: vi.fn(), exit: vi.fn() };
183+184+const result = await method.runNonInteractive({
185+authChoice: "github-copilot",
186+config: { agents: { defaults: { model: { fallbacks: ["openai/gpt-5.4"] } } } },
187+baseConfig: {},
188+opts: { secretInputMode: "ref" },
189+ runtime,
190+ agentDir,
191+resolveApiKey: vi.fn(async () => ({
192+key: "ghu_from_env",
193+source: "env" as const,
194+envVarName: "COPILOT_GITHUB_TOKEN",
195+})),
196+toApiKeyCredential: vi.fn(),
197+});
198+199+expect(runtime.error).not.toHaveBeenCalled();
200+expect(result?.agents?.defaults?.model).toEqual({
201+fallbacks: ["openai/gpt-5.4"],
202+primary: "github-copilot/claude-opus-4.7",
203+});
204+205+const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
206+expect(profile).toEqual({
207+type: "token",
208+provider: "github-copilot",
209+tokenRef: {
210+source: "env",
211+provider: "default",
212+id: "COPILOT_GITHUB_TOKEN",
213+},
214+});
215+});
216+217+it("falls back to GH_TOKEN during non-interactive onboarding", async () => {
218+const provider = registerProviderWithPluginConfig({});
219+const method = provider.auth[0];
220+const agentDir = await createAgentDir();
221+const runtime = { error: vi.fn(), exit: vi.fn() };
222+const resolveApiKey = vi.fn(async ({ envVar }: { envVar?: string }) =>
223+envVar === "GH_TOKEN"
224+ ? {
225+key: "ghu_from_gh_token",
226+source: "env" as const,
227+envVarName: "GH_TOKEN",
228+}
229+ : null,
230+);
231+232+const result = await method.runNonInteractive({
233+authChoice: "github-copilot",
234+config: {},
235+baseConfig: {},
236+opts: {},
237+ runtime,
238+ agentDir,
239+ resolveApiKey,
240+toApiKeyCredential: vi.fn(),
241+});
242+243+expect(runtime.error).not.toHaveBeenCalled();
244+expect(resolveApiKey).toHaveBeenCalledWith(
245+expect.objectContaining({ envVar: "COPILOT_GITHUB_TOKEN" }),
246+);
247+expect(resolveApiKey).toHaveBeenCalledWith(expect.objectContaining({ envVar: "GH_TOKEN" }));
248+expect(result?.auth?.profiles?.["github-copilot:github"]).toEqual({
249+provider: "github-copilot",
250+mode: "token",
251+});
252+253+const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
254+expect(profile).toEqual({
255+type: "token",
256+provider: "github-copilot",
257+token: "ghu_from_gh_token",
258+});
259+});
260+261+it("preserves an existing primary model during non-interactive onboarding", async () => {
262+const provider = registerProviderWithPluginConfig({});
263+const method = provider.auth[0];
264+const agentDir = await createAgentDir();
265+const runtime = { error: vi.fn(), exit: vi.fn() };
266+267+const result = await method.runNonInteractive({
268+authChoice: "github-copilot",
269+config: {
270+agents: {
271+defaults: {
272+model: {
273+primary: "github-copilot/gpt-5.4",
274+fallbacks: ["openai/gpt-5.4"],
275+},
276+models: {
277+"github-copilot/gpt-5.4": { label: "Existing" },
278+},
279+},
280+},
281+},
282+baseConfig: {},
283+opts: { githubCopilotToken: "ghu_test" },
284+ runtime,
285+ agentDir,
286+resolveApiKey: vi.fn(async () => ({
287+key: "ghu_test",
288+source: "flag" as const,
289+})),
290+toApiKeyCredential: vi.fn(),
291+});
292+293+expect(runtime.error).not.toHaveBeenCalled();
294+expect(result?.agents?.defaults?.model).toEqual({
295+primary: "github-copilot/gpt-5.4",
296+fallbacks: ["openai/gpt-5.4"],
297+});
298+expect(result?.agents?.defaults?.models).toEqual({
299+"github-copilot/gpt-5.4": { label: "Existing" },
300+});
301+});
302+303+it("reuses an existing token profile during non-interactive onboarding", async () => {
304+const provider = registerProviderWithPluginConfig({});
305+const method = provider.auth[0];
306+const agentDir = await createAgentDir();
307+const runtime = { error: vi.fn(), exit: vi.fn() };
308+await fs.writeFile(
309+path.join(agentDir, "auth-profiles.json"),
310+JSON.stringify({
311+version: 1,
312+profiles: {
313+"github-copilot:github": {
314+type: "token",
315+provider: "github-copilot",
316+token: "existing-token",
317+},
318+},
319+}),
320+);
321+322+const result = await method.runNonInteractive({
323+authChoice: "github-copilot",
324+config: {},
325+baseConfig: {},
326+opts: {},
327+ runtime,
328+ agentDir,
329+resolveApiKey: vi.fn(async () => null),
330+toApiKeyCredential: vi.fn(),
331+});
332+333+expect(runtime.error).not.toHaveBeenCalled();
334+expect(result?.auth?.profiles?.["github-copilot:github"]).toEqual({
335+provider: "github-copilot",
336+mode: "token",
337+});
338+});
339+340+it("does not emit a second missing-token error after ref-mode flag validation fails", async () => {
341+const provider = registerProviderWithPluginConfig({});
342+const method = provider.auth[0];
343+const agentDir = await createAgentDir();
344+const runtime = { error: vi.fn(), exit: vi.fn() };
345+346+const result = await method.runNonInteractive({
347+authChoice: "github-copilot",
348+config: {},
349+baseConfig: {},
350+opts: {
351+githubCopilotToken: "ghu_secret",
352+secretInputMode: "ref",
353+},
354+ runtime,
355+ agentDir,
356+resolveApiKey: vi.fn(async () => null),
357+toApiKeyCredential: vi.fn(),
358+});
359+360+expect(result).toBeNull();
361+expect(runtime.error).toHaveBeenCalledTimes(1);
362+expect(runtime.error).toHaveBeenCalledWith(
363+[
364+"--github-copilot-token cannot be used with --secret-input-mode ref unless COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN is set in env.",
365+"Set one of those env vars and omit --github-copilot-token, or use --secret-input-mode plaintext.",
366+].join("\n"),
367+);
368+});
119369});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。