返回 DeepSeek-Reasonix
provider-image-input.test.tsx
根目录 / desktop / frontend / src / __tests__ / provider-image-input.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { act } from "react";
4
5
6 import { ModelImageInputControl } from "../components/ModelImageInputControl";
7 import { LocaleProvider } from "../lib/i18n";
8 import { imageInputHardBlocked, imageInputState, mergeImageInputModes } from "../lib/providerImageInput";
9 import type { AppBindings } from "../lib/bridge";
10 import type { ProviderModelCapabilityView, ProviderView } from "../lib/types";
11 import { installDesktopHostStub } from "./desktopHostStub";
12
13 const dom = new JSDOM('<div id="root"></div>', { url: "http://localhost/", pretendToBeVisual: true });
14 Object.assign(globalThis, { window: dom.window, document: dom.window.document, Node: dom.window.Node, HTMLElement: dom.window.HTMLElement, Event: dom.window.Event, CustomEvent: dom.window.CustomEvent, MouseEvent: dom.window.MouseEvent, localStorage: dom.window.localStorage, sessionStorage: dom.window.sessionStorage, IS_REACT_ACT_ENVIRONMENT: true });
15 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
16 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
17 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
18 window.scrollTo = () => {};
19 window.HTMLDialogElement.prototype.showModal = function() { this.setAttribute("open", ""); };
20 const { createRoot } = await import("react-dom/client");
21 await import("../components/ProviderModelDialog");
22 const { ProviderEditor, ProviderEditorModelPicker } = await import("../components/SettingsPanel");
23 const el = document.getElementById("root")!;
24 const root = createRoot(el);
25 const unknown: ProviderModelCapabilityView = { model: "relay-model", state: "unknown", source: "adapter", inputModalities: [], automaticState: "unknown", automaticSource: "adapter", imageInputEnableAllowed: true };
26 const initial: ProviderView = { name: "relay", kind: "openai", baseUrl: "https://relay.test/v1", requestUrl: "https://relay.test/v1/chat/completions", modelsUrl: "", models: ["relay-model"], visionModels: [], builtIn: false, added: true, default: "relay-model", apiKeyEnv: "TEST_KEY", keySet: true, balanceUrl: "", contextWindow: 0, reasoningProtocol: "", thinking: "", webSearch: false, supportedEfforts: [], defaultEffort: "", modelCapabilities: [unknown], modelOverrides: [{ model: "relay-model", vision: null, contextWindow: 123456, maxOutputTokens: 4321, reasoningProtocol: "openai", supportedEfforts: ["low"], defaultEffort: "low" }] };
27 const saved: ProviderView[] = [];
28 let cancelled = 0;
29 const render = async (node: React.ReactNode) => { await act(async () => { root.render(<LocaleProvider>{node}</LocaleProvider>); }); };
30 const editor = (key: string) => <ProviderEditor key={key} initial={initial} kinds={["openai"]} busy={false} onSave={(p) => { saved.push(p); }} onCancel={() => { cancelled++; }} />;
31 const modeInput = (mode: string) => el.querySelector<HTMLInputElement>(`[role="radiogroup"][aria-label="Image input mode for relay-model"] input[value="${mode}"]`)!;
32 const openModel = async () => { await act(async()=>{(el.querySelector('.provider-model-draft__option button') as HTMLButtonElement).click(); await new Promise(r=>setTimeout(r,30));}); };
33 const choose = async (mode: string) => {
34 await openModel();
35 const d=document.querySelector('dialog')!;
36 await act(async()=>{
37 if(mode === 'auto') (d.querySelector('aside > button') as HTMLButtonElement).click();
38 else { const input=d.querySelector<HTMLInputElement>('aside input[type="checkbox"]:not(:disabled)')!; assert(input); if(input.checked !== (mode === 'on')) input.click(); }
39 });
40 await act(async()=>d.querySelector('form')!.dispatchEvent(new window.Event('submit',{bubbles:true,cancelable:true})));
41 };
42 const click = async (label: string) => { await act(async () => { const b = [...el.querySelectorAll("button")].find((b) => (b.textContent?.trim() === label || b.getAttribute("aria-label") === label)); assert(b, `missing button ${label}`); b.click(); }); };
43
44 await render(editor("first"));
45 assert(el.textContent?.includes("Image capability unknown"));
46 assert.equal(el.querySelector('[role="radiogroup"]'),null,"model details stay in the dialog");
47 await choose("on");
48 assert(el.textContent?.includes("Image input"));
49 assert.equal(saved.length, 0, "draft must not write config");
50 await click("Save changes");
51 assert.equal(saved[0].modelOverrides?.[0].vision, true);
52 assert.equal(saved[0].modelOverrides?.[0].contextWindow, 123456);
53 assert.equal(saved[0].modelOverrides?.[0].maxOutputTokens, 4321);
54 await choose("auto");
55 await click("Save changes");
56 assert.equal(saved[1].modelOverrides?.[0].vision, null);
57 assert.equal(saved[1].modelOverrides?.[0].reasoningProtocol, "openai");
58 await choose("off");
59 await click("Cancel");
60 assert.equal(cancelled, 1);
61 assert.equal(saved.length, 2);
62
63 await render(<ProviderEditor key="legacy-backend" initial={{ ...initial, modelCapabilities: undefined, modelOverrides: [{ ...initial.modelOverrides![0], vision: true }] }} kinds={["openai"]} busy={false} onSave={() => {}} onCancel={() => {}} />);
64 await choose("auto");
65 assert(el.textContent?.includes("Image capability unknown"), "auto must not reuse the previous explicit override as its fallback");
66
67 // A controlled Promise completes only after unmount. It must not modify the
68 // replacement editor or enable a model from the previous provider request.
69 let complete!: (v: ProviderModelCapabilityView[]) => void;
70 installDesktopHostStub(({ main: { App: { FetchProviderModelCatalog: () => new Promise((resolve) => { complete = resolve; }) } as Partial<AppBindings> as AppBindings } }).main.App);
71 await render(editor("fetching"));
72 await click("Refresh models");
73 await render(editor("replacement"));
74 await act(async () => { complete([{ ...unknown, model: "stale-model", state: "supported" }]); });
75 assert(!el.textContent?.includes("stale-model"));
76 assert(!el.textContent?.includes("stale-model"));
77
78 await render(<ModelImageInputControl model="relay-model" mode="auto" capability={{ ...unknown, state: "unsupported", automaticState: "unsupported", imageInputEnableAllowed: false, imageInputBlockReason: "official_deepseek_text_model" }} disabled={false} onChange={() => {}} />);
79 assert(modeInput("on").disabled);
80 assert(!modeInput("off").disabled);
81 assert.equal(imageInputState("auto", { ...unknown, automaticState: undefined, source: "override", state: "supported" }), "unknown", "old backend must not fabricate automatic support");
82 assert.equal(imageInputState("on", { ...unknown, imageInputEnableAllowed: false }), "unsupported");
83 assert(imageInputHardBlocked("https://api.deepseek.com", "deepseek-v4-pro"));
84 assert(!imageInputHardBlocked("https://eu.deepseek.com/anthropic", "future-vision"));
85 assert(!imageInputHardBlocked("https://api.deepseek.com.relay.test", "deepseek-v4-pro"));
86 assert(!imageInputHardBlocked("https://api.deepseek.com", "deepseek-v4-flash"));
87 assert(!imageInputHardBlocked("https://api.deepseek.com", "deepseek-v4-flash-vision-exp"));
88 assert.equal(mergeImageInputModes(initial.modelOverrides, initial.models, { "RELAY-MODEL": "off" })[0].vision, null);
89
90 // The real dialog enforces a backend hard block as well as the primitive control.
91 await render(<ProviderEditor key="blocked" initial={{...initial,modelCapabilities:[{...unknown,imageInputEnableAllowed:false,state:"unsupported"}]}} kinds={["openai"]} busy={false} onSave={()=>{}} onCancel={()=>{}}/>);
92 await openModel();
93 assert.equal(document.querySelectorAll('dialog aside input:disabled').length,5,"blocked image input cannot be enabled");
94 await act(async () => root.unmount());
95 dom.window.close();
96 console.log("PASS provider image input: draft/save/auto/cancel/disabled/hard-limit/stale discovery");
97
97 lines Plain Text