agent-ecosystem/test/main/services/team/RuntimeRunTombstoneStore.test.ts
2026-04-21 20:28:22 +03:00

159 lines
4.3 KiB
TypeScript

import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
assertRuntimeEvidenceRunMatches,
createRuntimeRunTombstoneStore,
RuntimeStaleEvidenceError,
} from '../../../../src/main/services/team/opencode/store/RuntimeRunTombstoneStore';
let tempDir: string;
let now: Date;
let nextId: number;
describe('RuntimeRunTombstoneStore', () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-tombstones-'));
now = new Date('2026-04-21T12:00:00.000Z');
nextId = 1;
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('adds tombstone atomically and rejects matching stale evidence', async () => {
const store = createStore();
await expect(
store.add({
teamName: 'team-a',
runId: 'run-1',
reason: 'stop_requested',
evidenceKinds: ['sse_event', 'permission_reply'],
diagnostic: 'manual stop',
})
).resolves.toMatchObject({
tombstoneId: 'tombstone-1',
teamName: 'team-a',
runId: 'run-1',
reason: 'stop_requested',
evidenceKinds: ['permission_reply', 'sse_event'],
diagnostic: 'manual stop',
});
await expect(
store.assertEvidenceAccepted({
teamName: 'team-a',
runId: 'run-1',
currentRunId: 'run-1',
evidenceKind: 'sse_event',
})
).rejects.toMatchObject({
reason: 'run_tombstoned',
evidenceKind: 'sse_event',
runId: 'run-1',
});
await expect(
store.assertEvidenceAccepted({
teamName: 'team-a',
runId: 'run-1',
currentRunId: 'run-1',
evidenceKind: 'delivery_call',
})
).resolves.toBeUndefined();
});
it('rejects stale run mismatch before tombstone lookup', async () => {
const store = createStore();
await expect(
store.assertEvidenceAccepted({
teamName: 'team-a',
runId: 'old-run',
currentRunId: 'new-run',
evidenceKind: 'delivery_call',
})
).rejects.toMatchObject({
reason: 'run_mismatch',
evidenceKind: 'delivery_call',
runId: 'old-run',
});
});
it('deduplicates same tombstone and compacts expired records', async () => {
const store = createStore();
await store.add({
teamName: 'team-a',
runId: 'run-1',
reason: 'relaunch_started',
ttlMs: 1000,
});
await store.add({
teamName: 'team-a',
runId: 'run-1',
reason: 'relaunch_started',
ttlMs: 1000,
});
await expect(store.list('team-a')).resolves.toHaveLength(1);
now = new Date('2026-04-21T12:00:02.000Z');
await expect(store.compact()).resolves.toBe(1);
await expect(store.list('team-a')).resolves.toEqual([]);
});
it('quarantines invalid tombstone store instead of accepting evidence from corrupt data', async () => {
const filePath = path.join(tempDir, 'tombstones.json');
await fs.writeFile(
filePath,
JSON.stringify({
schemaVersion: 1,
updatedAt: '2026-04-21T12:00:00.000Z',
data: [{ tombstoneId: '', teamName: 'team-a' }],
}),
'utf8'
);
const store = createStore(filePath);
await expect(store.list('team-a')).rejects.toMatchObject({
reason: 'invalid_data',
});
const files = await fs.readdir(tempDir);
expect(files.some((file) => file.includes('invalid_data'))).toBe(true);
});
});
describe('assertRuntimeEvidenceRunMatches', () => {
it('rejects missing evidence run and missing current run explicitly', () => {
expect(() =>
assertRuntimeEvidenceRunMatches({
teamName: 'team-a',
runId: null,
currentRunId: 'run-1',
evidenceKind: 'heartbeat',
})
).toThrow(RuntimeStaleEvidenceError);
expect(() =>
assertRuntimeEvidenceRunMatches({
teamName: 'team-a',
runId: 'run-1',
currentRunId: null,
evidenceKind: 'heartbeat',
})
).toThrow('Rejected runtime evidence without current run: heartbeat');
});
});
function createStore(filePath = path.join(tempDir, 'tombstones.json')) {
return createRuntimeRunTombstoneStore({
filePath,
idFactory: () => `tombstone-${nextId++}`,
clock: () => now,
});
}