| import assert from 'node:assert/strict'; |
| import { mkdtemp, rm } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
| import { describe, it } from 'node:test'; |
| import type { LlmConnection, SessionEvent, SessionHeader } from '@maka/core'; |
| import { createSqliteRuntimeStore } from '@maka/storage'; |
| import { createSessionEventMapMemory, mapSessionEventToRuntimeEvent } from '../ai-sdk-flow.js'; |
| import type { InvocationContext } from '../invocation-context.js'; |
| import { PermissionEngine } from '../permission-engine.js'; |
| import { ToolRuntime, type MakaTool } from '../tool-runtime.js'; |
| |
| describe('ToolRuntime with real SQLite boundary', () => { |
| it('persists one atomic prepared/outcome pair around the real implementation', async () => { |
| const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-')); |
| const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite')); |
| try { |
| const permissionEngine = new PermissionEngine({ newId: nextId(), now: () => 1 }); |
| permissionEngine.beginTurn('turn-1'); |
| let implementationCalls = 0; |
| const runtime = new ToolRuntime({ |
| sessionId: 'session-1', |
| header: header(), |
| connection: connection(), |
| modelId: 'model-1', |
| appendMessage: async () => {}, |
| permissionEngine, |
| newId: nextId(), |
| now: nextNow(), |
| getPermissionPauseTarget: () => null, |
| getCurrentRunId: () => 'run-1', |
| getCurrentInvocationId: () => 'invocation-1', |
| runtimeCommitSink: store, |
| }); |
| const tool: MakaTool = { |
| name: 'Read', |
| description: 'read', |
| parameters: {}, |
| permissionRequired: false, |
| recoveryMode: 'replay_safe', |
| impl: async () => { |
| implementationCalls += 1; |
| return { ok: true, text: 'contents' }; |
| }, |
| }; |
| |
| const published: SessionEvent[] = []; |
| |
| await runtime.settleToolCall({ |
| tool, |
| turnId: 'turn-1', |
| toolCallId: 'provider-call-1', |
| input: {}, |
| abortSignal: new AbortController().signal, |
| eventSink: { |
| push: (event) => published.push(event), |
| pushAndWaitUntilConsumed: async (event) => { |
| published.push(event); |
| }, |
| }, |
| }); |
| |
| assert.equal(implementationCalls, 1); |
| const events = await store.readRuntimeEvents('session-1', 'run-1'); |
| assert.deepEqual( |
| events.map((event) => event.content?.kind), |
| ['function_call', undefined, 'function_response'], |
| ); |
| const operationId = events[0]?.refs?.operationId; |
| assert.ok(operationId); |
| assert.equal((await store.readToolOperation(operationId))?.currentState, 'outcome_committed'); |
| assert.deepEqual( |
| events.map((event) => event.invocationId), |
| ['invocation-1', 'invocation-1', 'invocation-1'], |
| ); |
| assert.deepEqual( |
| (await store.readToolJournal(operationId)).map((event) => event.state), |
| ['prepared', 'outcome_committed'], |
| ); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3); |
| |
| const context = invocationContext(); |
| const memory = createSessionEventMapMemory(); |
| const durableEvents = published.filter( |
| (event) => event.type === 'tool_start' || event.type === 'tool_result', |
| ); |
| assert.equal(durableEvents.length, 2); |
| const mappedEvents = durableEvents.map((event) => |
| mapSessionEventToRuntimeEvent(event, context, memory), |
| ); |
| assert.deepEqual( |
| mappedEvents, |
| events.filter( |
| (event) => |
| event.content?.kind === 'function_call' || event.content?.kind === 'function_response', |
| ), |
| ); |
| |
| assert.equal((await store.readRuntimeEvents('session-1', 'run-1')).length, 3); |
| assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3); |
| } finally { |
| store.close(); |
| await rm(root, { recursive: true, force: true }); |
| } |
| }); |
| |
| it('persists the same normalized error event that the Runtime flow later observes', async () => { |
| const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-error-')); |
| const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite')); |
| try { |
| const permissionEngine = new PermissionEngine({ newId: nextId(), now: () => 1 }); |
| permissionEngine.beginTurn('turn-1'); |
| const runtime = new ToolRuntime({ |
| sessionId: 'session-1', |
| header: header(), |
| connection: connection(), |
| modelId: 'model-1', |
| appendMessage: async () => {}, |
| permissionEngine, |
| newId: nextId(), |
| now: nextNow(), |
| getPermissionPauseTarget: () => null, |
| getCurrentRunId: () => 'run-1', |
| getCurrentInvocationId: () => 'invocation-1', |
| runtimeCommitSink: store, |
| }); |
| const published: SessionEvent[] = []; |
| const tool: MakaTool = { |
| name: 'Read', |
| description: 'read', |
| parameters: {}, |
| permissionRequired: false, |
| recoveryMode: 'replay_safe', |
| impl: async () => { |
| throw new Error('disk read failed'); |
| }, |
| }; |
| |
| await runtime.settleToolCall({ |
| tool, |
| turnId: 'turn-1', |
| toolCallId: 'provider-call-1', |
| input: {}, |
| abortSignal: new AbortController().signal, |
| eventSink: { |
| push: (event) => published.push(event), |
| pushAndWaitUntilConsumed: async (event) => { |
| published.push(event); |
| }, |
| }, |
| }); |
| |
| const memory = createSessionEventMapMemory(); |
| const mappedEvents = published |
| .filter((item) => item.type === 'tool_start' || item.type === 'tool_result') |
| .map((event) => mapSessionEventToRuntimeEvent(event, invocationContext(), memory)); |
| const events = await store.readRuntimeEvents('session-1', 'run-1'); |
| assert.deepEqual( |
| mappedEvents, |
| events.filter( |
| (event) => |
| event.content?.kind === 'function_call' || event.content?.kind === 'function_response', |
| ), |
| ); |
| assert.equal(events.length, 3); |
| assert.equal(events[2]?.content?.kind, 'function_response'); |
| assert.equal( |
| events[2]?.content?.kind === 'function_response' ? events[2].content.isError : undefined, |
| true, |
| ); |
| } finally { |
| store.close(); |
| await rm(root, { recursive: true, force: true }); |
| } |
| }); |
| }); |
| |
| function header(): SessionHeader { |
| return { |
| id: 'session-1', |
| workspaceRoot: '/workspace/repo', |
| cwd: '/workspace/repo', |
| createdAt: 1, |
| lastUsedAt: 1, |
| name: 'test', |
| titleIsManual: false, |
| isFlagged: false, |
| labels: [], |
| isArchived: false, |
| status: 'active', |
| statusUpdatedAt: 1, |
| hasUnread: false, |
| backend: 'ai-sdk', |
| llmConnectionSlug: 'connection-1', |
| connectionLocked: true, |
| model: 'model-1', |
| permissionMode: 'ask', |
| schemaVersion: 1, |
| }; |
| } |
| |
| function invocationContext(): InvocationContext { |
| return { |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| source: 'test', |
| startedAt: 1, |
| newId: nextId(), |
| now: () => 1, |
| request: { |
| sessionId: 'session-1', |
| invocationId: 'invocation-1', |
| runId: 'run-1', |
| turnId: 'turn-1', |
| text: 'test', |
| source: 'test', |
| }, |
| }; |
| } |
| |
| function connection(): LlmConnection { |
| return { |
| slug: 'connection-1', |
| name: 'test', |
| providerType: 'openai', |
| defaultModel: 'model-1', |
| enabled: true, |
| createdAt: 1, |
| updatedAt: 1, |
| }; |
| } |
| |
| function nextId(): () => string { |
| let value = 0; |
| return () => `id-${++value}`; |
| } |
| |
| function nextNow(): () => number { |
| let value = 0; |
| return () => ++value; |
| } |