mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* fix(runner): prevent Windows process probes from blocking session lifecycle * perf(cli): reduce Windows process probes during session shutdown * test(cli): update PowerShell fixtures for complete process snapshots
78 lines
5.6 KiB
TypeScript
78 lines
5.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
const state = vi.hoisted(() => ({ home: '', auth: 'token', processes: new Map<number, string | undefined>() }));
|
|
vi.mock('@/configuration', () => ({ configuration: { get happyHomeDir() { return state.home; }, apiUrl: 'hub', get cliApiToken() { return state.auth; } } }));
|
|
vi.mock('@/utils/process', () => ({ isProcessAlive: (pid: number) => state.processes.has(pid), getProcessStartMarker: (pid: number) => state.processes.get(pid) }));
|
|
import { findRuntime, readRuntimes, runtimeAlive, runtimeAuthHash, runtimeMayBeAlive, saveRuntime, withThreadOwnership, type CodexRuntimeRecord } from './registry';
|
|
|
|
const directories: string[] = [];
|
|
afterEach(async () => { state.processes.clear(); state.auth = 'token'; await Promise.all(directories.splice(0).map(path => rm(path, { recursive: true, force: true }))); });
|
|
async function fixture(): Promise<CodexRuntimeRecord> {
|
|
const home = await mkdtemp(join(tmpdir(), 'hapi-owner-')); directories.push(home); state.home = join(home, 'hapi');
|
|
return { id: 'owner', pid: 1111, marker: 'worker-start', serverPid: 2222, serverMarker: 'server-start', command: 'codex', args: [],
|
|
codexHome: join(home, 'codex'), endpoint: 'unix://private', hub: 'hub', authHash: runtimeAuthHash(),
|
|
sessions: { sid: { threadId: 'thread', namespace: 'ns', active: true } } };
|
|
}
|
|
describe('shared runtime ownership', () => {
|
|
it('uses one complete process snapshot without per-record OS probes', async () => {
|
|
const owner = await fixture();
|
|
const probe = vi.spyOn(state.processes, 'has');
|
|
try {
|
|
for (let scan = 0; scan < 3; scan++) {
|
|
for (let index = 0; index < 50; index++) {
|
|
expect(runtimeMayBeAlive({ ...owner, pid: 3000 + index, serverPid: 4000 + index }, new Map())).toBe(false);
|
|
}
|
|
}
|
|
expect(probe).not.toHaveBeenCalled();
|
|
} finally { probe.mockRestore(); }
|
|
});
|
|
it('retains live wrappers, orphan engines and unknown generations in a fresh snapshot', async () => {
|
|
const owner = await fixture();
|
|
expect(runtimeMayBeAlive(owner, new Map([[1111, owner.marker]]))).toBe(true);
|
|
expect(runtimeMayBeAlive(owner, new Map([[2222, owner.serverMarker!]]))).toBe(true);
|
|
expect(runtimeMayBeAlive(owner, new Map([[1111, null]]))).toBe(true);
|
|
expect(runtimeMayBeAlive({ ...owner, serverMarker: undefined }, new Map([[2222, 'unverified-start']]))).toBe(true);
|
|
});
|
|
it('does not confuse old runtime generations with reused PIDs or retain a stale live snapshot', async () => {
|
|
const owner = await fixture(); state.processes.set(1111, owner.marker);
|
|
expect(runtimeMayBeAlive(owner, new Map([[1111, 'new-wrapper'], [2222, 'new-engine']]))).toBe(false);
|
|
expect(runtimeMayBeAlive(owner, new Map())).toBe(false);
|
|
expect(runtimeMayBeAlive(owner, new Map([[1111, owner.marker]]))).toBe(true);
|
|
});
|
|
it('distinguishes PID reuse from an unknown generation and a live orphan engine', async () => {
|
|
const owner = await fixture(); state.processes.set(1111, 'different-start');
|
|
expect(runtimeAlive(owner)).toBe(false); expect(runtimeMayBeAlive(owner)).toBe(false);
|
|
state.processes.set(1111, undefined); expect(runtimeMayBeAlive(owner)).toBe(true);
|
|
state.processes.delete(1111); state.processes.set(2222, 'server-start'); expect(runtimeMayBeAlive(owner)).toBe(true);
|
|
await saveRuntime(owner);
|
|
await expect(withThreadOwnership(owner.codexHome, 'thread', 'new', async () => {})).rejects.toThrow('orphaned');
|
|
});
|
|
it('protects one native store even across HAPI homes and scopes attach by authentication', async () => {
|
|
const owner = await fixture(); state.processes.set(1111, owner.marker); await saveRuntime(owner);
|
|
expect(await findRuntime('sid')).toMatchObject({ id: owner.id });
|
|
state.auth = 'other'; expect(await findRuntime('sid')).toBeUndefined();
|
|
state.home = join(directories[0], 'different-hapi');
|
|
await expect(withThreadOwnership(owner.codexHome, 'thread', 'new', async () => {})).rejects.toThrow('hapi resume sid');
|
|
state.processes.clear(); expect(await withThreadOwnership(owner.codexHome, 'thread', 'new', async () => 'safe')).toBe('safe');
|
|
});
|
|
it('fails closed on corrupt records and unknown thread IDs after a creation timeout', async () => {
|
|
const owner = await fixture(); owner.pendingCreations = ['unbound']; state.processes.set(1111, owner.marker); await saveRuntime(owner);
|
|
await expect(withThreadOwnership(owner.codexHome, 'unknown-thread', 'new', async () => {})).rejects.toThrow('unconfirmed creation');
|
|
const directory = join(owner.codexHome, 'hapi-runtime-owners'); await mkdir(directory, { recursive: true });
|
|
await writeFile(join(directory, 'broken.json'), '{');
|
|
await expect(withThreadOwnership(owner.codexHome, 'thread', 'new', async () => {})).rejects.toThrow('Cannot verify');
|
|
});
|
|
it('readRuntimes({ strict: true }) fails closed on corrupt hub registry files', async () => {
|
|
// Soft [] would let stopSession argv-sweep tree-kill shared wrappers (#1911).
|
|
const owner = await fixture();
|
|
const directory = join(state.home, 'codex-runtimes');
|
|
await mkdir(directory, { recursive: true });
|
|
await writeFile(join(directory, 'broken.json'), '{');
|
|
expect(await readRuntimes()).toEqual([]);
|
|
await expect(readRuntimes({ strict: true })).rejects.toThrow('Cannot verify');
|
|
});
|
|
});
|