mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
fix(wsl): turning the WSL hosts setting off keeps saved hosts serving; it stops only new hosts and moves (#36783)
* fix(wsl): turning the WSL hosts setting off keeps saved hosts serving; it stops only new hosts and moves * chore(wsl): drop the stale run-refusal doc comment
This commit is contained in:
@@ -92,7 +92,8 @@ export function registerWslMoveWiring(store: Store, runtime: OrcaRuntimeService)
|
||||
() => app.getPath('userData'),
|
||||
(senderId) => getTrustedUIRendererWebContents()?.id === senderId,
|
||||
() => createWslMoveDriverDeps(store, runtime, 'user'),
|
||||
() => createWslMoveUndoPorts(store, app.getPath('userData'))
|
||||
() => createWslMoveUndoPorts(store, app.getPath('userData')),
|
||||
() => store.getSettings()
|
||||
)
|
||||
registerWslMoveOrchestrator(store, () => createWslMoveDriverDeps(store, runtime, 'automatic'))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate'
|
||||
import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture'
|
||||
import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding'
|
||||
import { listWslMoveCutovers } from '../persistence/moving-wsl-projects/wsl-move-cutover-journal'
|
||||
import {
|
||||
loadWslMoveFence,
|
||||
resetWslMoveFenceForTests
|
||||
} from '../persistence/moving-wsl-projects/wsl-move-scope-guard'
|
||||
import { setWslMoveReloadableWindow } from '../persistence/moving-wsl-projects/wsl-move-window-write-hold'
|
||||
|
||||
type Handler = (event: unknown, request?: unknown) => unknown
|
||||
|
||||
const handlers = vi.hoisted(() => new Map<string, Handler>())
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
removeHandler: (channel: string) => handlers.delete(channel),
|
||||
handle: (channel: string, handler: Handler) => handlers.set(channel, handler)
|
||||
}
|
||||
}))
|
||||
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
|
||||
|
||||
import { registerWslMoveHandlers } from './wsl-move-handlers'
|
||||
|
||||
const MAIN_WINDOW_ID = 7
|
||||
const sender = { id: MAIN_WINDOW_ID, session: { flushStorageData: vi.fn() } }
|
||||
const invoke = (channel: string, request?: unknown) => handlers.get(channel)?.({ sender }, request)
|
||||
|
||||
// Stage 1: the WSL hosts setting governs new work only; with it off, a moved project still serves.
|
||||
describe('WSL move IPC with the WSL hosts setting off', () => {
|
||||
let userDataPath: string
|
||||
const driverDeps = vi.fn(() => {
|
||||
throw new Error('a new move must not build the driver')
|
||||
})
|
||||
const stopHostCopies = vi.fn(async () => 'stopped' as const)
|
||||
|
||||
beforeEach(() => {
|
||||
userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-setting-off-'))
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed'))
|
||||
loadWslMoveFence(userDataPath)
|
||||
driverDeps.mockClear()
|
||||
stopHostCopies.mockClear()
|
||||
registerWslMoveHandlers(
|
||||
() => userDataPath,
|
||||
(senderId) => senderId === MAIN_WINDOW_ID,
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a refused move never calls it; the stub throws if one does.
|
||||
driverDeps as never,
|
||||
() => ({ stopHostCopies }),
|
||||
() => ({ experimentalWslHosts: false })
|
||||
)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
resetWslMoveFenceForTests()
|
||||
setWslMoveReloadableWindow(() => null)
|
||||
rmSync(userDataPath, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('refuses a new move and its preview, building nothing', async () => {
|
||||
for (const channel of ['wslMove:preview', 'wslMove:start']) {
|
||||
expect(await invoke(channel, { distro: 'Ubuntu', repoIds: ['repo-1'] })).toEqual({
|
||||
outcome: 'refused',
|
||||
code: 'wsl_move_unavailable',
|
||||
detail: 'wsl-hosts-off'
|
||||
})
|
||||
}
|
||||
expect(driverDeps).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still brings a moved project back ("Use on this computer again")', async () => {
|
||||
const page = { loaded: () => {} }
|
||||
const mainWindow = {
|
||||
id: MAIN_WINDOW_ID,
|
||||
isDestroyed: () => false,
|
||||
reload: vi.fn(),
|
||||
allowUnload: () => () => {},
|
||||
onLoadedOrGone: (listener: () => void) => {
|
||||
page.loaded = listener
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
setWslMoveReloadableWindow(() => mainWindow)
|
||||
const result = await handlers.get('wslMove:useProjectHereAgain')?.(
|
||||
{ sender: mainWindow },
|
||||
{ kind: 'repo', id: 'repo-1' }
|
||||
)
|
||||
expect(result).toMatchObject({ released: true })
|
||||
expect(stopHostCopies).toHaveBeenCalledOnce()
|
||||
expect(listWslMoveCutovers(userDataPath)).toEqual([])
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
page.loaded()
|
||||
})
|
||||
})
|
||||
@@ -30,7 +30,14 @@ import { listUnfinishedWslMoves } from '../persistence/moving-wsl-projects/wsl-m
|
||||
import { runTrackedWslMove } from '../telemetry/wsl-move-telemetry'
|
||||
import { runTrackedWslMoveUndo } from '../telemetry/wsl-move-undo-telemetry'
|
||||
import { isAnyWslMoveRunning } from '../persistence/moving-wsl-projects/wsl-move-running-moves'
|
||||
import { wslHostsAcceptNewWork, wslHostsGateOpen, wslHostsServing } from '../wsl/wsl-host-gate'
|
||||
import {
|
||||
wslHostsAcceptNewWork,
|
||||
wslHostsGateOpen,
|
||||
wslHostsOptedIn,
|
||||
wslHostsServing
|
||||
} from '../wsl/wsl-host-gate'
|
||||
|
||||
type WslHostsSetting = Parameters<typeof wslHostsOptedIn>[0]
|
||||
import type { WslMoveUndoPorts } from './wsl-move-undo-wiring'
|
||||
import { releaseWslMovedProjectHere } from './wsl-move-release-here'
|
||||
|
||||
@@ -54,7 +61,9 @@ export function registerWslMoveHandlers(
|
||||
/** Built per call, only once the gate is open. */
|
||||
driverDeps?: () => WslMoveDriverDeps,
|
||||
/** Undo's reach into the host; absent, there is no host to stop. */
|
||||
undoPorts: () => WslMoveUndoPorts = () => ({ stopHostCopies: async () => 'stopped' })
|
||||
undoPorts: () => WslMoveUndoPorts = () => ({ stopHostCopies: async () => 'stopped' }),
|
||||
/** The user's settings: the WSL hosts setting governs new moves; with it off, moved projects keep serving. */
|
||||
readSettings: () => WslHostsSetting = () => ({ experimentalWslHosts: true })
|
||||
): void {
|
||||
for (const channel of WSL_MOVE_HANDLER_CHANNELS) {
|
||||
ipcMain.removeHandler(channel)
|
||||
@@ -87,14 +96,15 @@ export function registerWslMoveHandlers(
|
||||
)
|
||||
}
|
||||
)
|
||||
registerWslMoveDriverHandlers(getUserDataPath, isMainWindowRenderer, driverDeps)
|
||||
registerWslMoveDriverHandlers(getUserDataPath, isMainWindowRenderer, driverDeps, readSettings)
|
||||
registerWslMoveUseHereHandler(getUserDataPath, isMainWindowRenderer, undoPorts)
|
||||
}
|
||||
|
||||
function registerWslMoveDriverHandlers(
|
||||
getUserDataPath: () => string,
|
||||
isMainWindowRenderer: (senderId: number) => boolean,
|
||||
driverDeps: (() => WslMoveDriverDeps) | undefined
|
||||
driverDeps: (() => WslMoveDriverDeps) | undefined,
|
||||
readSettings: () => WslHostsSetting
|
||||
): void {
|
||||
// Gate first: with it closed, no dependency is built and nothing is read. A new move also needs
|
||||
// no block learned this run; cancelling a running one never does.
|
||||
@@ -110,12 +120,18 @@ function registerWslMoveDriverHandlers(
|
||||
? null
|
||||
: { outcome: 'refused', code: 'wsl_move_unavailable', detail: 'not-main-window' }
|
||||
}
|
||||
// A new move is new work: with the setting off it is refused, as serve-only refuses it.
|
||||
const refuseNewMove = (event: RendererEvent): WslMoveRefusal | null =>
|
||||
refuseUnlessOpen(event) ??
|
||||
(wslHostsOptedIn(readSettings())
|
||||
? null
|
||||
: { outcome: 'refused', code: 'wsl_move_unavailable', detail: 'wsl-hosts-off' })
|
||||
ipcMain.handle(
|
||||
'wslMove:preview',
|
||||
async (event: RendererEvent, request: unknown): Promise<WslMovePreview> => {
|
||||
const parsed = parseWslMoveRequest(request)
|
||||
return (
|
||||
refuseUnlessOpen(event) ??
|
||||
refuseNewMove(event) ??
|
||||
(parsed && driverDeps ? previewWslMove(driverDeps(), parsed) : invalidRequest())
|
||||
)
|
||||
}
|
||||
@@ -125,7 +141,7 @@ function registerWslMoveDriverHandlers(
|
||||
async (event: RendererEvent, request: unknown): Promise<WslMoveResult> => {
|
||||
const parsed = parseWslMoveRequest(request)
|
||||
return (
|
||||
refuseUnlessOpen(event) ??
|
||||
refuseNewMove(event) ??
|
||||
(parsed && driverDeps
|
||||
? runTrackedWslMove(driverDeps(), (deps) => moveProjectsToWslHost(deps, parsed))
|
||||
: invalidRequest())
|
||||
|
||||
@@ -163,10 +163,17 @@ const profile = vi.hoisted(() => ({
|
||||
repos: [{ id: 'r1', path: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\app', connectionId: null }]
|
||||
}))
|
||||
|
||||
// The user's WSL hosts setting: moves are new work, so they need it on.
|
||||
const setting = { on: true }
|
||||
|
||||
async function register(): Promise<void> {
|
||||
const { registerWslMoveOrchestrator } = await import('./wsl-move-orchestrator-wiring')
|
||||
const store = { getRepos: () => profile.repos, getFolderWorkspaces: () => [] }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The wiring reads only the projects of the store, and the driver is mocked.
|
||||
const store = {
|
||||
getRepos: () => profile.repos,
|
||||
getFolderWorkspaces: () => [],
|
||||
getSettings: () => ({ experimentalWslHosts: setting.on })
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The wiring reads only the projects and the WSL hosts setting of the store, and the driver is mocked.
|
||||
registerWslMoveOrchestrator(store as never, () => ({ now: () => new Date(0) }) as never)
|
||||
}
|
||||
|
||||
@@ -367,6 +374,54 @@ describe('WSL move orchestrator wiring', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Setting off is like serve-only: moved projects keep serving, but nothing moves or is added.
|
||||
it('moves nothing and adds no host with the WSL hosts setting off', async () => {
|
||||
setting.on = false
|
||||
try {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
await register()
|
||||
await handlers.get('wslMoveOrchestrator:projectOpened')?.(
|
||||
{ sender: { id: MAIN_WINDOW_ID } },
|
||||
'Ubuntu'
|
||||
)
|
||||
const retry = await handlers.get('wslMoveOrchestrator:userAction')?.(
|
||||
{ sender: { id: MAIN_WINDOW_ID } },
|
||||
{ distro: 'Ubuntu', action: 'retry' }
|
||||
)
|
||||
await answerWindow()
|
||||
expect(retry).toBeNull()
|
||||
expect(host.added).toEqual([])
|
||||
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
setting.on = true
|
||||
}
|
||||
})
|
||||
|
||||
// Like a live block: a move already under way stays listed and can still be settled.
|
||||
it('keeps listing a recorded move and accepts Not now with the setting off', async () => {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
await register()
|
||||
for (const listener of releases.listeners) {
|
||||
listener('Ubuntu')
|
||||
}
|
||||
setting.on = false
|
||||
try {
|
||||
expect(await handlers.get('wslMoveOrchestrator:list')?.({})).toEqual([
|
||||
expect.objectContaining({ distro: 'Ubuntu' })
|
||||
])
|
||||
const notNow = await handlers.get('wslMoveOrchestrator:userAction')?.(
|
||||
{ sender: { id: MAIN_WINDOW_ID } },
|
||||
{ distro: 'Ubuntu', action: 'not-now' }
|
||||
)
|
||||
expect(notNow).toEqual({ record: expect.objectContaining({ distro: 'Ubuntu' }) })
|
||||
await answerWindow()
|
||||
expect(host.added).toEqual([])
|
||||
expect(driver.moveProjectsToWslHost).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
setting.on = true
|
||||
}
|
||||
})
|
||||
|
||||
it('T2: never adds a host for a stopped distro or one whose state cannot be read', async () => {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
await register()
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '../telemetry/wsl-move-orchestrator-telemetry'
|
||||
import { runTrackedWslMove } from '../telemetry/wsl-move-telemetry'
|
||||
import { readWslDistroRegistrations } from '../wsl/wsl-distro-registry-id'
|
||||
import { wslHostsAcceptNewWork, wslHostsGateOpen } from '../wsl/wsl-host-gate'
|
||||
import { wslHostsAcceptNewWork, wslHostsGateOpen, wslHostsOptedIn } from '../wsl/wsl-host-gate'
|
||||
import { wslHostAddRefusal } from '../wsl/wsl-host-add-admission'
|
||||
import { runAsWslUserAct } from '../wsl/wsl-unc-access-gate'
|
||||
import { getWslHostService } from '../wsl/wsl-host-service-instance'
|
||||
@@ -127,9 +127,9 @@ function createOrchestrator(
|
||||
return new WslMoveOrchestrator({
|
||||
now: () => Date.now(),
|
||||
appVersion: app.getVersion(),
|
||||
// A block learned this run stops automatic moves at once, like the opt-out env.
|
||||
// A block learned this run, or the setting turned off, stops automatic moves at once.
|
||||
autoMoveEnabled: () =>
|
||||
process.env[WSL_MOVE_ORCHESTRATOR_AUTO_ENV] !== '0' && wslHostsAcceptNewWork(),
|
||||
process.env[WSL_MOVE_ORCHESTRATOR_AUTO_ENV] !== '0' && wslHostsOptedIn(store.getSettings()),
|
||||
autoMovesBridgeProjects: wslMoveAutoMovesBridgeProjects,
|
||||
prepareHost: (distro, stillWanted) => prepareHost(userDataPath, distro, stillWanted),
|
||||
isDistroInUse: (distro) => isDistroInUse(store, userDataPath, distro),
|
||||
@@ -214,6 +214,7 @@ export function registerWslMoveOrchestrator(
|
||||
}
|
||||
const isMainWindow = (sender: WebContents | undefined): boolean =>
|
||||
!!sender && getTrustedUIRendererWebContents()?.id === sender.id
|
||||
// Not setting-gated: a move already in flight when the setting turns off stays visible.
|
||||
ipcMain.handle('wslMoveOrchestrator:list', (): WslMoveOrchestratorRecord[] =>
|
||||
orchestrator && wslHostsGateOpen() ? orchestrator.list() : []
|
||||
)
|
||||
@@ -223,7 +224,7 @@ export function registerWslMoveOrchestrator(
|
||||
(event: { sender?: WebContents }, distro: unknown): void => {
|
||||
if (
|
||||
orchestrator &&
|
||||
wslHostsAcceptNewWork() &&
|
||||
wslHostsOptedIn(store.getSettings()) &&
|
||||
isMainWindow(event.sender) &&
|
||||
typeof distro === 'string'
|
||||
) {
|
||||
@@ -247,8 +248,8 @@ export function registerWslMoveOrchestrator(
|
||||
return null
|
||||
}
|
||||
const action = USER_ACTIONS.find((candidate) => candidate === request.action)
|
||||
// Retry starts a move; Restore and Not now stay available after a block learned this run.
|
||||
if (!action || (action === 'retry' && !wslHostsAcceptNewWork())) {
|
||||
// Retry starts a move; Restore and Not now stay available after a block or with the setting off.
|
||||
if (!action || (action === 'retry' && !wslHostsOptedIn(store.getSettings()))) {
|
||||
return null
|
||||
}
|
||||
const unsaved =
|
||||
|
||||
@@ -51,20 +51,27 @@ export function wslHostRemovalPendingMessage(distro: string): string {
|
||||
return `Orca didn’t finish removing the WSL host ${distro}. Remove it again in Settings → Remote Orca Servers, or connect it to keep it.`
|
||||
}
|
||||
|
||||
/** Why no WSL host may run at all right now; null when they may. */
|
||||
/**
|
||||
* Why a saved host can't run now. The WSL hosts setting governs new work only (Stage 1): with it
|
||||
* off, saved hosts keep serving their projects, as in serve-only.
|
||||
*/
|
||||
export function wslHostRunRefusal(gate: {
|
||||
isEnabled: () => boolean
|
||||
hasServerFiles: () => boolean
|
||||
}): WslHostAddRefusal | null {
|
||||
if (!gate.isEnabled()) {
|
||||
return wslHostAddRefusal('disabled', 'turnedOff')
|
||||
}
|
||||
// Reject missing packaged templates before C4 deploy emits an opaque deep failure.
|
||||
return gate.hasServerFiles()
|
||||
? null
|
||||
: wslHostAddRefusal('server-files-missing', 'serverFilesMissing')
|
||||
}
|
||||
|
||||
/** Why a new host can't be added now: the setting is off, or no saved host could run either. */
|
||||
export function wslHostNewWorkRefusal(gate: {
|
||||
isEnabled: () => boolean
|
||||
hasServerFiles: () => boolean
|
||||
}): WslHostAddRefusal | null {
|
||||
return gate.isEnabled() ? wslHostRunRefusal(gate) : wslHostAddRefusal('disabled', 'turnedOff')
|
||||
}
|
||||
|
||||
export function sameWslDistro(left: string, right: string): boolean {
|
||||
return wslDistroHolderKey(left) === wslDistroHolderKey(right)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ import { errorMessage } from '../../shared/error-message'
|
||||
import { addWslHostEnvironment } from '../../shared/runtime-environment-wsl-host-store'
|
||||
import type { WslHostDeploymentRecord } from '../../shared/wsl-host-deployment-store'
|
||||
import { wslHostEnvironmentLabel, type WslHostAddResult } from '../../shared/wsl-host-types'
|
||||
import { admitWslHostDistro, wslHostAddRefusal, wslHostRunRefusal } from './wsl-host-add-admission'
|
||||
import {
|
||||
admitWslHostDistro,
|
||||
wslHostAddRefusal,
|
||||
wslHostNewWorkRefusal
|
||||
} from './wsl-host-add-admission'
|
||||
import { abandonWslHostAdd, connectForWslHostAdd } from './wsl-host-first-connect'
|
||||
import type { WslHostConnector, WslHostServiceDependencies } from './wsl-host-service'
|
||||
import { wslHostSshManagedRefusal } from './wsl-host-ssh-orcad-conflict'
|
||||
@@ -26,7 +30,7 @@ export async function registerWslHost(
|
||||
context: WslHostRegistrationContext,
|
||||
args: { distro: string; startIfStopped?: boolean }
|
||||
): Promise<WslHostAddResult> {
|
||||
const refusal = wslHostRunRefusal(context.deps)
|
||||
const refusal = wslHostNewWorkRefusal(context.deps)
|
||||
if (refusal) {
|
||||
return refusal
|
||||
}
|
||||
@@ -51,7 +55,7 @@ export async function registerWslHost(
|
||||
const environmentId = defaultWslOrcadEnvironmentId(distro.name)
|
||||
const connected = await connectForWslHostAdd(context.wsl(), distro.name, () =>
|
||||
generation === context.disableGeneration()
|
||||
? wslHostRunRefusal(context.deps)
|
||||
? wslHostNewWorkRefusal(context.deps)
|
||||
: wslHostAddRefusal('disabled', 'turnedOff')
|
||||
)
|
||||
if (!('connection' in connected)) {
|
||||
|
||||
@@ -281,7 +281,8 @@ describe('WslHostService', () => {
|
||||
expect(connector.connect).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('refuses a connect while the experiment is off, but still removes the host', async () => {
|
||||
// Stage 1: the setting governs new work only; a saved host keeps serving, as in serve-only.
|
||||
it('keeps serving a saved host with the setting off: connect and Remove still work', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
await service.add({ distro: 'Ubuntu' })
|
||||
@@ -289,17 +290,24 @@ describe('WslHostService', () => {
|
||||
enabled = false
|
||||
await service.enabledChanged(false)
|
||||
|
||||
await expect(service.connect('wsl:ubuntu')).rejects.toThrow('turned off in Settings')
|
||||
service.setVisibleWork(['wsl:ubuntu'])
|
||||
await expect(service.ensureForRuntimeCall('wsl:ubuntu')).rejects.toThrow(
|
||||
'turned off in Settings'
|
||||
)
|
||||
expect(connector.connect).not.toHaveBeenCalled()
|
||||
await service.connect('wsl:ubuntu')
|
||||
expect(connector.connect).toHaveBeenCalledExactlyOnceWith('Ubuntu', expect.anything())
|
||||
|
||||
await service.remove('wsl:ubuntu')
|
||||
expect(listEnvironments(userDataPath)).toEqual([])
|
||||
})
|
||||
|
||||
it('refuses a new host while the setting is off', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
enabled = false
|
||||
await expect(service.add({ distro: 'Ubuntu' })).resolves.toMatchObject({
|
||||
ok: false,
|
||||
kind: 'disabled'
|
||||
})
|
||||
expect(listEnvironments(userDataPath)).toEqual([])
|
||||
})
|
||||
|
||||
it('refuses to add or connect, before any deploy work, when this build lacks the server files', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
@@ -358,7 +366,7 @@ describe('WslHostService', () => {
|
||||
expect(removed).toHaveBeenCalledWith('wsl:ubuntu')
|
||||
})
|
||||
|
||||
it('turning the experiment off closes every connection and releases holders; on resumes lazily', async () => {
|
||||
it('turning the setting off keeps every connection and holder; turning it on boots nothing', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
await service.add({ distro: 'Ubuntu' })
|
||||
@@ -367,21 +375,16 @@ describe('WslHostService', () => {
|
||||
|
||||
enabled = false
|
||||
await service.enabledChanged(false)
|
||||
expect(connector.disconnect).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(published.at(-1)).toMatchObject({ environmentId: 'wsl:ubuntu', state: 'closed' })
|
||||
// Disabled experiments cannot relaunch for visible work.
|
||||
expect(service.hasUserVisibleWork('Ubuntu')).toBe(false)
|
||||
service.setVisibleWork(['wsl:ubuntu'])
|
||||
expect(connector.connect).not.toHaveBeenCalled()
|
||||
expect(connector.disconnect).not.toHaveBeenCalled()
|
||||
expect(connector.releaseHolder).not.toHaveBeenCalled()
|
||||
expect(published.at(-1)).not.toMatchObject({ state: 'closed' })
|
||||
// A moved project's work keeps its host up.
|
||||
expect(service.hasUserVisibleWork('Ubuntu')).toBe(true)
|
||||
|
||||
enabled = true
|
||||
await service.enabledChanged(true)
|
||||
// Nothing boots on enable; the window re-reports its focused host, which may.
|
||||
expect(connector.connect).not.toHaveBeenCalled()
|
||||
expect(service.hasUserVisibleWork('Ubuntu')).toBe(true)
|
||||
service.focusHost('wsl:ubuntu')
|
||||
await vi.waitFor(() => expect(connector.connect).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('turning the experiment on with nothing open connects nothing', async () => {
|
||||
@@ -596,7 +599,7 @@ describe('WslHostService', () => {
|
||||
expect(connector.connect).toHaveBeenCalledWith('Ubuntu', { noBoot: true })
|
||||
})
|
||||
|
||||
it('a due automation boots a parked host once, and never with the experiment off', async () => {
|
||||
it('a due automation boots a parked host once, with the setting off too (it serves saved hosts)', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
await service.add({ distro: 'Ubuntu' })
|
||||
@@ -609,9 +612,6 @@ describe('WslHostService', () => {
|
||||
connector.connect.mockClear()
|
||||
enabled = false
|
||||
await service.wakeForAutomation('wsl:ubuntu')
|
||||
expect(connector.connect).not.toHaveBeenCalled()
|
||||
enabled = true
|
||||
await service.wakeForAutomation('wsl:ubuntu')
|
||||
expect(connector.connect).toHaveBeenCalledExactlyOnceWith('Ubuntu')
|
||||
await expect(service.isConnected('wsl:ubuntu')).resolves.toBe(true)
|
||||
})
|
||||
@@ -764,14 +764,14 @@ describe('WslHostService', () => {
|
||||
expect(isWanted('Debian')).toBe(true)
|
||||
})
|
||||
|
||||
it('releases every startup holder while WSL hosts are off', async () => {
|
||||
it("keeps a saved host's startup holder while the setting is off", async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
await service.add({ distro: 'Ubuntu' })
|
||||
enabled = false
|
||||
await service.start()
|
||||
const [isWanted] = connector.adoptHolders.mock.calls[0]
|
||||
expect(isWanted('Ubuntu')).toBe(false)
|
||||
expect(isWanted('Ubuntu')).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -158,21 +158,13 @@ export class WslHostService {
|
||||
return removal.serverStop
|
||||
}
|
||||
|
||||
/** Disable closes connections/watches and releases holders; reenable resumes only hosts needed by visible work. */
|
||||
/**
|
||||
* The setting governs new work only (Stage 1): turning it off refuses an add still in flight,
|
||||
* but saved hosts keep serving their projects, as in serve-only. Turning it on boots nothing.
|
||||
*/
|
||||
async enabledChanged(enabled: boolean): Promise<void> {
|
||||
const connector = this.connector
|
||||
if (!connector) {
|
||||
return
|
||||
}
|
||||
if (enabled) {
|
||||
// Nothing boots here: the window re-reports its focused host, which connects only if running (E9).
|
||||
return
|
||||
}
|
||||
this.disableGeneration += 1
|
||||
for (const record of this.records()) {
|
||||
await connector.disconnect(record.distro)
|
||||
await connector.releaseHolder(record.distro)
|
||||
this.setStatus(record, { state: 'closed' })
|
||||
if (!enabled) {
|
||||
this.disableGeneration += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectWslHostsServing } from './use-wsl-hosts-enabled'
|
||||
import { useMemo } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
|
||||
@@ -34,7 +35,7 @@ export function useSidebarHostScopeOptions(): {
|
||||
runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides,
|
||||
wslHostStatuses,
|
||||
wslHostsEnabled: settings?.experimentalWslHosts === true,
|
||||
wslHostsEnabled: selectWslHostsServing({ settings, runtimeEnvironments }),
|
||||
wslHostServerFilesMissing
|
||||
}),
|
||||
[
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { isWslHostRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
|
||||
/** Preload exposes WSL-host APIs only when this desktop launch supports them. */
|
||||
export function wslHostsAvailable(): boolean {
|
||||
@@ -10,16 +12,37 @@ export function useWslHostServerFilesMissing(): boolean {
|
||||
return useAppStore((s) => s.wslHostServerFiles === 'missing')
|
||||
}
|
||||
|
||||
/** Available, and the user-level Experimental switch (shown only behind the gate) is on. */
|
||||
export function useWslHostsEnabled(): boolean {
|
||||
const enabled = useAppStore((s) => s.settings?.experimentalWslHosts === true)
|
||||
return enabled && wslHostsAvailable()
|
||||
/**
|
||||
* Saved hosts serve their projects whatever the setting: it governs new work only (Stage 1), so
|
||||
* with it off a moved project stays reachable, as in serve-only.
|
||||
*/
|
||||
export function selectWslHostsServing(state: {
|
||||
settings: AppState['settings']
|
||||
/** Optional: some list builders run before the environments load. */
|
||||
runtimeEnvironments?: AppState['runtimeEnvironments']
|
||||
}): boolean {
|
||||
return (
|
||||
state.settings?.experimentalWslHosts === true ||
|
||||
(state.runtimeEnvironments ?? []).some(isWslHostRuntimeEnvironment)
|
||||
)
|
||||
}
|
||||
|
||||
/** Enabled, and not serve-only: the entry points that add a host or move a project. */
|
||||
/** Available, and serving: the setting is on, or saved hosts keep serving with it off. */
|
||||
export function useWslHostsEnabled(): boolean {
|
||||
const serving = useAppStore(selectWslHostsServing)
|
||||
return serving && wslHostsAvailable()
|
||||
}
|
||||
|
||||
/** Available, and the user's WSL hosts setting is on. */
|
||||
export function useWslHostsSettingOn(): boolean {
|
||||
const settingOn = useAppStore((s) => s.settings?.experimentalWslHosts === true)
|
||||
return settingOn && wslHostsAvailable()
|
||||
}
|
||||
|
||||
/** The setting on, and not serve-only: the entry points that add a host or move a project. */
|
||||
export function useWslHostsAcceptNewWork(): boolean {
|
||||
const accepts = useAppStore((s) => s.wslHostsAcceptNewWork)
|
||||
return useWslHostsEnabled() && accepts
|
||||
return useWslHostsSettingOn() && accepts
|
||||
}
|
||||
|
||||
/** Serve-only: saved hosts work, but adding hosts and moving projects is paused. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectWslHostsServing } from '../../use-wsl-hosts-enabled'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { persistedUIValuesEqual } from '../../../../../../shared/persisted-ui-equality'
|
||||
@@ -56,7 +57,7 @@ function createSidebarHostOptionsSelector() {
|
||||
state.runtimeEnvironments,
|
||||
hostLabelOverrides,
|
||||
state.wslHostStatuses,
|
||||
settings?.experimentalWslHosts === true,
|
||||
selectWslHostsServing(state),
|
||||
state.wslHostServerFiles === 'missing'
|
||||
]
|
||||
if (shallow(inputs, nextInputs)) {
|
||||
@@ -72,7 +73,7 @@ function createSidebarHostOptionsSelector() {
|
||||
runtimeStatusByEnvironmentId: state.runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides,
|
||||
wslHostStatuses: state.wslHostStatuses,
|
||||
wslHostsEnabled: settings?.experimentalWslHosts === true,
|
||||
wslHostsEnabled: selectWslHostsServing(state),
|
||||
wslHostServerFilesMissing: state.wslHostServerFiles === 'missing'
|
||||
})
|
||||
if (!persistedUIValuesEqual(result.map(sidebarHostSemantics), next.map(sidebarHostSemantics))) {
|
||||
|
||||
@@ -6,6 +6,8 @@ const state = vi.hoisted(() => {
|
||||
return { repos, folders }
|
||||
})
|
||||
const openDialog = vi.hoisted(() => vi.fn())
|
||||
const toastError = vi.hoisted(() => vi.fn())
|
||||
vi.mock('sonner', () => ({ toast: { error: toastError } }))
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => state } }))
|
||||
vi.mock('@/store/workspace-record-selectors', () => ({
|
||||
selectFolderWorkspaces: (current: typeof state) => current.folders
|
||||
@@ -21,6 +23,7 @@ describe("the offer's move of the project it added (B3)", () => {
|
||||
state.repos = []
|
||||
state.folders = []
|
||||
openDialog.mockReset()
|
||||
toastError.mockReset()
|
||||
})
|
||||
|
||||
it('finds the added repo by its path, however its slashes are written', () => {
|
||||
@@ -40,8 +43,29 @@ describe("the offer's move of the project it added (B3)", () => {
|
||||
expect(openDialog).toHaveBeenCalledWith('Ubuntu', { kind: 'repo', id: 'r1' })
|
||||
})
|
||||
|
||||
it('opens nothing when the add made no project', () => {
|
||||
it('finds the innermost stored repo that contains an added subfolder', () => {
|
||||
state.repos = [
|
||||
{ id: 'outer', path: '\\\\wsl.localhost\\Ubuntu\\home\\me' },
|
||||
{ id: 'r1', path: PATH },
|
||||
{ id: 'sibling', path: `${PATH}-two` }
|
||||
]
|
||||
moveWslHostOfferProject({
|
||||
path: `${PATH}\\packages\\app`,
|
||||
distro: 'Ubuntu',
|
||||
source: 'local_folder_picker'
|
||||
})
|
||||
expect(openDialog).toHaveBeenCalledWith('Ubuntu', { kind: 'repo', id: 'r1' })
|
||||
expect(toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not take a sibling whose path only shares a prefix', () => {
|
||||
state.repos = [{ id: 'sibling', path: `${PATH}-two` }]
|
||||
expect(wslHostOfferProjectRef(PATH)).toBeNull()
|
||||
})
|
||||
|
||||
it('says so instead of doing nothing when no stored project matches', () => {
|
||||
moveWslHostOfferProject({ path: PATH, distro: 'Ubuntu', source: 'local_folder_picker' })
|
||||
expect(openDialog).not.toHaveBeenCalled()
|
||||
expect(toastError).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { selectFolderWorkspaces } from '@/store/workspace-record-selectors'
|
||||
import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path'
|
||||
import {
|
||||
isPathInsideOrEqual,
|
||||
normalizeRuntimePathForComparison
|
||||
} from '../../../../shared/cross-platform-path'
|
||||
import type { WslMoveRowRef } from '../../../../shared/wsl-move/wsl-move-driver-types'
|
||||
import { openWslMoveDialogFor } from '../wsl-move/wsl-move-entry'
|
||||
import type { WslHostOffer } from './wsl-host-offer-actions'
|
||||
@@ -16,7 +21,14 @@ export function wslHostOfferProjectRef(path: string): WslMoveRowRef | null {
|
||||
const folder = selectFolderWorkspaces(state).find(
|
||||
(entry) => normalizeRuntimePathForComparison(entry.folderPath) === wanted
|
||||
)
|
||||
return folder ? { kind: 'folder', id: folder.id } : null
|
||||
if (folder) {
|
||||
return { kind: 'folder', id: folder.id }
|
||||
}
|
||||
// A subfolder add is stored as its git toplevel; the innermost enclosing repo is the one added.
|
||||
const enclosing = state.repos
|
||||
.filter((entry) => isPathInsideOrEqual(entry.path, path))
|
||||
.sort((a, b) => b.path.length - a.path.length)[0]
|
||||
return enclosing ? { kind: 'repo', id: enclosing.id } : null
|
||||
}
|
||||
|
||||
/** Opens the move with only the offered project checked; the user's Move is what moves it. */
|
||||
@@ -24,5 +36,13 @@ export function moveWslHostOfferProject(offer: WslHostOffer): void {
|
||||
const only = wslHostOfferProjectRef(offer.path)
|
||||
if (only) {
|
||||
openWslMoveDialogFor(offer.distro, only)
|
||||
return
|
||||
}
|
||||
toast.error(
|
||||
translate(
|
||||
'sidebar.wslHostOffer.projectNotFound',
|
||||
'Could not find the added project to move. Use "Move to {{distro}} host…" on the project in the sidebar.',
|
||||
{ distro: offer.distro }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { selectWslHostUnavailableReason } from './use-wsl-host-unavailable-reason'
|
||||
import { selectWslHostsServing } from '../sidebar/use-wsl-hosts-enabled'
|
||||
|
||||
const WSL_ENV = 'wsl:ubuntu'
|
||||
const WORKTREE = 'wsl-repo::/home/me/app'
|
||||
|
||||
function state(experimentalWslHosts: boolean, withHost = true): AppState {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the selectors read only these fields.
|
||||
return {
|
||||
settings: { ...getDefaultSettings('/tmp'), experimentalWslHosts },
|
||||
wslHostServerFiles: 'present',
|
||||
folderWorkspaces: [],
|
||||
projectGroups: [],
|
||||
repos: [
|
||||
{
|
||||
id: 'wsl-repo',
|
||||
path: '/home/me/app',
|
||||
displayName: 'app',
|
||||
badgeColor: '',
|
||||
addedAt: 0,
|
||||
executionHostId: `runtime:${WSL_ENV}`
|
||||
}
|
||||
],
|
||||
runtimeEnvironments: withHost
|
||||
? [
|
||||
{
|
||||
id: WSL_ENV,
|
||||
name: 'Ubuntu (WSL)',
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
preferredEndpointId: 'ws',
|
||||
endpoints: [],
|
||||
orcadDeployment: { kind: 'wsl', distro: 'Ubuntu', localPort: 42000, remotePort: 6768 }
|
||||
}
|
||||
]
|
||||
: []
|
||||
} as unknown as AppState
|
||||
}
|
||||
|
||||
// Stage 1: the setting governs new work only; a moved project on a saved host stays reachable.
|
||||
describe('a WSL host project with the WSL hosts setting off', () => {
|
||||
it('stays reachable: its panes and explorer show no "turned off" state', () => {
|
||||
expect(selectWslHostUnavailableReason(state(false), WORKTREE)).toBeNull()
|
||||
expect(selectWslHostsServing(state(false))).toBe(true)
|
||||
})
|
||||
|
||||
it('serves nothing when there is no saved host and the setting is off', () => {
|
||||
expect(selectWslHostsServing(state(false, false))).toBe(false)
|
||||
expect(selectWslHostsServing(state(true, false))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectWslHostsServing } from '../sidebar/use-wsl-hosts-enabled'
|
||||
import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { isWslHostRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import { workspaceOwnerHostId } from '@/lib/workspace-row-owner-host'
|
||||
@@ -25,7 +26,8 @@ export function selectWslHostUnavailableReason(
|
||||
if (state.wslHostServerFiles === 'missing') {
|
||||
return 'server-files-missing'
|
||||
}
|
||||
return state.settings?.experimentalWslHosts === true ? null : 'turned-off'
|
||||
// A saved host keeps serving with the setting off (it governs new work only), as in serve-only.
|
||||
return selectWslHostsServing(state) ? null : 'turned-off'
|
||||
}
|
||||
|
||||
export function useWslHostUnavailableReason(
|
||||
|
||||
@@ -80,19 +80,24 @@ describe('WSL host focus entries (E9)', () => {
|
||||
unsubs.forEach((unsub) => unsub())
|
||||
})
|
||||
|
||||
it('records no focus while WSL hosts are off, and reports the focused host when turned on', () => {
|
||||
seed(false, WSL_WORKTREE)
|
||||
// Stage 1: the setting governs new work only, so a saved host keeps serving with it off.
|
||||
it('reports the focused saved host even with the WSL hosts setting off', () => {
|
||||
seed(false, LOCAL_WORKTREE)
|
||||
const unsubs: (() => void)[] = []
|
||||
registerWslHostIpcBridge(unsubs)
|
||||
useAppStore.setState({ activeWorktreeId: WSL_WORKTREE })
|
||||
expect(api.focusHost).toHaveBeenCalledWith({ environmentId: WSL_ENV })
|
||||
unsubs.forEach((unsub) => unsub())
|
||||
})
|
||||
|
||||
it('records no focus with the setting off and no saved host to serve', () => {
|
||||
seed(false, LOCAL_WORKTREE)
|
||||
useAppStore.setState({ runtimeEnvironments: [] })
|
||||
const unsubs: (() => void)[] = []
|
||||
registerWslHostIpcBridge(unsubs)
|
||||
useAppStore.setState({ activeWorktreeId: LOCAL_WORKTREE })
|
||||
useAppStore.setState({ activeWorktreeId: WSL_WORKTREE })
|
||||
expect(api.focusHost).not.toHaveBeenCalled()
|
||||
expect(api.setVisibleWork).not.toHaveBeenCalled()
|
||||
|
||||
useAppStore.setState({
|
||||
settings: { ...getDefaultSettings('/tmp'), experimentalWslHosts: true }
|
||||
})
|
||||
expect(api.focusHost).toHaveBeenCalledExactlyOnceWith({ environmentId: WSL_ENV })
|
||||
unsubs.forEach((unsub) => unsub())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectWslHostsServing } from '@/components/sidebar/use-wsl-hosts-enabled'
|
||||
import type { WslHostCapability, WslHostStatus } from '../../../../shared/wsl-host-types'
|
||||
import { isWslHostRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
@@ -84,8 +85,8 @@ export function registerWslHostIpcBridge(unsubs: (() => void)[]): void {
|
||||
let focusEntry = ''
|
||||
const focusedThisSession = new Set<string>()
|
||||
const report = (state: AppState): void => {
|
||||
if (state.settings?.experimentalWslHosts !== true) {
|
||||
// Focus while WSL hosts are off is not an act on a host; turning them on re-reports.
|
||||
if (!selectWslHostsServing(state)) {
|
||||
// With no host to serve, focus is not an act on one; turning WSL hosts on re-reports.
|
||||
inputs = []
|
||||
focusEntry = ''
|
||||
return
|
||||
|
||||
@@ -214,7 +214,8 @@
|
||||
"wslHostOffer": {
|
||||
"title": "Run {{distro}} as a host (recommended)",
|
||||
"why": "Git, files and terminals then run natively in Linux instead of across the Windows file share.",
|
||||
"accept": "Run as host"
|
||||
"accept": "Run as host",
|
||||
"projectNotFound": "Could not find the added project to move. Use \"Move to {{distro}} host…\" on the project in the sidebar."
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
|
||||
@@ -355,19 +355,38 @@ test('adds a WSL distro as a host, adds a project on it, and runs a terminal the
|
||||
await hubRuntime.call('terminal.create', { worktree: `path:${hostOnlyPath}` })
|
||||
expect(await hubTerminalsIn(hostOnlyPath)).toBeGreaterThan(0)
|
||||
|
||||
// Experiment off: the host stays listed so its projects never seem to vanish, but turned off.
|
||||
// Setting off (Stage 1): it governs new work only, so the host keeps serving its projects,
|
||||
// as in serve-only. Only adding a host and moving projects stop.
|
||||
await setWslHostsEnabled(page, false)
|
||||
// Through the desktop's own host connection, not the hub directly: the host still serves.
|
||||
const callHostFromDesktop = (method: string, params: unknown): Promise<boolean> =>
|
||||
page.evaluate(async (args) => (await window.api.runtimeEnvironments.call(args)).ok, {
|
||||
selector: WSL_ENVIRONMENT_ID,
|
||||
method,
|
||||
params,
|
||||
timeoutMs: 30_000
|
||||
})
|
||||
expect(await callHostFromDesktop('terminal.create', { worktree: `path:${hostOnlyPath}` })).toBe(
|
||||
true
|
||||
)
|
||||
await expect.poll(() => hubTerminalsIn(hostOnlyPath), { timeout: 30_000 }).toBeGreaterThan(1)
|
||||
expect(await callHostFromDesktop('terminal.list', {})).toBe(true)
|
||||
expect(
|
||||
await page.evaluate(() => window.api.runtimeEnvironments.wslHosts?.add({ distro: 'Ubuntu' }))
|
||||
).toMatchObject({ ok: false, kind: 'disabled' })
|
||||
expect(
|
||||
await page.evaluate(() => window.api.wslMove.start({ distro: 'Ubuntu', repoIds: [] }))
|
||||
).toMatchObject({ outcome: 'refused', code: 'wsl_move_unavailable' })
|
||||
await openSidebarProjectDialog(page)
|
||||
await page
|
||||
.getByRole('dialog', { name: /Add a project/i })
|
||||
.getByRole('combobox')
|
||||
.click()
|
||||
const offRow = page.locator('[cmdk-item]').filter({ hasText: WSL_HOST_LABEL })
|
||||
await expect(offRow).toContainText('Turned off in Settings → Experimental')
|
||||
await expect(offRow.getByRole('button', { name: 'Turn on' })).toBeVisible()
|
||||
await expect(offRow.getByRole('button', { name: 'Connect' })).toHaveCount(0)
|
||||
await expect(offRow).toBeVisible()
|
||||
await expect(offRow).not.toContainText('Turned off in Settings → Experimental')
|
||||
await settleAnimations(page)
|
||||
await page.screenshot({ path: testInfo.outputPath('wsl-host-row-turned-off.png') })
|
||||
await page.screenshot({ path: testInfo.outputPath('wsl-host-row-setting-off.png') })
|
||||
await page.keyboard.press('Escape')
|
||||
const addProjectAgain = page.getByRole('dialog', { name: /Add a project/i })
|
||||
await expect(offRow).toBeHidden()
|
||||
@@ -383,10 +402,12 @@ test('adds a WSL distro as a host, adds a project on it, and runs a terminal the
|
||||
await expect(page.getByText('No saved servers', { exact: true })).toBeVisible()
|
||||
const section = page.locator('[data-settings-section="wsl-hosts"]')
|
||||
await section.scrollIntoViewIfNeeded()
|
||||
await expect(section).toContainText('WSL hosts are turned off in Settings → Experimental')
|
||||
await expect(section.getByRole('button', { name: 'Connect' })).toBeDisabled()
|
||||
await expect(section).toContainText(WSL_HOST_LABEL)
|
||||
await expect(section).not.toContainText('WSL hosts are turned off in Settings → Experimental')
|
||||
// No new host while the setting is off.
|
||||
await expect(section.getByRole('button', { name: 'Add WSL distro' })).toHaveCount(0)
|
||||
await settleAnimations(page)
|
||||
await section.screenshot({ path: testInfo.outputPath('wsl-hosts-settings-turned-off.png') })
|
||||
await section.screenshot({ path: testInfo.outputPath('wsl-hosts-settings-setting-off.png') })
|
||||
|
||||
// Remove asks first, with WSL-specific copy, then drops the host.
|
||||
await setWslHostsEnabled(page, true)
|
||||
|
||||
Reference in New Issue
Block a user