diff --git a/src/core/prompts/sections/__tests__/system-info.spec.ts b/src/core/prompts/sections/__tests__/system-info.spec.ts index 7c3b53c426..9ba84dc5db 100644 --- a/src/core/prompts/sections/__tests__/system-info.spec.ts +++ b/src/core/prompts/sections/__tests__/system-info.spec.ts @@ -12,7 +12,7 @@ vi.mock("../../../../utils/shell", () => ({ import { getSystemInfoSection } from "../system-info" import osName from "os-name" -const mockOsName = osName as unknown as ReturnType +const mockOsName = vi.mocked(osName) describe("getSystemInfoSection", () => { const mockCwd = "/test/workspace" @@ -20,7 +20,7 @@ describe("getSystemInfoSection", () => { beforeEach(() => { vi.spyOn(os, "homedir").mockReturnValue(mockHomeDir) - vi.spyOn(os, "platform").mockReturnValue("linux" as any) + vi.spyOn(os, "platform").mockReturnValue("linux") vi.spyOn(os, "release").mockReturnValue("5.15.0") }) @@ -34,6 +34,7 @@ describe("getSystemInfoSection", () => { afterEach(() => { vi.clearAllMocks() + vi.restoreAllMocks() }) it("should return system info with os-name when available", () => { @@ -47,29 +48,64 @@ describe("getSystemInfoSection", () => { expect(result).toContain(`Current Workspace Directory: ${mockCwd}`) }) - it("should fallback to platform and release when os-name throws error", () => { - mockOsName.mockImplementation(() => { - throw new Error("Command failed with ENOENT: powershell") + it.each([ + { + platform: "linux" as const, + release: "5.15.0", + makeLookupThrow: () => + mockOsName.mockImplementation(() => { + throw new Error("Command failed with ENOENT: powershell") + }), + }, + { + platform: "win32" as const, + release: "10.0.26300", + makeLookupThrow: () => + vi.spyOn(os, "version").mockImplementation(() => { + throw new Error("uv_os_uname failed") + }), + }, + ])( + "falls back to platform and release when the OS lookup throws on $platform", + ({ platform, release, makeLookupThrow }) => { + vi.spyOn(os, "platform").mockReturnValue(platform) + vi.spyOn(os, "release").mockReturnValue(release) + makeLookupThrow() + + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain(`Operating System: ${platform} ${release}`) + expect(result).toContain("Default Shell: /bin/bash") + expect(result).toContain(`Home Directory: ${mockHomeDir}`) + expect(result).toContain(`Current Workspace Directory: ${mockCwd}`) + }, + ) + + describe("on Windows", () => { + beforeEach(() => { + vi.spyOn(os, "platform").mockReturnValue("win32") + vi.spyOn(os, "release").mockReturnValue("10.0.26300") }) - const result = getSystemInfoSection(mockCwd, policyFor()) + it("reads the product name from os.version() without calling os-name", () => { + // os-name runs wmic and PowerShell synchronously on Windows; a PowerShell that never + // exits freezes the extension host, so it must not be reached at all. + vi.spyOn(os, "version").mockReturnValue("Windows 11 Pro") - expect(result).toContain("Operating System: linux 5.15.0") - expect(result).toContain("Default Shell: /bin/bash") - expect(result).toContain(`Home Directory: ${mockHomeDir}`) - expect(result).toContain(`Current Workspace Directory: ${mockCwd}`) - }) + const result = getSystemInfoSection(mockCwd, policyFor()) - it("should handle Windows platform in fallback", () => { - mockOsName.mockImplementation(() => { - throw new Error("Command failed with ENOENT: powershell") + expect(result).toContain("Operating System: Windows 11 Pro") + expect(mockOsName).not.toHaveBeenCalled() }) - vi.spyOn(os, "platform").mockReturnValue("win32" as any) - vi.spyOn(os, "release").mockReturnValue("10.0.19043") - const result = getSystemInfoSection(mockCwd, policyFor()) + it("falls back to platform and release when os.version() is empty", () => { + vi.spyOn(os, "version").mockReturnValue("") - expect(result).toContain("Operating System: win32 10.0.19043") + const result = getSystemInfoSection(mockCwd, policyFor()) + + expect(result).toContain("Operating System: win32 10.0.26300") + expect(mockOsName).not.toHaveBeenCalled() + }) }) it("omits the terminal sentence when execute_command is absent", () => { diff --git a/src/core/prompts/sections/system-info.ts b/src/core/prompts/sections/system-info.ts index 003c555d75..224929d6bb 100644 --- a/src/core/prompts/sections/system-info.ts +++ b/src/core/prompts/sections/system-info.ts @@ -5,6 +5,28 @@ import { getShell } from "../../../utils/shell" import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" +/** + * Describes the host operating system for the system prompt. + * + * Windows bypasses `os-name`: its `windows-release` dependency runs `wmic`, then PowerShell, + * through `execaSync` with no timeout. This section is built for every request, so that call + * blocks the extension host for as long as the process runs. `os.version()` reads the product + * name without starting a process. + */ +function getOsInfo(): string { + const fallback = `${os.platform()} ${os.release()}` + + try { + if (os.platform() === "win32") { + return os.version() || fallback + } + + return osName() + } catch { + return fallback + } +} + /** * Builds the SYSTEM INFORMATION section of the system prompt. * @@ -16,16 +38,7 @@ import type { EffectiveToolPolicy } from "../tools/effective-tool-policy" * @param policy The request's effective tool policy. */ export function getSystemInfoSection(cwd: string, policy: EffectiveToolPolicy): string { - // Try to get detailed OS name, fall back to basic info if it fails - let osInfo: string - try { - osInfo = osName() - } catch { - // Fallback when os-name fails (e.g., PowerShell not available on Windows) - const platform = os.platform() - const release = os.release() - osInfo = `${platform} ${release}` - } + const osInfo = getOsInfo() const executeCommandAvailable = policy.tools.has("execute_command") diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index ae3d504003..d5b845997a 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -644,11 +644,6 @@ "count": 55 } }, - "core/prompts/sections/__tests__/system-info.spec.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 2 - } - }, "core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts": { "@typescript-eslint/no-explicit-any": { "count": 14