fix(onboard): address copilot review feedback

This commit is contained in:
Val Alexander
2026-04-26 22:38:02 -05:00
parent 92d7f0574c
commit aadac2c8d4
2 changed files with 170 additions and 15 deletions

View File

@@ -214,6 +214,92 @@ describe("github-copilot plugin", () => {
});
});
it("falls back to GH_TOKEN during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
const resolveApiKey = vi.fn(async ({ envVar }: { envVar?: string }) =>
envVar === "GH_TOKEN"
? {
key: "ghu_from_gh_token",
source: "env" as const,
envVarName: "GH_TOKEN",
}
: null,
);
const result = await method.runNonInteractive({
authChoice: "github-copilot",
config: {},
baseConfig: {},
opts: {},
runtime,
agentDir,
resolveApiKey,
toApiKeyCredential: vi.fn(),
});
expect(runtime.error).not.toHaveBeenCalled();
expect(resolveApiKey).toHaveBeenCalledWith(
expect.objectContaining({ envVar: "COPILOT_GITHUB_TOKEN" }),
);
expect(resolveApiKey).toHaveBeenCalledWith(expect.objectContaining({ envVar: "GH_TOKEN" }));
expect(result?.auth?.profiles?.["github-copilot:github"]).toEqual({
provider: "github-copilot",
mode: "token",
});
const profile = ensureAuthProfileStore(agentDir).profiles["github-copilot:github"];
expect(profile).toEqual({
type: "token",
provider: "github-copilot",
token: "ghu_from_gh_token",
});
});
it("preserves an existing primary model during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
const agentDir = await createAgentDir();
const runtime = { error: vi.fn(), exit: vi.fn() };
const result = await method.runNonInteractive({
authChoice: "github-copilot",
config: {
agents: {
defaults: {
model: {
primary: "github-copilot/gpt-5.4",
fallbacks: ["openai/gpt-5.4"],
},
models: {
"github-copilot/gpt-5.4": { label: "Existing" },
},
},
},
},
baseConfig: {},
opts: { githubCopilotToken: "ghu_test" },
runtime,
agentDir,
resolveApiKey: vi.fn(async () => ({
key: "ghu_test",
source: "flag" as const,
})),
toApiKeyCredential: vi.fn(),
});
expect(runtime.error).not.toHaveBeenCalled();
expect(result?.agents?.defaults?.model).toEqual({
primary: "github-copilot/gpt-5.4",
fallbacks: ["openai/gpt-5.4"],
});
expect(result?.agents?.defaults?.models).toEqual({
"github-copilot/gpt-5.4": { label: "Existing" },
});
});
it("reuses an existing token profile during non-interactive onboarding", async () => {
const provider = registerProviderWithPluginConfig({});
const method = provider.auth[0];
@@ -267,16 +353,17 @@ describe("github-copilot plugin", () => {
},
runtime,
agentDir,
resolveApiKey: vi.fn(async () => {
runtime.error("resolver error");
runtime.exit(1);
return null;
}),
resolveApiKey: vi.fn(async () => null),
toApiKeyCredential: vi.fn(),
});
expect(result).toBeNull();
expect(runtime.error).toHaveBeenCalledTimes(1);
expect(runtime.error).toHaveBeenCalledWith("resolver error");
expect(runtime.error).toHaveBeenCalledWith(
[
"--github-copilot-token cannot be used with --secret-input-mode ref unless COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN is set in env.",
"Set one of those env vars and omit --github-copilot-token, or use --secret-input-mode plaintext.",
].join("\n"),
);
});
});

View File

@@ -38,6 +38,15 @@ async function loadGithubCopilotRuntime() {
function applyCopilotDefaultModel(cfg: OpenClawConfig): OpenClawConfig {
const defaults = cfg.agents?.defaults;
const existingModel = defaults?.model;
const existingPrimary =
typeof existingModel === "string"
? existingModel.trim()
: typeof existingModel === "object" && typeof existingModel?.primary === "string"
? existingModel.primary.trim()
: "";
if (existingPrimary) {
return cfg;
}
const fallbacks =
typeof existingModel === "object" && existingModel !== null && "fallbacks" in existingModel
? (existingModel as { fallbacks?: string[] }).fallbacks
@@ -76,20 +85,79 @@ function resolveExistingCopilotTokenProfileId(agentDir?: string): string | undef
});
}
async function resolveCopilotNonInteractiveToken(
ctx: ProviderAuthMethodNonInteractiveContext,
flagValue: string | undefined,
) {
const resolveFromEnvChain = async () => {
for (const envVar of COPILOT_ENV_VARS) {
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagName: "--github-copilot-token",
envVar,
envVarName: envVar,
allowProfile: false,
required: false,
});
if (resolved) {
return resolved;
}
}
return null;
};
if (ctx.opts.secretInputMode === "ref") {
const resolved = await resolveFromEnvChain();
if (resolved) {
return resolved;
}
if (flagValue) {
ctx.runtime.error(
[
"--github-copilot-token cannot be used with --secret-input-mode ref unless COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN is set in env.",
"Set one of those env vars and omit --github-copilot-token, or use --secret-input-mode plaintext.",
].join("\n"),
);
ctx.runtime.exit(1);
}
return null;
}
const primary = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue,
flagName: "--github-copilot-token",
envVar: COPILOT_ENV_VARS[0],
envVarName: COPILOT_ENV_VARS[0],
allowProfile: false,
required: false,
});
if (primary || flagValue) {
return primary;
}
for (const envVar of COPILOT_ENV_VARS.slice(1)) {
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagName: "--github-copilot-token",
envVar,
envVarName: envVar,
allowProfile: false,
required: false,
});
if (resolved) {
return resolved;
}
}
return null;
}
async function runGitHubCopilotNonInteractiveAuth(
ctx: ProviderAuthMethodNonInteractiveContext,
): Promise<OpenClawConfig | null> {
const opts = ctx.opts as Record<string, unknown> | undefined;
const flagValue = normalizeOptionalSecretInput(opts?.githubCopilotToken);
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue,
flagName: "--github-copilot-token",
envVar: "COPILOT_GITHUB_TOKEN",
envVarName: "COPILOT_GITHUB_TOKEN",
allowProfile: false,
required: false,
});
const resolved = await resolveCopilotNonInteractiveToken(ctx, flagValue);
let profileId = DEFAULT_COPILOT_PROFILE_ID;
if (resolved) {