From 96038ecfdd6f3671655bd4f692a3c87ad07dbacf Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:19:40 -0700 Subject: [PATCH] test(wsl-mover): prove the host's server update is held during a real move (#35404) --- .../wsl-orcad-update-move-hold-driver.test.ts | 316 ++++++++++++++++++ .../settings/managed-server-copy.test.ts | 13 + 2 files changed, 329 insertions(+) create mode 100644 src/main/wsl/wsl-orcad-update-move-hold-driver.test.ts create mode 100644 src/renderer/src/components/settings/managed-server-copy.test.ts diff --git a/src/main/wsl/wsl-orcad-update-move-hold-driver.test.ts b/src/main/wsl/wsl-orcad-update-move-hold-driver.test.ts new file mode 100644 index 00000000000..7f06e298ef9 --- /dev/null +++ b/src/main/wsl/wsl-orcad-update-move-hold-driver.test.ts @@ -0,0 +1,316 @@ +/** + * #34823 end to end in-process: the real WSL orcad launch admission against a real move run of + * the WSL move driver over a real orcad profile store (the fault-injection rig). An update or a + * connect's activation of the host's server attempted after the move has journaled its cutover is + * held with the "moving onto this server" outcome; once the move settles (moved, failed, + * cancelled, or killed mid-run and recovered on relaunch) the same update deploys. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' +import { setAppEnvironment } from '../../shared/app-environment' +import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate' +import { ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE } from '../../shared/wsl-host-types' +import type { WslMoveResult } from '../../shared/wsl-move/wsl-move-driver-types' +import type { OrcadActivationRecord } from '../managed-server/activation/orcad-activation-record' +import { resetOrcadUpdateFailuresForTests } from '../managed-server/lifecycle/orcad-managed-update-failure-memory' +import type * as ManagedRuntimeContext from '../managed-server/lifecycle/orcad-managed-runtime-context' +import type * as RemoteVersionedInstall from '../managed-server/install/remote-versioned-install' +import { testState } from '../persistence/persistence-test-harness' +import { + createWslMoveKillRig, + removeWslMoveRigDirs, + restartWslMoveRig, + WSL_MOVE_RIG_REQUEST, + wslMoveRigOutcome, + type WslMoveRig +} from '../persistence/__fixtures__/wsl-mover/wsl-move-kill-rig' +import { resetWslMoveDesktopRewriteStoresForTests } from '../persistence/moving-wsl-projects/wsl-move-desktop-rewrite-stores' +import { moveProjectsToWslHost } from '../persistence/moving-wsl-projects/wsl-move-driver' +import { + cancelWslMove, + isWslMoveRunning, + isWslMoveUnderwayOnHost +} from '../persistence/moving-wsl-projects/wsl-move-running-moves' +import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform' +import type { OrcadHostTransport } from '../ssh/orcad-host-transport' +import type { WslDistroHolder } from './wsl-distro-holder' +import type { WslOrcadHostTarget } from './wsl-orcad-host-target' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) +vi.mock('../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../telemetry/cohort-classifier', () => ({ + getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 1 })) +})) + +// The host side of the server launch: every call into the distro is faked at its module. +const host = vi.hoisted(() => ({ + exec: vi.fn(), + resolveContext: vi.fn(), + deploy: vi.fn(), + probe: vi.fn(), + adoptRunning: vi.fn(), + localVersion: vi.fn(), + readRecord: vi.fn() +})) +vi.mock('../managed-server/activation/orcad-activation-transaction-store', () => ({ + readOrcadActivationTransaction: async () => null +})) +vi.mock('../managed-server/activation/orcad-activation-record-store', () => ({ + readOrcadActivationRecord: host.readRecord +})) +vi.mock('../managed-server/install/remote-versioned-install', async (importOriginal) => ({ + ...(await importOriginal()), + readLocalFullVersion: host.localVersion +})) +vi.mock('./wsl-orcad-instance-adoption', () => ({ + adoptUnrecordedWslOrcad: async () => ({ outcome: 'none' }) +})) +vi.mock('../ssh/orcad-running-version-adoption', () => ({ + adoptRunningOrcadVersion: host.adoptRunning +})) +vi.mock('../ssh/remote-exec/remote-exec-command', () => ({ execCommand: host.exec })) +vi.mock('../managed-server/install/orcad-remote-context', () => ({ + resolveOrcadRemoteContext: host.resolveContext +})) +vi.mock('../managed-server/activation/orcad-activation-recovery', () => ({ + recoverInterruptedOrcadActivation: async () => ({ outcome: 'none' }) +})) +vi.mock('../orcad/orcad-artifact-materializer', () => ({ + materializeOrcadArtifact: async () => '/local/orcad' +})) +vi.mock('../ssh/orcad-remote-deploy', () => ({ deployOrcad: host.deploy })) +vi.mock('../managed-server/lifecycle/orcad-managed-version-gc', () => ({ + provenLiveDaemonVersion: () => null, + pruneManagedOrcadVersions: vi.fn() +})) +vi.mock('../managed-server/lifecycle/orcad-managed-runtime-context', async (importOriginal) => ({ + ...(await importOriginal()), + probeManagedOrcadReadiness: host.probe +})) + +import { deployAndLaunchWslOrcad } from './wsl-orcad-deploy' + +const DISTRO = WSL_MOVE_RIG_REQUEST.distro +const RUNNING = '1.5.0+aaaaaaaaaaaa' +const BUNDLED = '1.6.0+bbbbbbbbbbbb' +const READINESS = { + runtimeId: 'r1', + boundEndpoint: 'ws://127.0.0.1:6790', + advertisedEndpoint: null, + managedWslCliReconciliation: 'settled' as const, + pairing: { available: false as const, reason: 'disabled_by_operator', guidance: '' } +} +const NO_TERMINALS = { liveSessions: 0, startedSinceActivation: 0, daemonProtocolVersion: null } +const RECORD: OrcadActivationRecord = { + schemaVersion: 1, + active: RUNNING, + previous: null, + activatedAt: '2026-10-01T00:00:00.000Z', + snapshot: null, + activeAppVersion: '1.5.0' +} + +function holder(): WslDistroHolder { + return { + ensureHeld: vi.fn(async (distro: string) => ({ + outcome: 'spawned' as const, + record: { version: 1 as const, distro, pid: 9, creationTimeMs: 1, startedAtMs: 1 } + })), + adoptHolders: vi.fn(), + release: vi.fn(), + releaseAll: vi.fn(), + holderState: vi.fn() + } +} + +function transport(target: WslOrcadHostTarget): OrcadHostTransport { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every host call is mocked at its module. + return { getTarget: () => target } as unknown as OrcadHostTransport +} + +/** The host's server launched from Settings (Update) or by a connect (its activation). */ +function launchServer(intent: 'update' | 'connect') { + return deployAndLaunchWslOrcad(DISTRO, { + intent, + holder: holder(), + createTransport: transport, + readRegistryId: async () => null, + readIncumbent: async () => ({ + census: NO_TERMINALS, + readiness: null, + recount: async () => NO_TERMINALS, + releaseAdmission: async () => {} + }) + }) +} + +let rig: WslMoveRig +// A failed assertion must not leave the next test's move refused as already running. +let pausedMove: { run: Promise; release: () => void } | null = null + +beforeEach(() => { + vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1') + // A killed run's catch handlers warn about the calls that now throw. + vi.spyOn(console, 'warn').mockImplementation(() => {}) + resetOrcadUpdateFailuresForTests() + rig = createWslMoveKillRig() + // The launch reads the move journal from the same profile the move writes. + setAppEnvironment( + fakeAppEnvironment({ + getVersion: () => '1.6.0', + getPath: (name) => (name === 'userData' ? rig.userDataPath : testState.dir) + }) + ) + host.exec.mockImplementation(async (_conn: unknown, command: string) => + command.includes('uname -r') + ? '__ORCA_WSL_KERNEL__ 5.15.167.4-microsoft-standard-WSL2\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n' + : '4242\n' + ) + host.resolveContext.mockImplementation(async (target: WslOrcadHostTarget, connection) => ({ + activationRecord: RECORD, + serverTarget: 'linux-x64-glibc', + connection, + host: getRemoteHostPlatform('linux-x64'), + remoteHome: '/home/u', + target, + userDataDir: '/home/u/.orca' + })) + host.readRecord.mockResolvedValue(RECORD) + host.localVersion.mockReturnValue(BUNDLED) + host.adoptRunning.mockResolvedValue({ version: RUNNING, readiness: READINESS, reprobe: vi.fn() }) + host.deploy.mockResolvedValue({ + outcome: 'installed-and-activated', + fullVersion: BUNDLED, + verdict: {} + }) + host.probe.mockResolvedValue(READINESS) +}) + +afterEach(async () => { + pausedMove?.release() + await pausedMove?.run + pausedMove = null + vi.clearAllMocks() + vi.restoreAllMocks() + vi.unstubAllEnvs() + resetWslMoveDesktopRewriteStoresForTests() + removeWslMoveRigDirs() +}) + +/** A move run stopped where it first stages on the host, after it has journaled its cutover. */ +async function moveAtStage(): Promise<{ + run: Promise + release: () => void +}> { + let reached!: () => void + const atStage = new Promise((resolve) => { + reached = resolve + }) + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + const connect = rig.world.deps.connectHost + rig.world.deps.connectHost = async (distro) => { + const moveHost = await connect(distro) + if (!moveHost) { + return null + } + const { stage } = moveHost.catalog + return { + ...moveHost, + catalog: { + ...moveHost.catalog, + stage: async (manifest) => (reached(), await gate, stage(manifest)) + } + } + } + const run = moveProjectsToWslHost(rig.world.deps, WSL_MOVE_RIG_REQUEST).catch( + () => 'killed' as const + ) + pausedMove = { run, release } + await atStage + return { run, release } +} + +async function expectHeld(): Promise { + expect(isWslMoveUnderwayOnHost(rig.userDataPath, DISTRO)).toBe(true) + for (const intent of ['update', 'connect'] as const) { + const launched = await launchServer(intent) + // The running server keeps serving the move; Settings shows the code as "Not done: a project + // is moving onto this server…" (managed-server-copy.test.ts). + expect(launched).toMatchObject({ + outcome: 'ready', + fullVersion: RUNNING, + deferredUpdate: { + candidateVersion: BUNDLED, + code: ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE, + forceable: false + } + }) + } + expect(host.deploy).not.toHaveBeenCalled() +} + +async function expectUpdates(): Promise { + expect(isWslMoveRunning(DISTRO)).toBe(false) + expect(await launchServer('update')).toMatchObject({ + outcome: 'ready', + fullVersion: BUNDLED + }) + expect(host.deploy).toHaveBeenCalledTimes(1) + expect(host.deploy.mock.calls[0][0]).toMatchObject({ localOrcadDir: '/local/orcad' }) +} + +describe("the WSL host's server update while a real project move onto it runs (#34823)", () => { + it('is held while the move runs, and updates once it has moved', async () => { + const move = await moveAtStage() + await expectHeld() + + move.release() + expect(await move.run).toMatchObject({ outcome: 'moved' }) + await expectUpdates() + }) + + it('updates once the move fails because its host went away', async () => { + const move = await moveAtStage() + await expectHeld() + + rig.hostPresent = false + move.release() + // Put back: nothing moved, the projects are usable here again. + expect(await move.run).toMatchObject({ outcome: 'reset' }) + rig.hostPresent = true + await expectUpdates() + }) + + it('updates once the user cancels the move', async () => { + const move = await moveAtStage() + await expectHeld() + + expect(cancelWslMove(DISTRO)).toBe(true) + move.release() + expect(await move.run).toMatchObject({ outcome: 'refused', code: 'wsl_move_cancelled' }) + await expectUpdates() + }) + + it('updates after the desktop is killed mid-move and the relaunch recovers it', async () => { + rig.kill.killAt = 'stage:before' + const move = await moveAtStage() + await expectHeld() + + move.release() + await move.run + expect(rig.kill.dead).toBe(true) + await restartWslMoveRig(rig, { hostUp: true, orcadRestarted: false }) + expect(wslMoveRigOutcome(rig)).not.toBe('stuck') + await expectUpdates() + }) +}) diff --git a/src/renderer/src/components/settings/managed-server-copy.test.ts b/src/renderer/src/components/settings/managed-server-copy.test.ts new file mode 100644 index 00000000000..1eac6a225c7 --- /dev/null +++ b/src/renderer/src/components/settings/managed-server-copy.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE } from '../../../../shared/wsl-host-types' +import { managedServerOutcomeLabel } from './managed-server-copy' + +describe('managedServerOutcomeLabel', () => { + it('says a WSL server update waits for the project moving onto it', () => { + expect( + managedServerOutcomeLabel({ outcome: 'deferred', code: ORCAD_UPDATE_WSL_MOVE_RUNNING_CODE }) + ).toBe( + 'Not done: a project is moving onto this server. Orca updates it once the move finishes.' + ) + }) +})