From 5a02a9365c2c3820e52e1404b651caa4bfd19bb7 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:51:34 -0700 Subject: [PATCH] fix(native-chat): restore runtime refusals behind the win32 gate 506d375de3 replaced the project-runtime checks with a bare platform test, so a WSL or repair-required runtime resolution would no longer refuse structured chat off-win32. Keep the unconditional win32 refusal and re-run the runtime resolution after it, so the gate does not depend on the resolver's own platform guard. Tests inject WSL and repair-required resolutions on darwin/linux and fail against the regressed gate. --- ...tructured-native-chat-availability.test.ts | 69 +++++++++++++++++++ .../structured-native-chat-availability.ts | 9 ++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/lib/structured-native-chat-availability.test.ts b/src/renderer/src/lib/structured-native-chat-availability.test.ts index 9e31bea2669..00abdbd1b86 100644 --- a/src/renderer/src/lib/structured-native-chat-availability.test.ts +++ b/src/renderer/src/lib/structured-native-chat-availability.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store/types' +import type * as localPreflightContext from '@/lib/local-preflight-context' +import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import { canUseStructuredNativeChat } from './structured-native-chat-availability' const { mockGetRendererAppPlatform } = vi.hoisted(() => ({ @@ -10,6 +12,43 @@ vi.mock('@/lib/renderer-app-platform', () => ({ getRendererAppPlatform: mockGetRendererAppPlatform })) +type GetLocalProjectExecutionRuntimeContext = + typeof localPreflightContext.getLocalProjectExecutionRuntimeContext + +const projectRuntimeMock = vi.hoisted(() => ({ + fn: vi.fn(), + actual: undefined as GetLocalProjectExecutionRuntimeContext | undefined +})) + +vi.mock('@/lib/local-preflight-context', async (importOriginal) => { + const actual = await importOriginal() + projectRuntimeMock.actual = actual.getLocalProjectExecutionRuntimeContext + return { ...actual, getLocalProjectExecutionRuntimeContext: projectRuntimeMock.fn } +}) + +const wslRuntimeResolution: ProjectExecutionRuntimeResolution = { + status: 'resolved', + runtime: { + kind: 'wsl', + hostPlatform: 'wsl', + projectId: 'repo-1', + distro: 'Ubuntu', + reason: 'project-override', + cacheKey: 'repo-1|wsl|Ubuntu' + } +} + +const repairRequiredResolution: ProjectExecutionRuntimeResolution = { + status: 'repair-required', + repair: { + projectId: 'repo-1', + preferredRuntime: { kind: 'wsl', distro: null }, + reason: 'wsl-distro-required', + source: 'project-override', + cacheKey: 'repo-1|wsl|repair' + } +} + function stateFor(input: { connectionId?: string | null windowsRuntime?: 'windows-host' | 'wsl' @@ -46,6 +85,13 @@ function stateFor(input: { describe('canUseStructuredNativeChat', () => { beforeEach(() => { mockGetRendererAppPlatform.mockReturnValue('darwin') + projectRuntimeMock.fn.mockReset() + projectRuntimeMock.fn.mockImplementation((...args) => { + if (!projectRuntimeMock.actual) { + throw new Error('real getLocalProjectExecutionRuntimeContext was never captured') + } + return projectRuntimeMock.actual(...args) + }) }) it('allows the structured stack on a local worktree', () => { @@ -101,4 +147,27 @@ describe('canUseStructuredNativeChat', () => { } as unknown as AppState expect(canUseStructuredNativeChat(state, 'folder:folder-1')).toBe(true) }) + + it.each(['darwin', 'linux'] as const)('allows a supported local worktree on %s', (platform) => { + mockGetRendererAppPlatform.mockReturnValue(platform) + expect(canUseStructuredNativeChat(stateFor({}), 'wt-1')).toBe(true) + }) + + it.each(['darwin', 'linux'] as const)( + 'refuses a WSL project runtime even when the renderer reports %s', + (platform) => { + mockGetRendererAppPlatform.mockReturnValue(platform) + projectRuntimeMock.fn.mockReturnValue(wslRuntimeResolution) + expect(canUseStructuredNativeChat(stateFor({}), 'wt-1')).toBe(false) + } + ) + + it.each(['darwin', 'linux'] as const)( + 'refuses a repair-required runtime even when the renderer reports %s', + (platform) => { + mockGetRendererAppPlatform.mockReturnValue(platform) + projectRuntimeMock.fn.mockReturnValue(repairRequiredResolution) + expect(canUseStructuredNativeChat(stateFor({}), 'wt-1')).toBe(false) + } + ) }) diff --git a/src/renderer/src/lib/structured-native-chat-availability.ts b/src/renderer/src/lib/structured-native-chat-availability.ts index 28df6ad0c40..9a22831bc11 100644 --- a/src/renderer/src/lib/structured-native-chat-availability.ts +++ b/src/renderer/src/lib/structured-native-chat-availability.ts @@ -1,4 +1,5 @@ import type { AppState } from '@/store/types' +import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import { getRendererAppPlatform } from '@/lib/renderer-app-platform' @@ -13,5 +14,11 @@ export function canUseStructuredNativeChat(state: AppState, worktreeId: string): // the host advertises that proof, refuse every local Windows execution path — // windows-host, WSL, and keys that resolve no project runtime (folder // workspaces, floating terminal) — so create cannot fail after the click. - return getRendererAppPlatform() !== 'win32' + if (getRendererAppPlatform() === 'win32') { + return false + } + // Refuse WSL and repair-required runtimes even if resolution ever runs + // off-win32; the gate must not depend on the resolver's platform guard. + const projectRuntime = getLocalProjectExecutionRuntimeContext(state, worktreeId) + return !(projectRuntime?.status === 'repair-required' || projectRuntime?.runtime.kind === 'wsl') }