| 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | import { EnvironmentId } from "@t3tools/contracts"; |
| 3 | |
| 4 | import { |
| 5 | filterAvailableSettingsSearchItems, |
| 6 | getSettingsSearchTargetScope, |
| 7 | getThreadAutoSettlementSearchAvailability, |
| 8 | isSettingsOverviewVisible, |
| 9 | isSettingsSearchScopeAvailable, |
| 10 | searchableSetting, |
| 11 | searchSettings, |
| 12 | SETTINGS_SEARCH_ITEMS, |
| 13 | type SettingsSearchItem, |
| 14 | } from "./settingsSearch"; |
| 15 | |
| 16 | const ITEMS: ReadonlyArray<SettingsSearchItem> = [ |
| 17 | { |
| 18 | id: "word-wrap", |
| 19 | title: "Word wrap", |
| 20 | to: "/settings/general", |
| 21 | searchTerms: ["long lines in code previews"], |
| 22 | }, |
| 23 | { |
| 24 | id: "network-access", |
| 25 | title: "Network access", |
| 26 | to: "/settings/connections", |
| 27 | searchTerms: ["remote pairing backend"], |
| 28 | }, |
| 29 | { |
| 30 | id: "providers", |
| 31 | title: "Providers", |
| 32 | to: "/settings/providers", |
| 33 | searchTerms: ["claude codex agents"], |
| 34 | }, |
| 35 | { |
| 36 | id: "provider-updates", |
| 37 | title: "Update checks", |
| 38 | to: "/settings/general", |
| 39 | }, |
| 40 | { |
| 41 | id: "automatic-updates", |
| 42 | title: "Automatic updates", |
| 43 | to: "/settings/general", |
| 44 | }, |
| 45 | ]; |
| 46 | |
| 47 | describe("searchSettings", () => { |
| 48 | it.each(["OTel", "OTLP", "telemetry export", "Grafana"])( |
| 49 | "finds environment telemetry settings for %s", |
| 50 | (query) => { |
| 51 | expect(searchSettings(query)).toContainEqual( |
| 52 | expect.objectContaining({ |
| 53 | id: "telemetry-export", |
| 54 | to: "/settings/diagnostics", |
| 55 | targetId: "telemetry-export", |
| 56 | scope: "environment", |
| 57 | }), |
| 58 | ); |
| 59 | }, |
| 60 | ); |
| 61 | it.each(["send shortcut", "multiline", "new line"])("finds Send shortcut for %s", (query) => { |
| 62 | expect(searchSettings(query).map((item) => item.id)).toContain("send-shortcut"); |
| 63 | }); |
| 64 | |
| 65 | it("matches titles, sections, and remembered setting details", () => { |
| 66 | expect(searchSettings("word", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]); |
| 67 | expect(searchSettings("network", ITEMS).map((item) => item.id)).toEqual(["network-access"]); |
| 68 | expect(searchSettings("connections", ITEMS).map((item) => item.id)).toEqual(["network-access"]); |
| 69 | expect(searchSettings("claude", ITEMS).map((item) => item.id)).toEqual(["providers"]); |
| 70 | expect(searchSettings("long lines", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]); |
| 71 | }); |
| 72 | |
| 73 | it("matches normalized title substrings", () => { |
| 74 | expect(searchSettings(" WORD WRAP ", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]); |
| 75 | expect(searchSettings("glass").map((item) => item.id)).toEqual(["setting-glass-opacity"]); |
| 76 | expect(searchSettings("panel animations").map((item) => item.id)).toEqual(["panel-animations"]); |
| 77 | expect(searchSettings("thè\u{1ab0}mes")[0]?.id).toBe("theme"); |
| 78 | const localeLowerCase = vi.spyOn(String.prototype, "toLocaleLowerCase").mockReturnValue("gıt"); |
| 79 | try { |
| 80 | expect(searchSettings("GIT")[0]?.id).toBe("git-fetch-interval"); |
| 81 | expect(localeLowerCase).not.toHaveBeenCalled(); |
| 82 | } finally { |
| 83 | localeLowerCase.mockRestore(); |
| 84 | } |
| 85 | expect(searchSettings("xyzzy")).toEqual([]); |
| 86 | }); |
| 87 | |
| 88 | it("keeps catalog order for multiple title matches", () => { |
| 89 | expect(searchSettings("update", ITEMS).map((item) => item.id)).toEqual([ |
| 90 | "provider-updates", |
| 91 | "automatic-updates", |
| 92 | ]); |
| 93 | }); |
| 94 | |
| 95 | it("matches query words across fields and ranks the strongest result first", () => { |
| 96 | expect(searchSettings("pairing remote", ITEMS).map((item) => item.id)).toEqual([ |
| 97 | "network-access", |
| 98 | ]); |
| 99 | expect( |
| 100 | searchSettings("remote pairing") |
| 101 | .slice(0, 2) |
| 102 | .map((item) => item.id), |
| 103 | ).toEqual(["network-access", "connections-environment"]); |
| 104 | }); |
| 105 | |
| 106 | it("finds settings that used to be reachable only through their section", () => { |
| 107 | expect(searchSettings("pull request template")[0]?.id).toBe("follow-change-request-templates"); |
| 108 | expect(searchSettings("git security keys")[0]?.id).toBe("git-fetch-interval"); |
| 109 | expect(searchSettings("push notifications")[0]?.id).toBe("publish-agent-activity"); |
| 110 | expect(searchSettings("battery saver")[0]?.id).toBe("background-activity"); |
| 111 | expect(searchSettings("binary path")[0]?.id).toBe("providers"); |
| 112 | expect(searchSettings("Antigravity")[0]?.id).toBe("providers"); |
| 113 | expect(searchSettings("Google sign in")[0]?.id).toBe("providers"); |
| 114 | expect(searchSettings("authorized clients")[0]?.id).toBe("connections-environment"); |
| 115 | expect(searchSettings("administrative access")[0]?.id).toBe("connections-environment"); |
| 116 | }); |
| 117 | |
| 118 | it("lists thread confirmations in panel order", () => { |
| 119 | expect(searchSettings("confirmation").map((item) => item.id)).toEqual([ |
| 120 | "unpin-confirmation", |
| 121 | "archive-confirmation", |
| 122 | "delete-confirmation", |
| 123 | ]); |
| 124 | }); |
| 125 | |
| 126 | it.each(["usage providers", "CLIProxyAPI", "CLI proxy hub", "management key"])( |
| 127 | "finds usage-provider management by %s", |
| 128 | (query) => { |
| 129 | expect(searchSettings(query)[0]).toMatchObject({ |
| 130 | id: "usage-providers", |
| 131 | to: "/settings/providers", |
| 132 | }); |
| 133 | }, |
| 134 | ); |
| 135 | |
| 136 | it("returns no results for an empty query", () => { |
| 137 | expect(searchSettings(" ", ITEMS)).toEqual([]); |
| 138 | }); |
| 139 | |
| 140 | it("hides desktop-only settings from browser search", () => { |
| 141 | expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "quit-confirmation")).toBe(true); |
| 142 | expect(searchSettings("hold to quit")).toEqual([]); |
| 143 | expect(searchSettings("wsl")).toEqual([]); |
| 144 | expect(searchSettings("update track")).toEqual([]); |
| 145 | }); |
| 146 | |
| 147 | it("finds the About update rows", () => { |
| 148 | expect(searchSettings("check for updates").map((item) => item.id)).toContain("app-version"); |
| 149 | expect(SETTINGS_SEARCH_ITEMS.find((item) => item.id === "update-track")).toMatchObject({ |
| 150 | title: "Update track", |
| 151 | to: "/settings/general", |
| 152 | releaseChannelOnly: true, |
| 153 | }); |
| 154 | }); |
| 155 | |
| 156 | it("hides macOS-only settings on other platforms", () => { |
| 157 | vi.stubGlobal("navigator", { platform: "Win32" }); |
| 158 | try { |
| 159 | expect(searchSettings("font smoothing")).toEqual([]); |
| 160 | } finally { |
| 161 | vi.unstubAllGlobals(); |
| 162 | } |
| 163 | }); |
| 164 | |
| 165 | it("registers the WSL backend as a desktop-only setting", () => { |
| 166 | expect(SETTINGS_SEARCH_ITEMS.find((item) => item.id === "wsl-backend")).toMatchObject({ |
| 167 | id: "wsl-backend", |
| 168 | title: "WSL backend", |
| 169 | to: "/settings/connections", |
| 170 | desktopOnly: true, |
| 171 | windowsOnly: true, |
| 172 | }); |
| 173 | }); |
| 174 | |
| 175 | it("hides settings whose controls are unavailable", () => { |
| 176 | const available = filterAvailableSettingsSearchItems({ |
| 177 | hasCloudPublicConfig: false, |
| 178 | hasEnvironment: false, |
| 179 | hasProviderSettingsEnvironment: false, |
| 180 | hasMacProviderSettingsEnvironment: false, |
| 181 | canManageLocalBackend: false, |
| 182 | isWslSettingsRowVisible: false, |
| 183 | hasThreadAutoSettlement: false, |
| 184 | }); |
| 185 | |
| 186 | const gatedIds = new Set<string>([ |
| 187 | "follow-change-request-templates", |
| 188 | "git-fetch-interval", |
| 189 | "network-access", |
| 190 | "publish-agent-activity", |
| 191 | "provider-health-check-interval", |
| 192 | "cursor-keychain-usage", |
| 193 | "source-control-writer-model", |
| 194 | "review-guide-model", |
| 195 | "review-guide-instructions", |
| 196 | "source-control-writing-style", |
| 197 | "t3-connect", |
| 198 | "hold-webhooks-while-offline", |
| 199 | "tailscale-https", |
| 200 | "wsl-backend", |
| 201 | "auto-settle-inactive-threads", |
| 202 | "auto-settle-merged-threads", |
| 203 | "days-before-auto-settle", |
| 204 | ]); |
| 205 | expect(available.map((item) => item.id).filter((id) => gatedIds.has(id))).toEqual([]); |
| 206 | }); |
| 207 | |
| 208 | it("offers Cursor Keychain settings only when a macOS provider environment is available", () => { |
| 209 | const availability = { |
| 210 | hasCloudPublicConfig: false, |
| 211 | hasEnvironment: true, |
| 212 | hasProviderSettingsEnvironment: true, |
| 213 | hasMacProviderSettingsEnvironment: false, |
| 214 | canManageLocalBackend: false, |
| 215 | isWslSettingsRowVisible: false, |
| 216 | hasThreadAutoSettlement: false, |
| 217 | }; |
| 218 | const itemIds = (macAvailable: boolean) => |
| 219 | filterAvailableSettingsSearchItems({ |
| 220 | ...availability, |
| 221 | hasMacProviderSettingsEnvironment: macAvailable, |
| 222 | }).map((item) => item.id); |
| 223 | expect(itemIds(false)).not.toContain("cursor-keychain-usage"); |
| 224 | expect(itemIds(true)).toContain("cursor-keychain-usage"); |
| 225 | }); |
| 226 | |
| 227 | it("keeps the local toggle searchable without offering hidden host publishing controls", () => { |
| 228 | const availability = { |
| 229 | hasCloudPublicConfig: true, |
| 230 | hasEnvironment: true, |
| 231 | hasProviderSettingsEnvironment: true, |
| 232 | hasMacProviderSettingsEnvironment: false, |
| 233 | canManageLocalBackend: false, |
| 234 | isWslSettingsRowVisible: false, |
| 235 | hasThreadAutoSettlement: false, |
| 236 | }; |
| 237 | const remoteOnly = filterAvailableSettingsSearchItems({ |
| 238 | ...availability, |
| 239 | localEnvironmentDisabled: true, |
| 240 | }).map((item) => item.id); |
| 241 | expect(remoteOnly).toContain("local-environment"); |
| 242 | expect(remoteOnly).not.toContain("t3-connect"); |
| 243 | expect(remoteOnly).not.toContain("publish-agent-activity"); |
| 244 | expect(remoteOnly).not.toContain("wsl-backend"); |
| 245 | // Browsers without access:write still render CloudLinkRow for their host. |
| 246 | const browser = filterAvailableSettingsSearchItems(availability).map((item) => item.id); |
| 247 | expect(browser).toContain("publish-agent-activity"); |
| 248 | }); |
| 249 | |
| 250 | it("offers webhook holding only while the managed tunnel is on, like its row", () => { |
| 251 | const availability = { |
| 252 | hasCloudPublicConfig: true, |
| 253 | hasEnvironment: true, |
| 254 | hasProviderSettingsEnvironment: true, |
| 255 | hasMacProviderSettingsEnvironment: false, |
| 256 | canManageLocalBackend: true, |
| 257 | isWslSettingsRowVisible: false, |
| 258 | hasThreadAutoSettlement: false, |
| 259 | }; |
| 260 | const itemIds = (managedTunnelActive: boolean) => |
| 261 | filterAvailableSettingsSearchItems({ ...availability, managedTunnelActive }).map( |
| 262 | (item) => item.id, |
| 263 | ); |
| 264 | expect(itemIds(false)).not.toContain("hold-webhooks-while-offline"); |
| 265 | expect(itemIds(true)).toContain("hold-webhooks-while-offline"); |
| 266 | }); |
| 267 | |
| 268 | it("shows automatic settlement settings when the server supports them", () => { |
| 269 | const available = filterAvailableSettingsSearchItems({ |
| 270 | hasCloudPublicConfig: false, |
| 271 | hasEnvironment: false, |
| 272 | hasProviderSettingsEnvironment: false, |
| 273 | hasMacProviderSettingsEnvironment: false, |
| 274 | canManageLocalBackend: false, |
| 275 | isWslSettingsRowVisible: false, |
| 276 | hasThreadAutoSettlement: true, |
| 277 | }); |
| 278 | |
| 279 | expect(searchSettings("auto-settle", available).map((item) => item.id)).toEqual([ |
| 280 | "auto-settle-inactive-threads", |
| 281 | "auto-settle-merged-threads", |
| 282 | "days-before-auto-settle", |
| 283 | ]); |
| 284 | }); |
| 285 | |
| 286 | it("finds keybinding commands by label, command id, and default key", () => { |
| 287 | expect(searchSettings("toggle sidebar")[0]?.id).toBe("keybinding-sidebar.toggle"); |
| 288 | expect(searchSettings("sidebar.toggle")[0]?.id).toBe("keybinding-sidebar.toggle"); |
| 289 | expect(searchSettings("mod+b")[0]?.id).toBe("keybinding-sidebar.toggle"); |
| 290 | expect(searchSettings("copy link")[0]).toMatchObject({ |
| 291 | id: "keybinding-thread.copyReference", |
| 292 | to: "/settings/keybindings", |
| 293 | }); |
| 294 | }); |
| 295 | |
| 296 | it("ranks keybinding commands after other settings", () => { |
| 297 | const ids = searchSettings("model").map((item) => item.id); |
| 298 | expect(ids[0]).toBe("default-model"); |
| 299 | expect(ids.indexOf("keybinding-modelPicker.toggle")).toBeGreaterThan( |
| 300 | ids.indexOf("text-generation-model"), |
| 301 | ); |
| 302 | }); |
| 303 | |
| 304 | it("sends commands without a default binding to the section", () => { |
| 305 | expect(searchSettings("thread.stop")[0]).toMatchObject({ |
| 306 | id: "keybinding-thread.stop", |
| 307 | targetId: "keybindings", |
| 308 | }); |
| 309 | expect(searchSettings("sidebar.toggle")[0]?.targetId).toBeUndefined(); |
| 310 | }); |
| 311 | |
| 312 | it("keeps catalog result ids unique", () => { |
| 313 | const ids = SETTINGS_SEARCH_ITEMS.map((item) => item.id); |
| 314 | expect(new Set(ids).size).toBe(ids.length); |
| 315 | }); |
| 316 | |
| 317 | it("serves anchor props to panels from the catalog", () => { |
| 318 | expect(searchableSetting("word-wrap")).toEqual({ id: "word-wrap", title: "Word wrap" }); |
| 319 | expect(searchableSetting("archive")).toEqual({ id: "archive", title: "Archived threads" }); |
| 320 | }); |
| 321 | |
| 322 | it("routes appearance settings to their current section", () => { |
| 323 | expect(searchSettings("theme")[0]).toMatchObject({ |
| 324 | id: "theme", |
| 325 | to: "/settings/appearance", |
| 326 | }); |
| 327 | expect(searchSettings("word wrap")[0]).toMatchObject({ |
| 328 | id: "word-wrap", |
| 329 | to: "/settings/appearance", |
| 330 | }); |
| 331 | expect(searchSettings("composer context")[0]).toMatchObject({ |
| 332 | id: "composer-context", |
| 333 | to: "/settings/appearance", |
| 334 | }); |
| 335 | expect(searchSettings("environment identification")[0]).toMatchObject({ |
| 336 | id: "environment-identification", |
| 337 | to: "/settings/appearance", |
| 338 | targetId: "appearance-interface", |
| 339 | }); |
| 340 | }); |
| 341 | |
| 342 | it.each([ |
| 343 | "capture accessibility data", |
| 344 | "capture shortcut", |
| 345 | "capture sound", |
| 346 | "capture flash", |
| 347 | "capture animations", |
| 348 | ])("hides disabled SnapShots settings for %s", (query) => { |
| 349 | expect(searchSettings(query).some((item) => item.to === "/settings/snap-shot")).toBe(false); |
| 350 | }); |
| 351 | |
| 352 | it("routes browser recording quality to integrations", () => { |
| 353 | const result = searchSettings("recording frame rate")[0]; |
| 354 | expect(result).toMatchObject({ |
| 355 | id: "browser-recording-frame-rate", |
| 356 | to: "/settings/integrations", |
| 357 | }); |
| 358 | expect(result).not.toHaveProperty("targetId"); |
| 359 | }); |
| 360 | |
| 361 | it("routes where links open to integrations", () => { |
| 362 | expect(searchSettings("open links in")[0]).toMatchObject({ |
| 363 | id: "browser-link-target", |
| 364 | to: "/settings/integrations", |
| 365 | }); |
| 366 | expect(searchSettings("external links")[0]).toMatchObject({ id: "browser-link-target" }); |
| 367 | }); |
| 368 | |
| 369 | it("finds the default browser profile action in the profiles list", () => { |
| 370 | expect(searchSettings("default profile")[0]).toMatchObject({ |
| 371 | id: "browser-default-profile", |
| 372 | to: "/settings/integrations", |
| 373 | targetId: "browser-profiles", |
| 374 | }); |
| 375 | }); |
| 376 | |
| 377 | it.each([ |
| 378 | ["default model", "default-model", "/settings/general"], |
| 379 | ["new threads", "new-threads", "/settings/general"], |
| 380 | ["agent browser access", "agent-browser-access", "/settings/integrations"], |
| 381 | ["automatically pull", "automatic-pull", "/settings/source-control"], |
| 382 | ["actions", "project-actions", "/settings/projects"], |
| 383 | ["project overview", "project-overview", "/settings/projects"], |
| 384 | ])("routes %s to its owning category", (query, id, to) => { |
| 385 | expect(searchSettings(query)[0]).toMatchObject({ id, to }); |
| 386 | }); |
| 387 | |
| 388 | it("keeps environment settings discoverable without a primary environment", () => { |
| 389 | const available = filterAvailableSettingsSearchItems({ |
| 390 | hasCloudPublicConfig: false, |
| 391 | hasEnvironment: true, |
| 392 | hasProviderSettingsEnvironment: true, |
| 393 | hasMacProviderSettingsEnvironment: false, |
| 394 | canManageLocalBackend: false, |
| 395 | isWslSettingsRowVisible: false, |
| 396 | hasThreadAutoSettlement: true, |
| 397 | }); |
| 398 | expect(searchSettings("writing style", available)[0]?.id).toBe("source-control-writing-style"); |
| 399 | expect(searchSettings("auto-settle", available)).toHaveLength(3); |
| 400 | }); |
| 401 | }); |
| 402 | |
| 403 | describe("settings search targets", () => { |
| 404 | it("requires an environment for the resolved telemetry export destination", () => { |
| 405 | const result = searchSettings("OpenTelemetry export").find( |
| 406 | (item) => item.id === "telemetry-export", |
| 407 | )!; |
| 408 | const target = getSettingsSearchTargetScope(result.targetId ?? result.id)!; |
| 409 | expect(isSettingsSearchScopeAvailable(target.scope, "all")).toBe(false); |
| 410 | expect(isSettingsSearchScopeAvailable(target.scope, "project")).toBe(false); |
| 411 | expect(isSettingsSearchScopeAvailable(target.scope, "environment")).toBe(true); |
| 412 | }); |
| 413 | it.each([ |
| 414 | "auto-settle-inactive-threads", |
| 415 | "auto-settle-merged-threads", |
| 416 | "days-before-auto-settle", |
| 417 | ])("retains the capability requirement for %s", (targetId) => { |
| 418 | expect(getSettingsSearchTargetScope(targetId)).toMatchObject({ |
| 419 | scope: "project-defaults", |
| 420 | requiresThreadAutoSettlement: true, |
| 421 | }); |
| 422 | }); |
| 423 | |
| 424 | it("treats device-local rows as reachable from every selection", () => { |
| 425 | const setting = getSettingsSearchTargetScope("time-format")!; |
| 426 | expect(setting).toEqual({ title: "Time format", scope: null }); |
| 427 | expect(isSettingsSearchScopeAvailable(setting.scope, "project")).toBe(true); |
| 428 | expect(isSettingsSearchScopeAvailable(setting.scope, "all")).toBe(true); |
| 429 | expect(getSettingsSearchTargetScope("appearance")).toMatchObject({ scope: null }); |
| 430 | expect(getSettingsSearchTargetScope("missing-setting")).toBeNull(); |
| 431 | }); |
| 432 | |
| 433 | it.each(["all", "environment", "project", "checkout"] as const)( |
| 434 | "makes browser access editable at the %s scope", |
| 435 | (kind) => { |
| 436 | const setting = getSettingsSearchTargetScope("agent-browser-access")!; |
| 437 | expect(isSettingsSearchScopeAvailable(setting.scope, kind)).toBe(true); |
| 438 | expect(isSettingsSearchScopeAvailable(setting.scope, "unavailable")).toBe(false); |
| 439 | }, |
| 440 | ); |
| 441 | |
| 442 | it("lets project-scopable rows resolve at every server-backed scope", () => { |
| 443 | const model = getSettingsSearchTargetScope("text-generation-model")!; |
| 444 | expect(isSettingsSearchScopeAvailable(model.scope, "all")).toBe(true); |
| 445 | expect(isSettingsSearchScopeAvailable(model.scope, "environment")).toBe(true); |
| 446 | expect(isSettingsSearchScopeAvailable(model.scope, "project")).toBe(true); |
| 447 | }); |
| 448 | |
| 449 | it("reaches source control discovery and git fetch interval from the default scope", () => { |
| 450 | for (const id of ["source-control", "git-fetch-interval"]) { |
| 451 | const item = getSettingsSearchTargetScope(id)!; |
| 452 | expect(isSettingsSearchScopeAvailable(item.scope, "all")).toBe(true); |
| 453 | expect(isSettingsSearchScopeAvailable(item.scope, "environment")).toBe(true); |
| 454 | expect(isSettingsSearchScopeAvailable(item.scope, "project")).toBe(false); |
| 455 | } |
| 456 | }); |
| 457 | |
| 458 | it("keeps environment-wide settings out of project scopes", () => { |
| 459 | const updates = getSettingsSearchTargetScope("provider-update-checks")!; |
| 460 | expect(updates.scope).toBe("environment-defaults"); |
| 461 | expect(isSettingsSearchScopeAvailable(updates.scope, "environment")).toBe(true); |
| 462 | expect(isSettingsSearchScopeAvailable(updates.scope, "all")).toBe(true); |
| 463 | expect(isSettingsSearchScopeAvailable(updates.scope, "project")).toBe(false); |
| 464 | const streaming = getSettingsSearchTargetScope("response-streaming")!; |
| 465 | expect(streaming.scope).toBe("project-defaults"); |
| 466 | expect(isSettingsSearchScopeAvailable(streaming.scope, "project")).toBe(true); |
| 467 | for (const id of ["legacy-plan-mode", "legacy-context-window-indicator", "legacy-sidebar"]) { |
| 468 | expect(getSettingsSearchTargetScope(id)!.scope).toBeNull(); |
| 469 | } |
| 470 | }); |
| 471 | }); |
| 472 | |
| 473 | describe("auto-settlement search availability", () => { |
| 474 | function environment(id: string, { connected = true, loaded = true, supported = true } = {}) { |
| 475 | return { |
| 476 | environmentId: EnvironmentId.make(id), |
| 477 | connection: { phase: connected ? ("connected" as const) : ("offline" as const) }, |
| 478 | serverConfig: loaded |
| 479 | ? { environment: { capabilities: { threadAutoSettlement: supported } } } |
| 480 | : null, |
| 481 | }; |
| 482 | } |
| 483 | |
| 484 | const capable = environment("capable"); |
| 485 | const unsupported = environment("unsupported", { supported: false }); |
| 486 | const offline = environment("offline", { connected: false }); |
| 487 | const loading = environment("loading", { loaded: false }); |
| 488 | const environments = [capable, unsupported, offline, loading]; |
| 489 | |
| 490 | it("keeps results discoverable when one connected environment supports them", () => { |
| 491 | const availability = getThreadAutoSettlementSearchAvailability(environments); |
| 492 | expect(availability.eligibleEnvironmentIds).toEqual([capable.environmentId]); |
| 493 | const items = filterAvailableSettingsSearchItems({ |
| 494 | hasCloudPublicConfig: false, |
| 495 | hasEnvironment: true, |
| 496 | hasProviderSettingsEnvironment: true, |
| 497 | hasMacProviderSettingsEnvironment: false, |
| 498 | canManageLocalBackend: false, |
| 499 | isWslSettingsRowVisible: false, |
| 500 | hasThreadAutoSettlement: availability.eligibleEnvironmentIds.length > 0, |
| 501 | }); |
| 502 | expect(searchSettings("auto-settle", items).map((item) => item.id)).toEqual([ |
| 503 | "auto-settle-inactive-threads", |
| 504 | "auto-settle-merged-threads", |
| 505 | "days-before-auto-settle", |
| 506 | ]); |
| 507 | }); |
| 508 | |
| 509 | it("offers only capable environments when an aggregate has mixed capabilities", () => { |
| 510 | expect( |
| 511 | getThreadAutoSettlementSearchAvailability(environments, { |
| 512 | kind: "all", |
| 513 | environmentIds: environments.map((entry) => entry.environmentId), |
| 514 | }), |
| 515 | ).toEqual({ eligibleEnvironmentIds: [capable.environmentId], isTargetAvailable: false }); |
| 516 | }); |
| 517 | |
| 518 | it("allows a capable named environment regardless of other environments' capabilities", () => { |
| 519 | expect( |
| 520 | getThreadAutoSettlementSearchAvailability(environments, { |
| 521 | kind: "environment", |
| 522 | environmentIds: [capable.environmentId], |
| 523 | }).isTargetAvailable, |
| 524 | ).toBe(true); |
| 525 | }); |
| 526 | |
| 527 | it.each([unsupported, offline, loading])( |
| 528 | "does not render the target on $environmentId or silently fall back", |
| 529 | (selected) => { |
| 530 | expect( |
| 531 | getThreadAutoSettlementSearchAvailability(environments, { |
| 532 | kind: "environment", |
| 533 | environmentIds: [selected.environmentId], |
| 534 | }), |
| 535 | ).toEqual({ eligibleEnvironmentIds: [capable.environmentId], isTargetAvailable: false }); |
| 536 | }, |
| 537 | ); |
| 538 | |
| 539 | it("offers a capable environment instead of a dead target at an unavailable scope", () => { |
| 540 | expect( |
| 541 | getThreadAutoSettlementSearchAvailability(environments, { |
| 542 | kind: "unavailable", |
| 543 | environmentIds: [capable.environmentId], |
| 544 | }), |
| 545 | ).toEqual({ eligibleEnvironmentIds: [capable.environmentId], isTargetAvailable: false }); |
| 546 | }); |
| 547 | |
| 548 | it("ignores offline and unloaded targets when all connected targets support the setting", () => { |
| 549 | const selected = [capable, offline, loading]; |
| 550 | expect( |
| 551 | getThreadAutoSettlementSearchAvailability(selected, { |
| 552 | kind: "all", |
| 553 | environmentIds: selected.map((entry) => entry.environmentId), |
| 554 | }).isTargetAvailable, |
| 555 | ).toBe(true); |
| 556 | }); |
| 557 | |
| 558 | it("offers no unavailable environments when none can render the setting", () => { |
| 559 | const selected = [unsupported, offline, loading]; |
| 560 | expect( |
| 561 | getThreadAutoSettlementSearchAvailability(selected, { |
| 562 | kind: "all", |
| 563 | environmentIds: selected.map((entry) => entry.environmentId), |
| 564 | }), |
| 565 | ).toEqual({ eligibleEnvironmentIds: [], isTargetAvailable: false }); |
| 566 | expect(getThreadAutoSettlementSearchAvailability([]).eligibleEnvironmentIds).toEqual([]); |
| 567 | }); |
| 568 | }); |
| 569 | |
| 570 | describe("settings sidebar scope", () => { |
| 571 | it("shows Overview only for project and checkout targets", () => { |
| 572 | expect(isSettingsOverviewVisible({})).toBe(false); |
| 573 | expect(isSettingsOverviewVisible({ machine: "remote" })).toBe(false); |
| 574 | expect(isSettingsOverviewVisible({ project: "project" })).toBe(true); |
| 575 | expect(isSettingsOverviewVisible({ project: "project", checkout: "checkout" })).toBe(true); |
| 576 | }); |
| 577 | }); |