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 { return { nonce: 'nonce-42', pid: 42, profile: 'default', startMarker: 'os-start-123', ...overrides } } function ownershipEntry(overrides: Partial = {}) { 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(done => { resolve = done }) return { promise, resolve } } function createOwnership(store = memoryStore(), overrides: Partial[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) })