Files
orca/tests/tools/win-update-e2e/app-shutdown.test.mjs
T
OrcaWinandm4air 4b6fe95943 fix(windows): preserve relocated terminals and native process scans (#22872)
* fix(windows): ship the process-table addon to the relocated daemon host

The Windows terminal daemon runs from a copy of the app under
%LOCALAPPDATA%\Orca\daemon-host\<version>. That copy took node-pty but not
@vscode/windows-process-tree, so the daemon's bare require of the addon found
nothing and every process-table read (foreground tracking, descendant sweeps)
fell back to a powershell.exe Get-CimInstance scan (#16905).

- Copy the addon's runtime files (package.json, lib/, the .node binary) into
  the host; the ~25MB of gyp intermediates beside them are filtered out.
- Treat a host missing those files as unmaterialized, so hosts built before
  this are rebuilt, and skip relocation if the install itself lacks them.
- Log the daemon's native/CIM capability at startup and warn once when the
  process table falls back to CIM.

Revives #19525 on current main.

* test(windows): locate update-survival loss before relaunch

* test(windows): preserve daemon tree before update-survival proof

* test(windows): distinguish Electron exit from launcher close timeout

* test(windows): verify process exit when inherited pipes delay close

* test(windows): trace installer process checks in isolated survival runs

* fix(windows): probe process-query capability before installer sweep

* fix(windows): match installer probe and process-check profile behavior

* fix(windows): use NSIS separators for the process-check include

* test(windows): dismiss session-search overlay in survival harness

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-26 14:31:07 -07:00

120 lines
4.0 KiB
JavaScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { closeApp } from './app-driver.mjs'
const processes = {
isPidAlive: vi.fn().mockReturnValue(true),
killTree: vi.fn()
}
const strictCloseOptions = { allowForceKill: false, processes }
afterEach(() => {
vi.useRealTimers()
vi.clearAllMocks()
processes.isPidAlive.mockReturnValue(true)
})
describe('update-survival shutdown', () => {
it('keeps the force-kill fallback enabled for best-effort cleanup', async () => {
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, stdio: [] }),
close: vi.fn().mockRejectedValue(new Error('quit rejected'))
}
await closeApp(app, 45_000, { processes })
expect(processes.killTree).toHaveBeenCalledExactlyOnceWith(123)
})
it('fails before installation instead of killing the daemon tree after a rejected close', async () => {
const failure = new Error('quit rejected')
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, stdio: [] }),
close: vi.fn().mockRejectedValue(failure)
}
await expect(closeApp(app, 45_000, strictCloseOptions)).rejects.toBe(failure)
expect(processes.killTree).not.toHaveBeenCalled()
})
it('allows normal teardown past ten seconds and still rejects a wedged quit without a tree kill', async () => {
vi.useFakeTimers()
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, stdio: [] }),
close: vi.fn(() => new Promise(() => {}))
}
const closed = closeApp(app, 45_000, strictCloseOptions)
const rejected = expect(closed).rejects.toThrow('close timeout')
await vi.advanceTimersByTimeAsync(30_000)
expect(vi.getTimerCount()).toBe(1)
expect(processes.killTree).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(15_000)
await rejected
expect(processes.killTree).not.toHaveBeenCalled()
expect(vi.getTimerCount()).toBe(0)
})
it('rejects a resolved close while the authoritative main remains live', async () => {
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, stdio: [] }),
close: vi.fn().mockResolvedValue(undefined)
}
await expect(closeApp(app, 45_000, strictCloseOptions)).rejects.toThrow(
'authoritative Electron PID remains live'
)
expect(processes.isPidAlive).toHaveBeenCalledWith(123)
expect(processes.killTree).not.toHaveBeenCalled()
})
it('accepts verified main and launcher exit while inherited pipes keep close pending', async () => {
vi.useFakeTimers()
processes.isPidAlive.mockReturnValue(false)
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, exitCode: 0, stdio: [{ readableEnded: false }] }),
close: vi.fn(() => new Promise(() => {}))
}
const closed = expect(closeApp(app, 45_000, strictCloseOptions)).resolves.toBeUndefined()
await vi.advanceTimersByTimeAsync(45_000)
await closed
expect(processes.isPidAlive).toHaveBeenCalledWith(123)
expect(processes.isPidAlive).toHaveBeenCalledWith(124)
expect(processes.killTree).not.toHaveBeenCalled()
expect(vi.getTimerCount()).toBe(0)
})
it.each(['live', 'unverifiable'])(
'rejects a %s launcher even when the main exited',
async (state) => {
vi.useFakeTimers()
processes.isPidAlive.mockImplementation((pid) => {
if (pid === 123) {
return false
}
if (state === 'unverifiable') {
throw new Error('process query failed')
}
return true
})
const app = {
evaluate: vi.fn().mockResolvedValue(123),
process: () => ({ pid: 124, stdio: [] }),
close: vi.fn(() => new Promise(() => {}))
}
const closed = expect(closeApp(app, 45_000, strictCloseOptions)).rejects.toThrow(
'close timeout'
)
await vi.advanceTimersByTimeAsync(45_000)
await closed
expect(processes.killTree).not.toHaveBeenCalled()
}
)
})