mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
* fix(ssh): relay version GC deletes only on an exited verdict and keeps the previous build The relay records .relay-pid in its version dir once it owns its socket. GC calls a relay version dir exited only when that PID is provably dead and every relay-*.sock refuses a connection; a dir without a PID file keeps the test -S rule. The most recently completed other relay build is pinned like orcad's rollback target. Design D5 GC liveness. * feat(ssh): collect the shared runtimes/ Node store and give it its own owner runtimes/ gets its own owner in the install model, so no version-dir GC (new or old clients, whose listings are prefix-scoped) can list or delete it. A store pass removes node-<sha> only when no retained dir references it, it is neither a current pin nor the newest other verified runtime, and a ps or /proc check ran and found no process using it. Legacy relay-*/orcad-* dirs are read for references and reported as diagnostics only (design D10 two-step). Wired behind orcad GC's nodeRuntimePins. * fix(ssh): runtime store process check holds runtimes reached through a symlinked home /proc exe resolves symlinks and argv keeps whatever spelling launched the runtime, so filtering on the exact $root path missed in-use runtimes on hosts like /home -> /var/home. Filter on the store segment instead; the parser already attributes holds root-agnostically. * test(ssh): wait for the holder process to spawn instead of a fixed delay --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import { execFileSync } from 'node:child_process'
|
||
import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import type * as DeployHelpers from './ssh-relay-deploy-helpers'
|
||
|
||
vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({
|
||
...(await importOriginal<typeof DeployHelpers>()),
|
||
execCommand: vi.fn()
|
||
}))
|
||
|
||
import type { SshConnection } from './ssh-connection'
|
||
import { gcOldRelayVersions } from './remote-install-gc'
|
||
import { ORCAD_INSTALL_MODEL, RELAY_INSTALL_MODEL } from './remote-install-model'
|
||
import {
|
||
listCompletedInstallsNewestFirstCommand,
|
||
parseCompletedInstallsNewestFirst,
|
||
REMOTE_INSTALL_ORDER_OK
|
||
} from './remote-install-previous-version'
|
||
import { execCommand } from './ssh-relay-deploy-helpers'
|
||
import { getRemoteHostPlatform } from './ssh-remote-platform'
|
||
|
||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock.
|
||
const conn = {} as SshConnection
|
||
const host = getRemoteHostPlatform('linux-x64')
|
||
const mockExec = vi.mocked(execCommand)
|
||
|
||
describe('parseCompletedInstallsNewestFirst', () => {
|
||
it('keeps only this model’s version dirs, in host order', () => {
|
||
const output = [
|
||
'/h/.orca-remote/relay-0.2.0+bbb/.install-complete',
|
||
'/h/.orca-remote/orcad-0.2.0+bbb/.install-complete',
|
||
'/h/.orca-remote/relay-0.1.0+aaa/.install-complete',
|
||
REMOTE_INSTALL_ORDER_OK
|
||
].join('\n')
|
||
expect(parseCompletedInstallsNewestFirst(output, RELAY_INSTALL_MODEL)).toEqual([
|
||
'relay-0.2.0+bbb',
|
||
'relay-0.1.0+aaa'
|
||
])
|
||
})
|
||
|
||
it('is null when the host did not finish the listing', () => {
|
||
expect(
|
||
parseCompletedInstallsNewestFirst(
|
||
'/h/.orca-remote/relay-0.1.0+aaa/.install-complete',
|
||
RELAY_INSTALL_MODEL
|
||
)
|
||
).toBeNull()
|
||
})
|
||
})
|
||
|
||
const posixOnly = process.platform === 'win32' ? describe.skip : describe
|
||
|
||
posixOnly('listCompletedInstallsNewestFirstCommand (real shell)', () => {
|
||
let base: string
|
||
beforeEach(() => {
|
||
base = mkdtempSync(join(tmpdir(), 'install-order-'))
|
||
})
|
||
afterEach(() => {
|
||
rmSync(base, { recursive: true, force: true })
|
||
})
|
||
|
||
function install(name: string, mtimeSeconds: number, complete = true): void {
|
||
mkdirSync(join(base, name))
|
||
if (complete) {
|
||
const marker = join(base, name, '.install-complete')
|
||
writeFileSync(marker, '')
|
||
utimesSync(marker, mtimeSeconds, mtimeSeconds)
|
||
}
|
||
}
|
||
|
||
function run(): string[] | null {
|
||
const out = execFileSync(
|
||
'/bin/sh',
|
||
['-c', listCompletedInstallsNewestFirstCommand(host, base, RELAY_INSTALL_MODEL)],
|
||
{ encoding: 'utf8' }
|
||
)
|
||
return parseCompletedInstallsNewestFirst(out, RELAY_INSTALL_MODEL)
|
||
}
|
||
|
||
it('orders completed installs by marker mtime and skips torn ones', () => {
|
||
install('relay-0.1.0+aaa', 1_000)
|
||
install('relay-0.3.0+ccc', 3_000)
|
||
install('relay-0.2.0+bbb', 2_000)
|
||
install('relay-0.4.0+ddd', 4_000, false)
|
||
install('orcad-0.9.0+eee', 9_000)
|
||
|
||
expect(run()).toEqual(['relay-0.3.0+ccc', 'relay-0.2.0+bbb', 'relay-0.1.0+aaa'])
|
||
})
|
||
|
||
it('answers an empty order for a base without completed installs', () => {
|
||
expect(run()).toEqual([])
|
||
})
|
||
|
||
it('is scoped to the model prefix', () => {
|
||
install('orcad-0.9.0+eee', 9_000)
|
||
const out = execFileSync(
|
||
'/bin/sh',
|
||
['-c', listCompletedInstallsNewestFirstCommand(host, base, ORCAD_INSTALL_MODEL)],
|
||
{ encoding: 'utf8' }
|
||
)
|
||
expect(parseCompletedInstallsNewestFirst(out, ORCAD_INSTALL_MODEL)).toEqual(['orcad-0.9.0+eee'])
|
||
})
|
||
})
|
||
|
||
describe('relay GC keeps the previous build', () => {
|
||
beforeEach(() => {
|
||
mockExec.mockReset()
|
||
mockExec.mockResolvedValue('')
|
||
})
|
||
|
||
it('never probes or removes the most recent other completed install', async () => {
|
||
mockExec
|
||
.mockResolvedValueOnce('relay-0.1.0+aaa\n')
|
||
.mockResolvedValueOnce(
|
||
[
|
||
'/home/u/.orca-remote/relay-0.2.0+bbb/.install-complete',
|
||
'/home/u/.orca-remote/relay-0.1.0+aaa/.install-complete',
|
||
REMOTE_INSTALL_ORDER_OK
|
||
].join('\n')
|
||
)
|
||
|
||
await gcOldRelayVersions(conn, '/home/u', '/home/u/.orca-remote/relay-0.2.0+bbb', host)
|
||
|
||
expect(mockExec).toHaveBeenCalledTimes(2)
|
||
})
|
||
|
||
it('deletes nothing when the host cannot report install order', async () => {
|
||
mockExec
|
||
.mockResolvedValueOnce('relay-0.1.0+aaa\nrelay-0.0.9+zzz\n')
|
||
.mockRejectedValueOnce(
|
||
Object.assign(new Error('ls failed'), { sshChannelCloseConfirmed: true })
|
||
)
|
||
|
||
await gcOldRelayVersions(conn, '/home/u', '/home/u/.orca-remote/relay-0.2.0+bbb', host)
|
||
|
||
expect(mockExec).toHaveBeenCalledTimes(2)
|
||
})
|
||
|
||
it('keeps an old build whose liveness probe times out', async () => {
|
||
mockExec
|
||
.mockResolvedValueOnce('relay-0.1.0+aaa\n')
|
||
.mockResolvedValueOnce(`relay-0.1.5+fff\n${REMOTE_INSTALL_ORDER_OK}`)
|
||
.mockResolvedValueOnce('OPEN')
|
||
.mockResolvedValueOnce('COMPLETE')
|
||
.mockRejectedValueOnce(
|
||
Object.assign(new Error('SSH command timed out'), { sshChannelCloseConfirmed: true })
|
||
)
|
||
|
||
await gcOldRelayVersions(conn, '/home/u', '/home/u/.orca-remote/relay-0.2.0+bbb', host, {
|
||
nodePath: '/usr/bin/node'
|
||
})
|
||
|
||
const commands = mockExec.mock.calls.map(([, command]) => command)
|
||
expect(commands[4]).toContain('.relay-pid')
|
||
expect(commands.some((command) => command.includes('gc-claim'))).toBe(false)
|
||
expect(mockExec).toHaveBeenCalledTimes(5)
|
||
})
|
||
})
|