1209 lines
40 KiB
TypeScript
1209 lines
40 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 {
|
|
buildOpenCodePromptDeliveryAttemptId,
|
|
createOpenCodePromptDeliveryLedgerStore,
|
|
hashOpenCodePromptDeliveryPayload,
|
|
isOpenCodePromptDeliveryAttemptDue,
|
|
isOpenCodeSessionRefreshResponseState,
|
|
} 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('upgrades legacy pending records with message kind without changing payload identity', async () => {
|
|
const store = createStore();
|
|
const payloadHash = hashOpenCodePromptDeliveryPayload({
|
|
text: 'Work sync check',
|
|
replyRecipient: 'team-lead',
|
|
actionMode: 'do',
|
|
source: 'watcher',
|
|
});
|
|
|
|
const legacy = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-work-sync',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'team-lead',
|
|
actionMode: 'do',
|
|
taskRefs: [],
|
|
payloadHash,
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
const envelope = JSON.parse(await fs.readFile(ledgerPath(), 'utf8')) as {
|
|
data: Record<string, unknown>[];
|
|
};
|
|
delete envelope.data[0].messageKind;
|
|
await fs.writeFile(ledgerPath(), `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
|
|
|
|
const upgraded = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-work-sync',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
messageKind: 'member_work_sync_nudge',
|
|
replyRecipient: 'team-lead',
|
|
actionMode: 'do',
|
|
taskRefs: [],
|
|
payloadHash,
|
|
now: '2026-04-25T10:00:30.000Z',
|
|
});
|
|
|
|
expect(upgraded.id).toBe(legacy.id);
|
|
expect(upgraded.messageKind).toBe('member_work_sync_nudge');
|
|
expect(upgraded.payloadHash).toBe(payloadHash);
|
|
expect(upgraded.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('preserves missing taskRefs as the pending reason for insufficient destination proof', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-taskrefs',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:taskrefs',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
const missingTaskRefs = await store.applyDestinationProof({
|
|
id: record.id,
|
|
visibleReplyInbox: 'user',
|
|
visibleReplyMessageId: 'reply-taskrefs',
|
|
visibleReplyCorrelation: 'relayOfMessageId',
|
|
semanticallySufficient: false,
|
|
diagnostics: ['visible_reply_missing_task_refs_after_merge'],
|
|
observedAt: '2026-04-25T10:00:01.000Z',
|
|
});
|
|
|
|
expect(missingTaskRefs.status).toBe('pending');
|
|
expect(missingTaskRefs.responseState).toBe('responded_visible_message');
|
|
expect(missingTaskRefs.lastReason).toBe('visible_reply_missing_task_refs');
|
|
expect(missingTaskRefs.diagnostics).toContain('visible_reply_missing_task_refs_after_merge');
|
|
});
|
|
|
|
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,
|
|
sessionId: 'oc-session-1',
|
|
runtimePromptMessageId: 'msg_prompt_1',
|
|
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);
|
|
expect(emptyResult.runtimeSessionId).toBe('oc-session-1');
|
|
expect(emptyResult.runtimePromptMessageId).toBe('msg_prompt_1');
|
|
|
|
const noAssistant = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-no-assistant',
|
|
inboxTimestamp: '2026-04-25T09:59:05.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:no-assistant',
|
|
now: '2026-04-25T10:00:06.000Z',
|
|
});
|
|
const noAssistantResult = await store.applyDeliveryResult({
|
|
id: noAssistant.id,
|
|
accepted: true,
|
|
attempted: true,
|
|
responseObservation: {
|
|
state: 'prompt_delivered_no_assistant_message',
|
|
deliveredUserMessageId: 'oc-user-no-assistant',
|
|
assistantMessageId: null,
|
|
toolCallNames: [],
|
|
visibleMessageToolCallId: null,
|
|
visibleReplyMessageId: null,
|
|
visibleReplyCorrelation: null,
|
|
latestAssistantPreview: null,
|
|
reason: 'prompt_delivered_no_assistant_message',
|
|
},
|
|
now: '2026-04-25T10:00:07.000Z',
|
|
});
|
|
|
|
expect(noAssistantResult.status).toBe('unanswered');
|
|
expect(noAssistantResult.responseState).toBe('prompt_delivered_no_assistant_message');
|
|
|
|
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('tracks accepted runtime prompt ids without double-counting recovered command status', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-accepted',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:accepted',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
const firstAccepted = await store.applyDeliveryResult({
|
|
id: record.id,
|
|
accepted: true,
|
|
attempted: true,
|
|
sessionId: 'oc-session-1',
|
|
runtimePromptMessageId: 'msg_prompt_1',
|
|
deliveryAttemptId: 'attempt-1',
|
|
now: '2026-04-25T10:00:05.000Z',
|
|
});
|
|
expect(firstAccepted).toMatchObject({
|
|
status: 'accepted',
|
|
attempts: 1,
|
|
runtimePromptMessageId: 'msg_prompt_1',
|
|
lastRuntimePromptMessageId: 'msg_prompt_1',
|
|
lastDeliveryAttemptIdWithAcceptedPrompt: 'attempt-1',
|
|
});
|
|
expect(firstAccepted.runtimePromptMessageIds).toEqual(['msg_prompt_1']);
|
|
|
|
const recoveredSamePrompt = await store.applyDeliveryResult({
|
|
id: record.id,
|
|
accepted: true,
|
|
attempted: true,
|
|
sessionId: 'oc-session-1',
|
|
runtimePromptMessageId: 'msg_prompt_1',
|
|
deliveryAttemptId: 'attempt-1',
|
|
now: '2026-04-25T10:00:06.000Z',
|
|
});
|
|
expect(recoveredSamePrompt.attempts).toBe(1);
|
|
expect(recoveredSamePrompt.runtimePromptMessageIds).toEqual(['msg_prompt_1']);
|
|
|
|
const retryAccepted = await store.applyDeliveryResult({
|
|
id: record.id,
|
|
accepted: true,
|
|
attempted: true,
|
|
sessionId: 'oc-session-2',
|
|
runtimePromptMessageId: 'msg_prompt_2',
|
|
deliveryAttemptId: 'attempt-2',
|
|
now: '2026-04-25T10:01:00.000Z',
|
|
});
|
|
expect(retryAccepted.attempts).toBe(2);
|
|
expect(retryAccepted.runtimePromptMessageIds).toEqual(['msg_prompt_1', 'msg_prompt_2']);
|
|
expect(retryAccepted.lastRuntimePromptMessageId).toBe('msg_prompt_2');
|
|
});
|
|
|
|
it('tracks session refresh retries without consuming normal delivery attempts', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-session-stale',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:session-stale',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
expect(buildOpenCodePromptDeliveryAttemptId(record)).toBe(
|
|
`${record.id}:1:${record.payloadHash.slice(0, 12)}`
|
|
);
|
|
|
|
const stale = await store.applyDeliveryResult({
|
|
id: record.id,
|
|
accepted: false,
|
|
attempted: true,
|
|
responseObservation: {
|
|
state: 'session_stale',
|
|
deliveredUserMessageId: null,
|
|
assistantMessageId: null,
|
|
toolCallNames: [],
|
|
visibleMessageToolCallId: null,
|
|
visibleReplyMessageId: null,
|
|
visibleReplyCorrelation: null,
|
|
latestAssistantPreview: null,
|
|
reason: 'resolved_behavior_changed:old->new',
|
|
},
|
|
diagnostics: ['OpenCode session reconcile skipped because the stored session is stale'],
|
|
now: '2026-04-25T10:00:05.000Z',
|
|
});
|
|
|
|
expect(stale.attempts).toBe(0);
|
|
expect(stale.responseState).toBe('session_stale');
|
|
expect(stale.lastSessionRefreshReason).toBe('resolved_behavior_changed:old->new');
|
|
|
|
const scheduled = await store.markSessionRefreshScheduled({
|
|
id: record.id,
|
|
nextAttemptAt: '2026-04-25T10:00:10.000Z',
|
|
reason: 'resolved_behavior_changed:old->new',
|
|
scheduledAt: '2026-04-25T10:00:06.000Z',
|
|
});
|
|
|
|
expect(scheduled.status).toBe('retry_scheduled');
|
|
expect(scheduled.attempts).toBe(0);
|
|
expect(scheduled.sessionRefreshAttempts).toBe(1);
|
|
expect(buildOpenCodePromptDeliveryAttemptId(scheduled)).toBe(
|
|
`${record.id}:1:${record.payloadHash.slice(0, 12)}:refresh1`
|
|
);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'opencode_app_mcp_transport_changed:old->new',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: '(resolved_behavior_changed:old->new)',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:old.hash/1=abc->new.hash/2=def.',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:tool_error->session_error',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:responded_non_visible_tool->pending',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:permission_blocked->pending',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason:
|
|
'resolved_behavior_changed:old->new opencode_app_mcp_transport_changed:a->b',
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode session is stale (resolved_behavior_changed:old->new); reading historical messages for log projection only',
|
|
],
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode session is stale (resolved_behavior_changed:old->new); unexpected detail',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new unexpected detail'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['OpenCode API error', 'resolved_behavior_changed:old->new'],
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['OpenCode API error:', 'resolved_behavior_changed:old->new'],
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['OpenCode API errorresolved_behavior_changed:old->new'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['OpenCode API error.', 'opencode_app_mcp_transport_changed:old->new'],
|
|
})
|
|
).toBe(true);
|
|
for (const reason of [
|
|
'opencode_prompt_delivery_session_refresh_scheduled',
|
|
'opencode_session_refresh_scheduled_after_resolved_behavior_changed',
|
|
'OpenCode session refresh scheduled after resolved behavior changed',
|
|
'OpenCode session changed; refreshing the session before retry.',
|
|
]) {
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'pending',
|
|
reason,
|
|
})
|
|
).toBe(true);
|
|
}
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode session is stale (resolved_behavior_changed:old->new); permission denied',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode session is stale (resolved_behavior_changed:old->new); network timeout',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode session is stale (resolved_behavior_changed:old->new); visible_reply_missing_task_refs',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new', 'unable to connect to provider'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'OpenCode API error',
|
|
'resolved_behavior_changed:old->new',
|
|
'permission denied',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new', 'auth_unavailable'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: [
|
|
'resolved_behavior_changed:old->new',
|
|
'Key limit exceeded (total limit). Manage it using OpenRouter settings.',
|
|
],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new', '429 too many requests'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:old->new permission denied',
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:old->new;permission_denied',
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:old->new:permission_denied',
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'resolved_behavior_changed:old->new_permission_denied',
|
|
})
|
|
).toBe(false);
|
|
for (const suffix of ['error', 'failed', 'failure', 'aborted', 'canceled', 'cancelled', 'interrupted', 'enospc']) {
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: `resolved_behavior_changed:old->new_${suffix}`,
|
|
})
|
|
).toBe(false);
|
|
}
|
|
for (const reason of [
|
|
'resolved_behavior_changed:old->new/auth_unavailable',
|
|
'resolved_behavior_changed:old->new permission denied',
|
|
'resolved_behavior_changed:old->new permission_blocked',
|
|
'resolved_behavior_changed:old->new login required',
|
|
'resolved_behavior_changed:old->new not logged in',
|
|
'resolved_behavior_changed:old->new missing credentials',
|
|
'resolved_behavior_changed:old->new access denied',
|
|
'resolved_behavior_changed:old->new 401',
|
|
'resolved_behavior_changed:old->new;key limit exceeded',
|
|
'resolved_behavior_changed:old->new-network_timeout',
|
|
'resolved_behavior_changed:old->new(non_visible_tool_without_task_progress)',
|
|
'resolved_behavior_changed:old->new interrupted',
|
|
'opencode_app_mcp_transport_changed:old->new/permission_denied',
|
|
'opencode_app_mcp_transport_changed:old->new;visible_reply_missing_task_refs',
|
|
]) {
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason,
|
|
})
|
|
).toBe(false);
|
|
}
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new', 'cancelled'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['resolved_behavior_changed:old->new', 'login required'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
reason: 'opencode_app_mcp_transport_changed:old->new/permission_denied',
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
diagnostics: ['opencode_app_mcp_transport_changed:old->new:permission_denied'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['permission denied'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['permission_blocked'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['authentication_failed'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['Free usage exceeded, subscribe to Go'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['visible_reply_missing_task_refs'],
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isOpenCodeSessionRefreshResponseState({
|
|
responseState: 'session_stale',
|
|
diagnostics: ['OpenCode session reconcile skipped because the stored session is stale'],
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('does not treat session_stale with action-required diagnostics as a refresh retry', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-session-stale-auth-error',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:session-stale-auth-error',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
const staleWithAuthFailure = await store.applyDeliveryResult({
|
|
id: record.id,
|
|
accepted: false,
|
|
attempted: true,
|
|
responseObservation: {
|
|
state: 'session_stale',
|
|
deliveredUserMessageId: null,
|
|
assistantMessageId: null,
|
|
toolCallNames: [],
|
|
visibleMessageToolCallId: null,
|
|
visibleReplyMessageId: null,
|
|
visibleReplyCorrelation: null,
|
|
latestAssistantPreview: null,
|
|
reason: 'permission denied',
|
|
},
|
|
diagnostics: ['permission denied'],
|
|
now: '2026-04-25T10:00:05.000Z',
|
|
});
|
|
|
|
expect(staleWithAuthFailure.attempts).toBe(1);
|
|
expect(staleWithAuthFailure.responseState).toBe('session_stale');
|
|
expect(staleWithAuthFailure.lastSessionRefreshReason).toBeNull();
|
|
expect(buildOpenCodePromptDeliveryAttemptId(staleWithAuthFailure)).toBe(
|
|
`${record.id}:2:${record.payloadHash.slice(0, 12)}`
|
|
);
|
|
});
|
|
|
|
it('keeps schema-1 legacy prompt-id fields compatible and normalizes when touched', async () => {
|
|
const store = createStore();
|
|
const legacy = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-legacy-runtime-prompt',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:legacy-runtime-prompt',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
const envelope = JSON.parse(await fs.readFile(ledgerPath(), 'utf8')) as {
|
|
data: Record<string, unknown>[];
|
|
};
|
|
delete envelope.data[0].runtimePromptMessageIds;
|
|
delete envelope.data[0].lastRuntimePromptMessageId;
|
|
delete envelope.data[0].lastDeliveryAttemptIdWithAcceptedPrompt;
|
|
await fs.writeFile(ledgerPath(), `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
|
|
|
|
await expect(store.list()).resolves.toHaveLength(1);
|
|
|
|
const touched = await store.applyDeliveryResult({
|
|
id: legacy.id,
|
|
accepted: true,
|
|
attempted: true,
|
|
runtimePromptMessageId: 'msg_prompt_legacy_touch',
|
|
deliveryAttemptId: 'attempt-legacy-touch',
|
|
now: '2026-04-25T10:00:05.000Z',
|
|
});
|
|
expect(touched.runtimePromptMessageIds).toEqual(['msg_prompt_legacy_touch']);
|
|
expect(touched.lastRuntimePromptMessageId).toBe('msg_prompt_legacy_touch');
|
|
expect(touched.lastDeliveryAttemptIdWithAcceptedPrompt).toBe('attempt-legacy-touch');
|
|
});
|
|
|
|
it('accepts task stall remediation message kind across ledger validation', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'task-stall:team-a:jack:task-a',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watchdog',
|
|
messageKind: 'task_stall_remediation',
|
|
replyRecipient: 'team-lead',
|
|
actionMode: 'do',
|
|
payloadHash: 'sha256:task-stall',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
|
|
expect(record.messageKind).toBe('task_stall_remediation');
|
|
await expect(store.list()).resolves.toMatchObject([
|
|
{ messageKind: 'task_stall_remediation' },
|
|
]);
|
|
});
|
|
|
|
it('upgrades acceptance-unknown records when exact observation finds the prompt', async () => {
|
|
const store = createStore();
|
|
const record = await store.ensurePending({
|
|
teamName: 'team-a',
|
|
memberName: 'jack',
|
|
laneId: 'secondary:opencode:jack',
|
|
inboxMessageId: 'msg-observed-later',
|
|
inboxTimestamp: '2026-04-25T09:59:00.000Z',
|
|
source: 'watcher',
|
|
replyRecipient: 'user',
|
|
payloadHash: 'sha256:observed-later',
|
|
now: '2026-04-25T10:00:00.000Z',
|
|
});
|
|
const unknown = await store.markAcceptanceUnknown({
|
|
id: record.id,
|
|
reason: 'opencode_prompt_acceptance_unknown_after_bridge_timeout',
|
|
nextAttemptAt: '2026-04-25T10:01:00.000Z',
|
|
markedAt: '2026-04-25T10:00:45.000Z',
|
|
});
|
|
expect(unknown.acceptanceUnknown).toBe(true);
|
|
|
|
const observed = await store.applyObservation({
|
|
id: record.id,
|
|
sessionId: 'oc-session-recovered',
|
|
runtimePromptMessageId: 'msg_prompt_recovered',
|
|
responseObservation: {
|
|
state: 'pending',
|
|
deliveredUserMessageId: 'msg_prompt_recovered',
|
|
assistantMessageId: null,
|
|
toolCallNames: [],
|
|
visibleMessageToolCallId: null,
|
|
visibleReplyMessageId: null,
|
|
visibleReplyCorrelation: null,
|
|
latestAssistantPreview: null,
|
|
reason: 'assistant_response_pending',
|
|
},
|
|
observedAt: '2026-04-25T10:00:50.000Z',
|
|
});
|
|
|
|
expect(observed.status).toBe('accepted');
|
|
expect(observed.acceptanceUnknown).toBe(false);
|
|
expect(observed.acceptedAt).toBe('2026-04-25T10:00:50.000Z');
|
|
expect(observed.runtimeSessionId).toBe('oc-session-recovered');
|
|
expect(observed.runtimePromptMessageIds).toEqual(['msg_prompt_recovered']);
|
|
});
|
|
|
|
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,
|
|
]);
|
|
});
|
|
});
|