fix(models-auth): land #38951 from @MumuTW

Co-authored-by: MumuTW <MumuTW@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-07 18:50:17 +00:00
parent 02f99c0ff3
commit 2ada1b71b6
3 changed files with 93 additions and 22 deletions

View File

@@ -246,6 +246,7 @@ Docs: https://docs.openclaw.ai
- CLI/bootstrap Node version hint maintenance: replace hardcoded nvm `22` instructions in `openclaw.mjs` with `MIN_NODE_MAJOR` interpolation so future minimum-Node bumps keep startup guidance in sync automatically. (#39056) Thanks @onstash.
- Discord/native slash command auth: honor `commands.allowFrom.discord` (and `commands.allowFrom["*"]`) in guild slash-command pre-dispatch authorization so allowlisted senders are no longer incorrectly rejected as unauthorized. (#38794) Thanks @jskoiz and @thewilloftheshadow.
- Outbound/message target normalization: ignore empty legacy `to`/`channelId` fields when explicit `target` is provided so valid target-based sends no longer fail legacy-param validation; includes regression coverage. (#38944) Thanks @Narcooo.
- Models/auth token prompts: guard cancelled manual token prompts so `Symbol(clack:cancel)` values cannot be persisted into auth profiles; adds regression coverage for cancelled `models auth paste-token`. (#38951) Thanks @MumuTW.
## 2026.3.2

View File

@@ -3,10 +3,16 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
const mocks = vi.hoisted(() => ({
clackCancel: vi.fn(),
clackConfirm: vi.fn(),
clackIsCancel: vi.fn((value: unknown) => value === Symbol.for("clack:cancel")),
clackSelect: vi.fn(),
clackText: vi.fn(),
resolveDefaultAgentId: vi.fn(),
resolveAgentDir: vi.fn(),
resolveAgentWorkspaceDir: vi.fn(),
resolveDefaultAgentWorkspaceDir: vi.fn(),
upsertAuthProfile: vi.fn(),
resolvePluginProviders: vi.fn(),
createClackPrompter: vi.fn(),
loginOpenAICodexOAuth: vi.fn(),
@@ -17,6 +23,14 @@ const mocks = vi.hoisted(() => ({
openUrl: vi.fn(),
}));
vi.mock("@clack/prompts", () => ({
cancel: mocks.clackCancel,
confirm: mocks.clackConfirm,
isCancel: mocks.clackIsCancel,
select: mocks.clackSelect,
text: mocks.clackText,
}));
vi.mock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
resolveAgentDir: mocks.resolveAgentDir,
@@ -27,6 +41,10 @@ vi.mock("../../agents/workspace.js", () => ({
resolveDefaultAgentWorkspaceDir: mocks.resolveDefaultAgentWorkspaceDir,
}));
vi.mock("../../agents/auth-profiles.js", () => ({
upsertAuthProfile: mocks.upsertAuthProfile,
}));
vi.mock("../../plugins/providers.js", () => ({
resolvePluginProviders: mocks.resolvePluginProviders,
}));
@@ -64,7 +82,7 @@ vi.mock("../onboard-helpers.js", () => ({
openUrl: mocks.openUrl,
}));
const { modelsAuthLoginCommand } = await import("./auth.js");
const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand } = await import("./auth.js");
function createRuntime(): RuntimeEnv {
return {
@@ -102,6 +120,14 @@ describe("modelsAuthLoginCommand", () => {
restoreStdin = withInteractiveStdin();
currentConfig = {};
lastUpdatedConfig = null;
mocks.clackCancel.mockReset();
mocks.clackConfirm.mockReset();
mocks.clackIsCancel.mockImplementation(
(value: unknown) => value === Symbol.for("clack:cancel"),
);
mocks.clackSelect.mockReset();
mocks.clackText.mockReset();
mocks.upsertAuthProfile.mockReset();
mocks.resolveDefaultAgentId.mockReturnValue("main");
mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw/agents/main");
@@ -179,4 +205,28 @@ describe("modelsAuthLoginCommand", () => {
"No provider plugins found.",
);
});
it("does not persist a cancelled manual token entry", async () => {
const runtime = createRuntime();
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((
code?: string | number | null,
) => {
throw new Error(`exit:${String(code ?? "")}`);
}) as typeof process.exit);
try {
const cancelSymbol = Symbol.for("clack:cancel");
mocks.clackText.mockResolvedValue(cancelSymbol);
mocks.clackIsCancel.mockImplementation((value: unknown) => value === cancelSymbol);
await expect(modelsAuthPasteTokenCommand({ provider: "openai" }, runtime)).rejects.toThrow(
"exit:0",
);
expect(mocks.upsertAuthProfile).not.toHaveBeenCalled();
expect(mocks.updateConfig).not.toHaveBeenCalled();
expect(mocks.logConfigUpdated).not.toHaveBeenCalled();
} finally {
exitSpy.mockRestore();
}
});
});

View File

@@ -1,4 +1,10 @@
import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts";
import {
cancel,
confirm as clackConfirm,
isCancel,
select as clackSelect,
text as clackText,
} from "@clack/prompts";
import {
resolveAgentDir,
resolveAgentWorkspaceDir,
@@ -34,24 +40,38 @@ import {
} from "../provider-auth-helpers.js";
import { loadValidConfigOrThrow, updateConfig } from "./shared.js";
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
clackConfirm({
...params,
message: stylePromptMessage(params.message),
});
const text = (params: Parameters<typeof clackText>[0]) =>
clackText({
...params,
message: stylePromptMessage(params.message),
});
const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
clackSelect({
...params,
message: stylePromptMessage(params.message),
options: params.options.map((opt) =>
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
),
});
function guardCancel<T>(value: T | symbol): T {
if (typeof value === "symbol" || isCancel(value)) {
cancel("Cancelled.");
process.exit(0);
}
return value;
}
const confirm = async (params: Parameters<typeof clackConfirm>[0]) =>
guardCancel(
await clackConfirm({
...params,
message: stylePromptMessage(params.message),
}),
);
const text = async (params: Parameters<typeof clackText>[0]) =>
guardCancel(
await clackText({
...params,
message: stylePromptMessage(params.message),
}),
);
const select = async <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
guardCancel(
await clackSelect({
...params,
message: stylePromptMessage(params.message),
options: params.options.map((opt) =>
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
),
}),
);
type TokenProvider = "anthropic";
@@ -165,13 +185,13 @@ export async function modelsAuthPasteTokenCommand(
}
export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) {
const provider = (await select({
const provider = await select({
message: "Token provider",
options: [
{ value: "anthropic", label: "anthropic" },
{ value: "custom", label: "custom (type provider id)" },
],
})) as TokenProvider | "custom";
});
const providerId =
provider === "custom"