返回 DeepSeek-Reasonix
provider-editor-model-picker.test.tsx
根目录 / desktop / frontend / src / __tests__ / provider-editor-model-picker.test.tsx
1 // Run: tsx src/__tests__/provider-editor-model-picker.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React from "react";
5 import { act } from "react";
6 import { createRoot } from "react-dom/client";
7 import { LocaleProvider } from "../lib/i18n";
8 import { ProviderEditor, ProviderEditorModelPicker, providerSupportsServerWebSearch } from "../components/SettingsPanel";
9 import type { ProviderView } from "../lib/types";
10
11 let passed = 0;
12 let failed = 0;
13
14 function ok(value: boolean, label: string) {
15 if (value) {
16 process.stdout.write(` PASS ${label}\n`);
17 passed += 1;
18 } else {
19 process.stdout.write(` FAIL ${label}\n`);
20 failed += 1;
21 }
22 }
23
24 function flushPromises(): Promise<void> {
25 return new Promise((resolve) => setTimeout(resolve, 0));
26 }
27
28 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
29 pretendToBeVisual: true,
30 url: "http://localhost/",
31 });
32 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
33 globalThis.window = dom.window as unknown as Window & typeof globalThis;
34 globalThis.document = dom.window.document;
35 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
36 globalThis.Node = dom.window.Node;
37 globalThis.HTMLElement = dom.window.HTMLElement;
38 globalThis.Event = dom.window.Event;
39 globalThis.CustomEvent = dom.window.CustomEvent;
40 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
41 globalThis.MouseEvent = dom.window.MouseEvent;
42 globalThis.localStorage = dom.window.localStorage;
43 globalThis.sessionStorage = dom.window.sessionStorage;
44 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
45 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
46 window.scrollTo = () => {};
47
48 function renderPicker(candidates: string[], visionCapability: "configurable" | "unsupported" = "configurable") {
49 return (
50 <LocaleProvider>
51 <ProviderEditorModelPicker
52 candidates={candidates}
53 selectedModels={[]}
54 visionModels={[]}
55 visionCapability={visionCapability}
56 contextWindows={{}}
57 disabled={false}
58 onToggleModel={() => undefined}
59 onToggleVision={() => undefined}
60 onContextWindowChange={() => undefined}
61 onSelectAll={() => undefined}
62 onClear={() => undefined}
63 />
64 </LocaleProvider>
65 );
66 }
67
68 console.log("\nprovider editor model picker");
69
70 const rootEl = document.getElementById("root");
71 if (!rootEl) throw new Error("missing root");
72 const root = createRoot(rootEl);
73
74 let threw = false;
75 try {
76 await act(async () => {
77 root.render(renderPicker([]));
78 await flushPromises();
79 });
80 await act(async () => {
81 root.render(renderPicker(["zen-v1", "zen-v1-pro"]));
82 await flushPromises();
83 });
84 } catch (error) {
85 threw = true;
86 process.stdout.write(` ERROR ${String(error)}\n`);
87 }
88
89 ok(!threw, "model picker can render after async model fetch returns candidates");
90 ok(rootEl.textContent?.includes("zen-v1") === true, "model picker shows fetched custom provider models");
91
92 await act(async () => {
93 root.render(renderPicker(["deepseek-v4-flash"], "unsupported"));
94 await flushPromises();
95 });
96 ok(rootEl.textContent?.includes("No image input") === true, "known text-only DeepSeek models show a read-only image capability");
97 ok(rootEl.querySelectorAll('input[type="checkbox"]').length === 1, "text-only model card does not render a second image checkbox");
98 ok(providerSupportsServerWebSearch("responses", "https://api.deepseek.com"), "DeepSeek Responses exposes server-side web search");
99 ok(providerSupportsServerWebSearch("anthropic", "https://api.deepseek.com/anthropic"), "DeepSeek Anthropic exposes server-side web search");
100 ok(!providerSupportsServerWebSearch("openai", "https://api.deepseek.com"), "DeepSeek Chat Completions does not expose server-side web search");
101 ok(!providerSupportsServerWebSearch("responses", "https://relay.deepseek.com"), "DeepSeek-like subdomains do not inherit official defaults");
102 ok(!providerSupportsServerWebSearch("responses", "https://api.deepseek.com/anthropic"), "Responses rejects the Anthropic base path");
103 ok(!providerSupportsServerWebSearch("anthropic", "https://api.deepseek.com"), "Anthropic requires its documented base path");
104 ok(!providerSupportsServerWebSearch("responses", "http://api.deepseek.com"), "insecure DeepSeek URLs do not inherit official defaults");
105
106 const builtInProvider: ProviderView = {
107 name: "deepseek",
108 builtIn: true,
109 added: true,
110 kind: "openai",
111 baseUrl: "https://api.deepseek.com",
112 models: ["deepseek-chat"],
113 visionModels: [],
114 visionModelsConfigured: false,
115 modelsUrl: "",
116 default: "deepseek-chat",
117 apiKeyEnv: "",
118 keySet: false,
119 balanceUrl: "",
120 contextWindow: 128_000,
121 reasoningProtocol: "deepseek",
122 thinking: "",
123 supportedEfforts: [],
124 defaultEffort: "",
125 };
126
127 const deepSeekResponsesProvider: ProviderView = {
128 ...builtInProvider,
129 name: "deepseek-responses",
130 builtIn: false,
131 kind: "responses",
132 models: ["deepseek-v4-flash"],
133 default: "deepseek-v4-flash",
134 webSearch: true,
135 };
136
137 function renderProviderEditor(initial?: ProviderView) {
138 return (
139 <LocaleProvider>
140 <ProviderEditor
141 initial={initial}
142 kinds={["openai"]}
143 busy={false}
144 onCancel={() => undefined}
145 onSave={() => undefined}
146 />
147 </LocaleProvider>
148 );
149 }
150
151 let editorThrew = false;
152 try {
153 await act(async () => {
154 root.render(renderProviderEditor(builtInProvider));
155 await flushPromises();
156 });
157 await act(async () => {
158 root.render(renderProviderEditor());
159 await flushPromises();
160 });
161 } catch (error) {
162 editorThrew = true;
163 process.stdout.write(` ERROR ${String(error)}\n`);
164 }
165
166 ok(!editorThrew, "provider editor can switch from built-in to custom without changing hook order");
167 ok(rootEl.textContent?.includes("OpenAI-compatible") === true, "provider editor renders the custom provider fields after the switch");
168
169 await act(async () => {
170 root.render(<div />);
171 await flushPromises();
172 });
173 await act(async () => {
174 root.render(renderProviderEditor(deepSeekResponsesProvider));
175 await flushPromises();
176 });
177 const webSearchSwitch = rootEl.querySelector<HTMLInputElement>('input[role="switch"]');
178 ok(rootEl.textContent?.includes("Server-side web search") === true, "DeepSeek Responses editor separates service capabilities from model selection");
179 ok(webSearchSwitch?.checked === true, "curated DeepSeek Responses capability is enabled in the editor");
180 ok(rootEl.textContent?.includes("No image input") === true, "DeepSeek Responses editor labels image input as unsupported");
181
182 await act(async () => {
183 root.unmount();
184 });
185 dom.window.close();
186
187 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
188 if (failed > 0) process.exit(1);
189
189 lines Plain Text