mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
Every writer sharing a userData dir -- main's local PTY path, the daemon fork, and the daemons of other builds that outlive the app that spawned them -- wrote one fixed `shell-ready/` tree. Last writer won, and the guard only re-checked that the files were present, never that they were this build's. A daemon whose spawn env no longer agreed with the wrapper on disk kept launching shells it could not read: the ready marker never fired and every startup command waited out the full 15s timeout, silently, with restarting the app powerless to fix it because the daemon outlives the app. Measured 15.17s vs 1.10s once the tree matched. The five live daemons on the machine this was found on spanned app versions 1.4.181-1.4.185, all from the same installed app across auto-updates, so this reaches ordinary installs. Name each tree after a hash of its contents, at `<userData>/shell-wrappers/<hash>/shell-ready/`. Different bytes are a different directory, so "present" means "written by this build" again. The hash sits above the `shell-ready` leaf because ZDOTDIR self-reference guards match that exact suffix. Nothing collects old trees: ~48KB each, a couple of MB a year against a userData dir in the tens of GB, not worth an `rm -rf` on the spawn path. Also publishes the resolved root to WSL over WSLENV `/p`, since the in-guest script cannot derive a hash, and reports readiness failures to the daemon's NDJSON log rather than a console the detached daemon discards. Generated wrapper content is byte-identical; snapshots unchanged.
75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, sep } from 'node:path'
|
|
import {
|
|
resolveShellWrapperRoot,
|
|
type ShellWrapperFileBuilder
|
|
} from './shell-wrapper-content-address'
|
|
import { writeShellWrapperFiles } from './shell-wrapper-file-writer'
|
|
|
|
function builderFor(marker: string): ShellWrapperFileBuilder {
|
|
// Why the root is embedded: mirrors .zshenv, the one wrapper file that names
|
|
// its own tree. The digest must stay stable despite it.
|
|
return (root) => [
|
|
[join(root, 'zsh', '.zshrc'), `zshrc ${marker} root=${root}`],
|
|
[join(root, 'bash', 'rcfile'), `rcfile ${marker}`]
|
|
]
|
|
}
|
|
|
|
function makeBase(): string {
|
|
return mkdtempSync(join(tmpdir(), 'orca-wrapper-address-'))
|
|
}
|
|
|
|
describe('resolveShellWrapperRoot', () => {
|
|
it('gives builds with different wrapper contents different trees', () => {
|
|
const base = makeBase()
|
|
expect(resolveShellWrapperRoot(base, builderFor('ORCA_SHELL_READY_MARKER'))).not.toEqual(
|
|
resolveShellWrapperRoot(base, builderFor('ORCA_SHELL_FEATURES'))
|
|
)
|
|
})
|
|
|
|
it('is stable for identical contents', () => {
|
|
const base = makeBase()
|
|
expect(resolveShellWrapperRoot(base, builderFor('a'))).toEqual(
|
|
resolveShellWrapperRoot(base, builderFor('a'))
|
|
)
|
|
})
|
|
|
|
// Why: `*/shell-ready/zsh` globs baked into the wrapper scripts, plus TS
|
|
// guards, detect a self-referential ZDOTDIR by this exact suffix. Losing it
|
|
// makes a wrapper source itself until zsh hits its recursion limit.
|
|
it('keeps the zsh dir ending in /shell-ready/zsh', () => {
|
|
const root = resolveShellWrapperRoot(makeBase(), builderFor('a'))
|
|
// Compare in POSIX form: the guards match a POSIX suffix, and the value they
|
|
// see is built with POSIX concatenation regardless of the host separator.
|
|
expect(join(root, 'zsh').split(sep).join('/').endsWith('/shell-ready/zsh')).toBe(true)
|
|
})
|
|
|
|
it('lets two contracts coexist instead of clobbering each other', () => {
|
|
const base = makeBase()
|
|
const oldBuild = builderFor('old')
|
|
const newBuild = builderFor('new')
|
|
const oldRoot = resolveShellWrapperRoot(base, oldBuild)
|
|
const newRoot = resolveShellWrapperRoot(base, newBuild)
|
|
|
|
writeShellWrapperFiles(oldBuild(oldRoot), '[test]')
|
|
writeShellWrapperFiles(newBuild(newRoot), '[test]')
|
|
|
|
// The regression: the second writer used to overwrite the first writer's
|
|
// file in place, leaving the first build launching shells it cannot read.
|
|
expect(readFileSync(join(oldRoot, 'bash', 'rcfile'), 'utf8')).toBe('rcfile old')
|
|
expect(readFileSync(join(newRoot, 'bash', 'rcfile'), 'utf8')).toBe('rcfile new')
|
|
})
|
|
|
|
// Why: older builds still write the unversioned `<userData>/shell-ready/` tree
|
|
// and long-lived daemons launch shells from it. Addressing by content has to
|
|
// land somewhere that cannot collide with it.
|
|
it('never resolves onto the legacy unversioned tree', () => {
|
|
const userData = makeBase()
|
|
const root = resolveShellWrapperRoot(join(userData, 'shell-wrappers'), builderFor('a'))
|
|
expect(root.startsWith(join(userData, 'shell-wrappers') + sep)).toBe(true)
|
|
expect(root).not.toEqual(join(userData, 'shell-ready'))
|
|
})
|
|
})
|