agent-ecosystem/test/main/services/team/OpenCodePromptDeliveryLedger.test.ts
2026-05-05 00:47:05 +03:00

575 lines
18 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 {
createOpenCodePromptDeliveryLedgerStore,
hashOpenCodePromptDeliveryPayload,
isOpenCodePromptDeliveryAttemptDue,
} from '@main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger';
describe('OpenCodePromptDeliveryLedger', () => {
let tempDir = '';
const corruptionCases: Array<[string, (record: Record<string, unknown>) => void]> = [
[
'unknown delivery status',
(record) => {
record.status = 'quietly_broken';
},
],
[
'unknown response state',
(record) => {
record.responseState = 'assistant_maybe_replied';
},
],
[
'invalid task reference shape',
(record) => {
record.taskRefs = [{ taskId: 'task-1', displayId: '#1' }];
},
],
[
'invalid diagnostic array',
(record) => {
record.diagnostics = ['ok', 42];
},
],
[
'invalid visible reply correlation',
(record) => {
record.visibleReplyCorrelation = 'guessed_from_text';
},
],
];
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-prompt-ledger-'));
});
afterEach(async () => {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
function createStore() {
return createOpenCodePromptDeliveryLedgerStore({
filePath: path.join(tempDir, 'opencode-prompt-delivery-ledger.json'),
clock: () => new Date('2026-04-25T10:00:00.000Z'),
});
}
function ledgerPath() {
return path.join(tempDir, 'opencode-prompt-delivery-ledger.json');
}
async function writeCorruptedLedgerRecord(
mutate: (record: Record<string, unknown>) => void
): Promise<ReturnType<typeof createStore>> {
const store = createStore();
await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-corrupt',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash: 'sha256:corrupt',
now: '2026-04-25T10:00:00.000Z',
});
const envelope = JSON.parse(await fs.readFile(ledgerPath(), 'utf8')) as {
data: Record<string, unknown>[];
};
mutate(envelope.data[0]);
await fs.writeFile(ledgerPath(), `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
return store;
}
it('is idempotent for the same inbox message and payload hash', async () => {
const store = createStore();
const payloadHash = hashOpenCodePromptDeliveryPayload({
text: 'Please answer',
replyRecipient: 'user',
actionMode: 'ask',
source: 'watcher',
});
const first = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash,
now: '2026-04-25T10:00:00.000Z',
});
const second = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash,
now: '2026-04-25T10:00:30.000Z',
});
expect(second.id).toBe(first.id);
expect(second.attempts).toBe(0);
await expect(store.list()).resolves.toHaveLength(1);
});
it.each(corruptionCases)('rejects corrupted persisted records with %s', async (_name, mutate) => {
const store = await writeCorruptedLedgerRecord(mutate);
await expect(store.list()).rejects.toMatchObject({
reason: 'invalid_data',
});
await expect(fs.readdir(tempDir)).resolves.toContain(
'opencode-prompt-delivery-ledger.json'
);
expect((await fs.readdir(tempDir)).some((name) => name.includes('.invalid_data.'))).toBe(true);
});
it('marks same logical delivery with a different payload hash terminal', async () => {
const store = createStore();
const original = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:first',
now: '2026-04-25T10:00:00.000Z',
});
const mismatch = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:second',
now: '2026-04-25T10:00:30.000Z',
});
expect(mismatch.id).toBe(original.id);
expect(mismatch.status).toBe('failed_terminal');
expect(mismatch.lastReason).toBe('opencode_prompt_delivery_payload_mismatch');
expect(mismatch.diagnostics.join('\n')).toContain('payload hash does not match');
await expect(store.list()).resolves.toHaveLength(1);
});
it('keeps ack-only destination proof nonterminal and due retry checks deterministic', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:first',
now: '2026-04-25T10:00:00.000Z',
});
const ackOnly = await store.applyDestinationProof({
id: record.id,
visibleReplyInbox: 'user',
visibleReplyMessageId: 'reply-1',
visibleReplyCorrelation: 'relayOfMessageId',
semanticallySufficient: false,
observedAt: '2026-04-25T10:00:01.000Z',
});
expect(ackOnly.status).toBe('pending');
expect(ackOnly.responseState).toBe('responded_visible_message');
expect(ackOnly.lastReason).toBe('visible_reply_ack_only_still_requires_answer');
const scheduled = await store.markNextAttemptScheduled({
id: record.id,
status: 'retry_scheduled',
nextAttemptAt: '2026-04-25T10:00:30.000Z',
reason: 'visible_reply_ack_only_still_requires_answer',
scheduledAt: '2026-04-25T10:00:02.000Z',
});
expect(isOpenCodePromptDeliveryAttemptDue(scheduled, Date.parse('2026-04-25T10:00:29.000Z'))).toBe(
false
);
expect(isOpenCodePromptDeliveryAttemptDue(scheduled, Date.parse('2026-04-25T10:00:30.000Z'))).toBe(
true
);
});
it('records empty assistant delivery results as unanswered and stores plain text previews', async () => {
const store = createStore();
const unanswered = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-empty',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:empty',
now: '2026-04-25T10:00:00.000Z',
});
const emptyResult = await store.applyDeliveryResult({
id: unanswered.id,
accepted: true,
attempted: true,
responseObservation: {
state: 'empty_assistant_turn',
deliveredUserMessageId: 'oc-user-1',
assistantMessageId: 'oc-assistant-1',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: 'empty_assistant_turn',
},
now: '2026-04-25T10:00:05.000Z',
});
expect(emptyResult.status).toBe('unanswered');
expect(emptyResult.responseState).toBe('empty_assistant_turn');
expect(emptyResult.attempts).toBe(1);
const plain = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-plain',
inboxTimestamp: '2026-04-25T09:59:10.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:plain',
now: '2026-04-25T10:00:10.000Z',
});
const observed = await store.applyObservation({
id: plain.id,
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-2',
assistantMessageId: 'oc-assistant-2',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: 'Понял',
reason: null,
},
observedAt: '2026-04-25T10:00:15.000Z',
});
expect(observed.status).toBe('responded');
expect(observed.observedAssistantPreview).toBe('Понял');
});
it('keeps plain-text responses active until their visible inbox reply is materialized', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-plain-visible',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash: 'sha256:plain-visible',
now: '2026-04-25T10:00:00.000Z',
});
const responded = await store.applyDeliveryResult({
id: record.id,
accepted: true,
attempted: true,
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-plain',
assistantMessageId: 'oc-assistant-plain',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: 'Concrete visible answer.',
reason: null,
},
now: '2026-04-25T10:00:05.000Z',
});
expect(responded.status).toBe('responded');
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
})).resolves.toMatchObject({
id: record.id,
responseState: 'responded_plain_text',
});
const materialized = await store.applyDestinationProof({
id: record.id,
visibleReplyInbox: 'user',
visibleReplyMessageId: 'opencode-plain-reply-1',
visibleReplyCorrelation: 'plain_assistant_text',
semanticallySufficient: true,
observedAt: '2026-04-25T10:00:06.000Z',
});
expect(materialized).toMatchObject({
status: 'responded',
responseState: 'responded_plain_text',
visibleReplyCorrelation: 'plain_assistant_text',
});
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
})).resolves.toBeNull();
});
it('does not keep responded live deliveries active when no inbox commit is needed', async () => {
const store = createStore();
const direct = await store.ensurePending({
teamName: 'team-a',
memberName: 'bob',
laneId: 'secondary:opencode:bob',
inboxMessageId: 'direct-ui-send',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'ui-send',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash: 'sha256:direct',
now: '2026-04-25T10:00:00.000Z',
});
const responded = await store.applyDeliveryResult({
id: direct.id,
accepted: true,
attempted: true,
responseObservation: {
state: 'responded_visible_message',
deliveredUserMessageId: 'oc-user-direct',
assistantMessageId: 'oc-assistant-direct',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'tool-call-direct',
visibleReplyMessageId: 'reply-direct',
visibleReplyCorrelation: 'direct_child_message_send',
latestAssistantPreview: 'I will send the requested update.',
reason: null,
},
now: '2026-04-25T10:00:05.000Z',
});
expect(responded.status).toBe('responded');
expect(responded.inboxReadCommittedAt).toBeNull();
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'bob',
laneId: 'secondary:opencode:bob',
})).resolves.toBeNull();
const peer = await store.ensurePending({
teamName: 'team-a',
memberName: 'bob',
laneId: 'secondary:opencode:bob',
inboxMessageId: 'peer-relay',
inboxTimestamp: '2026-04-25T10:01:00.000Z',
source: 'manual',
replyRecipient: 'jack',
actionMode: 'delegate',
taskRefs: [],
payloadHash: 'sha256:peer',
now: '2026-04-25T10:01:00.000Z',
});
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'bob',
laneId: 'secondary:opencode:bob',
})).resolves.toMatchObject({
id: peer.id,
inboxMessageId: 'peer-relay',
});
});
it('lists due nonterminal records in deterministic due order', async () => {
const store = createStore();
const first = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:first',
now: '2026-04-25T10:00:00.000Z',
});
const second = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-2',
inboxTimestamp: '2026-04-25T09:59:10.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:second',
now: '2026-04-25T10:00:01.000Z',
});
await store.markNextAttemptScheduled({
id: first.id,
status: 'retry_scheduled',
nextAttemptAt: '2026-04-25T10:00:20.000Z',
reason: 'empty_assistant_turn',
scheduledAt: '2026-04-25T10:00:02.000Z',
});
await store.markNextAttemptScheduled({
id: second.id,
status: 'retry_scheduled',
nextAttemptAt: '2026-04-25T10:00:10.000Z',
reason: 'empty_assistant_turn',
scheduledAt: '2026-04-25T10:00:02.000Z',
});
const dueBefore = await store.listDue({
teamName: 'team-a',
now: new Date('2026-04-25T10:00:15.000Z'),
limit: 10,
});
expect(dueBefore.map((record) => record.inboxMessageId)).toEqual(['msg-2']);
const dueAfter = await store.listDue({
teamName: 'team-a',
now: new Date('2026-04-25T10:00:21.000Z'),
limit: 10,
});
expect(dueAfter.map((record) => record.inboxMessageId)).toEqual(['msg-2', 'msg-1']);
});
it('rebuilds missing ledger rows as acceptance-unknown retryable records', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-1',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watchdog',
replyRecipient: 'user',
payloadHash: 'sha256:first',
now: '2026-04-25T10:00:00.000Z',
});
const rebuilt = await store.markAcceptanceUnknown({
id: record.id,
reason: 'opencode_prompt_delivery_ledger_rebuilt_from_unread_inbox',
nextAttemptAt: '2026-04-25T10:00:00.000Z',
markedAt: '2026-04-25T10:00:00.000Z',
});
expect(rebuilt.status).toBe('failed_retryable');
expect(rebuilt.acceptanceUnknown).toBe(true);
expect(rebuilt.responseState).toBe('not_observed');
expect(rebuilt.lastReason).toBe('opencode_prompt_delivery_ledger_rebuilt_from_unread_inbox');
});
it('prunes only terminal records after their retention windows', async () => {
const store = createStore();
const responded = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'responded',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:responded',
now: '2026-04-25T10:00:00.000Z',
});
await store.applyDestinationProof({
id: responded.id,
visibleReplyInbox: 'user',
visibleReplyMessageId: 'reply-1',
visibleReplyCorrelation: 'relayOfMessageId',
semanticallySufficient: true,
observedAt: '2026-04-25T10:00:01.000Z',
});
await store.markInboxReadCommitted({
id: responded.id,
committedAt: '2026-04-25T10:00:02.000Z',
});
const failed = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'failed',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:failed',
now: '2026-04-25T10:00:00.000Z',
});
await store.markFailedTerminal({
id: failed.id,
reason: 'opencode_runtime_not_active',
failedAt: '2026-04-25T10:00:03.000Z',
});
const active = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'active',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:active',
now: '2026-04-25T10:00:00.000Z',
});
await expect(store.pruneTerminalRecords({
now: new Date('2026-04-25T10:00:20.000Z'),
respondedRetentionMs: 10_000,
failedRetentionMs: 30_000,
})).resolves.toEqual({ pruned: 1, remaining: 2 });
expect((await store.list()).map((record) => record.inboxMessageId).sort()).toEqual([
active.inboxMessageId,
failed.inboxMessageId,
]);
await expect(store.pruneTerminalRecords({
now: new Date('2026-04-25T10:00:40.000Z'),
respondedRetentionMs: 10_000,
failedRetentionMs: 30_000,
})).resolves.toEqual({ pruned: 1, remaining: 1 });
expect((await store.list()).map((record) => record.inboxMessageId)).toEqual([
active.inboxMessageId,
]);
});
});