agent-ecosystem/test/features/member-work-sync/main/FileRuntimeTurnSettledEventStore.test.ts

160 lines
5.5 KiB
TypeScript

import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { FileRuntimeTurnSettledEventStore } from '@features/member-work-sync/main/infrastructure/FileRuntimeTurnSettledEventStore';
import { RuntimeTurnSettledSpoolPaths } from '@features/member-work-sync/main/infrastructure/RuntimeTurnSettledSpoolPaths';
const roots: string[] = [];
async function makePaths(): Promise<RuntimeTurnSettledSpoolPaths> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'runtime-turn-settled-'));
roots.push(root);
return new RuntimeTurnSettledSpoolPaths(root);
}
afterEach(async () => {
await Promise.allSettled(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
});
describe('FileRuntimeTurnSettledEventStore', () => {
it('claims incoming payloads by atomically moving them to processing', async () => {
const paths = await makePaths();
await fs.mkdir(paths.getIncomingDir(), { recursive: true });
await fs.writeFile(
path.join(paths.getIncomingDir(), '20260429-1.claude.json'),
'{"hook_event_name":"Stop"}',
'utf8'
);
const store = new FileRuntimeTurnSettledEventStore({
paths,
now: () => new Date('2026-04-29T12:00:00.000Z'),
});
const claimed = await store.claimPending(10);
expect(claimed).toHaveLength(1);
expect(claimed[0]).toMatchObject({
fileName: '20260429-1.claude.json',
provider: 'claude',
raw: '{"hook_event_name":"Stop"}',
});
await expect(
fs.stat(path.join(paths.getProcessingDir(), '20260429-1.claude.json'))
).resolves.toMatchObject({ isFile: expect.any(Function) });
await expect(
fs.stat(path.join(paths.getIncomingDir(), '20260429-1.claude.json'))
).rejects.toMatchObject({ code: 'ENOENT' });
});
it('moves processed payload and writes diagnostic metadata', async () => {
const paths = await makePaths();
await fs.mkdir(paths.getIncomingDir(), { recursive: true });
await fs.writeFile(
path.join(paths.getIncomingDir(), '20260429-1.claude.json'),
'{"hook_event_name":"Stop"}',
'utf8'
);
const store = new FileRuntimeTurnSettledEventStore({
paths,
now: () => new Date('2026-04-29T12:00:00.000Z'),
});
const [claimed] = await store.claimPending(1);
await store.markProcessed(claimed, {
outcome: 'enqueued',
teamName: 'team-a',
memberName: 'alice',
event: {
schemaVersion: 1,
provider: 'claude',
hookEventName: 'Stop',
sourceId: 'source-1',
payloadHash: 'hash',
recordedAt: '2026-04-29T12:00:00.000Z',
},
processedAt: '2026-04-29T12:01:00.000Z',
});
const processedPath = path.join(paths.getProcessedDir(), '20260429-1.claude.json');
await expect(fs.stat(processedPath)).resolves.toMatchObject({ isFile: expect.any(Function) });
const meta = JSON.parse(await fs.readFile(`${processedPath}.meta.json`, 'utf8')) as {
outcome?: string;
teamName?: string;
};
expect(meta).toMatchObject({ outcome: 'enqueued', teamName: 'team-a' });
});
it('reclaims stale processing payloads before claiming pending events', async () => {
const paths = await makePaths();
await fs.mkdir(paths.getProcessingDir(), { recursive: true });
const filePath = path.join(paths.getProcessingDir(), '20260429-1.codex.json');
await fs.writeFile(filePath, '{"eventName":"runtime_turn_settled"}', 'utf8');
await fs.utimes(
filePath,
new Date('2026-04-29T11:00:00.000Z'),
new Date('2026-04-29T11:00:00.000Z')
);
const store = new FileRuntimeTurnSettledEventStore({
paths,
now: () => new Date('2026-04-29T12:00:00.000Z'),
processingStaleMs: 60_000,
});
const claimed = await store.claimPending(10);
expect(claimed).toHaveLength(1);
expect(claimed[0]).toMatchObject({
fileName: '20260429-1.codex.json',
provider: 'codex',
raw: '{"eventName":"runtime_turn_settled"}',
});
});
it('claims OpenCode runtime turn-settled payloads', async () => {
const paths = await makePaths();
await fs.mkdir(paths.getIncomingDir(), { recursive: true });
await fs.writeFile(
path.join(paths.getIncomingDir(), '20260429-1.opencode.json'),
'{"eventName":"runtime_turn_settled"}',
'utf8'
);
const store = new FileRuntimeTurnSettledEventStore({
paths,
now: () => new Date('2026-04-29T12:00:00.000Z'),
});
const claimed = await store.claimPending(10);
expect(claimed).toHaveLength(1);
expect(claimed[0]).toMatchObject({
fileName: '20260429-1.opencode.json',
provider: 'opencode',
raw: '{"eventName":"runtime_turn_settled"}',
});
});
it('does not reclaim fresh processing payloads from an active drain', async () => {
const paths = await makePaths();
await fs.mkdir(paths.getProcessingDir(), { recursive: true });
const filePath = path.join(paths.getProcessingDir(), '20260429-1.codex.json');
await fs.writeFile(filePath, '{"eventName":"runtime_turn_settled"}', 'utf8');
await fs.utimes(
filePath,
new Date('2026-04-29T11:59:45.000Z'),
new Date('2026-04-29T11:59:45.000Z')
);
const store = new FileRuntimeTurnSettledEventStore({
paths,
now: () => new Date('2026-04-29T12:00:00.000Z'),
processingStaleMs: 60_000,
});
const claimed = await store.claimPending(10);
expect(claimed).toHaveLength(0);
await expect(fs.stat(filePath)).resolves.toMatchObject({ isFile: expect.any(Function) });
});
});