Files
wehub-resource-sync f9447f8e5f
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:07:03 +08:00

416 lines
12 KiB
TypeScript

import { afterEach, describe, expect, it } from 'bun:test';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import { createProcessRegistry, isPidAlive } from '../../src/supervisor/process-registry.js';
function makeTempDir(): string {
return path.join(tmpdir(), `claude-mem-supervisor-${Date.now()}-${Math.random().toString(36).slice(2)}`);
}
const tempDirs: string[] = [];
describe('supervisor ProcessRegistry', () => {
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
rmSync(dir, { recursive: true, force: true });
}
}
});
describe('isPidAlive', () => {
it('treats current process as alive', () => {
expect(isPidAlive(process.pid)).toBe(true);
});
it('treats an impossibly high PID as dead', () => {
expect(isPidAlive(2147483647)).toBe(false);
});
it('treats negative PID as dead', () => {
expect(isPidAlive(-1)).toBe(false);
});
it('treats non-integer PID as dead', () => {
expect(isPidAlive(3.14)).toBe(false);
});
});
describe('persistence', () => {
it('persists entries to disk and reloads them on initialize', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
mkdirSync(tempDir, { recursive: true });
const registryPath = path.join(tempDir, 'supervisor.json');
const registry1 = createProcessRegistry(registryPath);
registry1.register('worker:1', {
pid: process.pid,
type: 'worker',
startedAt: '2026-03-15T00:00:00.000Z'
});
expect(existsSync(registryPath)).toBe(true);
const diskData = JSON.parse(readFileSync(registryPath, 'utf-8'));
expect(diskData.processes['worker:1']).toBeDefined();
const registry2 = createProcessRegistry(registryPath);
registry2.initialize();
const records = registry2.getAll();
expect(records).toHaveLength(1);
expect(records[0]?.id).toBe('worker:1');
expect(records[0]?.pid).toBe(process.pid);
});
it('prunes dead processes on initialize', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
mkdirSync(tempDir, { recursive: true });
const registryPath = path.join(tempDir, 'supervisor.json');
writeFileSync(registryPath, JSON.stringify({
processes: {
alive: {
pid: process.pid,
type: 'worker',
startedAt: '2026-03-15T00:00:00.000Z'
},
dead: {
pid: 2147483647,
type: 'mcp',
startedAt: '2026-03-15T00:00:01.000Z'
}
}
}));
const registry = createProcessRegistry(registryPath);
registry.initialize();
const records = registry.getAll();
expect(records).toHaveLength(1);
expect(records[0]?.id).toBe('alive');
expect(existsSync(registryPath)).toBe(true);
});
it('handles corrupted registry file gracefully', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
mkdirSync(tempDir, { recursive: true });
const registryPath = path.join(tempDir, 'supervisor.json');
writeFileSync(registryPath, '{ not valid json!!!');
const registry = createProcessRegistry(registryPath);
registry.initialize();
expect(registry.getAll()).toHaveLength(0);
});
});
describe('register and unregister', () => {
it('register adds an entry retrievable by getAll', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
expect(registry.getAll()).toHaveLength(0);
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:00.000Z'
});
const records = registry.getAll();
expect(records).toHaveLength(1);
expect(records[0]?.id).toBe('sdk:1');
expect(records[0]?.type).toBe('sdk');
});
it('unregister removes an entry', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:00.000Z'
});
expect(registry.getAll()).toHaveLength(1);
registry.unregister('sdk:1');
expect(registry.getAll()).toHaveLength(0);
});
it('unregister is a no-op for unknown IDs', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.unregister('nonexistent');
expect(registry.getAll()).toHaveLength(1);
});
});
describe('getAll', () => {
it('returns records sorted by startedAt ascending', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('newest', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:02.000Z'
});
registry.register('oldest', {
pid: process.pid,
type: 'worker',
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.register('middle', {
pid: process.pid,
type: 'mcp',
startedAt: '2026-03-15T00:00:01.000Z'
});
const records = registry.getAll();
expect(records).toHaveLength(3);
expect(records[0]?.id).toBe('oldest');
expect(records[1]?.id).toBe('middle');
expect(records[2]?.id).toBe('newest');
});
it('returns empty array when no entries exist', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
expect(registry.getAll()).toEqual([]);
});
});
describe('getBySession', () => {
it('filters records by session id', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
sessionId: 42,
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.register('sdk:2', {
pid: process.pid,
type: 'sdk',
sessionId: 'other',
startedAt: '2026-03-15T00:00:01.000Z'
});
const records = registry.getBySession(42);
expect(records).toHaveLength(1);
expect(records[0]?.id).toBe('sdk:1');
});
it('returns empty array when no processes match the session', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
sessionId: 42,
startedAt: '2026-03-15T00:00:00.000Z'
});
expect(registry.getBySession(999)).toHaveLength(0);
});
it('matches string and numeric session IDs by string comparison', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
sessionId: '42',
startedAt: '2026-03-15T00:00:00.000Z'
});
expect(registry.getBySession(42)).toHaveLength(1);
});
});
describe('pruneDeadEntries', () => {
it('removes entries with dead PIDs and preserves live ones', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registryPath = path.join(tempDir, 'supervisor.json');
const registry = createProcessRegistry(registryPath);
registry.register('alive', {
pid: process.pid,
type: 'worker',
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.register('dead', {
pid: 2147483647,
type: 'mcp',
startedAt: '2026-03-15T00:00:01.000Z'
});
const removed = registry.pruneDeadEntries();
expect(removed).toBe(1);
expect(registry.getAll()).toHaveLength(1);
expect(registry.getAll()[0]?.id).toBe('alive');
});
it('returns 0 when all entries are alive', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('alive', {
pid: process.pid,
type: 'worker',
startedAt: '2026-03-15T00:00:00.000Z'
});
const removed = registry.pruneDeadEntries();
expect(removed).toBe(0);
expect(registry.getAll()).toHaveLength(1);
});
it('persists changes to disk after pruning', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registryPath = path.join(tempDir, 'supervisor.json');
const registry = createProcessRegistry(registryPath);
registry.register('dead', {
pid: 2147483647,
type: 'mcp',
startedAt: '2026-03-15T00:00:01.000Z'
});
registry.pruneDeadEntries();
const diskData = JSON.parse(readFileSync(registryPath, 'utf-8'));
expect(Object.keys(diskData.processes)).toHaveLength(0);
});
});
describe('clear', () => {
it('removes all entries', () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registryPath = path.join(tempDir, 'supervisor.json');
const registry = createProcessRegistry(registryPath);
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.register('sdk:2', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:01.000Z'
});
expect(registry.getAll()).toHaveLength(2);
registry.clear();
expect(registry.getAll()).toHaveLength(0);
const diskData = JSON.parse(readFileSync(registryPath, 'utf-8'));
expect(Object.keys(diskData.processes)).toHaveLength(0);
});
});
describe('createProcessRegistry', () => {
it('creates an isolated instance with a custom path', () => {
const tempDir1 = makeTempDir();
const tempDir2 = makeTempDir();
tempDirs.push(tempDir1, tempDir2);
const registry1 = createProcessRegistry(path.join(tempDir1, 'supervisor.json'));
const registry2 = createProcessRegistry(path.join(tempDir2, 'supervisor.json'));
registry1.register('sdk:1', {
pid: process.pid,
type: 'sdk',
startedAt: '2026-03-15T00:00:00.000Z'
});
expect(registry1.getAll()).toHaveLength(1);
expect(registry2.getAll()).toHaveLength(0);
});
});
describe('reapSession', () => {
it('unregisters dead processes for the given session', async () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:99:50001', {
pid: 2147483640,
type: 'sdk',
sessionId: 99,
startedAt: '2026-03-15T00:00:00.000Z'
});
registry.register('mcp:99:50002', {
pid: 2147483641,
type: 'mcp',
sessionId: 99,
startedAt: '2026-03-15T00:00:01.000Z'
});
registry.register('sdk:100:50003', {
pid: process.pid,
type: 'sdk',
sessionId: 100,
startedAt: '2026-03-15T00:00:02.000Z'
});
const reaped = await registry.reapSession(99);
expect(reaped).toBe(2);
expect(registry.getBySession(99)).toHaveLength(0);
expect(registry.getBySession(100)).toHaveLength(1);
});
it('returns 0 when no processes match the session', async () => {
const tempDir = makeTempDir();
tempDirs.push(tempDir);
const registry = createProcessRegistry(path.join(tempDir, 'supervisor.json'));
registry.register('sdk:1', {
pid: process.pid,
type: 'sdk',
sessionId: 42,
startedAt: '2026-03-15T00:00:00.000Z'
});
const reaped = await registry.reapSession(999);
expect(reaped).toBe(0);
expect(registry.getAll()).toHaveLength(1);
});
});
});