mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Relay node_modules lived inside `~/.orca-remote/relay-<version>+<hash>`, so any byte change in `src/relay/` or the `src/shared/` it pulls in minted a new directory and a fresh `npm install node-pty@1.1.0 @parcel/watcher@2.5.6`. On Linux node-pty has no prebuild, so that is a node-gyp source compile on every new bundle — eight of the fifteen deploy minutes, daily, on a dependency set that is a pinned constant (#18009). The tree now lives at `~/.orca-remote/native/<platform>-<depsHash>/node_modules` and each relay directory symlinks to it. depsHash covers RELAY_NATIVE_DEPS, an explicit epoch, and the bytes of every shipped `node-pty-*` patch artifact, so a patch change mints a new entry rather than leaving hosts on a stale tree. Three rules make one tree safe to share: - A published entry is immutable. `.deps-complete` is written last, only after a probe on that host loaded both addons. Nothing installs, rebuilds or resets into a published entry: every `npm install` is prefixed with a symlink detach, so a repair on one directory can never `rm -rf node_modules/node-pty` out from under a live relay sharing the tree. - Publication elects one winner with `mkdir`, so no client-side lock is needed and two deploys never write one tree. A loser keeps its own copy. - Every failure degrades to today's per-directory install. GC follows remote-install-gc.ts' discipline: a listing that does not end in its own OK token, an unreadable link, or a reference whose shape this client never writes aborts the whole pass. Deletion is tombstone-rename, re-read references under the rename, then remove — a deploy that linked between the listing and the rename gets its tree moved back. It only runs for a connection that could compute a key, and never removes a pinned one. Migration: a first deploy after this ships seeds its tree from a sibling relay directory whose manifest pins the same versions, so an existing host does not recompile once more. The seed is not trusted — it is a plain private install until the normal probe loads it, and only then is it published. Windows keeps the per-directory install: node-pty ships win32 prebuilts, so there is no compile to avoid, and the console-list agent patch mutates the installed tree in place, which rule 1 forbids for a shared one. The POSIX scripts are exercised against a real tree under both /bin/sh and dash, not just asserted on as strings.
244 lines
9.5 KiB
TypeScript
244 lines
9.5 KiB
TypeScript
// The cache's safety argument is made of `sh`, not TypeScript: `mkdir` elects the publisher,
|
|
// `.deps-complete` gates linking, and a failed publish must put the tree back. Asserting on the
|
|
// command strings cannot show any of that, so these run the real scripts against a real tree.
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readlinkSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
existsSync,
|
|
lstatSync
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
|
|
import { getRemoteHostPlatform } from './ssh-remote-platform'
|
|
import {
|
|
computeRelayNativeDepsCacheKey,
|
|
relayNativeDepsCacheEntryDir,
|
|
relayNativeDepsCacheNodeModulesPath
|
|
} from './ssh-relay-native-deps-cache'
|
|
import {
|
|
ensureRelayNativeDepsCacheCommand,
|
|
listRelayNativeDepsCacheEntriesCommand,
|
|
listRelayNativeDepsCacheReferencesCommand,
|
|
promoteRelayNativeDepsCacheCommand,
|
|
RELAY_NATIVE_CACHE_LINKED,
|
|
RELAY_NATIVE_CACHE_LIST_OK,
|
|
RELAY_NATIVE_CACHE_MISS,
|
|
RELAY_NATIVE_CACHE_NOT_PROMOTED,
|
|
RELAY_NATIVE_CACHE_PROMOTED,
|
|
RELAY_NATIVE_CACHE_REFS_OK,
|
|
RELAY_NATIVE_CACHE_SEEDED
|
|
} from './ssh-relay-native-deps-cache-commands'
|
|
|
|
const HOST = getRemoteHostPlatform('linux-x64')
|
|
const DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const
|
|
const KEY = computeRelayNativeDepsCacheKey({ platform: 'linux-x64', deps: DEPS })
|
|
|
|
// Debian and Ubuntu point /bin/sh at dash, which is stricter than the bash-in-sh-mode that macOS
|
|
// ships; run against both when both exist so a bashism cannot pass here and fail on a host.
|
|
const SHELLS = ['/bin/sh', '/bin/dash'].filter((shell) => existsSync(shell))
|
|
|
|
describe.runIf(process.platform !== 'win32').each(SHELLS)(
|
|
'relay native-deps cache shell scripts (%s)',
|
|
(shell) => {
|
|
let home: string
|
|
|
|
const relayDir = (version: string): string => join(home, '.orca-remote', `relay-${version}`)
|
|
|
|
function sh(command: string): string {
|
|
return execFileSync(shell, ['-c', command], { encoding: 'utf-8' })
|
|
}
|
|
|
|
/** A relay directory holding its own installed tree, exactly as `npm install` leaves it. */
|
|
function makePrivateInstall(version: string, deps: Record<string, string> = DEPS): string {
|
|
const dir = relayDir(version)
|
|
mkdirSync(join(dir, 'node_modules', 'node-pty', 'build'), { recursive: true })
|
|
mkdirSync(join(dir, 'node_modules', '@parcel', 'watcher'), { recursive: true })
|
|
writeFileSync(join(dir, 'node_modules', 'node-pty', 'build', 'pty.node'), 'binary')
|
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({ dependencies: deps }))
|
|
return dir
|
|
}
|
|
|
|
function ensure(version: string): string {
|
|
return sh(
|
|
ensureRelayNativeDepsCacheCommand(
|
|
{ host: HOST, remoteHome: home, relayDir: relayDir(version), key: KEY },
|
|
DEPS
|
|
)
|
|
).trim()
|
|
}
|
|
|
|
function promote(version: string): string {
|
|
return sh(
|
|
promoteRelayNativeDepsCacheCommand({
|
|
host: HOST,
|
|
remoteHome: home,
|
|
relayDir: relayDir(version),
|
|
key: KEY
|
|
})
|
|
).trim()
|
|
}
|
|
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'orca-relay-cache-'))
|
|
mkdirSync(join(home, '.orca-remote'), { recursive: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(home, { recursive: true, force: true })
|
|
})
|
|
|
|
it('publishes a probe-verified tree and links the relay directory to it', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
|
|
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED)
|
|
|
|
const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)
|
|
expect(readlinkSync(join(relayDir('0.1.0+aaa'), 'node_modules'))).toBe(target)
|
|
expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
|
|
expect(
|
|
existsSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete'))
|
|
).toBe(true)
|
|
})
|
|
|
|
it('links a second bundle to the published tree with no install of its own', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
|
|
// A different bundle: fresh directory, no node_modules, nothing installed.
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_LINKED)
|
|
|
|
const linked = join(relayDir('0.1.0+bbb'), 'node_modules')
|
|
expect(readlinkSync(linked)).toBe(relayNativeDepsCacheNodeModulesPath(HOST, home, KEY))
|
|
expect(statSync(join(linked, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
|
|
})
|
|
|
|
it('will not link an entry whose completion sentinel is absent', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
rmSync(join(relayNativeDepsCacheEntryDir(HOST, home, KEY), '.deps-complete'))
|
|
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
|
|
expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false)
|
|
})
|
|
|
|
it('seeds from a sibling install rather than recompiling once more', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_SEEDED)
|
|
|
|
const seeded = join(relayDir('0.1.0+bbb'), 'node_modules')
|
|
expect(lstatSync(seeded).isSymbolicLink()).toBe(false)
|
|
expect(statSync(join(seeded, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
|
|
// The source is untouched, so a relay running out of it is unaffected.
|
|
expect(existsSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'))).toBe(true)
|
|
})
|
|
|
|
it('refuses to seed from a sibling pinned to different versions', () => {
|
|
makePrivateInstall('0.1.0+aaa', { 'node-pty': '1.0.0', '@parcel/watcher': '2.5.6' })
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
|
|
expect(existsSync(join(relayDir('0.1.0+bbb'), 'node_modules'))).toBe(false)
|
|
})
|
|
|
|
it('refuses to seed from a sibling that had node-pty skipped', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
rmSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty'), { recursive: true })
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
|
|
})
|
|
|
|
it('leaves a directory that installed for itself alone', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
const own = makePrivateInstall('0.1.0+bbb')
|
|
|
|
expect(ensure('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_MISS)
|
|
expect(lstatSync(join(own, 'node_modules')).isSymbolicLink()).toBe(false)
|
|
})
|
|
|
|
it('elects exactly one publisher and leaves the loser its own tree', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
makePrivateInstall('0.1.0+bbb')
|
|
|
|
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_PROMOTED)
|
|
expect(promote('0.1.0+bbb')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED)
|
|
|
|
// The loser must not have handed its tree to an entry it lost the race for.
|
|
const loser = join(relayDir('0.1.0+bbb'), 'node_modules')
|
|
expect(lstatSync(loser).isSymbolicLink()).toBe(false)
|
|
expect(statSync(join(loser, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
|
|
})
|
|
|
|
it('never republishes through a symlink it already holds', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
|
|
expect(promote('0.1.0+aaa')).toBe(RELAY_NATIVE_CACHE_NOT_PROMOTED)
|
|
expect(statSync(join(relayDir('0.1.0+aaa'), 'node_modules', 'node-pty')).isDirectory()).toBe(
|
|
true
|
|
)
|
|
})
|
|
|
|
it('survives removing a linked relay directory without touching the shared tree', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
ensure('0.1.0+bbb')
|
|
|
|
// Exactly what version GC does to an idle directory.
|
|
sh(`rm -rf ${JSON.stringify(relayDir('0.1.0+bbb'))}`)
|
|
|
|
const target = relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)
|
|
expect(statSync(join(target, 'node-pty', 'build', 'pty.node')).isFile()).toBe(true)
|
|
})
|
|
|
|
it('reports the published entry and every symlink that references it', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
|
|
const entries = sh(listRelayNativeDepsCacheEntriesCommand(HOST, home)).trim().split('\n')
|
|
expect(entries).toEqual([`ENTRY ${KEY}`, RELAY_NATIVE_CACHE_LIST_OK])
|
|
|
|
const refs = sh(listRelayNativeDepsCacheReferencesCommand(HOST, home)).trim().split('\n')
|
|
expect(refs).toEqual([
|
|
`REF ${relayNativeDepsCacheNodeModulesPath(HOST, home, KEY)}`,
|
|
RELAY_NATIVE_CACHE_REFS_OK
|
|
])
|
|
})
|
|
|
|
it('reports a symlink no Orca version wrote, so GC can refuse the pass', () => {
|
|
makePrivateInstall('0.1.0+aaa')
|
|
promote('0.1.0+aaa')
|
|
mkdirSync(relayDir('0.1.0+bbb'), { recursive: true })
|
|
symlinkSync('../relay-0.1.0+aaa/node_modules', join(relayDir('0.1.0+bbb'), 'node_modules'))
|
|
|
|
const refs = sh(listRelayNativeDepsCacheReferencesCommand(HOST, home)).trim().split('\n')
|
|
expect(refs).toContain('REF ../relay-0.1.0+aaa/node_modules')
|
|
expect(refs.at(-1)).toBe(RELAY_NATIVE_CACHE_REFS_OK)
|
|
})
|
|
|
|
it('answers cleanly on a host that has never installed anything', () => {
|
|
expect(sh(listRelayNativeDepsCacheEntriesCommand(HOST, home)).trim()).toBe(
|
|
RELAY_NATIVE_CACHE_LIST_OK
|
|
)
|
|
expect(sh(listRelayNativeDepsCacheReferencesCommand(HOST, home)).trim()).toBe(
|
|
RELAY_NATIVE_CACHE_REFS_OK
|
|
)
|
|
})
|
|
}
|
|
)
|