Files
aiturk-hermes-ide/apps/desktop/electron/backend-ownership.test.ts
T

401 lines
12 KiB
TypeScript

import assert from 'node:assert/strict'
import { test, vi } from 'vitest'
import {
backendCommandMatches,
type BackendIdentity,
createBackendOwnership,
createBackendShutdownCoordinator,
parseBackendOwnership
} from './backend-ownership'
function memoryStore(initial = '') {
let contents = initial
return {
read: () => contents,
value: () => contents,
write: (next: string) => {
contents = next
}
}
}
function identity(overrides: Partial<BackendIdentity> = {}): BackendIdentity {
return {
nonce: 'nonce-42',
pid: 42,
profile: 'default',
startMarker: 'os-start-123',
...overrides
}
}
function ownershipEntry(overrides: Partial<BackendIdentity> = {}) {
return { command: 'hermes serve --port 0', ...identity(overrides) }
}
function stored(entries: object[]): string {
return JSON.stringify({ backends: entries })
}
function deferred() {
let resolve!: () => void
const promise = new Promise<void>(done => {
resolve = done
})
return { promise, resolve }
}
function createOwnership(store = memoryStore(), overrides: Partial<Parameters<typeof createBackendOwnership>[0]> = {}) {
return createBackendOwnership({
matchesIdentity: async () => true,
// Unknown parent (no record / legacy) preserves the pre-parent behaviour.
matchesParent: async () => undefined,
stop: () => {},
store,
...overrides
})
}
test('claim persists the caller-supplied exact identity before resolving', async () => {
const store = memoryStore()
const ownership = createOwnership(store)
const claim = ownershipEntry()
assert.deepEqual(await ownership.claim(claim), claim)
assert.deepEqual(parseBackendOwnership(store.value()), [claim])
})
test('incomplete claims and persisted records are rejected', async () => {
const store = memoryStore(
stored([
ownershipEntry(),
{ ...ownershipEntry({ pid: 43 }), startMarker: '' },
{ ...ownershipEntry({ pid: 44 }), nonce: undefined },
{ ...ownershipEntry({ pid: 45 }), profile: undefined }
])
)
const ownership = createOwnership(store)
await assert.rejects(ownership.claim({ ...ownershipEntry(), startMarker: '' }), /complete process identity/)
assert.deepEqual(parseBackendOwnership(store.value()), [ownershipEntry()])
})
test('failed persistence awaits asynchronous cleanup of the exact identity', async () => {
const cleanup = deferred()
const stop = vi.fn(() => cleanup.promise)
const expected = new Error('disk full')
const claim = ownershipEntry({ pid: 43 })
const ownership = createOwnership(memoryStore(), {
stop,
store: {
read: () => null,
write: () => {
throw expected
}
}
})
let rejected = false
const result = ownership.claim(claim).catch(error => {
rejected = true
throw error
})
await Promise.resolve()
assert.equal(rejected, false)
assert.deepEqual(stop.mock.calls, [[claim]])
cleanup.resolve()
await assert.rejects(result, expected)
assert.equal(rejected, true)
})
test('startup reap drops a confirmed PID reuse mismatch without stopping it', async () => {
const entry = ownershipEntry()
const store = memoryStore(stored([entry]))
const matchesIdentity = vi.fn(async () => false)
const stop = vi.fn()
const ownership = createOwnership(store, { matchesIdentity, stop })
assert.deepEqual(await ownership.reapOrphans(), [])
assert.deepEqual(matchesIdentity.mock.calls, [[entry]])
assert.equal(stop.mock.calls.length, 0)
assert.deepEqual(parseBackendOwnership(store.value()), [])
})
test('startup reap preserves records when exact identity probing is uncertain or fails', async () => {
const uncertain = ownershipEntry({ pid: 50, nonce: 'uncertain' })
const failed = ownershipEntry({ pid: 51, nonce: 'failed' })
const store = memoryStore(stored([uncertain, failed]))
const stop = vi.fn()
const ownership = createOwnership(store, {
matchesIdentity: async entry => {
if (entry.pid === failed.pid) {
throw new Error('process table unavailable')
}
return undefined
},
stop
})
assert.deepEqual(await ownership.reapOrphans(), [])
assert.equal(stop.mock.calls.length, 0)
assert.deepEqual(parseBackendOwnership(store.value()), [uncertain, failed])
})
test('startup reap passes the full confirmed identity to stop', async () => {
const entry = ownershipEntry({ pid: 52 })
const store = memoryStore(stored([entry]))
const stop = vi.fn()
const ownership = createOwnership(store, { stop })
assert.deepEqual(await ownership.reapOrphans(), [52])
assert.deepEqual(stop.mock.calls, [[entry]])
assert.deepEqual(parseBackendOwnership(store.value()), [])
})
test('startup reap preserves failed stops for the next launch', async () => {
const entry = ownershipEntry({ pid: 53 })
const store = memoryStore(stored([entry]))
const ownership = createOwnership(store, {
stop: () => {
throw new Error('permission denied')
}
})
assert.deepEqual(await ownership.reapOrphans(), [])
assert.deepEqual(parseBackendOwnership(store.value()), [entry])
})
test('startup reap stops at the deadline and preserves the unprocessed records', async () => {
const first = ownershipEntry({ pid: 60 })
const second = ownershipEntry({ pid: 61 })
const store = memoryStore(stored([first, second]))
const stop = vi.fn()
const ownership = createOwnership(store, {
// Each probe is slow enough to blow a 1ms budget after the first entry.
matchesIdentity: async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return false
},
stop,
reapDeadlineMs: 1
})
assert.deepEqual(await ownership.reapOrphans(), [])
// The first entry was processed (dropped); the second was preserved for the
// next launch instead of stalling boot on a slow identity probe.
assert.deepEqual(parseBackendOwnership(store.value()), [second])
})
test('startup reap preserves would-be-reaped records when the budget runs out', async () => {
const first = ownershipEntry({ pid: 62 })
const second = ownershipEntry({ pid: 63 })
const store = memoryStore(stored([first, second]))
const stop = vi.fn()
const ownership = createOwnership(store, {
matchesIdentity: async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return true
},
stop,
reapDeadlineMs: 1
})
assert.deepEqual(await ownership.reapOrphans(), [62])
// The second would have been reaped too, but the budget ran out — it is
// preserved so a later launch retries it.
assert.deepEqual(parseBackendOwnership(store.value()), [second])
})
test('startup reap never stops a backend whose parent Electron is still alive', async () => {
const entry = { ...ownershipEntry({ pid: 54 }), parentPid: 100, parentStartMarker: 'os-start-parent' }
const store = memoryStore(stored([entry]))
const stop = vi.fn()
const ownership = createOwnership(store, {
matchesParent: async () => true,
stop
})
assert.deepEqual(await ownership.reapOrphans(), [])
assert.equal(stop.mock.calls.length, 0)
assert.deepEqual(parseBackendOwnership(store.value()), [entry])
})
test('startup reap still reaps a backend whose parent is gone or reused', async () => {
const gone = { ...ownershipEntry({ pid: 55 }), parentPid: 200, parentStartMarker: 'os-start-dead' }
const reused = { ...ownershipEntry({ pid: 56 }), parentPid: 201, parentStartMarker: 'os-start-old' }
const store = memoryStore(stored([gone, reused]))
const stop = vi.fn()
const ownership = createOwnership(store, {
matchesParent: async entry => (entry.parentPid === 201 ? true : false),
stop
})
assert.deepEqual(await ownership.reapOrphans(), [55])
assert.deepEqual(stop.mock.calls, [[gone]])
assert.deepEqual(parseBackendOwnership(store.value()), [reused])
})
test('startup reap preserves a record when parent liveness probing fails', async () => {
const entry = { ...ownershipEntry({ pid: 57 }), parentPid: 300, parentStartMarker: 'os-start-parent' }
const store = memoryStore(stored([entry]))
const stop = vi.fn()
const ownership = createOwnership(store, {
matchesParent: async () => {
throw new Error('process table unavailable')
},
stop
})
assert.deepEqual(await ownership.reapOrphans(), [])
assert.equal(stop.mock.calls.length, 0)
assert.deepEqual(parseBackendOwnership(store.value()), [entry])
})
test('claim persists the parent identity so a later reap can see it', async () => {
const store = memoryStore()
const ownership = createOwnership(store)
const claim = { ...ownershipEntry(), parentPid: 42, parentStartMarker: 'os-start-parent' }
const entry = await ownership.claim(claim)
assert.equal(entry.parentPid, 42)
assert.equal(entry.parentStartMarker, 'os-start-parent')
assert.deepEqual(parseBackendOwnership(store.value())[0].parentPid, 42)
assert.deepEqual(parseBackendOwnership(store.value())[0].parentStartMarker, 'os-start-parent')
})
test('release removes only the exact identity rather than every record for its PID', () => {
const oldProcess = ownershipEntry({ nonce: 'old', startMarker: 'start-old' })
const reusedPid = ownershipEntry({ nonce: 'new', startMarker: 'start-new' })
const store = memoryStore(stored([oldProcess, reusedPid]))
const ownership = createOwnership(store)
ownership.release(oldProcess)
assert.deepEqual(parseBackendOwnership(store.value()), [reusedPid])
})
test('backend identity check matches only serve and dashboard invocation shapes', () => {
assert.equal(backendCommandMatches('/venv/bin/hermes serve --port 0'), true)
assert.equal(backendCommandMatches('python -m hermes_cli.main dashboard --no-open'), true)
assert.equal(backendCommandMatches('/venv/bin/hermes --profile work serve --port 0'), true)
assert.equal(backendCommandMatches('"C:\\Hermes Runtime\\hermes.exe" dashboard --no-open'), true)
assert.equal(backendCommandMatches('hermes chat --query serve'), false)
assert.equal(backendCommandMatches('unrelated dashboard'), false)
})
test('shutdown coordinator returns one promise and awaits teardown exactly once', async () => {
const completion = deferred()
const teardown = vi.fn(() => completion.promise)
const coordinator = createBackendShutdownCoordinator(teardown)
const first = coordinator.run()
const second = coordinator.run()
assert.equal(first, second)
assert.equal(coordinator.hasStarted(), true)
await Promise.resolve()
assert.equal(teardown.mock.calls.length, 1)
let finished = false
first.then(() => {
finished = true
})
await Promise.resolve()
assert.equal(finished, false)
completion.resolve()
await second
assert.equal(finished, true)
assert.equal(coordinator.run(), first)
})
// #89298: a corrupt ownership file must never be silently rewritten as [] —
// that permanently erases the only record of still-running backends. The reap
// sweep quarantines the file and skips; a later healthy write recreates it.
test('reapOrphans on a corrupt file quarantines and does not rewrite', async () => {
let contents = '{ this is not json'
let quarantined = 0
const writes: string[] = []
const stopped: number[] = []
const store = {
read: () => contents,
value: () => contents,
write: (next: string) => {
writes.push(next)
contents = next
},
quarantine: () => {
quarantined += 1
}
}
const ownership = createOwnership(store, {
stop: identityArg => {
stopped.push(identityArg.pid)
}
})
assert.deepEqual(await ownership.reapOrphans(), [])
assert.equal(quarantined, 1)
assert.deepEqual(writes, [])
assert.deepEqual(stopped, [])
})
test('reapOrphans on a corrupt file without a quarantine hook still skips the rewrite', async () => {
const writes: string[] = []
const store = {
read: () => 'garbage{{{',
value: () => 'garbage{{{',
write: (next: string) => {
writes.push(next)
}
}
const ownership = createOwnership(store)
assert.deepEqual(await ownership.reapOrphans(), [])
assert.deepEqual(writes, [])
})
test('an empty or missing ownership file is NOT corrupt — reap sweeps normally', async () => {
const writes: string[] = []
const store = {
read: () => '',
value: () => '',
write: (next: string) => {
writes.push(next)
},
quarantine: () => assert.fail('empty file must not be quarantined')
}
const ownership = createOwnership(store)
assert.deepEqual(await ownership.reapOrphans(), [])
// Empty roster: rewriting [] is harmless and keeps the legacy behavior.
assert.equal(writes.length, 1)
})