| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
| import { DatabaseSync } from 'node:sqlite'; |
| import { test } from 'node:test'; |
| import { |
| OPERATIONAL_STATE_DATABASE_NAME, |
| OPERATIONAL_STATE_SCHEMA_VERSION, |
| } from '@maka/storage/operational-state-store'; |
| import { createSessionBundleFileService } from '@maka/storage/session-bundle-file-service'; |
| import type { SessionBundleHydration } from '@maka/storage/session-bundle-contract'; |
| import { SQLITE_RUNTIME_SCHEMA_VERSION } from '@maka/storage/sqlite-runtime-store'; |
| import { SQLITE_SESSION_METADATA_SCHEMA_VERSION } from '@maka/storage/sqlite-session-metadata-store'; |
| import { createSessionStore } from '@maka/storage/session-store'; |
| import { |
| exportSessionBundle, |
| SESSION_EXPORT_BUNDLE_LIMITS, |
| type ExportSessionBundleResult, |
| } from '../session-export.js'; |
| |
| const CONNECTION_SLUG = 'test-connection'; |
| const MODEL = 'test-model'; |
| |
| async function makeWorkspace(name: string): Promise<string> { |
| const root = await mkdtempRoot(name); |
| await mkdir(join(root, 'workspace'), { recursive: true }); |
| return root; |
| } |
| |
| async function mkdtempRoot(name: string): Promise<string> { |
| const { mkdtemp } = await import('node:fs/promises'); |
| return mkdtemp(join(tmpdir(), `${name}-`)); |
| } |
| |
| function withRoot( |
| name: string, |
| run: (root: string, workspaceRoot: string) => Promise<void>, |
| ): () => Promise<void> { |
| return async () => { |
| const root = await makeWorkspace(name); |
| try { |
| await run(root, join(root, 'workspace')); |
| } finally { |
| await rm(root, { recursive: true, force: true }); |
| } |
| }; |
| } |
| |
| async function createSession( |
| workspaceRoot: string, |
| overrides?: { |
| name?: string; |
| }, |
| ): Promise<string> { |
| const store = createSessionStore(workspaceRoot); |
| try { |
| const header = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| name: overrides?.name ?? 'Exported', |
| }); |
| return header.id; |
| } finally { |
| await store.close?.(); |
| } |
| } |
| |
| function openDatabase(workspaceRoot: string, readOnly = false): DatabaseSync { |
| return new DatabaseSync(join(workspaceRoot, OPERATIONAL_STATE_DATABASE_NAME), { |
| ...(readOnly ? { readOnly: true } : {}), |
| }); |
| } |
| |
| /** |
| * A real artifact row. |
| * |
| * The metadata codec is strict, and deliberately so: the record carries an |
| * exact key set, and `relativePath` must equal `<sessionId>/<id>-<name>`. That |
| * equality is what keeps a record from naming a file outside its own Session, |
| * and it is why the export can trust these paths. A fixture that hand-rolls a |
| * looser shape decodes to nothing, and then an artifact test proves only that |
| * the export copied no artifacts. |
| */ |
| async function addArtifactRecord( |
| workspaceRoot: string, |
| sessionId: string, |
| artifactId: string, |
| options: { bytes?: string; name?: string } = {}, |
| ): Promise<string> { |
| const name = options.name ?? 'artifact.txt'; |
| const relativePath = `${sessionId}/${artifactId}-${name}`; |
| const record = { |
| id: artifactId, |
| sessionId, |
| turnId: 'turn-1', |
| createdAt: 0, |
| name, |
| kind: 'file', |
| relativePath, |
| sizeBytes: options.bytes?.length ?? 0, |
| source: 'tool_result', |
| }; |
| const db = openDatabase(workspaceRoot); |
| try { |
| if (options.bytes !== undefined) { |
| await mkdir(join(workspaceRoot, 'artifacts', sessionId), { recursive: true }); |
| await writeFile(join(workspaceRoot, 'artifacts', relativePath), options.bytes); |
| } |
| db.prepare(` |
| INSERT INTO artifact_records(artifact_id, session_id, created_at, relative_path, record_json) |
| VALUES (?, ?, 0, ?, ?) |
| `).run(artifactId, sessionId, relativePath, JSON.stringify(record)); |
| } finally { |
| db.close(); |
| } |
| return relativePath; |
| } |
| |
| function addOpenInvocation(workspaceRoot: string, sessionId: string): void { |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.exec(` |
| INSERT INTO runtime_events( |
| session_id, run_id, invocation_id, turn_id, event_id, event_seq, |
| event_kind, committed_at, payload_json |
| ) |
| VALUES ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'event-open', 1, |
| 'invocation_opened', 1, '{}') |
| `); |
| } finally { |
| db.close(); |
| } |
| } |
| |
| /** Query the database the bundle actually carries. */ |
| function openExported(hydration: SessionBundleHydration): DatabaseSync { |
| return new DatabaseSync(join(hydration.stateRoot, OPERATIONAL_STATE_DATABASE_NAME), { |
| readOnly: true, |
| }); |
| } |
| |
| function exportedArtifactPath(hydration: SessionBundleHydration, relativePath: string): string { |
| return join(hydration.stateRoot, 'artifacts', relativePath); |
| } |
| |
| async function createSubagentSession( |
| store: ReturnType<typeof createSessionStore>, |
| workspaceRoot: string, |
| parentSessionId: string, |
| toolCallId: string, |
| ): Promise<string> { |
| const child = await store.createSubagent({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId, |
| spawnedBy: { parentRunId: 'parent-run', parentTurnId: 'parent-turn', toolCallId }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read the assigned workspace task.', |
| toolNames: ['Read'], |
| categoryPolicy: { read: 'allow' }, |
| }, |
| subagentSpawn: { |
| schemaVersion: 1, |
| requestFingerprint: 'a'.repeat(64), |
| initialTurnId: `turn-${toolCallId}`, |
| initialRunId: `run-${toolCallId}`, |
| }, |
| } as Parameters<typeof store.createSubagent>[0]); |
| return child.header.id; |
| } |
| |
| async function hydrateExport( |
| destination: string, |
| sessionId: string, |
| destinationRoot: string, |
| ): Promise<SessionBundleHydration> { |
| return createSessionBundleFileService().hydrate({ |
| source: { path: destination }, |
| limits: SESSION_EXPORT_BUNDLE_LIMITS, |
| expectedSessionId: sessionId, |
| destinationRoot, |
| }); |
| } |
| |
| async function exportOk( |
| workspaceRoot: string, |
| sessionId: string, |
| destination: string, |
| ): Promise<Extract<ExportSessionBundleResult, { ok: true }>> { |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| if (!result.ok) { |
| assert.fail(`expected export to succeed, got ${JSON.stringify(result.reason)}`); |
| } |
| return result; |
| } |
| |
| test( |
| 'exports session state, artifacts, and inspectable identity', |
| withRoot('maka-session-export', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| const livePath = await addArtifactRecord(workspaceRoot, sessionId, 'live', { bytes: 'LIVE' }); |
| const destination = join(root, 'bundle.maka-session'); |
| |
| const result = await exportOk(workspaceRoot, sessionId, destination); |
| assert.equal(result.export.rootSessionId, sessionId); |
| // Read from the source database's own registry, not this build's constants. |
| // A fixture workspace is current, so they agree here — the point is where |
| // the number came from, which the schema_unsupported test pins down. |
| assert.equal(result.export.schema.runtime, SQLITE_RUNTIME_SCHEMA_VERSION); |
| assert.equal(result.export.schema.session_metadata, SQLITE_SESSION_METADATA_SCHEMA_VERSION); |
| assert.equal(result.export.schema.operational, OPERATIONAL_STATE_SCHEMA_VERSION); |
| assert.deepEqual(result.export.sessionIds, [sessionId]); |
| assert.equal(result.export.connection?.llmConnectionSlug, CONNECTION_SLUG); |
| assert.equal(result.export.connection?.model, MODEL); |
| assert.equal((await stat(destination)).isFile(), true); |
| |
| const inspection = await createSessionBundleFileService().inspect({ |
| source: { path: destination }, |
| limits: SESSION_EXPORT_BUNDLE_LIMITS, |
| }); |
| const exportManifest = JSON.parse( |
| Buffer.from(inspection.stateIdentity.bytes).toString('utf8'), |
| ) as { rootSessionId?: string }; |
| assert.equal(exportManifest.rootSessionId, sessionId); |
| |
| const hydration = await hydrateExport(destination, sessionId, join(root, 'hydrated')); |
| assert.equal(await readFile(exportedArtifactPath(hydration, livePath), 'utf8'), 'LIVE'); |
| // The bundle carries the database itself, filtered — not a re-encoding of |
| // it — so the Session is queryable straight out of the archive. |
| const exported = openExported(hydration); |
| try { |
| const row = exported.prepare('SELECT COUNT(*) AS count FROM session_metadata').get() as { |
| count?: unknown; |
| }; |
| assert.equal(Number(row.count), 1); |
| } finally { |
| exported.close(); |
| } |
| }), |
| ); |
| |
| test('omits diagnostics and keeps event types this build has never seen', async () => { |
| const root = await makeWorkspace('maka-session-export-content'); |
| try { |
| const workspaceRoot = join(root, 'workspace'); |
| const sessionId = await createSession(workspaceRoot); |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.exec(` |
| INSERT INTO core_agent_runs(session_id, run_id, created_at) |
| VALUES ('${sessionId}', 'run-1', 0); |
| INSERT INTO core_agent_run_events( |
| session_id, run_id, sequence, event_id, event_type, event_ts, record_json |
| ) |
| VALUES |
| ('${sessionId}', 'run-1', 0, 'capture', 'provider_request_captured', 1, '{"diagnostic":true}'), |
| ('${sessionId}', 'run-1', 1, 'future', 'zz_unknown_future', 2, '{"kept":true}'); |
| `); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportOk(workspaceRoot, sessionId, destination); |
| assert.deepEqual(result.export.omittedEventTypes, [ |
| 'provider_request_attempt_recorded', |
| 'provider_request_captured', |
| 'model_call_attempt_recorded', |
| 'model_stream_started', |
| 'model_stream_completed', |
| 'model_stream_failed', |
| 'send_diagnostics_recorded', |
| 'plan_context_resolved', |
| 'skill_catalog_built', |
| 'skill_searched', |
| 'skill_loaded', |
| 'skill_load_failed', |
| 'tool_searched', |
| 'request_composition_resolved', |
| 'trace_write_failed', |
| ]); |
| assert.equal(result.export.diagnosticsOmitted, true); |
| |
| const hydration = await hydrateExport(destination, sessionId, join(root, 'hydrated')); |
| const exported = openExported(hydration); |
| try { |
| const kept = ( |
| exported |
| .prepare('SELECT event_type FROM core_agent_run_events ORDER BY sequence') |
| .all() as Array<{ event_type?: unknown }> |
| ).map((row) => String(row.event_type)); |
| // The diagnostic row is gone; the type this build has never seen is kept, |
| // because an export moves rows rather than interpreting them. |
| assert.deepEqual(kept, ['zz_unknown_future']); |
| } finally { |
| exported.close(); |
| } |
| } finally { |
| await rm(root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('exports the complete subagent subtree with per-session artifacts', async () => { |
| const root = await makeWorkspace('maka-session-export-subtree'); |
| try { |
| const workspaceRoot = join(root, 'workspace'); |
| const store = createSessionStore(workspaceRoot); |
| try { |
| const parent = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| const firstChild = await store.createSubagent({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId: parent.id, |
| spawnedBy: { |
| parentRunId: 'parent-run', |
| parentTurnId: 'parent-turn', |
| toolCallId: 'call-1', |
| }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read the assigned workspace task.', |
| toolNames: ['Read'], |
| categoryPolicy: { read: 'allow' }, |
| }, |
| subagentSpawn: { |
| schemaVersion: 1, |
| requestFingerprint: 'a'.repeat(64), |
| initialTurnId: 'child-turn', |
| initialRunId: 'child-run', |
| }, |
| } as Parameters<typeof store.createSubagent>[0]); |
| const secondChild = await store.createSubagent({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId: parent.id, |
| spawnedBy: { |
| parentRunId: 'parent-run', |
| parentTurnId: 'parent-turn', |
| toolCallId: 'call-2', |
| }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read the assigned workspace task.', |
| toolNames: ['Read'], |
| categoryPolicy: { read: 'allow' }, |
| }, |
| subagentSpawn: { |
| schemaVersion: 1, |
| requestFingerprint: 'b'.repeat(64), |
| initialTurnId: 'child-turn', |
| initialRunId: 'child-run', |
| }, |
| } as Parameters<typeof store.createSubagent>[0]); |
| const grandchild = await store.createSubagent({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId: firstChild.header.id, |
| spawnedBy: { parentRunId: 'child-run', parentTurnId: 'child-turn', toolCallId: 'call-3' }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read the assigned workspace task.', |
| toolNames: ['Read'], |
| categoryPolicy: { read: 'allow' }, |
| }, |
| subagentSpawn: { |
| schemaVersion: 1, |
| requestFingerprint: 'c'.repeat(64), |
| initialTurnId: 'child-turn', |
| initialRunId: 'child-run', |
| }, |
| } as Parameters<typeof store.createSubagent>[0]); |
| |
| const childArtifactPath = await addArtifactRecord( |
| workspaceRoot, |
| firstChild.header.id, |
| 'child', |
| { bytes: 'CHILD' }, |
| ); |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportOk(workspaceRoot, parent.id, destination); |
| assert.equal(result.export.sessionIds.length, 4); |
| assert.equal(result.export.rootSessionId, parent.id); |
| // Membership is what the export promises; the order is the traversal's, |
| // and sibling order comes from the id sort rather than creation time. |
| // Asserting creation order here passes or fails on which random UUID |
| // happens to sort first. |
| assert.deepEqual( |
| [...result.export.sessionIds].sort(), |
| [parent.id, firstChild.header.id, secondChild.header.id, grandchild.header.id].sort(), |
| ); |
| // The root leads, and each level's siblings are sorted, so the same tree |
| // exports byte-identically on every run. |
| assert.equal(result.export.sessionIds[0], parent.id); |
| const siblings = [firstChild.header.id, secondChild.header.id].sort(); |
| assert.deepEqual(result.export.sessionIds.slice(1, 3), siblings); |
| assert.equal(result.export.sessionIds[3], grandchild.header.id); |
| |
| const hydration = await hydrateExport(destination, parent.id, join(root, 'hydrated')); |
| const exported = openExported(hydration); |
| try { |
| const carried = ( |
| exported |
| .prepare('SELECT session_id FROM session_metadata ORDER BY session_id') |
| .all() as Array<{ session_id?: unknown }> |
| ).map((row) => String(row.session_id)); |
| assert.deepEqual(carried, [...result.export.sessionIds].sort()); |
| } finally { |
| exported.close(); |
| } |
| // The link is not the child Session: `subagent_spawns` records WHICH |
| // tool call spawned it. A filter that does not recognise this table's |
| // ownership columns empties it, and the bundle then holds two Sessions |
| // with nothing joining them. |
| const links = openExported(hydration); |
| try { |
| const rows = links |
| .prepare('SELECT parent_session_id, child_session_id FROM subagent_spawns') |
| .all() as Array<{ parent_session_id?: unknown; child_session_id?: unknown }>; |
| assert.deepEqual( |
| rows.map((row) => String(row.child_session_id)).sort(), |
| [firstChild.header.id, secondChild.header.id, grandchild.header.id].sort(), |
| ); |
| } finally { |
| links.close(); |
| } |
| // A child's artifact bytes travel with it, under the child's own id. |
| assert.equal( |
| await readFile(exportedArtifactPath(hydration, childArtifactPath), 'utf8'), |
| 'CHILD', |
| ); |
| } finally { |
| await store.close?.(); |
| } |
| } finally { |
| await rm(root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test( |
| 'rejects an active session before export', |
| withRoot('maka-session-export-active', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| addOpenInvocation(workspaceRoot, sessionId); |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'session_active'); |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'reports a missing live artifact without creating the destination', |
| withRoot('maka-session-export-artifact-missing', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| // A record with no bytes behind it. The export refuses rather than shipping |
| // a bundle whose own metadata names a file it does not contain. |
| await addArtifactRecord(workspaceRoot, sessionId, 'missing'); |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'artifact_missing'); |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'refuses to overwrite an existing destination', |
| withRoot('maka-session-export-destination', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| const destination = join(root, 'bundle.maka-session'); |
| await writeFile(destination, 'original'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.deepEqual(result, { ok: false, reason: { kind: 'destination_exists' } }); |
| assert.equal(await readFile(destination, 'utf8'), 'original'); |
| }), |
| ); |
| |
| test( |
| 'separates a directory that is not a workspace from a Session that is not there', |
| withRoot('maka-session-export-not-found', async (root, workspaceRoot) => { |
| const destination = join(root, 'bundle.maka-session'); |
| |
| // No state database yet: the directory was never a workspace. Reporting a |
| // missing Session here would read a mistyped path as an empty catalog. |
| const beforeAnyStore = await exportSessionBundle({ |
| workspaceRoot, |
| sessionId: 'missing-session', |
| destination, |
| }); |
| assert.deepEqual(beforeAnyStore, { |
| ok: false, |
| reason: { kind: 'workspace_not_found', workspaceRoot }, |
| }); |
| |
| // A real workspace holding no such Session is the other answer. Creating |
| // one Session is what makes the database exist. |
| const store = createSessionStore(workspaceRoot); |
| try { |
| await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| } finally { |
| store.close?.(); |
| } |
| const withStore = await exportSessionBundle({ |
| workspaceRoot, |
| sessionId: 'missing-session', |
| destination, |
| }); |
| assert.deepEqual(withStore, { ok: false, reason: { kind: 'session_not_found' } }); |
| |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'carries JSON columns as bytes rather than re-encoding them', |
| withRoot('maka-session-export-bytes', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| // Values chosen because a JSON.parse/stringify round trip changes them: |
| // the integer exceeds Number.MAX_SAFE_INTEGER and the spacing is not what |
| // a serializer emits. The bundle carries the database itself, so these |
| // must come back as the same bytes rather than as an equivalent encoding. |
| const payloadJson = '{ "big": 9007199254740993, "spaced" : true }'; |
| const recordJson = '{ "big": 9007199254740993, "note":"kept" }'; |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.exec(` |
| INSERT INTO runtime_events( |
| session_id, run_id, invocation_id, turn_id, event_id, event_seq, |
| event_kind, committed_at, payload_json |
| ) |
| VALUES ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'event-bytes', 1, |
| 'text', 1, '${payloadJson}'); |
| INSERT INTO runtime_events( |
| session_id, run_id, invocation_id, turn_id, event_id, event_seq, |
| event_kind, committed_at, payload_json |
| ) |
| VALUES ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'event-done', 2, |
| 'completed', 2, '{"status":"completed"}'); |
| INSERT INTO core_agent_runs(session_id, run_id, created_at) |
| VALUES ('${sessionId}', 'run-1', 0); |
| INSERT INTO core_agent_run_events( |
| session_id, run_id, sequence, event_id, event_type, event_ts, record_json |
| ) |
| VALUES ('${sessionId}', 'run-1', 0, 'bytes', 'turn_started', 1, '${recordJson}'); |
| `); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| await exportOk(workspaceRoot, sessionId, destination); |
| const hydration = await hydrateExport(destination, sessionId, join(root, 'hydrated')); |
| const exported = openExported(hydration); |
| try { |
| const carriedPayload = ( |
| exported |
| .prepare("SELECT payload_json FROM runtime_events WHERE event_id = 'event-bytes'") |
| .get() as { payload_json?: unknown } |
| ).payload_json; |
| const carriedRecord = ( |
| exported |
| .prepare("SELECT record_json FROM core_agent_run_events WHERE event_id = 'bytes'") |
| .get() as { record_json?: unknown } |
| ).record_json; |
| // Strict equality on the stored string, not JSON equivalence. |
| assert.equal(carriedPayload, payloadJson); |
| assert.equal(carriedRecord, recordJson); |
| } finally { |
| exported.close(); |
| } |
| }), |
| ); |
| |
| test( |
| 'exports the subtree under any node, not only a top-level Session', |
| withRoot('maka-session-export-any-node', async (root, workspaceRoot) => { |
| const store = createSessionStore(workspaceRoot); |
| let branchId: string; |
| let parentId: string; |
| let childId: string; |
| let grandchildId: string; |
| try { |
| const source = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| const branch = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| branchId = branch.id; |
| const parent = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| parentId = parent.id; |
| const child = await createSubagentSession(store, workspaceRoot, parent.id, 'call-1'); |
| childId = child; |
| grandchildId = await createSubagentSession(store, workspaceRoot, child, 'call-2'); |
| // A branch Session points at a source that a bundle rooted here will not |
| // contain. `parent_session_id` is a lineage pointer, not ownership: a |
| // filter that reads it as ownership deletes the very Session being |
| // exported. |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.prepare('UPDATE session_metadata SET parent_session_id = ? WHERE session_id = ?').run( |
| source.id, |
| branch.id, |
| ); |
| } finally { |
| db.close(); |
| } |
| } finally { |
| await store.close?.(); |
| } |
| |
| // Migration starts wherever it is pointed and takes what hangs below. |
| for (const [label, sessionId, expected] of [ |
| ['top-level parent', parentId, [parentId, childId, grandchildId]], |
| ['mid-tree child, parent outside the bundle', childId, [childId, grandchildId]], |
| ['leaf', grandchildId, [grandchildId]], |
| ['branch Session, source outside the bundle', branchId, [branchId]], |
| ] as const) { |
| const destination = join(root, `${sessionId}.maka-session`); |
| const result = await exportOk(workspaceRoot, sessionId, destination); |
| assert.deepEqual([...result.export.sessionIds].sort(), [...expected].sort(), label); |
| } |
| }), |
| ); |
| |
| async function bundleFileContents(hydration: SessionBundleHydration): Promise<string> { |
| const { readdir } = await import('node:fs/promises'); |
| const parts: string[] = []; |
| const walk = async (directory: string): Promise<void> => { |
| for (const entry of await readdir(directory, { withFileTypes: true })) { |
| const path = join(directory, entry.name); |
| if (entry.isDirectory()) await walk(path); |
| else parts.push(path, await readFile(path, 'utf8')); |
| } |
| }; |
| await walk(hydration.stateRoot); |
| await walk(hydration.workspaceRoot); |
| return parts.join('\n'); |
| } |
| |
| test( |
| 'names the connection without carrying any credential into the bundle', |
| withRoot('maka-session-export-credentials', async (root, workspaceRoot) => { |
| const secret = 'sk-EXPORT-MUST-NEVER-CARRY-THIS-TOKEN'; |
| // A vault beside the state the export reads. Nothing selects it, and this |
| // asserts that: a bundle is shared, so a credential reaching it is the one |
| // failure here that cannot be walked back. |
| await writeFile( |
| join(workspaceRoot, 'credential-vault.json'), |
| JSON.stringify({ connections: { [CONNECTION_SLUG]: { apiKey: secret } } }), |
| ); |
| const sessionId = await createSession(workspaceRoot); |
| const destination = join(root, 'bundle.maka-session'); |
| |
| const result = await exportOk(workspaceRoot, sessionId, destination); |
| assert.equal(result.export.connection?.llmConnectionSlug, CONNECTION_SLUG); |
| |
| const hydration = await hydrateExport(destination, sessionId, join(root, 'hydrated')); |
| const contents = await bundleFileContents(hydration); |
| // The slug is how the importing side finds a connection, so it must be here. |
| assert.ok(contents.includes(CONNECTION_SLUG)); |
| assert.ok(contents.includes(MODEL)); |
| // The key must not, under any name. |
| assert.equal(contents.includes(secret), false); |
| assert.equal(contents.includes('credential-vault'), false); |
| assert.equal(contents.includes('apiKey'), false); |
| }), |
| ); |
| |
| test( |
| 'refuses the whole tree when only a child session is still active', |
| withRoot('maka-session-export-child-active', async (root, workspaceRoot) => { |
| const store = createSessionStore(workspaceRoot); |
| let parentId: string; |
| let childId: string; |
| try { |
| const parent = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| parentId = parent.id; |
| const child = await store.createSubagent({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId: parent.id, |
| spawnedBy: { |
| parentRunId: 'parent-run', |
| parentTurnId: 'parent-turn', |
| toolCallId: 'call-1', |
| }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read the assigned workspace task.', |
| toolNames: ['Read'], |
| categoryPolicy: { read: 'allow' }, |
| }, |
| subagentSpawn: { |
| schemaVersion: 1, |
| requestFingerprint: 'a'.repeat(64), |
| initialTurnId: 'child-turn', |
| initialRunId: 'child-run', |
| }, |
| } as Parameters<typeof store.createSubagent>[0]); |
| childId = child.header.id; |
| } finally { |
| await store.close?.(); |
| } |
| |
| // The parent is quiescent; only the child holds an unfinished invocation. |
| // A bundle that skipped it would be a subtree with a hole, so the refusal |
| // covers the tree rather than the session that happens to be named. |
| addOpenInvocation(workspaceRoot, childId); |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ |
| workspaceRoot, |
| sessionId: parentId, |
| destination, |
| }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'session_active'); |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'terminates on a subagent parent link that points back into the tree', |
| withRoot('maka-session-export-cycle', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| // `subagent_parent_session_id` is a plain column; nothing stops a row from |
| // naming itself. The walk must end and must not list the Session twice. |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.exec( |
| `UPDATE session_metadata SET subagent_parent_session_id = '${sessionId}' WHERE session_id = '${sessionId}'`, |
| ); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportOk(workspaceRoot, sessionId, destination); |
| assert.deepEqual(result.export.sessionIds, [sessionId]); |
| }), |
| ); |
| |
| test( |
| 'leaves no excluded bytes in the bundled database file', |
| withRoot('maka-session-export-freelist', async (root, workspaceRoot) => { |
| const kept = await createSession(workspaceRoot, { name: 'kept' }); |
| const excludedMarker = 'EXCLUDED-SESSION-MARKER-9f3a'; |
| const excluded = await createSession(workspaceRoot, { name: excludedMarker }); |
| const db = openDatabase(workspaceRoot); |
| try { |
| // Enough rows that the excluded Session occupies pages of its own. |
| const insert = db.prepare(` |
| INSERT INTO runtime_events( |
| session_id, run_id, invocation_id, turn_id, event_id, event_seq, |
| event_kind, committed_at, payload_json |
| ) VALUES (?, 'run-1', 'invocation-1', 'turn-1', ?, ?, 'text', 1, ?) |
| `); |
| for (let index = 1; index <= 300; index += 1) { |
| insert.run(excluded, `evt-${index}`, index, JSON.stringify({ marker: excludedMarker })); |
| } |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| await exportOk(workspaceRoot, kept, destination); |
| const hydration = await hydrateExport(destination, kept, join(root, 'hydrated')); |
| const databasePath = join(hydration.stateRoot, OPERATIONAL_STATE_DATABASE_NAME); |
| |
| const exported = openExported(hydration); |
| try { |
| const free = exported.prepare('PRAGMA freelist_count').get() as Record<string, unknown>; |
| assert.equal(Number(Object.values(free)[0] ?? 0), 0); |
| } finally { |
| exported.close(); |
| } |
| // SQL sees no excluded rows either way. Deleting frees pages, it does not |
| // erase them, so the file itself is what has to be checked. |
| const raw = await readFile(databasePath); |
| assert.equal(raw.includes(Buffer.from(excludedMarker, 'utf8')), false); |
| }), |
| ); |
| |
| test( |
| 'drops a row that names no owning Session', |
| withRoot('maka-session-export-ownerless', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| const db = openDatabase(workspaceRoot); |
| try { |
| // An owner that is NULL owns nothing, so it belongs to no bundle. |
| db.exec( |
| "INSERT INTO usage_llm_calls(storage_key, id, ts, record_json, session_id) VALUES ('orphan', 'orphan', 0, '{}', NULL)", |
| ); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| await exportOk(workspaceRoot, sessionId, destination); |
| const hydration = await hydrateExport(destination, sessionId, join(root, 'hydrated')); |
| const exported = openExported(hydration); |
| try { |
| const row = exported |
| .prepare('SELECT COUNT(*) AS count FROM usage_llm_calls WHERE session_id IS NULL') |
| .get() as { count?: unknown }; |
| assert.equal(Number(row.count), 0); |
| } finally { |
| exported.close(); |
| } |
| }), |
| ); |
| |
| test( |
| 'refuses an artifact whose ancestor directory is a symlink', |
| withRoot('maka-session-export-ancestor-symlink', async (root, workspaceRoot) => { |
| const { mkdir: makeDir, symlink, writeFile: write } = await import('node:fs/promises'); |
| const sessionId = await createSession(workspaceRoot); |
| // A record that decodes perfectly, whose bytes live outside the workspace |
| // because the Session's artifact directory is a link. Checking only the |
| // final component lets `copyFile` follow the ancestor out of the root. |
| const outside = join(root, 'outside'); |
| await makeDir(outside, { recursive: true }); |
| await write(join(outside, `leak-secret.txt`), 'SECRET-OUTSIDE-THE-WORKSPACE'); |
| await makeDir(join(workspaceRoot, 'artifacts'), { recursive: true }); |
| await symlink(outside, join(workspaceRoot, 'artifacts', sessionId)); |
| |
| const db = openDatabase(workspaceRoot); |
| try { |
| const relativePath = `${sessionId}/leak-secret.txt`; |
| db.prepare(` |
| INSERT INTO artifact_records(artifact_id, session_id, created_at, relative_path, record_json) |
| VALUES ('leak', ?, 0, ?, ?) |
| `).run( |
| sessionId, |
| relativePath, |
| JSON.stringify({ |
| id: 'leak', |
| sessionId, |
| turnId: 'turn-1', |
| createdAt: 0, |
| name: 'secret.txt', |
| kind: 'file', |
| relativePath, |
| sizeBytes: 28, |
| source: 'tool_result', |
| }), |
| ); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'artifact_unsafe'); |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'refuses a Session holding a tool operation that never settled', |
| withRoot('maka-session-export-unsettled-tool', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| const db = openDatabase(workspaceRoot); |
| try { |
| // The invocation reached a terminal event -- the run failed -- while the |
| // operation itself is still prepared. An invocation check does not see it. |
| db.exec(` |
| INSERT INTO runtime_events( |
| session_id, run_id, invocation_id, turn_id, event_id, event_seq, |
| event_kind, committed_at, payload_json |
| ) |
| VALUES ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'call-event', 1, |
| 'function_call', 1, '{}'), |
| ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'terminal-event', 2, |
| 'failed', 2, '{"status":"failed"}'); |
| INSERT INTO tool_operations( |
| operation_id, invocation_id, run_id, turn_id, provider_tool_call_id, |
| tool_name, canonical_args_hash, recovery_mode, current_state, |
| call_event_id, result_event_id, version, dispatch_event_id |
| ) |
| VALUES ('op-1', 'invocation-1', 'run-1', 'turn-1', 'call-1', 'Bash', 'hash', |
| 'never_auto_retry', 'prepared', 'call-event', NULL, 1, 'call-event'); |
| `); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'session_active'); |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'reports an unreadable database as an environment failure, not a schema verdict', |
| withRoot('maka-session-export-unreadable', async (root, workspaceRoot) => { |
| const { chmod } = await import('node:fs/promises'); |
| const sessionId = await createSession(workspaceRoot); |
| const databasePath = join(workspaceRoot, OPERATIONAL_STATE_DATABASE_NAME); |
| await chmod(databasePath, 0o000); |
| try { |
| // Telling the user to upgrade when the real answer is that the file could |
| // not be opened sends them after the wrong problem. |
| const result = await exportSessionBundle({ |
| workspaceRoot, |
| sessionId, |
| destination: join(root, 'bundle.maka-session'), |
| }); |
| assert.equal(result.ok, false); |
| assert.notEqual(result.ok === false && result.reason.kind, 'schema_unsupported'); |
| assert.equal(result.ok === false && result.reason.kind, 'io_failed'); |
| } finally { |
| await chmod(databasePath, 0o600); |
| } |
| }), |
| ); |
| |
| test( |
| 'refuses a source whose schema is not current', |
| withRoot('maka-session-export-schema', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot); |
| const db = openDatabase(workspaceRoot); |
| try { |
| // A source behind this build. Exporting it would ship rows of one shape |
| // under a manifest describing another, and opening it the ordinary way |
| // would migrate someone else's workspace on the way past. |
| db.exec( |
| "UPDATE operational_schema_migrations SET version = version - 1 WHERE scope = 'usage'", |
| ); |
| } finally { |
| db.close(); |
| } |
| |
| const destination = join(root, 'bundle.maka-session'); |
| const result = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'schema_unsupported'); |
| |
| // The export must not have migrated the source on its way to failing. |
| const after = openDatabase(workspaceRoot, true); |
| try { |
| const row = after |
| .prepare("SELECT version FROM operational_schema_migrations WHERE scope = 'usage'") |
| .get() as { version?: unknown }; |
| assert.equal(typeof row.version, 'number'); |
| const current = openDatabase(workspaceRoot, true); |
| try { |
| assert.ok(Number(row.version) >= 0); |
| } finally { |
| current.close(); |
| } |
| } finally { |
| after.close(); |
| } |
| await assert.rejects(stat(destination)); |
| }), |
| ); |
| |
| test( |
| 'exports under authority the caller already holds', |
| withRoot('maka-export-lease', async (root, workspaceRoot) => { |
| const sessionId = await createSession(workspaceRoot, { name: 'Exported' }); |
| const destination = join(root, 'bundle.maka-session'); |
| |
| const { resolveStorageRoot, tryAcquireInteractiveRootOwner } = await import( |
| '@maka/storage/root-authority' |
| ); |
| // What a Runtime Host is: it took this authority at startup and holds it |
| // until it exits. The lock is an election taken with `tryLock`, so a second |
| // exclusive hold is refused even inside the process that already has it -- |
| // which is why a Host cannot reach the export by calling it, only by |
| // lending what it holds. |
| const capability = await resolveStorageRoot({ path: workspaceRoot, kind: 'interactive' }); |
| const owner = await tryAcquireInteractiveRootOwner(capability); |
| assert.ok(owner, 'the probe must hold the authority for this test to mean anything'); |
| try { |
| // A workspace that has actually been used: the context store exists and |
| // holds a payload, which is what makes the authority necessary at all. |
| const { openInteractiveContextOffloadStoreForWrite } = await import( |
| '@maka/storage/context-offload-store' |
| ); |
| const store = await openInteractiveContextOffloadStoreForWrite(owner.lease, { |
| limits: { |
| ownerMaxBytes: { read_image_snapshot: 4096, tool_result_archive: 4096 }, |
| sessionLogicalBytes: 1_000_000, |
| workspacePhysicalBytes: 10_000_000, |
| }, |
| }); |
| const put = await store.put({ |
| sessionId, |
| owner: { kind: 'read_image_snapshot', ownerId: 'shot-1' }, |
| bytes: new TextEncoder().encode('PAYLOAD'), |
| mediaType: 'image/png', |
| }); |
| assert.equal(put.ok, true); |
| await store.close(); |
| |
| const refused = await exportSessionBundle({ workspaceRoot, sessionId, destination }); |
| assert.equal(refused.ok, false, 'electing the authority cannot work while it is held'); |
| |
| const exported = await exportSessionBundle({ |
| workspaceRoot, |
| sessionId, |
| destination, |
| lease: owner.lease, |
| }); |
| if (!exported.ok) assert.fail(`export failed: ${JSON.stringify(exported.reason)}`); |
| assert.deepEqual(exported.export.sessionIds, [sessionId]); |
| assert.ok((await stat(destination)).size > 0); |
| } finally { |
| await owner?.close(); |
| } |
| }), |
| ); |