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

234 lines
6.5 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 { stableHash } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
import {
createOpenCodeBridgeCommandLeaseStore,
createOpenCodeBridgeCommandLedgerStore,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore';
describe('OpenCodeBridgeCommandLedgerStore', () => {
let tempDir: string;
let now: Date;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-bridge-ledger-'));
now = new Date('2026-04-21T12:00:00.000Z');
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('blocks idempotency key reuse with a different payload', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
await expect(
ledger.begin({
idempotencyKey: 'same',
requestId: 'req-1',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash: stableHash({ prompt: 'first' }),
})
).resolves.toBe('started');
await expect(
ledger.begin({
idempotencyKey: 'same',
requestId: 'req-2',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash: stableHash({ prompt: 'second' }),
})
).rejects.toThrow('OpenCode bridge idempotency key reused with different payload');
});
it('marks timeout as unknown outcome and blocks retry until recovery', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
const requestHash = stableHash({ teamName: 'team-a', runId: 'run-1' });
await ledger.begin({
idempotencyKey: 'launch:team-a:run-1',
requestId: 'req-1',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
});
await ledger.markUnknownAfterTimeout({
idempotencyKey: 'launch:team-a:run-1',
error: 'timeout',
});
await expect(
ledger.begin({
idempotencyKey: 'launch:team-a:run-1',
requestId: 'req-2',
command: 'opencode.launchTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).rejects.toThrow('OpenCode bridge command outcome must be reconciled before retry');
await expect(ledger.getByIdempotencyKey('launch:team-a:run-1')).resolves.toMatchObject({
status: 'unknown_after_timeout',
retryable: false,
lastError: 'timeout',
});
});
it('allows same-payload duplicate only after a completed command', async () => {
const ledger = createOpenCodeBridgeCommandLedgerStore({
filePath: path.join(tempDir, 'ledger.json'),
clock: () => now,
});
const requestHash = stableHash({ body: 'same' });
await ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-1',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
});
await expect(
ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-2',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).rejects.toThrow('OpenCode bridge command already started');
await ledger.markCompleted({
idempotencyKey: 'key-1',
response: { ok: true, runId: 'run-1' },
});
await expect(
ledger.begin({
idempotencyKey: 'key-1',
requestId: 'req-3',
command: 'opencode.stopTeam',
teamName: 'team-a',
runId: 'run-1',
requestHash,
})
).resolves.toBe('duplicate_same_payload_completed');
});
});
describe('OpenCodeBridgeCommandLeaseStore', () => {
let tempDir: string;
let now: Date;
let nextId: number;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-bridge-lease-'));
now = new Date('2026-04-21T12:00:00.000Z');
nextId = 1;
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('serializes state-changing commands per team through an active lease', async () => {
const leaseStore = createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(tempDir, 'leases.json'),
idFactory: () => `lease-${nextId++}`,
clock: () => now,
});
const first = await leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.launchTeam',
ttlMs: 10_000,
});
expect(first).toMatchObject({
leaseId: 'lease-1',
state: 'active',
expiresAt: '2026-04-21T12:00:10.000Z',
});
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.stopTeam',
ttlMs: 10_000,
})
).rejects.toThrow('OpenCode bridge command lease already active: lease-1');
await leaseStore.release('lease-1');
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.stopTeam',
ttlMs: 10_000,
})
).resolves.toMatchObject({
leaseId: 'lease-2',
command: 'opencode.stopTeam',
state: 'active',
});
});
it('expires stale active leases before acquiring a new one', async () => {
const leaseStore = createOpenCodeBridgeCommandLeaseStore({
filePath: path.join(tempDir, 'leases.json'),
idFactory: () => `lease-${nextId++}`,
clock: () => now,
});
await leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.launchTeam',
ttlMs: 1000,
});
now = new Date('2026-04-21T12:00:02.000Z');
await expect(
leaseStore.acquire({
teamName: 'team-a',
runId: 'run-1',
command: 'opencode.reconcileTeam',
ttlMs: 1000,
})
).resolves.toMatchObject({
leaseId: 'lease-2',
state: 'active',
});
const persisted = JSON.parse(
await fs.readFile(path.join(tempDir, 'leases.json'), 'utf8')
) as {
data: Array<{ leaseId: string; state: string }>;
};
expect(persisted.data).toEqual([
expect.objectContaining({ leaseId: 'lease-1', state: 'expired' }),
expect.objectContaining({ leaseId: 'lease-2', state: 'active' }),
]);
});
});