


























@@ -0,0 +1,147 @@
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+import type { ModelScanResult } from "../../agents/model-scan.js";
3+import type { RuntimeEnv } from "../../runtime.js";
4+5+const mocks = vi.hoisted(() => ({
6+loadModelsConfig: vi.fn(),
7+resolveApiKeyForProvider: vi.fn(),
8+scanOpenRouterModels: vi.fn(),
9+}));
10+11+vi.mock("./load-config.js", () => ({
12+loadModelsConfig: mocks.loadModelsConfig,
13+}));
14+15+vi.mock("../../agents/model-auth.js", () => ({
16+resolveApiKeyForProvider: mocks.resolveApiKeyForProvider,
17+}));
18+19+vi.mock("../../agents/model-scan.js", () => ({
20+scanOpenRouterModels: mocks.scanOpenRouterModels,
21+}));
22+23+const { modelsScanCommand } = await import("./scan.js");
24+25+function createRuntime(): RuntimeEnv & { lines: string[] } {
26+const lines: string[] = [];
27+return {
28+ lines,
29+log: (...args: unknown[]) => lines.push(args.join(" ")),
30+error: (...args: unknown[]) => lines.push(args.join(" ")),
31+exit: (code?: number) => {
32+throw new Error(`exit ${code ?? 0}`);
33+},
34+} as RuntimeEnv & { lines: string[] };
35+}
36+37+function scanResult(overrides: Partial<ModelScanResult> = {}): ModelScanResult {
38+return {
39+id: "acme/free:free",
40+name: "ACME Free",
41+provider: "openrouter",
42+modelRef: "openrouter/acme/free:free",
43+contextLength: 128_000,
44+maxCompletionTokens: 8192,
45+supportedParametersCount: 2,
46+supportsToolsMeta: true,
47+modality: "text",
48+inferredParamB: 70,
49+createdAtMs: 1_700_000_000_000,
50+pricing: { prompt: 0, completion: 0, request: 0, image: 0, webSearch: 0, internalReasoning: 0 },
51+isFree: true,
52+tool: { ok: false, latencyMs: null, skipped: true },
53+image: { ok: false, latencyMs: null, skipped: true },
54+ ...overrides,
55+};
56+}
57+58+describe("models scan command", () => {
59+beforeEach(() => {
60+vi.clearAllMocks();
61+});
62+63+afterEach(() => {
64+vi.unstubAllEnvs();
65+});
66+67+it("does not load config or resolve secrets for metadata-only scans", async () => {
68+const runtime = createRuntime();
69+mocks.scanOpenRouterModels.mockResolvedValue([scanResult()]);
70+71+await modelsScanCommand({ probe: false }, runtime);
72+73+expect(mocks.loadModelsConfig).not.toHaveBeenCalled();
74+expect(mocks.resolveApiKeyForProvider).not.toHaveBeenCalled();
75+expect(mocks.scanOpenRouterModels).toHaveBeenCalledWith(
76+expect.objectContaining({ probe: false }),
77+);
78+expect(runtime.lines.join("\n")).toContain("metadata only");
79+expect(runtime.lines.join("\n")).toContain("Tool");
80+expect(runtime.lines.join("\n")).toContain("skip");
81+});
82+83+it("downgrades to metadata-only scan when no OpenRouter key is configured", async () => {
84+const runtime = createRuntime();
85+vi.stubEnv("OPENROUTER_API_KEY", undefined);
86+mocks.loadModelsConfig.mockResolvedValue({});
87+mocks.resolveApiKeyForProvider.mockResolvedValue({ apiKey: "" });
88+mocks.scanOpenRouterModels.mockResolvedValue([scanResult()]);
89+90+await modelsScanCommand({}, runtime);
91+92+expect(mocks.loadModelsConfig).toHaveBeenCalledTimes(1);
93+expect(mocks.resolveApiKeyForProvider).toHaveBeenCalledWith({
94+provider: "openrouter",
95+cfg: {},
96+});
97+expect(mocks.scanOpenRouterModels).toHaveBeenCalledWith(
98+expect.objectContaining({ probe: false }),
99+);
100+expect(runtime.lines.join("\n")).toContain("still require OPENROUTER_API_KEY");
101+});
102+103+it("uses OPENROUTER_API_KEY directly without loading model config", async () => {
104+const runtime = createRuntime();
105+vi.stubEnv("OPENROUTER_API_KEY", "sk-or-test");
106+mocks.scanOpenRouterModels.mockResolvedValue([
107+scanResult({ tool: { ok: false, latencyMs: null, skipped: false } }),
108+]);
109+110+await expect(modelsScanCommand({ json: true }, runtime)).rejects.toThrow(
111+/No tool-capable OpenRouter free models found/,
112+);
113+114+expect(mocks.loadModelsConfig).not.toHaveBeenCalled();
115+expect(mocks.resolveApiKeyForProvider).not.toHaveBeenCalled();
116+expect(mocks.scanOpenRouterModels).toHaveBeenCalledWith(
117+expect.objectContaining({
118+apiKey: "sk-or-test",
119+probe: true,
120+}),
121+);
122+});
123+124+it("rejects applying metadata-only scan results", async () => {
125+const runtime = createRuntime();
126+vi.stubEnv("OPENROUTER_API_KEY", undefined);
127+128+await expect(modelsScanCommand({ probe: false, setDefault: true }, runtime)).rejects.toThrow(
129+/Cannot apply metadata-only OpenRouter scan results/,
130+);
131+132+expect(mocks.scanOpenRouterModels).not.toHaveBeenCalled();
133+});
134+135+it("rejects applying auto-downgraded metadata-only scan results before scanning", async () => {
136+const runtime = createRuntime();
137+vi.stubEnv("OPENROUTER_API_KEY", undefined);
138+mocks.loadModelsConfig.mockResolvedValue({});
139+mocks.resolveApiKeyForProvider.mockResolvedValue({ apiKey: "" });
140+141+await expect(modelsScanCommand({ setDefault: true }, runtime)).rejects.toThrow(
142+/Cannot apply metadata-only OpenRouter scan results/,
143+);
144+145+expect(mocks.scanOpenRouterModels).not.toHaveBeenCalled();
146+});
147+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。