






















11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5-import { testing as cliBackendsTesting } from "../../agents/cli-backends.js";
4+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5+6+vi.hoisted(() => {
7+vi.resetModules();
8+});
9+610const authProfilesStoreMock = vi.hoisted(() => ({
711profiles: {} as Record<
812string,
913| { type: "api_key"; provider: string; key: string }
1014| { type: "oauth"; provider: string; access: string; refresh: string; expires: number }
1115>,
1216}));
17+const modelsCommandMock = vi.hoisted(() => ({
18+delegateToActual: false,
19+resolveModelsCommandReply: vi.fn(),
20+}));
21+22+function defaultModelsCommandReply() {
23+return {
24+text: [
25+"Providers:",
26+"- anthropic (1)",
27+"",
28+"Use: /models <provider>",
29+"Switch: /model <provider/model>",
30+].join("\n"),
31+};
32+}
33+34+function normalizeProviderForAuthTest(provider: string) {
35+return provider.trim().toLowerCase();
36+}
37+38+function hasAllowedPluginForAuthTest(cfg: unknown, pluginId: string): boolean {
39+if (!cfg || typeof cfg !== "object" || !("plugins" in cfg)) {
40+return false;
41+}
42+const plugins = (cfg as { plugins?: { allow?: unknown } }).plugins;
43+return Array.isArray(plugins?.allow) && plugins.allow.includes(pluginId);
44+}
13451446vi.mock("../../agents/auth-profiles.js", () => {
1547const store = () => ({
@@ -44,6 +76,68 @@ vi.mock("../../agents/auth-profiles.js", () => {
4476};
4577});
467879+vi.mock("./commands-models.js", () => ({
80+resolveModelsCommandReply: async (
81+params: Parameters<typeof import("./commands-models.js").resolveModelsCommandReply>[0],
82+) => {
83+modelsCommandMock.resolveModelsCommandReply(params);
84+if (modelsCommandMock.delegateToActual) {
85+const actual =
86+await vi.importActual<typeof import("./commands-models.js")>("./commands-models.js");
87+return actual.resolveModelsCommandReply(params);
88+}
89+return defaultModelsCommandReply();
90+},
91+}));
92+93+vi.mock("./directive-handling.auth.js", () => ({
94+formatAuthLabel: (auth: { label: string; source: string }) => {
95+if (!auth.source || auth.source === auth.label || auth.source === "missing") {
96+return auth.label;
97+}
98+return `${auth.label} (${auth.source})`;
99+},
100+resolveAuthLabel: async (
101+provider: string,
102+cfg: unknown,
103+_modelsPath: string,
104+_agentDir?: string,
105+_mode?: unknown,
106+workspaceDir?: string,
107+) => {
108+const providerKey = normalizeProviderForAuthTest(provider);
109+const matchingProfiles = Object.entries(authProfilesStoreMock.profiles).filter(
110+([, profile]) => normalizeProviderForAuthTest(profile.provider) === providerKey,
111+);
112+if (matchingProfiles.length > 0) {
113+return {
114+label: matchingProfiles
115+.map(([profileId, profile]) =>
116+profile.type === "oauth" ? `${profileId}=OAuth` : `${profileId}=${profile.key}`,
117+)
118+.join(", "),
119+source: `auth-profiles.json: /tmp/auth-profiles.json`,
120+};
121+}
122+if (
123+providerKey === "anthropic" &&
124+workspaceDir &&
125+((process.env.WORKSPACE_MODEL_CREDENTIALS &&
126+hasAllowedPluginForAuthTest(cfg, "workspace-model-auth")) ||
127+(process.env.WORKSPACE_MODEL_LIST_CREDENTIALS &&
128+hasAllowedPluginForAuthTest(cfg, "workspace-model-list")))
129+) {
130+return {
131+label: process.env.WORKSPACE_MODEL_CREDENTIALS
132+ ? "workspace model credentials"
133+ : "workspace model list credentials",
134+source: "",
135+};
136+}
137+return { label: "missing", source: "missing" };
138+},
139+}));
140+47141vi.mock("../../agents/auth-profiles/store.js", () => {
48142const store = () => ({
49143version: 1,
@@ -122,6 +216,50 @@ vi.mock("../../agents/provider-auth-aliases.js", () => ({
122216resolveProviderIdForAuth: (provider: string) => provider,
123217}));
124218219+vi.mock("../../agents/harness/selection.js", () => ({
220+resolveAgentHarnessPolicy: ({
221+ provider,
222+ modelId,
223+ config,
224+}: {
225+provider?: string;
226+modelId?: string;
227+config?: OpenClawConfig;
228+}) => {
229+const modelRuntime =
230+provider && modelId
231+ ? config?.agents?.defaults?.models?.[`${provider}/${modelId}`]?.agentRuntime?.id
232+ : undefined;
233+const providerRuntime = provider
234+ ? config?.models?.providers?.[provider]?.agentRuntime?.id
235+ : undefined;
236+const runtime =
237+modelRuntime === "default"
238+ ? undefined
239+ : (modelRuntime ??
240+(providerRuntime === "default" ? undefined : providerRuntime) ??
241+(provider === "openai" || provider === "openai-codex" ? "codex" : "auto"));
242+return {
243+ runtime,
244+runtimeSource: modelRuntime ? "model" : providerRuntime ? "provider" : "implicit",
245+};
246+},
247+}));
248+249+vi.mock("../../agents/runtime-plan/auth.js", () => ({
250+buildAgentRuntimeAuthPlan: ({
251+ provider,
252+ harnessRuntime,
253+}: {
254+provider: string;
255+harnessRuntime?: string;
256+}) => ({
257+providerForAuth: provider,
258+authProfileProviderForAuth: provider,
259+ ...(harnessRuntime === "codex" ? { harnessAuthProvider: "openai-codex" } : {}),
260+}),
261+}));
262+125263import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
126264import {
127265clearRuntimeAuthProfileStoreSnapshots,
@@ -141,13 +279,22 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js";
141279import type { ProviderPlugin } from "../../plugins/types.js";
142280import { withEnvAsync } from "../../test-utils/env.js";
143281import type { ElevatedLevel } from "../thinking.js";
144-import { handleDirectiveOnly } from "./directive-handling.impl.js";
145-import {
146-maybeHandleModelDirectiveInfo,
147-resolveModelSelectionFromDirective,
148-} from "./directive-handling.model.js";
149-import { parseInlineDirectives } from "./directive-handling.parse.js";
150-import { persistInlineDirectives } from "./directive-handling.persist.js";
282+283+let handleDirectiveOnly: typeof import("./directive-handling.impl.js").handleDirectiveOnly;
284+let cliBackendsTesting: typeof import("../../agents/cli-backends.js").testing;
285+let maybeHandleModelDirectiveInfo: typeof import("./directive-handling.model.js").maybeHandleModelDirectiveInfo;
286+let resolveModelSelectionFromDirective: typeof import("./directive-handling.model.js").resolveModelSelectionFromDirective;
287+let parseInlineDirectives: typeof import("./directive-handling.parse.js").parseInlineDirectives;
288+let persistInlineDirectives: typeof import("./directive-handling.persist.js").persistInlineDirectives;
289+290+beforeAll(async () => {
291+({ testing: cliBackendsTesting } = await import("../../agents/cli-backends.js"));
292+({ handleDirectiveOnly } = await import("./directive-handling.impl.js"));
293+({ maybeHandleModelDirectiveInfo, resolveModelSelectionFromDirective } =
294+await import("./directive-handling.model.js"));
295+({ parseInlineDirectives } = await import("./directive-handling.parse.js"));
296+({ persistInlineDirectives } = await import("./directive-handling.persist.js"));
297+});
151298152299const liveModelSwitchMocks = vi.hoisted(() => ({
153300requestLiveSessionModelSwitch: vi.fn(),
@@ -264,6 +411,10 @@ beforeEach(() => {
264411resolveRuntimeCliBackends: () => [],
265412});
266413setDirectiveTestProviders([]);
414+modelsCommandMock.resolveModelsCommandReply
415+.mockReset()
416+.mockResolvedValue(defaultModelsCommandReply());
417+modelsCommandMock.delegateToActual = false;
267418clearRuntimeAuthProfileStoreSnapshots();
268419replaceRuntimeAuthProfileStoreSnapshots([
269420{
@@ -459,7 +610,7 @@ describe("/model chat UX", () => {
459610expect(reply?.text).toContain("Switch: /model <provider/model>");
460611});
461612462-it("uses workspace-scoped auth evidence in /model list provider visibility", async () => {
613+it("passes workspace scope through the /model list browser alias", async () => {
463614const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-list-auth-label-"));
464615const workspaceDir = path.join(tempRoot, "workspace");
465616const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-model-list");
@@ -503,6 +654,7 @@ describe("/model chat UX", () => {
503654WORKSPACE_MODEL_LIST_CREDENTIALS: credentialPath,
504655},
505656async () => {
657+modelsCommandMock.delegateToActual = true;
506658const reply = await resolveModelInfoReply({
507659directives: parseInlineDirectives("/model list"),
508660 workspaceDir,
@@ -513,6 +665,15 @@ describe("/model chat UX", () => {
513665});
514666515667expect(reply?.text).toContain("- anthropic");
668+expect(modelsCommandMock.resolveModelsCommandReply).toHaveBeenCalledWith(
669+expect.objectContaining({
670+commandBodyNormalized: "/models",
671+ workspaceDir,
672+cfg: expect.objectContaining({
673+plugins: { allow: ["workspace-model-list"] },
674+}),
675+}),
676+);
516677},
517678);
518679} finally {
@@ -658,7 +819,6 @@ describe("/model chat UX", () => {
658819commands: { text: true },
659820agents: {
660821defaults: {
661-agentRuntime: { id: "codex" },
662822model: { primary: "openai/gpt-5.5" },
663823models: {
664824"codex/gpt-5.5": {},
@@ -702,7 +862,6 @@ describe("/model chat UX", () => {
702862commands: { text: true },
703863agents: {
704864defaults: {
705-agentRuntime: { id: "codex" },
706865model: { primary: "openai/gpt-5.5" },
707866models: {
708867"openai/gpt-5.5": {},
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。