返回 DeepSeek-Reasonix
startup-settings-contract.test.ts
根目录 / desktop / frontend / src / __tests__ / startup-settings-contract.test.ts
1 // Run: tsx src/__tests__/startup-settings-contract.test.ts
2
3 import { readFileSync } from "node:fs";
4 import { dirname, resolve } from "node:path";
5 import { fileURLToPath } from "node:url";
6 import {
7 ONBOARDING_DISMISSED_STORAGE_KEY,
8 dismissOnboarding,
9 onboardingWasDismissed,
10 shouldOpenOnboarding,
11 } from "../lib/onboarding";
12
13 let passed = 0;
14 let failed = 0;
15
16 function ok(cond: boolean, label: string) {
17 if (cond) {
18 process.stdout.write(` PASS ${label}\n`);
19 passed += 1;
20 } else {
21 process.stdout.write(` FAIL ${label}\n`);
22 failed += 1;
23 }
24 }
25
26 const here = dirname(fileURLToPath(import.meta.url));
27 const paletteSource = readFileSync(resolve(here, "../app-runtime/usePaletteCommands.tsx"), "utf8");
28 const bridgeSource = readFileSync(resolve(here, "../lib/bridge.ts"), "utf8");
29 const configWarningsSource = readFileSync(resolve(here, "../lib/useConfigLoadWarnings.ts"), "utf8");
30 const settingsSource = readFileSync(resolve(here, "../components/SettingsPanel.tsx"), "utf8");
31 const settingsNavigationSource = readFileSync(resolve(here, "../components/SettingsNavigation.tsx"), "utf8");
32 const stylesSource = readFileSync(resolve(here, "../styles.css"), "utf8") +
33 readFileSync(resolve(here, "../components/ProviderAccessSettings.css"), "utf8") +
34 readFileSync(resolve(here, "../components/SettingsPanel.css"), "utf8");
35 const enLocaleSource = readFileSync(resolve(here, "../locales/en.ts"), "utf8");
36 const zhLocaleSource = readFileSync(resolve(here, "../locales/zh.ts"), "utf8");
37 const zhTWLocaleSource = readFileSync(resolve(here, "../locales/zh-TW.ts"), "utf8");
38
39 console.log("\nstartup settings contract");
40
41 ok(
42 bridgeSource.includes("DesktopStartupSettings()"),
43 "bridge exposes a lightweight desktop startup settings call",
44 );
45 ok(
46 configWarningsSource.includes("revision < latestRevision.current") &&
47 configWarningsSource.includes("seenKeys.current.has(key)"),
48 "startup and reload barriers reject stale events while repeated session builds stay deduplicated",
49 );
50 ok(
51 bridgeSource.includes('displayMode: "standard", sessionExperience: "standard", reasoningDisplayMode: "auto", reasoningDisplayModeExplicit: false'),
52 "browser startup defaults include the canonical standard session experience",
53 );
54 ok(
55 /initialFocus\?\.target === "model-access"[\s\S]*?initialFocus\?\.target === "model-stats"[\s\S]*?"usage"/.test(settingsSource),
56 "model settings honor access and statistics focus targets while preserving usage as the default",
57 );
58 ok(
59 !settingsSource.includes("modelFocusHandledRef"),
60 "each fresh model focus object can re-target the same subtab again",
61 );
62 ok(
63 /setSettingsFocus\(\(current\) => \(\{[\s\S]*?target: "model-stats",[\s\S]*?requestId: \(current\?\.requestId \?\? 0\) \+ 1,[\s\S]*?\}\)\)/.test(paletteSource) &&
64 /initialFocus\?\.requestId/.test(settingsSource),
65 "usage statistics commands derive a monotonic request id from the shared focus state",
66 );
67 ok(
68 settingsSource.includes("ProviderCatalogPicker") && !settingsSource.includes("function providerPresetDescription"),
69 "provider setup uses the brand catalog instead of the former flat preset cards",
70 );
71 ok(
72 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every(source =>
73 ["settings.catalog.region", "settings.catalog.product", "settings.catalog.productCoding", "settings.providerProtocol"].every(key => source.includes(`"${key}"`))),
74 "account platform, plan and API format labels exist in every locale",
75 );
76 ok(
77 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
78 source.includes('"settings.reasoningProtocol.glm"'),
79 ),
80 "GLM reasoning protocol is localized in every supported locale",
81 );
82 ok(
83 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
84 source.includes('"settings.sessionExperience"') &&
85 source.includes('"settings.sessionExperienceHint"') &&
86 source.includes('"settings.sessionExperience.standard"') &&
87 source.includes('"settings.sessionExperience.deep"'),
88 ),
89 "session experience labels are localized in every supported locale",
90 );
91 ok(
92 stylesSource.includes(".settings-page--general .settings-section") &&
93 stylesSource.includes("grid-template-columns: minmax(260px, 1fr) max-content") &&
94 stylesSource.includes(".settings-field__copy--icon") &&
95 stylesSource.includes("container: settings-general / inline-size") &&
96 stylesSource.includes("@container settings-general (max-width: 620px)") &&
97 stylesSource.includes("@media (max-width: 980px)"),
98 "General controls use the selected flat responsive section layout",
99 );
100 ok(
101 settingsNavigationSource.includes('settings.searchPlaceholder') &&
102 settingsNavigationSource.includes('SETTINGS_TAB_GROUPS') &&
103 settingsNavigationSource.includes('settings-center__navgroup') &&
104 settingsNavigationSource.includes('settingsTabIcon(id)') &&
105 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
106 source.includes('"settings.navGroup.preferences"') &&
107 source.includes('"settings.searchNoResults"'),
108 ),
109 "settings navigation provides searchable localized groups and visible category icons",
110 );
111 ok(
112 /function settingsTabMeta[\s\S]*?case "skills":\s+return t\("settings\.tabSub\.skills"\)/.test(settingsSource) &&
113 enLocaleSource.includes('"settings.tab.skills": "Agent Skills"') &&
114 enLocaleSource.includes('"settings.tabSub.skills": "Reusable instructions, tools & workflows"') &&
115 zhLocaleSource.includes('"settings.tab.skills": "Agent Skills"') &&
116 zhLocaleSource.includes('"settings.tabSub.skills": "可复用的指令、工具与工作流"') &&
117 zhTWLocaleSource.includes('"settings.tab.skills": "Agent Skills"') &&
118 zhTWLocaleSource.includes('"settings.tabSub.skills": "可重複使用的指令、工具與工作流程"'),
119 "Agent Skills navigation uses the dedicated reusable-workflow description in every supported locale",
120 );
121 ok(
122 [settingsSource, enLocaleSource, zhLocaleSource, zhTWLocaleSource, stylesSource].every((source) =>
123 !source.includes("settings.workProcess") &&
124 !source.includes("settings-work-process"),
125 ),
126 "the superseded work-process-only visual group does not return",
127 );
128 ok(
129 [settingsSource, enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
130 !source.includes("settings.reasoningSummary"),
131 ),
132 "the retired standalone reasoning-summary setting does not return",
133 );
134 ok(
135 settingsSource.includes('t("settings.notificationVolume")') &&
136 settingsSource.includes("<NotificationVolumeSlider") &&
137 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
138 source.includes('"settings.notificationVolume"'),
139 ),
140 "sound settings expose the persisted notification volume control in every locale",
141 );
142 ok(
143 !/mockPreset\("deepseek-anthropic",/.test(bridgeSource),
144 "browser mock hides the redundant DeepSeek Anthropic preset",
145 );
146 ok(
147 bridgeSource.includes('value === "deepseek-upgrade"') &&
148 bridgeSource.includes('recommendedUpgradeAvailable: false') &&
149 bridgeSource.includes('headers: deepSeekUpgradeMock ? { "X-Route": "official-custom" } : undefined'),
150 "browser mock preserves custom DeepSeek headers without proposing the retired protocol upgrade",
151 );
152 ok(
153 /async ConnectKey\(apiKey: string\)[\s\S]*?await this\.AddOfficialProviderAccess\("deepseek", apiKey\)/.test(bridgeSource),
154 "browser onboarding installs the same current DeepSeek official template as production",
155 );
156 ok(
157 settingsSource.includes("officialProviders={s.officialProviders}") &&
158 settingsSource.includes('canAdd: true, status: "available"') &&
159 settingsSource.includes("keySet: Boolean(provider?.keySet)"),
160 "official templates allow separate connections while preserving credential status",
161 );
162 ok(
163 /onUpgradeRecommended=\{\(name\) => \{[\s\S]*?cancelGroupFetch\(group\.id\);[\s\S]*?return apply\(\(\) => saveModelSettings\(s, \{kind: "protocol_upgrade", name\}\)\)/.test(settingsSource) &&
164 settingsSource.includes("onConfirm={() => onUpgradeRecommended(canonicalOfficialProviderName(upgradeProvider.name))}") &&
165 settingsSource.includes('className="provider-protocol-upgrade"') &&
166 settingsSource.includes('t("settings.providerProtocol")}: OpenAI Chat Completions') &&
167 /<InlineConfirmButton[\s\S]*?label=\{<>\{t\("settings\.upgradeRecommendedProtocol"\)\}[\s\S]*?primary[\s\S]*?onConfirm=\{\(\) => onUpgradeRecommended/.test(settingsSource),
168 "legacy official DeepSeek cards expose an explicit recommended-protocol action",
169 );
170 ok(
171 settingsSource.includes("const providerNames = group.providers.map((provider) => provider.name)") &&
172 settingsSource.includes('kind: "web_search_capability", names: providerNames, enabled') &&
173 !settingsSource.includes("app.SaveProvider({ ...provider, webSearch: enabled })"),
174 "grouped DeepSeek profiles update server-side web search through one atomic backend call",
175 );
176 ok(
177 /<div className="provider-access-card__actions">[\s\S]*?<ProviderAccessMoreMenu[\s\S]*?<\/div>\s*<\/div>\s*\{group\.description && !editingProvider && <div className="provider-access-card__desc">[\s\S]*?\{upgradeProvider && \(/.test(settingsSource) &&
178 settingsSource.includes('className="provider-access-more__menu"') &&
179 settingsSource.includes('buttonRole="menuitem"') &&
180 stylesSource.includes(".provider-protocol-upgrade") &&
181 stylesSource.includes(".provider-access-more__menu"),
182 "provider protocol migration stays in a stable row and removal lives in the overflow menu",
183 );
184 ok(
185 settingsSource.includes('className="provider-technical-details"') &&
186 settingsSource.includes('t("settings.providerCapabilities")') &&
187 settingsSource.includes('showModelSummary && (') &&
188 settingsSource.includes('hiddenModelCount={hiddenModelCount ?? 0}') &&
189 !settingsSource.includes('className="provider-capability-badges"') &&
190 !settingsSource.includes('t("settings.anthropicCompatible")') &&
191 stylesSource.includes('.provider-card-block--inline') &&
192 stylesSource.includes('.provider-technical-details'),
193 "provider cards keep descriptions out of the action row and collapse repeated diagnostics into compact details",
194 );
195 ok(
196 !settingsSource.includes("return p.baseUrl;") &&
197 settingsSource.includes("group.providers.every(providerSupportsServerWebSearchForView)") &&
198 settingsSource.includes("group.providers.every((provider) => Boolean(provider.webSearch))") &&
199 settingsSource.includes("supported={supportsServerWebSearch}"),
200 "all provider cards keep endpoint details collapsed and use backend web-search capability authority",
201 );
202 ok(
203 /existing\.recommendedUpgradeAvailable = existing\.recommendedUpgradeAvailable \|\| Boolean\(p\.recommendedUpgradeAvailable\)/.test(settingsSource) &&
204 /case "deepseek-flash":\s*case "deepseek-pro":\s*return "deepseek";/.test(settingsSource),
205 "legacy DeepSeek aliases remain grouped into one official provider card",
206 );
207 ok(
208 settingsSource.includes('className="btn btn--small provider-profile-row__refresh"') &&
209 settingsSource.includes('className="btn btn--small provider-profile-row__configure"') &&
210 stylesSource.includes('"profile-refresh profile-configure"') &&
211 stylesSource.includes(".provider-profile-row__refresh") &&
212 stylesSource.includes(".provider-profile-row__configure"),
213 "multi-profile provider actions move into stable narrow-screen grid areas",
214 );
215 ok(
216 /@media \(max-width: 900px\)[\s\S]*?\.settings-section__head\s*\{[\s\S]*?flex-direction:\s*column;[\s\S]*?align-items:\s*stretch;/.test(stylesSource),
217 "narrow settings section headings stretch so descriptions wrap inside the viewport",
218 );
219 ok(
220 [enLocaleSource, zhLocaleSource, zhTWLocaleSource].every((source) =>
221 source.includes('"settings.upgradeRecommendedProtocol"') &&
222 source.includes('"settings.providerAccess"') &&
223 source.includes('"settings.providerBaseUrlLabel"') &&
224 source.includes('"settings.providerApiKeyEnv"') &&
225 !source.includes('"settings.anthropicCompatible"'),
226 ),
227 "the compact DeepSeek access and upgrade flows are localized in every supported locale",
228 );
229 ok(
230 enLocaleSource.includes('"settings.serverWebSearchHint": "Searches only when needed; content is sent to the current model provider') &&
231 zhLocaleSource.includes('"settings.serverWebSearchHint": "仅在需要时搜索;内容会发送给当前供应商') &&
232 zhTWLocaleSource.includes('"settings.serverWebSearchHint": "僅在需要時搜尋;內容會傳送給目前供應商'),
233 "server web-search disclosure stays provider-neutral for compatible services",
234 );
235 ok(
236 /mockPreset\("token-rhythm",\s*"Token Rhythm"/.test(bridgeSource),
237 "browser mock exposes the Token Rhythm preset",
238 );
239 ok(
240 /function mockProviderPresetDisplayRank\(id: string\): number \{\s*if \(id === "opencode-go-recommended"\) return -3;\s*if \(id === "deepseek-responses"\) return -2;/.test(bridgeSource),
241 "browser mock keeps OpenCode Go recommended first and DeepSeek Responses next",
242 );
243
244 const values = new Map<string, string>();
245 const storage = {
246 getItem: (key: string) => values.get(key) ?? null,
247 setItem: (key: string, value: string) => values.set(key, value),
248 removeItem: (key: string) => values.delete(key),
249 clear: () => values.clear(),
250 key: (index: number) => [...values.keys()][index] ?? null,
251 get length() { return values.size; },
252 } as Storage;
253
254 ok(!onboardingWasDismissed(storage), "fresh installs have no onboarding dismissal marker");
255 ok(shouldOpenOnboarding(true, storage), "missing providers open the guide before dismissal");
256 ok(!shouldOpenOnboarding(false, storage), "configured providers never open the guide");
257 dismissOnboarding(storage);
258 ok(values.get(ONBOARDING_DISMISSED_STORAGE_KEY) === "1", "skip persists a versioned dismissal marker");
259 ok(!shouldOpenOnboarding(true, storage), "persisted skip prevents repeated full-screen interruption");
260
261 console.log(`\n${passed} passed, ${failed} failed`);
262 if (failed > 0) process.exit(1);
263
263 lines TYPESCRIPT