UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

192 lines • 11.2 kB
function check(condition, message, actual) { if (!condition) { throw new Error(actual === undefined ? message : `${message} (got ${JSON.stringify(actual)})`); } } const ACTOR = { id: 'contract-actor', roles: ['member'], tenantRef: 't1' }; const ATTACHMENT = { mediaId: 'm1', url: 'https://files.test/m1', contentType: 'image/png', name: 'shot.png', }; async function contents(subject) { return (await subject.store.listQueue(subject.threadId)).map((message) => message.content); } /** * The behaviours a {@link ChatQueueStore} adapter owes the queue: FIFO order with head inserts, * round-tripping every field, edits, moves, conditional removal, the pause, and the * compare-and-set admission the drain relies on. Framework-agnostic — each case throws — so a store * outside this repo (a host's own) can run it from any test runner: * * ```ts * for (const contractCase of CHAT_QUEUE_STORE_CONTRACT) { * it(contractCase.name, async () => contractCase.run(await freshSubject())); * } * ``` */ export const CHAT_QUEUE_STORE_CONTRACT = [ { name: 'keeps FIFO order, with head inserts ahead of everything', async run(subject) { const { store, threadId } = subject; await store.enqueueMessage({ threadId, actor: ACTOR, content: 'a' }); await store.enqueueMessage({ threadId, actor: ACTOR, content: 'b' }); await store.enqueueMessage({ threadId, actor: ACTOR, content: 'now', at: 'head' }); await store.enqueueMessage({ threadId, actor: ACTOR, content: 'c' }); const order = await contents(subject); check(order.join(',') === 'now,a,b,c', 'queue order', order); }, }, { name: 'round-trips every field a queued message carries', async run({ store, threadId }) { const queued = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'with everything', attachments: [ATTACHMENT], agentName: 'research', persona: 'sql', model: 'fast-1', pageContext: { kind: 'invoice', id: 42 }, hostContext: { source: 'slack', delivery: { channel: 'C1', ts: '1.2' } }, interrupt: true, }); const read = await store.getQueuedMessage(queued.id); check(read !== null, 'getQueuedMessage finds it'); const [listed] = await store.listQueue(threadId); for (const message of [queued, read, listed]) { check(message?.threadId === threadId, 'threadId', message?.threadId); check(JSON.stringify(message?.actor) === JSON.stringify(ACTOR), 'actor', message?.actor); check(message?.content === 'with everything', 'content', message?.content); check(JSON.stringify(message?.attachments) === JSON.stringify([ATTACHMENT]), 'attachments', message?.attachments); check(message?.agentName === 'research', 'agentName', message?.agentName); check(message?.persona === 'sql', 'persona', message?.persona); check(message?.model === 'fast-1', 'model', message?.model); check(JSON.stringify(message?.pageContext) === JSON.stringify({ kind: 'invoice', id: 42 }), 'pageContext', message?.pageContext); check(JSON.stringify(message?.hostContext) === JSON.stringify({ source: 'slack', delivery: { channel: 'C1', ts: '1.2' } }), 'hostContext', message?.hostContext); check(message?.interrupt === true, 'interrupt', message?.interrupt); check(typeof message?.createdAt === 'string', 'createdAt is an ISO string'); } const plain = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'plain' }); const plainRead = await store.getQueuedMessage(plain.id); check(plainRead?.attachments === undefined, 'no attachments → absent', plainRead); check(plainRead?.interrupt !== true, 'no interrupt → not an interrupt', plainRead); check(plainRead?.persona === undefined, 'no persona → absent', plainRead); check(plainRead?.hostContext === undefined, 'no hostContext → absent', plainRead); check((await store.getQueuedMessage('missing')) === null, 'unknown id → null'); }, }, { name: 'edits text and attachments, and says when the message is gone', async run({ store, threadId }) { const queued = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'draft', attachments: [ATTACHMENT], }); const edited = await store.updateQueuedMessage(queued.id, { content: 'final' }); check(edited?.content === 'final', 'content edited', edited); check(edited?.attachments?.length === 1, 'attachments untouched by a text edit', edited); const stripped = await store.updateQueuedMessage(queued.id, { attachments: null }); check(stripped?.attachments === undefined, 'attachments dropped', stripped); check((await store.updateQueuedMessage('missing', { content: 'x' })) === null, 'editing an unknown message → null'); }, }, { name: 'turns a waiting message into an interrupt, and back', async run({ store, threadId }) { const queued = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'now', attachments: [ATTACHMENT], }); const promoted = await store.updateQueuedMessage(queued.id, { interrupt: true }); check(promoted?.interrupt === true, 'the patch answers the interrupt', promoted); check(promoted?.content === 'now', 'content untouched by an interrupt patch', promoted); check(promoted?.attachments?.length === 1, 'attachments untouched', promoted); const read = await store.getQueuedMessage(queued.id); check(read?.interrupt === true, 'the interrupt is stored', read); const listed = (await store.listQueue(threadId)).find((message) => message.id === queued.id); check(listed?.interrupt === true, 'the interrupt is listed', listed); const edited = await store.updateQueuedMessage(queued.id, { content: 'now!' }); check(edited?.interrupt === true, 'a text edit keeps the interrupt', edited); const demoted = await store.updateQueuedMessage(queued.id, { interrupt: false }); check(demoted?.interrupt !== true, 'interrupt: false clears it', demoted); }, }, { name: 'moves a message to an index, clamped', async run(subject) { const { store, threadId } = subject; const a = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'a' }); await store.enqueueMessage({ threadId, actor: ACTOR, content: 'b' }); const c = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'c' }); check(await store.moveQueuedMessage(c.id, 0), 'move reports success'); check((await contents(subject)).join(',') === 'c,a,b', 'moved to the head'); check(await store.moveQueuedMessage(c.id, 99), 'move past the end is clamped'); check((await contents(subject)).join(',') === 'a,b,c', 'moved to the tail'); check(await store.moveQueuedMessage(a.id, 1), 'move to the middle'); check((await contents(subject)).join(',') === 'b,a,c', 'moved to the middle'); // A later enqueue still lands at the tail after a reorder. await store.enqueueMessage({ threadId, actor: ACTOR, content: 'd' }); check((await contents(subject)).join(',') === 'b,a,c,d', 'enqueue after a move'); check(!(await store.moveQueuedMessage('missing', 0)), 'moving an unknown message → false'); }, }, { name: 'removes conditionally, and clears a thread', async run(subject) { const { store, threadId } = subject; const a = await store.enqueueMessage({ threadId, actor: ACTOR, content: 'a' }); await store.enqueueMessage({ threadId, actor: ACTOR, content: 'b' }); check(await store.removeQueuedMessage(a.id), 'first removal succeeds'); check(!(await store.removeQueuedMessage(a.id)), 'second removal of the same id → false'); check((await contents(subject)).join(',') === 'b', 'the rest stays'); check((await store.clearQueue(threadId)) === 1, 'clear answers how many it removed'); check((await store.listQueue(threadId)).length === 0, 'cleared'); }, }, { name: 'holds a pause until it is lifted', async run({ store, threadId }) { check((await store.queuePause(threadId)) === null, 'not paused at first'); const pause = { reason: 'run_failed', message: 'model down', at: '2026-01-02T03:04:05.000Z', }; await store.setQueuePause(threadId, pause); const read = await store.queuePause(threadId); check(JSON.stringify(read) === JSON.stringify(pause), 'pause round-trips', read); await store.setQueuePause(threadId, null); check((await store.queuePause(threadId)) === null, 'pause lifted'); }, }, { name: 'admits one run per thread (compare-and-set claim and release)', async run({ store, threadId }) { check(await store.claimActiveStream(threadId, 'run-a'), 'a free thread is claimed'); check(await store.claimActiveStream(threadId, 'run-a'), 'a repeated claim is idempotent'); check(!(await store.claimActiveStream(threadId, 'run-b')), 'a held thread refuses others'); check((await store.activeRunForThread(threadId)) === 'run-a', 'held by the winner'); check(await store.claimActiveStream(threadId, 'run-b', { replacing: 'run-a' }), 'handed over by the holder'); check(!(await store.releaseActiveStream(threadId, 'run-a')), 'a former holder cannot release'); check((await store.activeRunForThread(threadId)) === 'run-b', 'still held by the new holder'); check(await store.releaseActiveStream(threadId, 'run-b'), 'the holder releases'); check((await store.activeRunForThread(threadId)) === null, 'free again'); check(await store.claimActiveStream(threadId, 'run-c', { replacing: 'gone' }), 'a free thread is claimed whatever it replaces'); await store.releaseActiveStream(threadId, 'run-c'); const racers = await Promise.all([ store.claimActiveStream(threadId, 'racer-1'), store.claimActiveStream(threadId, 'racer-2'), ]); check(racers.filter(Boolean).length === 1, 'exactly one of two racing claims wins', racers); }, }, ]; //# sourceMappingURL=chat-queue-store-contract.js.map