mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
test: remove 859 low-value runtime and account tests (#38763)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,278 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ORCAD_RECOVERY_CHANGED_STATE_CODE } from '../../shared/orcad-managed-runtime'
|
||||
import { MANAGED_SERVER_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
import { MANAGED_SERVER_HANDLERS } from './managed-server'
|
||||
|
||||
function envelope(result: unknown) {
|
||||
return { id: 'r', ok: true, result, _meta: { runtimeId: 'runtime-1' } }
|
||||
}
|
||||
|
||||
function client(result: unknown, capabilities = [MANAGED_SERVER_RUNTIME_CAPABILITY]) {
|
||||
return vi.fn(async (method: string) =>
|
||||
method === 'status.get' ? envelope({ capabilities }) : envelope(result)
|
||||
)
|
||||
}
|
||||
|
||||
async function run(
|
||||
command: string,
|
||||
call: ReturnType<typeof client>,
|
||||
flags: [string, string | boolean][]
|
||||
) {
|
||||
const handler = MANAGED_SERVER_HANDLERS[command]
|
||||
if (!handler) {
|
||||
throw new Error(`no handler for ${command}`)
|
||||
}
|
||||
await handler({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handlers only call client.call.
|
||||
client: { call } as never,
|
||||
cwd: '/tmp',
|
||||
flags: new Map([['environment', 'build-box'], ...flags]),
|
||||
json: false
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('managed server CLI verbs', () => {
|
||||
it('refuses on a runtime that does not advertise managed servers, before calling the action', async () => {
|
||||
const call = client({ outcome: 'none' }, [])
|
||||
await expect(run('environment recover', call, [])).rejects.toMatchObject({
|
||||
code: 'incompatible_runtime'
|
||||
})
|
||||
expect(call).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reads an older runtime’s method_not_found as the same refusal', async () => {
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === 'status.get') {
|
||||
return envelope({ capabilities: [MANAGED_SERVER_RUNTIME_CAPABILITY] })
|
||||
}
|
||||
throw new RuntimeClientError('method_not_found', 'Unknown method')
|
||||
})
|
||||
await expect(run('environment status', call, [])).rejects.toMatchObject({
|
||||
code: 'incompatible_runtime'
|
||||
})
|
||||
})
|
||||
|
||||
it('needs --yes before stopping, and then calls the same stop the settings use', async () => {
|
||||
const stopped = {
|
||||
outcome: 'unlinked',
|
||||
verdict: 'exited',
|
||||
environmentId: 'env-1',
|
||||
sshTargetId: 'ssh-1',
|
||||
stoppedVersion: '1.0.0',
|
||||
retirement: null
|
||||
}
|
||||
const unconfirmed = client(stopped)
|
||||
await expect(run('environment stop', unconfirmed, [])).rejects.toMatchObject({
|
||||
code: 'confirmation_required'
|
||||
})
|
||||
expect(unconfirmed).not.toHaveBeenCalled()
|
||||
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
const confirmed = client(stopped)
|
||||
await run('environment stop', confirmed, [['yes', true]])
|
||||
expect(confirmed).toHaveBeenCalledWith(
|
||||
'managedServer.stop',
|
||||
{ selector: 'build-box' },
|
||||
{ timeoutMs: 1_200_000 }
|
||||
)
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('build-box'))
|
||||
})
|
||||
|
||||
it('forgets a server only with --yes, over its own method (P1-E)', async () => {
|
||||
const forgotten = {
|
||||
outcome: 'forgotten',
|
||||
verdict: 'unverifiable',
|
||||
environmentId: 'env-1',
|
||||
sshTargetId: 'ssh-1'
|
||||
}
|
||||
const unconfirmed = client(forgotten)
|
||||
await expect(run('environment forget', unconfirmed, [])).rejects.toMatchObject({
|
||||
code: 'confirmation_required'
|
||||
})
|
||||
expect(unconfirmed).not.toHaveBeenCalled()
|
||||
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
const confirmed = client(forgotten)
|
||||
await run('environment forget', confirmed, [['yes', true]])
|
||||
// Older desktops ignore flags; using stop for forget would decommission the server.
|
||||
expect(confirmed).toHaveBeenCalledWith(
|
||||
'managedServer.forget',
|
||||
{ selector: 'build-box' },
|
||||
{ timeoutMs: 1_200_000 }
|
||||
)
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('may still be running there'))
|
||||
})
|
||||
|
||||
it('fails the command with the refusal, so scripts see a non-zero exit', async () => {
|
||||
const refusal = {
|
||||
outcome: 'refused',
|
||||
verdict: 'live',
|
||||
code: 'orcad_stop_active_environment',
|
||||
reason:
|
||||
'This server is the default host for new projects. Choose another one under New projects open on before stopping it.'
|
||||
}
|
||||
await expect(run('environment stop', client(refusal), [['yes', true]])).rejects.toMatchObject({
|
||||
code: 'managed_server_refused',
|
||||
message: refusal.reason,
|
||||
data: refusal
|
||||
})
|
||||
})
|
||||
|
||||
it('passes --force to an update, and reports a deferred one as unsettled', async () => {
|
||||
const call = client({
|
||||
outcome: 'deferred',
|
||||
candidateVersion: '2.0.0',
|
||||
code: 'orcad_update_terminals_running',
|
||||
reason: 'Terminals are running.'
|
||||
})
|
||||
await expect(run('environment update', call, [])).rejects.toMatchObject({
|
||||
code: 'managed_server_deferred'
|
||||
})
|
||||
expect(call).toHaveBeenCalledWith(
|
||||
'managedServer.update',
|
||||
{ selector: 'build-box', force: false },
|
||||
{ timeoutMs: 1_200_000 }
|
||||
)
|
||||
|
||||
vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
const forced = client({
|
||||
outcome: 'updated',
|
||||
environment: { name: 'build-box' },
|
||||
activeVersion: '2.0.0'
|
||||
})
|
||||
await run('environment update', forced, [['force', true]])
|
||||
expect(forced).toHaveBeenCalledWith(
|
||||
'managedServer.update',
|
||||
{ selector: 'build-box', force: true },
|
||||
{ timeoutMs: 1_200_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('fails on an outcome a newer desktop added instead of printing undefined', async () => {
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
await expect(
|
||||
run('environment cancel-stop', client({ outcome: 'future-outcome' }), [])
|
||||
).rejects.toMatchObject({ code: 'managed_server_future-outcome' })
|
||||
expect(log).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prints a readable status', async () => {
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
const call = client({
|
||||
activeVersion: '1.0.0',
|
||||
previousVersion: '0.9.0',
|
||||
rollbackAvailable: true,
|
||||
recovery: null,
|
||||
terminals: { liveSessions: null },
|
||||
migration: null,
|
||||
deferredUpdate: null
|
||||
})
|
||||
await run('environment status', call, [])
|
||||
expect(call).toHaveBeenCalledWith('managedServer.status', { selector: 'build-box' }, undefined)
|
||||
expect(log.mock.calls[0]?.[0]).toBe(
|
||||
'Active version: 1.0.0\nPrevious version: 0.9.0 (rollback available)\nLive terminals: unverifiable'
|
||||
)
|
||||
})
|
||||
|
||||
it('reports a timed-out action as possibly still running, never as a failure of the action', async () => {
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === 'status.get') {
|
||||
return envelope({ capabilities: [MANAGED_SERVER_RUNTIME_CAPABILITY] })
|
||||
}
|
||||
throw new RuntimeClientError(
|
||||
'runtime_timeout',
|
||||
'Timed out waiting for the Orca runtime to respond.'
|
||||
)
|
||||
})
|
||||
await expect(run('environment update', call, [])).rejects.toMatchObject({
|
||||
code: 'managed_server_in_progress',
|
||||
message: expect.stringContaining('orca environment status')
|
||||
})
|
||||
})
|
||||
|
||||
it('reports what a restarting update left behind instead of failing on the closed connection', async () => {
|
||||
vi.useFakeTimers()
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
let updated = false
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === 'status.get') {
|
||||
return envelope({ capabilities: [MANAGED_SERVER_RUNTIME_CAPABILITY] })
|
||||
}
|
||||
if (method === 'managedServer.update') {
|
||||
updated = true
|
||||
throw new RuntimeClientError(
|
||||
'runtime_unavailable',
|
||||
'The Orca runtime closed the connection before responding.'
|
||||
)
|
||||
}
|
||||
return envelope({ activeVersion: '0.2.0+bb01', recovery: null, deferredUpdate: null })
|
||||
})
|
||||
try {
|
||||
const done = run('environment update', call, [])
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
await done
|
||||
expect(updated).toBe(true)
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('now runs 0.2.0+bb01'))
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('reports an update the restart left interrupted as needing recover', async () => {
|
||||
vi.useFakeTimers()
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method === 'status.get') {
|
||||
return envelope({ capabilities: [MANAGED_SERVER_RUNTIME_CAPABILITY] })
|
||||
}
|
||||
if (method === 'managedServer.update') {
|
||||
throw new RuntimeClientError('runtime_unavailable', 'closed')
|
||||
}
|
||||
return envelope({
|
||||
activeVersion: '0.1.0+aa01',
|
||||
recovery: { operation: 'activate', version: '0.2.0+bb01', phase: 'snapshot-captured' },
|
||||
deferredUpdate: null
|
||||
})
|
||||
})
|
||||
try {
|
||||
const done = run('environment update', call, []).catch((error: unknown) => error)
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
expect(await done).toMatchObject({ code: 'managed_server_interrupted' })
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('restores changed state only with --accept-changed-state --yes, and its refusal names the flags', async () => {
|
||||
const refusal = {
|
||||
outcome: 'refused',
|
||||
verdict: 'unverifiable',
|
||||
code: ORCAD_RECOVERY_CHANGED_STATE_CODE,
|
||||
reason: 'The launched build changed profile state. Recover to restore the prelaunch snapshot.'
|
||||
}
|
||||
await expect(run('environment recover', client(refusal), [])).rejects.toMatchObject({
|
||||
code: 'managed_server_refused',
|
||||
message: expect.stringContaining('--accept-changed-state --yes')
|
||||
})
|
||||
|
||||
const unconfirmed = client({ outcome: 'none' })
|
||||
await expect(
|
||||
run('environment recover', unconfirmed, [['accept-changed-state', true]])
|
||||
).rejects.toMatchObject({ code: 'confirmation_required' })
|
||||
expect(unconfirmed).not.toHaveBeenCalled()
|
||||
|
||||
vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
||||
const confirmed = client({ outcome: 'none' })
|
||||
await run('environment recover', confirmed, [
|
||||
['accept-changed-state', true],
|
||||
['yes', true]
|
||||
])
|
||||
expect(confirmed).toHaveBeenCalledWith(
|
||||
'managedServer.recover',
|
||||
{ selector: 'build-box', acceptChangedState: true },
|
||||
{ timeoutMs: 1_200_000 }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -2,26 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getServeSuperviseValidationError,
|
||||
superviseServeService,
|
||||
SERVE_INSTALL_HOLD_POLL_MS,
|
||||
type SupervisedServeOptions
|
||||
} from './serve-supervised-service'
|
||||
import {
|
||||
readServeSupervisorBrake,
|
||||
recordSupervisedFailure,
|
||||
emptyServeSupervisorBrake,
|
||||
SERVE_SUPERVISOR_FAILURE_LIMIT,
|
||||
SERVE_SUPERVISOR_FAILURE_WINDOW_MS,
|
||||
SERVE_SUPERVISOR_BACKOFF_MAX_MS,
|
||||
SERVE_GAVE_UP_ADVICE,
|
||||
type ServeSupervisorBrake
|
||||
} from './serve-supervisor-brake'
|
||||
import type { ServeInstallEvidence } from './serve-install-hold'
|
||||
import { resolveOrcadExitCode } from '../../main/orcad/orcad-exit-code'
|
||||
import { OrcadInstanceLockError } from '../../main/orcad/orcad-data-root-privacy'
|
||||
import { OrcadTerminalsNotDurableError } from '../../main/orcad/orcad-serve-supervision'
|
||||
import { superviseServeService, type SupervisedServeOptions } from './serve-supervised-service'
|
||||
import { readServeSupervisorBrake, SERVE_SUPERVISOR_FAILURE_LIMIT } from './serve-supervisor-brake'
|
||||
|
||||
/** Virtual clock: timers fire in order as soon as the loop awaits them. */
|
||||
function harness(overrides: Partial<SupervisedServeOptions> & { userDataPath: string }) {
|
||||
@@ -102,290 +84,5 @@ describe('superviseServeService', () => {
|
||||
expect(again.logs.join('\n')).toContain('--force')
|
||||
})
|
||||
|
||||
it('names a missing terminal daemon as the reason it gave up', async () => {
|
||||
const exitCode = resolveOrcadExitCode(new OrcadTerminalsNotDurableError('spawn failed'))
|
||||
const h = harness({ userDataPath, runOnce: async () => exitCode })
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(readServeSupervisorBrake(userDataPath)?.gaveUp?.reason).toContain(
|
||||
'the terminal daemon did not start'
|
||||
)
|
||||
})
|
||||
|
||||
it('starts over for a new build or with --force', async () => {
|
||||
const gaveUp: ServeSupervisorBrake = {
|
||||
...emptyServeSupervisorBrake('build-a'),
|
||||
gaveUp: { at: 1, reason: 'old' }
|
||||
}
|
||||
let runs = 0
|
||||
const newBuild = harness({
|
||||
userDataPath,
|
||||
readBrake: () => gaveUp,
|
||||
writeBrake: () => undefined,
|
||||
buildIdentity: () => 'build-b',
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
newBuild.stop()
|
||||
return 0
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(newBuild.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(1)
|
||||
|
||||
let cleared = false
|
||||
const forced = harness({
|
||||
userDataPath,
|
||||
force: true,
|
||||
readBrake: () => gaveUp,
|
||||
clearBrake: () => {
|
||||
cleared = true
|
||||
},
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
forced.stop()
|
||||
return 0
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(forced.options)).resolves.toBe(0)
|
||||
expect(cleared).toBe(true)
|
||||
expect(runs).toBe(2)
|
||||
})
|
||||
|
||||
it('keeps a give-up while the executable is missing mid-install', async () => {
|
||||
const gaveUp: ServeSupervisorBrake = {
|
||||
...emptyServeSupervisorBrake('build-a'),
|
||||
gaveUp: { at: 1, reason: 'old' }
|
||||
}
|
||||
const identities = ['missing', 'build-a']
|
||||
const evidence: ServeInstallEvidence[] = [{ kind: 'hold', reason: 'bundle-missing' }]
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
readBrake: () => gaveUp,
|
||||
buildIdentity: () => identities.shift() ?? 'build-a',
|
||||
readInstallEvidence: async () => evidence.shift() ?? { kind: 'clear' },
|
||||
runOnce: async () => (runs++, 0)
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(0)
|
||||
expect(h.logs.join('\n')).toContain('Not starting')
|
||||
expect(h.logs.join('\n')).toContain(SERVE_GAVE_UP_ADVICE)
|
||||
})
|
||||
|
||||
it('never spawns while the installer holds the bundle, even when the check fails', async () => {
|
||||
const evidence: ServeInstallEvidence[] = [
|
||||
{ kind: 'hold', reason: 'installer-running' },
|
||||
{ kind: 'hold', reason: 'installer-unknown' },
|
||||
{ kind: 'hold', reason: 'bundle-missing' },
|
||||
{ kind: 'clear' }
|
||||
]
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
readInstallEvidence: async () => evidence.shift() ?? { kind: 'clear' },
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
expect(evidence).toHaveLength(0)
|
||||
h.stop()
|
||||
return 0
|
||||
}
|
||||
})
|
||||
await superviseServeService(h.options)
|
||||
expect(runs).toBe(1)
|
||||
expect(h.sleeps).toEqual([
|
||||
SERVE_INSTALL_HOLD_POLL_MS,
|
||||
SERVE_INSTALL_HOLD_POLL_MS,
|
||||
SERVE_INSTALL_HOLD_POLL_MS
|
||||
])
|
||||
})
|
||||
|
||||
it('hands the restart to the service manager when the executable stays gone', async () => {
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
readInstallEvidence: async () => ({ kind: 'hold', reason: 'bundle-missing' }),
|
||||
runOnce: async () => (runs++, 0)
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(1)
|
||||
expect(runs).toBe(0)
|
||||
})
|
||||
|
||||
it('waits for a live peer without counting it as a failure', async () => {
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
if (runs === 20) {
|
||||
h.stop()
|
||||
}
|
||||
return 3
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(20)
|
||||
expect(readServeSupervisorBrake(userDataPath)).toBeNull()
|
||||
expect(Math.max(...h.sleeps)).toBe(60_000)
|
||||
})
|
||||
|
||||
// #32177: a lock this machine cannot judge (a shared home) is a peer, not a fault to give up on.
|
||||
it('waits on a lock holder it cannot judge, without giving up', async () => {
|
||||
const holderUnverifiable = resolveOrcadExitCode(
|
||||
new OrcadInstanceLockError('orcad_instance_lock_holder_unverifiable', 'shared home')
|
||||
)
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
if (runs === 10) {
|
||||
h.stop()
|
||||
}
|
||||
return holderUnverifiable
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(10)
|
||||
expect(readServeSupervisorBrake(userDataPath)).toBeNull()
|
||||
})
|
||||
|
||||
it('counts a thrown child failure and keeps supervising', async () => {
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => {
|
||||
runs++
|
||||
if (runs === 1) {
|
||||
throw new Error('Orca serve exited via SIGKILL.')
|
||||
}
|
||||
h.stop()
|
||||
return 0
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(2)
|
||||
expect(h.logs).toContain('[serve] Orca serve exited via SIGKILL; restarting in 1s.')
|
||||
})
|
||||
|
||||
it('restarts from a replaced AppImage before starting a child, not after it', async () => {
|
||||
const order: string[] = []
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => (order.push('spawn'), 0),
|
||||
appImageUpdate: {
|
||||
inspect: () => 'superseded',
|
||||
restart: async () => {
|
||||
order.push('restart')
|
||||
h.stop()
|
||||
return 'the service is stopping'
|
||||
}
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(order).toEqual(['restart'])
|
||||
})
|
||||
|
||||
it('does not let a give-up on the old build block a replaced AppImage', async () => {
|
||||
let restarts = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
readBrake: () => ({
|
||||
...emptyServeSupervisorBrake('build-a'),
|
||||
gaveUp: { at: 1, reason: 'crashed' }
|
||||
}),
|
||||
appImageUpdate: {
|
||||
inspect: () => 'superseded',
|
||||
restart: async () => {
|
||||
restarts++
|
||||
h.stop()
|
||||
return 'the service is stopping'
|
||||
}
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(restarts).toBe(1)
|
||||
expect(h.logs.join('\n')).not.toContain('Not starting')
|
||||
})
|
||||
|
||||
it('keeps serving the running build when the restart fails, without retrying it', async () => {
|
||||
let runs = 0
|
||||
let restarts = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => {
|
||||
if (++runs === 2) {
|
||||
h.stop()
|
||||
}
|
||||
return 1
|
||||
},
|
||||
appImageUpdate: {
|
||||
inspect: () => 'superseded',
|
||||
restart: async () => (restarts++, 'the new AppImage could not be unpacked')
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(restarts).toBe(1)
|
||||
expect(runs).toBe(2)
|
||||
expect(h.logs).toContain(
|
||||
'[serve] Kept the running build: the new AppImage could not be unpacked.'
|
||||
)
|
||||
})
|
||||
|
||||
it('waits for a half-copied AppImage instead of starting either build', async () => {
|
||||
const states = ['settling', 'settling', 'current'] as const
|
||||
let inspections = 0
|
||||
let runs = 0
|
||||
const h = harness({
|
||||
userDataPath,
|
||||
runOnce: async () => (runs++, h.stop(), 0),
|
||||
appImageUpdate: {
|
||||
inspect: () => states[Math.min(inspections++, states.length - 1)]!,
|
||||
restart: async () => 'unused'
|
||||
}
|
||||
})
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(h.sleeps).toEqual([SERVE_INSTALL_HOLD_POLL_MS, SERVE_INSTALL_HOLD_POLL_MS])
|
||||
expect(runs).toBe(1)
|
||||
})
|
||||
|
||||
it('exits 0 on a stop signal during backoff without starting again', async () => {
|
||||
let runs = 0
|
||||
const h = harness({ userDataPath, runOnce: async () => (runs++, 1) })
|
||||
h.options.setTimer = () => {
|
||||
queueMicrotask(() => h.stop())
|
||||
return () => undefined
|
||||
}
|
||||
await expect(superviseServeService(h.options)).resolves.toBe(0)
|
||||
expect(runs).toBe(1)
|
||||
})
|
||||
|
||||
it('refuses --supervise on Windows and with --recipe-json', () => {
|
||||
expect(
|
||||
getServeSuperviseValidationError({ supervise: true, recipeJson: false, platform: 'win32' })
|
||||
).toContain('Windows')
|
||||
expect(
|
||||
getServeSuperviseValidationError({ supervise: true, recipeJson: true, platform: 'linux' })
|
||||
).toContain('--recipe-json')
|
||||
expect(
|
||||
getServeSuperviseValidationError({ supervise: false, recipeJson: true, platform: 'win32' })
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recordSupervisedFailure', () => {
|
||||
it('ages failures out of the window and caps the delay', () => {
|
||||
let record = emptyServeSupervisorBrake('b')
|
||||
let now = 0
|
||||
const delays: number[] = []
|
||||
for (let i = 0; i < 20; i++) {
|
||||
now += SERVE_SUPERVISOR_FAILURE_WINDOW_MS / 4
|
||||
const next = recordSupervisedFailure(record, { now, reason: 'x', random: () => 0.5 })
|
||||
expect(next.action.kind).toBe('restart')
|
||||
if (next.action.kind === 'restart') {
|
||||
delays.push(next.action.delayMs)
|
||||
}
|
||||
record = next.record
|
||||
}
|
||||
expect(Math.max(...delays)).toBeLessThanOrEqual(SERVE_SUPERVISOR_BACKOFF_MAX_MS)
|
||||
expect(record.failures.length).toBeLessThan(SERVE_SUPERVISOR_FAILURE_LIMIT)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user