| /* |
| * 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, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { basename, dirname, join } from 'node:path'; |
| import { DatabaseSync } from 'node:sqlite'; |
| import { test } from 'node:test'; |
| import { OPERATIONAL_STATE_DATABASE_NAME } from '@maka/storage/operational-state-store'; |
| import { createSessionStore } from '@maka/storage/session-store'; |
| import { exportSessionBundle } from '../session-export.js'; |
| import { importSessionBundle } from '../session-import.js'; |
| |
| const CONNECTION_SLUG = 'test-connection'; |
| const MODEL = 'test-model'; |
| |
| async function makeWorkspace(name: string): Promise<{ root: string; workspaceRoot: string }> { |
| const root = await mkdtemp(join(tmpdir(), `${name}-`)); |
| const workspaceRoot = join(root, 'workspace'); |
| await mkdir(workspaceRoot, { recursive: true }); |
| return { root, workspaceRoot }; |
| } |
| |
| async function createSession(workspaceRoot: string, name = 'Exported'): Promise<string> { |
| const store = createSessionStore(workspaceRoot); |
| try { |
| const header = await store.create({ |
| cwd: workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| name, |
| }); |
| 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 } : {}), |
| }); |
| } |
| |
| /** Seed a Session with the history an import has to reproduce exactly. */ |
| async function seedHistory(workspaceRoot: string, sessionId: string): Promise<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', 'evt-user', 1, 'text', 1, |
| '{ "role": "user", "big": 9007199254740993 }'), |
| ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'evt-model', 2, 'text', 2, |
| '{"role":"model","text":"ok"}'), |
| ('${sessionId}', 'run-1', 'invocation-1', 'turn-1', 'evt-done', 3, 'completed', 3, |
| '{"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, 'ckpt', 'history_compact_checkpoint_recorded', 1, |
| '{ "checkpoint": {"kept":true} }'); |
| `); |
| } finally { |
| db.close(); |
| } |
| } |
| |
| function readSessionRows( |
| workspaceRoot: string, |
| sessionId: string, |
| ): { events: unknown[]; runEvents: unknown[]; metadata: unknown } { |
| const db = openDatabase(workspaceRoot, true); |
| try { |
| return { |
| events: db |
| .prepare('SELECT * FROM runtime_events WHERE session_id = ? ORDER BY event_seq') |
| .all(sessionId), |
| runEvents: db |
| .prepare('SELECT * FROM core_agent_run_events WHERE session_id = ? ORDER BY sequence') |
| .all(sessionId), |
| metadata: db.prepare('SELECT * FROM session_metadata WHERE session_id = ?').get(sessionId), |
| }; |
| } finally { |
| db.close(); |
| } |
| } |
| |
| test('round-trips a Session into another workspace, row for row', async () => { |
| const source = await makeWorkspace('maka-import-source'); |
| const target = await makeWorkspace('maka-import-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| // Somebody else's Session, so the target is not an empty database. |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| const bundle = join(source.root, 'bundle.maka-session'); |
| const exported = await exportSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| sessionId, |
| destination: bundle, |
| }); |
| assert.equal(exported.ok, true); |
| |
| const imported = await importSessionBundle({ |
| workspaceRoot: target.workspaceRoot, |
| source: bundle, |
| }); |
| if (!imported.ok) assert.fail(`import failed: ${JSON.stringify(imported.reason)}`); |
| assert.deepEqual(imported.sessionIds, [sessionId]); |
| |
| // The acceptance criterion: the rows the model reads are the same rows, |
| // as bytes. JSON equivalence would pass on a re-encoding that changed them. |
| assert.deepEqual( |
| readSessionRows(target.workspaceRoot, sessionId), |
| readSessionRows(source.workspaceRoot, sessionId), |
| ); |
| |
| // And the workspace it landed in kept what it already had. |
| const db = openDatabase(target.workspaceRoot, true); |
| try { |
| const count = db.prepare('SELECT COUNT(*) AS count FROM session_metadata').get() as { |
| count?: unknown; |
| }; |
| assert.equal(Number(count.count), 2); |
| } finally { |
| db.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a Session this workspace already has', async () => { |
| const source = await makeWorkspace('maka-import-conflict'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const bundle = join(source.root, 'bundle.maka-session'); |
| await exportSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| sessionId, |
| destination: bundle, |
| }); |
| |
| // Importing back into the workspace it came from. Session ids are |
| // generated, so one already present means this Session is already here. |
| const result = await importSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| source: bundle, |
| }); |
| assert.equal(result.ok, false); |
| assert.equal(result.ok === false && result.reason.kind, 'session_exists'); |
| |
| const db = openDatabase(source.workspaceRoot, true); |
| try { |
| const count = db.prepare('SELECT COUNT(*) AS count FROM runtime_events').get() as { |
| count?: unknown; |
| }; |
| // A refusal writes nothing: the history is what it was. |
| assert.equal(Number(count.count), 3); |
| } finally { |
| db.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('carries the subagent subtree and its artifact bytes across', async () => { |
| const source = await makeWorkspace('maka-import-subtree-source'); |
| const target = await makeWorkspace('maka-import-subtree-target'); |
| try { |
| const store = createSessionStore(source.workspaceRoot); |
| let parentId: string; |
| let childId: string; |
| try { |
| const parent = await store.create({ |
| cwd: source.workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| }); |
| parentId = parent.id; |
| const child = await store.createSubagent({ |
| cwd: source.workspaceRoot, |
| llmConnectionSlug: CONNECTION_SLUG, |
| model: MODEL, |
| permissionMode: 'ask', |
| subagentParent: { |
| kind: 'subagent' as const, |
| parentSessionId: parent.id, |
| spawnedBy: { parentRunId: 'r', parentTurnId: 't', toolCallId: 'call-1' }, |
| lifecycle: 'foreground', |
| }, |
| subagentRuntime: { |
| schemaVersion: 1, |
| definitionVersion: 1, |
| agentId: 'local-read', |
| agentName: 'Local Read', |
| profile: 'local_read', |
| systemPrompt: 'Read.', |
| 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?.(); |
| } |
| |
| const relativePath = `${childId}/child-file.txt`; |
| await mkdir(join(source.workspaceRoot, 'artifacts', childId), { recursive: true }); |
| await writeFile(join(source.workspaceRoot, 'artifacts', relativePath), 'CHILD-BYTES'); |
| const db = openDatabase(source.workspaceRoot); |
| try { |
| db.prepare(` |
| INSERT INTO artifact_records(artifact_id, session_id, created_at, relative_path, record_json) |
| VALUES ('child', ?, 0, ?, ?) |
| `).run( |
| childId, |
| relativePath, |
| JSON.stringify({ |
| id: 'child', |
| sessionId: childId, |
| turnId: 'turn-1', |
| createdAt: 0, |
| name: 'file.txt', |
| kind: 'file', |
| relativePath, |
| sizeBytes: 11, |
| source: 'tool_result', |
| }), |
| ); |
| } finally { |
| db.close(); |
| } |
| |
| const bundle = join(source.root, 'bundle.maka-session'); |
| await exportSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| sessionId: parentId, |
| destination: bundle, |
| }); |
| const imported = await importSessionBundle({ |
| workspaceRoot: target.workspaceRoot, |
| source: bundle, |
| }); |
| if (!imported.ok) assert.fail(`import failed: ${JSON.stringify(imported.reason)}`); |
| |
| assert.deepEqual([...imported.sessionIds].sort(), [parentId, childId].sort()); |
| assert.equal(imported.artifactFiles, 1); |
| // The link is not the child Session: without `subagent_spawns` the target |
| // holds two Sessions and nothing saying which tool call joined them. |
| const targetDb = openDatabase(target.workspaceRoot, true); |
| try { |
| const link = targetDb |
| .prepare('SELECT parent_session_id FROM subagent_spawns WHERE child_session_id = ?') |
| .get(childId) as { parent_session_id?: unknown }; |
| assert.equal(String(link?.parent_session_id), parentId); |
| } finally { |
| targetDb.close(); |
| } |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal( |
| await readFile(join(target.workspaceRoot, 'artifacts', relativePath), 'utf8'), |
| 'CHILD-BYTES', |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('leaves the shared workspace connection as it found it', async () => { |
| const source = await makeWorkspace('maka-import-pragma-source'); |
| const target = await makeWorkspace('maka-import-pragma-target'); |
| const { acquireOperationalStateDatabase } = await import('@maka/storage/operational-state-store'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| const bundle = join(source.root, 'bundle.maka-session'); |
| await exportSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| sessionId, |
| destination: bundle, |
| }); |
| |
| // Hold a lease across the import, the way a running Runtime Host does. The |
| // operational store hands out one reference-counted connection per |
| // workspace, and `PRAGMA foreign_keys` is per-connection: reading it on a |
| // fresh handle would report the default no matter what the import did. |
| // The import canonicalises the root before acquiring, so a lease taken on |
| // the uncanonicalised path would be a different connection and this test |
| // would observe nothing. |
| const { realpath } = await import('node:fs/promises'); |
| const held = acquireOperationalStateDatabase(await realpath(target.workspaceRoot)); |
| try { |
| const before = readForeignKeys(held.database); |
| assert.equal(before, 1); |
| |
| const imported = await importSessionBundle({ |
| workspaceRoot: target.workspaceRoot, |
| source: bundle, |
| }); |
| assert.equal(imported.ok, true); |
| |
| // The merge must disable foreign keys to insert in table order. Leaving |
| // the pragma off would silently disarm constraint checking for every |
| // later user of this workspace -- a failure nothing would report. |
| assert.equal(readForeignKeys(held.database), before); |
| } finally { |
| held.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| function readForeignKeys(database: DatabaseSync): number { |
| const row = (database.prepare('PRAGMA foreign_keys').get() ?? {}) as Record<string, unknown>; |
| return Number(Object.values(row)[0] ?? 0); |
| } |
| |
| test('refuses a bundle written against a different schema', async () => { |
| const source = await makeWorkspace('maka-import-schema-source'); |
| const target = await makeWorkspace('maka-import-schema-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| const bundle = join(source.root, 'bundle.maka-session'); |
| await exportSessionBundle({ |
| workspaceRoot: source.workspaceRoot, |
| sessionId, |
| destination: bundle, |
| }); |
| |
| // Hydrate and tamper, which is the only way to hold a bundle from a build |
| // that is not this one. The merge copies rows with `INSERT ... SELECT *`, |
| // which maps by position: a bundle whose tables carry the same column count |
| // in a different order would be inserted transposed -- rows that read as |
| // data and are not. |
| const { createSessionBundleFileService } = await import( |
| '@maka/storage/session-bundle-file-service' |
| ); |
| const { importSessionBundleState } = await import('@maka/storage/session-bundle-policy'); |
| const { SESSION_EXPORT_BUNDLE_LIMITS } = await import('../session-export.js'); |
| const hydration = await createSessionBundleFileService().hydrate({ |
| source: { path: bundle }, |
| limits: SESSION_EXPORT_BUNDLE_LIMITS, |
| expectedSessionId: sessionId, |
| destinationRoot: join(source.root, 'hydrated'), |
| }); |
| const bundleDb = new DatabaseSync(join(hydration.stateRoot, OPERATIONAL_STATE_DATABASE_NAME)); |
| try { |
| bundleDb.exec( |
| "UPDATE operational_schema_migrations SET version = version + 1 WHERE scope = 'usage'", |
| ); |
| } finally { |
| bundleDb.close(); |
| } |
| |
| await assert.rejects( |
| () => |
| importSessionBundleState({ |
| stateRoot: target.workspaceRoot, |
| bundleStateRoot: hydration.stateRoot, |
| }), |
| (error: unknown) => (error as { code?: string }).code === 'schema_unsupported', |
| ); |
| |
| const after = openDatabase(target.workspaceRoot, true); |
| try { |
| const count = after.prepare('SELECT COUNT(*) AS count FROM session_metadata').get() as { |
| count?: unknown; |
| }; |
| assert.equal(Number(count.count), 1); |
| } finally { |
| after.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| /** Give a workspace a context store holding one managed-file payload. */ |
| async function seedContext( |
| workspaceRoot: string, |
| sessionId: string, |
| bytes: string, |
| ): Promise<{ relativePath: string; blobId: Buffer }> { |
| // The offline context authority only works on a marked Storage Root, so a |
| // workspace holding context has to be one. |
| const { resolveStorageRoot } = await import('@maka/storage/root-authority'); |
| await resolveStorageRoot({ path: workspaceRoot, kind: 'interactive' }); |
| const { createHash } = await import('node:crypto'); |
| const digest = createHash('sha256').update(bytes).digest(); |
| const hex = digest.toString('hex'); |
| // The layout the context store uses: a payload is addressed by its content. |
| const relativePath = `sha256/${hex.slice(0, 2)}/${hex}`; |
| const values = join(workspaceRoot, 'context-offload-values'); |
| await mkdir(join(values, 'sha256', hex.slice(0, 2)), { recursive: true }); |
| await writeFile(join(values, relativePath), bytes); |
| |
| const db = new DatabaseSync(join(workspaceRoot, 'context-offload.sqlite')); |
| try { |
| // The real v3 shape, not an approximation: `reference_count`, an owner kind |
| // the CHECK accepts, and the columns the store actually writes. A fixture |
| // that only resembles it proves the SQL runs, not that the imported store |
| // is usable by the store that has to read it. |
| db.exec(` |
| PRAGMA user_version = 3; |
| CREATE TABLE context_blobs ( |
| blob_id BLOB PRIMARY KEY CHECK(length(blob_id) = 32), |
| storage_kind TEXT NOT NULL CHECK(storage_kind IN ('inline', 'managed_file')), |
| payload BLOB NOT NULL, |
| size_bytes INTEGER NOT NULL CHECK(size_bytes >= 0), |
| created_at INTEGER NOT NULL CHECK(created_at >= 0), |
| CHECK( |
| (storage_kind = 'inline' AND length(payload) = size_bytes) OR |
| (storage_kind = 'managed_file' AND length(payload) BETWEEN 1 AND 512) |
| ) |
| ); |
| CREATE TABLE context_refs ( |
| ref_id TEXT PRIMARY KEY, |
| session_id TEXT NOT NULL, |
| owner_kind TEXT NOT NULL CHECK( |
| owner_kind IN ('read_image_snapshot', 'tool_result_archive') |
| ), |
| owner_id TEXT NOT NULL, |
| blob_id BLOB NOT NULL REFERENCES context_blobs(blob_id) ON DELETE RESTRICT, |
| media_type TEXT NOT NULL, |
| created_at INTEGER NOT NULL CHECK(created_at >= 0), |
| UNIQUE(session_id, owner_kind, owner_id) |
| ); |
| CREATE TABLE context_gc_candidates ( |
| blob_id BLOB PRIMARY KEY REFERENCES context_blobs(blob_id) ON DELETE CASCADE, |
| unreferenced_at INTEGER NOT NULL CHECK(unreferenced_at >= 0) |
| ); |
| CREATE TABLE context_file_deletions ( |
| locator BLOB PRIMARY KEY CHECK(length(locator) BETWEEN 1 AND 512), |
| size_bytes INTEGER NOT NULL CHECK(size_bytes >= 0), |
| enqueued_at INTEGER NOT NULL CHECK(enqueued_at >= 0) |
| ); |
| CREATE TABLE context_session_usage ( |
| session_id TEXT PRIMARY KEY, |
| reference_count INTEGER NOT NULL CHECK(reference_count >= 0), |
| logical_bytes INTEGER NOT NULL CHECK(logical_bytes >= 0) |
| ); |
| CREATE TABLE context_store_usage ( |
| singleton INTEGER PRIMARY KEY CHECK(singleton = 1), |
| blob_count INTEGER NOT NULL CHECK(blob_count >= 0), |
| physical_bytes INTEGER NOT NULL CHECK(physical_bytes >= 0) |
| ); |
| INSERT INTO context_store_usage VALUES (1, 0, 0); |
| `); |
| db.prepare('INSERT INTO context_blobs VALUES (?, ?, ?, ?, 0)').run( |
| digest, |
| 'managed_file', |
| Buffer.from(relativePath, 'utf8'), |
| bytes.length, |
| ); |
| db.prepare('INSERT INTO context_refs VALUES (?, ?, ?, ?, ?, ?, 0)').run( |
| `ref-${sessionId}`, |
| sessionId, |
| 'read_image_snapshot', |
| `owner-${sessionId}`, |
| digest, |
| 'image/png', |
| ); |
| db.exec(` |
| INSERT INTO context_session_usage |
| SELECT r.session_id, count(*), sum(b.size_bytes) |
| FROM context_refs r JOIN context_blobs b USING(blob_id) GROUP BY r.session_id; |
| UPDATE context_store_usage SET |
| blob_count = (SELECT count(*) FROM context_blobs), |
| physical_bytes = (SELECT coalesce(sum(size_bytes), 0) FROM context_blobs) |
| WHERE singleton = 1; |
| `); |
| } finally { |
| db.close(); |
| } |
| return { relativePath, blobId: digest }; |
| } |
| |
| function readContextUsage(workspaceRoot: string): { |
| blobCount: number; |
| physicalBytes: number; |
| sessionRows: number; |
| } { |
| const db = new DatabaseSync(join(workspaceRoot, 'context-offload.sqlite'), { readOnly: true }); |
| try { |
| const store = db.prepare('SELECT blob_count, physical_bytes FROM context_store_usage').get() as |
| | { blob_count?: unknown; physical_bytes?: unknown } |
| | undefined; |
| const sessions = db.prepare('SELECT COUNT(*) AS count FROM context_session_usage').get() as { |
| count?: unknown; |
| }; |
| return { |
| blobCount: Number(store?.blob_count ?? -1), |
| physicalBytes: Number(store?.physical_bytes ?? -1), |
| sessionRows: Number(sessions.count ?? -1), |
| }; |
| } finally { |
| db.close(); |
| } |
| } |
| |
| test('carries managed context payloads and keeps usage accounting true', async () => { |
| const source = await makeWorkspace('maka-import-context-source'); |
| const target = await makeWorkspace('maka-import-context-target'); |
| try { |
| // Build the bundle's state tree directly. The merge is what these findings |
| // are about, and going through pack/hydrate to reach it only adds the |
| // Storage Root machinery to the fixture. |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const payload = 'IMG'; |
| const seeded = await seedContext(source.workspaceRoot, sessionId, payload); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| const { importSessionBundleState } = await import('@maka/storage/session-bundle-policy'); |
| const merged = await importSessionBundleState({ |
| stateRoot: target.workspaceRoot, |
| bundleStateRoot: source.workspaceRoot, |
| }); |
| assert.equal(merged.contextRefs, 1); |
| |
| // Read the payload rather than count the reference: a reference whose bytes |
| // never arrived counts exactly the same. Managed payloads live at |
| // `sha256/<prefix>/<hash>`, so a copy that visited only immediate children |
| // saw one directory, skipped it, and reported success. |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal( |
| await readFile( |
| join(target.workspaceRoot, 'context-offload-values', seeded.relativePath), |
| 'utf8', |
| ), |
| payload, |
| ); |
| |
| // These numbers drive quotas and the cleanup consistency checks, and no |
| // trigger maintains them, so a stale count is not a display problem. |
| const usage = readContextUsage(target.workspaceRoot); |
| assert.equal(usage.blobCount, 2); |
| assert.equal(usage.physicalBytes, 5); |
| assert.equal(usage.sessionRows, 2); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| async function seedArtifact( |
| workspaceRoot: string, |
| sessionId: string, |
| artifactId: string, |
| bytes: string, |
| ): Promise<string> { |
| const relativePath = `${sessionId}/${artifactId}-file.txt`; |
| await mkdir(join(workspaceRoot, 'artifacts', sessionId), { recursive: true }); |
| await writeFile(join(workspaceRoot, 'artifacts', relativePath), bytes); |
| const db = openDatabase(workspaceRoot); |
| try { |
| db.prepare(` |
| INSERT INTO artifact_records(artifact_id, session_id, created_at, relative_path, record_json) |
| VALUES (?, ?, 0, ?, ?) |
| `).run( |
| artifactId, |
| sessionId, |
| relativePath, |
| JSON.stringify({ |
| id: artifactId, |
| sessionId, |
| turnId: 'turn-1', |
| createdAt: 0, |
| name: 'file.txt', |
| kind: 'file', |
| relativePath, |
| sizeBytes: bytes.length, |
| source: 'tool_result', |
| }), |
| ); |
| } finally { |
| db.close(); |
| } |
| return relativePath; |
| } |
| |
| async function importState( |
| target: string, |
| bundle: string, |
| ): Promise<{ sessionIds: readonly string[]; artifactFiles: number; contextRefs: number }> { |
| const { importSessionBundleState } = await import('@maka/storage/session-bundle-policy'); |
| return importSessionBundleState({ stateRoot: target, bundleStateRoot: bundle }); |
| } |
| |
| test('a failed context merge leaves nothing published and the retry then succeeds', async () => { |
| const source = await makeWorkspace('maka-import-retry-source'); |
| const target = await makeWorkspace('maka-import-retry-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const artifactPath = await seedArtifact(source.workspaceRoot, sessionId, 'a1', 'BYTES'); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| const kept = await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| // Collide the reference ids so the context merge fails partway. Any context |
| // failure would do; this one needs no injection point in the code. |
| const collide = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| collide.prepare('UPDATE context_refs SET ref_id = ?').run(`ref-${sessionId}`); |
| } finally { |
| collide.close(); |
| } |
| |
| await assert.rejects(() => importState(target.workspaceRoot, source.workspaceRoot)); |
| |
| // The Session rows are the last thing written, so a failure reaching them |
| // leaves nothing published. Had they gone first, the ids would now be taken |
| // and the retry would report `session_exists` forever. |
| const after = openDatabase(target.workspaceRoot, true); |
| try { |
| const present = after |
| .prepare('SELECT COUNT(*) AS count FROM session_metadata WHERE session_id = ?') |
| .get(sessionId) as { count?: unknown }; |
| assert.equal(Number(present.count), 0); |
| } finally { |
| after.close(); |
| } |
| |
| // The artifact staged by the failed attempt is the part that used to make |
| // the retry impossible: `COPYFILE_EXCL` refused its own leftover and the |
| // import reported a conflict against itself, forever. |
| const repair = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| repair.prepare('UPDATE context_refs SET ref_id = ?').run(`ref-${targetSession}`); |
| } finally { |
| repair.close(); |
| } |
| const retried = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.deepEqual([...retried.sessionIds], [sessionId]); |
| assert.equal(retried.artifactFiles, 1); |
| assert.equal(retried.contextRefs, 1); |
| |
| // Both payloads, not just the count: the retry must neither lose the bytes |
| // it staged nor overwrite what was already in the target. |
| const { readFile } = await import('node:fs/promises'); |
| const values = join(target.workspaceRoot, 'context-offload-values'); |
| assert.equal( |
| await readFile(join(target.workspaceRoot, 'artifacts', artifactPath), 'utf8'), |
| 'BYTES', |
| ); |
| assert.equal(await readFile(join(values, seeded.relativePath), 'utf8'), 'ABC'); |
| assert.equal(await readFile(join(values, kept.relativePath), 'utf8'), 'ZZ'); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('a failure after the context commit is still retryable', async () => { |
| const source = await makeWorkspace('maka-import-late-source'); |
| const target = await makeWorkspace('maka-import-late-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await seedArtifact(source.workspaceRoot, sessionId, 'shared', 'BYTES'); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| // An artifact id already taken by a Session the bundle knows nothing about. |
| // The importability check reads Session ids, so this passes it and fails in |
| // the operational merge -- after the context transaction has committed. |
| await seedArtifact(target.workspaceRoot, targetSession, 'shared', 'OTHER'); |
| |
| await assert.rejects(() => importState(target.workspaceRoot, source.workspaceRoot)); |
| |
| const clear = openDatabase(target.workspaceRoot); |
| try { |
| clear.prepare('DELETE FROM artifact_records WHERE artifact_id = ?').run('shared'); |
| } finally { |
| clear.close(); |
| } |
| |
| // Context rows survived the failure, so the retry re-inserts rows that are |
| // already there. A plain INSERT makes that second attempt fail on its own |
| // predecessor. |
| const retried = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.deepEqual([...retried.sessionIds], [sessionId]); |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal( |
| await readFile( |
| join(target.workspaceRoot, 'context-offload-values', seeded.relativePath), |
| 'utf8', |
| ), |
| 'ABC', |
| ); |
| assert.equal(readContextUsage(target.workspaceRoot).blobCount, 2); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('keeps bytes queued for deletion in the physical total', async () => { |
| const source = await makeWorkspace('maka-import-pending-source'); |
| const target = await makeWorkspace('maka-import-pending-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| // A blob already dropped from the table whose file has not been drained |
| // yet. Its bytes are on disk and still charged; the drain will subtract |
| // them, so a total recomputed from live blobs alone underflows. |
| const pending = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| pending |
| .prepare('INSERT INTO context_file_deletions VALUES (?, 7, 0)') |
| .run(Buffer.from('sha256/aa/pending', 'utf8')); |
| pending.exec('UPDATE context_store_usage SET physical_bytes = physical_bytes + 7'); |
| } finally { |
| pending.close(); |
| } |
| |
| await importState(target.workspaceRoot, source.workspaceRoot); |
| |
| // 3 imported + 2 already there + 7 queued. |
| assert.equal(readContextUsage(target.workspaceRoot).physicalBytes, 12); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('imports context into a workspace that has never held a Session', async () => { |
| const source = await makeWorkspace('maka-import-fresh-source'); |
| const target = await makeWorkspace('maka-import-fresh-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| |
| // Nothing has run here: no Storage Root marker, no context database. The |
| // snapshot lock used to only *discover* a marked root, so the first import |
| // into a new workspace failed `root_unmarked` -- exactly the case a user |
| // hits when they receive a bundle before starting any Session. |
| const imported = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.deepEqual([...imported.sessionIds], [sessionId]); |
| assert.equal(imported.contextRefs, 1); |
| |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal( |
| await readFile( |
| join(target.workspaceRoot, 'context-offload-values', seeded.relativePath), |
| 'utf8', |
| ), |
| 'ABC', |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('accepts an identical artifact left by a crashed attempt, and only an identical one', async () => { |
| const source = await makeWorkspace('maka-import-leftover-source'); |
| const target = await makeWorkspace('maka-import-leftover-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const relativePath = await seedArtifact(source.workspaceRoot, sessionId, 'a1', 'BYTES'); |
| |
| // A crash between staging the artifact and the rollback leaves the file |
| // behind with no row to explain it. `COPYFILE_EXCL` refuses it, so without |
| // this the workspace can never import that bundle again. |
| const staged = join(target.workspaceRoot, 'artifacts', relativePath); |
| await mkdir(join(target.workspaceRoot, 'artifacts', sessionId), { recursive: true }); |
| await writeFile(staged, 'DIFFERENT'); |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /Artifact already present/, |
| 'a leftover naming different bytes is a real collision, not a retry', |
| ); |
| |
| await writeFile(staged, 'BYTES'); |
| const imported = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.deepEqual([...imported.sessionIds], [sessionId]); |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal(await readFile(staged, 'utf8'), 'BYTES'); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a context payload whose path already holds different bytes', async () => { |
| const source = await makeWorkspace('maka-import-payload-source'); |
| const target = await makeWorkspace('maka-import-payload-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // Payloads are addressed by the hash of their content, so the same path in |
| // two workspaces is supposed to mean the same bytes. When it does not, the |
| // target holds something this bundle cannot explain -- and the import used |
| // to swallow `EEXIST` and report success over it. |
| const occupied = join(target.workspaceRoot, 'context-offload-values', seeded.relativePath); |
| await mkdir(dirname(occupied), { recursive: true }); |
| await writeFile(occupied, 'NOT-THE-SAME'); |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /Context payload already names different content/, |
| ); |
| |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal(await readFile(occupied, 'utf8'), 'NOT-THE-SAME', 'the other payload survives'); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('publishes a context payload past a staging file a crashed attempt left', async () => { |
| const source = await makeWorkspace('maka-import-staging-source'); |
| const target = await makeWorkspace('maka-import-staging-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // A payload now becomes visible by `link`, so an interrupted copy leaves its |
| // half-written bytes under the staging name rather than at the path other |
| // Sessions read. The retry has to get past its own leftover. |
| const values = join(target.workspaceRoot, 'context-offload-values'); |
| const destination = join(values, seeded.relativePath); |
| const staging = join(dirname(destination), `.${basename(destination)}.import.tmp`); |
| await mkdir(dirname(destination), { recursive: true }); |
| await writeFile(staging, 'HALF'); |
| |
| const imported = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.equal(imported.contextRefs, 1); |
| |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal(await readFile(destination, 'utf8'), 'ABC', 'the leftover was not published'); |
| assert.deepEqual( |
| (await readdir(dirname(destination))).filter((name) => name.endsWith('.import.tmp')), |
| [], |
| 'staging files do not outlive the import', |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('imports under authority the caller already holds', async () => { |
| const source = await makeWorkspace('maka-import-lease-source'); |
| const target = await makeWorkspace('maka-import-lease-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| const { resolveStorageRoot, tryAcquireInteractiveRootOwner } = await import( |
| '@maka/storage/root-authority' |
| ); |
| const { importSessionBundleState } = await import('@maka/storage/session-bundle-policy'); |
| |
| // What a Runtime Host is: it took this authority at startup and holds it |
| // for its lifetime. |
| const capability = await resolveStorageRoot({ |
| path: target.workspaceRoot, |
| kind: 'interactive', |
| }); |
| const owner = await tryAcquireInteractiveRootOwner(capability); |
| assert.ok(owner, 'the probe must hold the authority for this test to mean anything'); |
| try { |
| // The lock is an election, not a mutex: it refuses a second hold even |
| // from the process already holding it, so electing here cannot work. |
| await assert.rejects( |
| () => |
| importSessionBundleState({ |
| stateRoot: target.workspaceRoot, |
| bundleStateRoot: source.workspaceRoot, |
| }), |
| /offline Storage Root/, |
| ); |
| |
| const imported = await importSessionBundleState({ |
| stateRoot: target.workspaceRoot, |
| bundleStateRoot: source.workspaceRoot, |
| lease: owner.lease, |
| }); |
| assert.deepEqual([...imported.sessionIds], [sessionId]); |
| assert.equal(imported.contextRefs, 1); |
| const { readFile } = await import('node:fs/promises'); |
| assert.equal( |
| await readFile( |
| join(target.workspaceRoot, 'context-offload-values', seeded.relativePath), |
| 'utf8', |
| ), |
| 'ABC', |
| ); |
| } finally { |
| await owner?.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a lease that names a different Storage Root', async () => { |
| const source = await makeWorkspace('maka-import-wrong-lease-source'); |
| const target = await makeWorkspace('maka-import-wrong-lease-target'); |
| const other = await makeWorkspace('maka-import-wrong-lease-other'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| await createSession(other.workspaceRoot, 'Elsewhere'); |
| |
| const { resolveStorageRoot, tryAcquireInteractiveRootOwner } = await import( |
| '@maka/storage/root-authority' |
| ); |
| const { importSessionBundleState } = await import('@maka/storage/session-bundle-policy'); |
| |
| // A valid lease is still only authority over the root it names. Accepting |
| // one for a different root would write to a directory nobody holds. |
| const capability = await resolveStorageRoot({ path: other.workspaceRoot, kind: 'interactive' }); |
| const owner = await tryAcquireInteractiveRootOwner(capability); |
| assert.ok(owner); |
| try { |
| await assert.rejects( |
| () => |
| importSessionBundleState({ |
| stateRoot: target.workspaceRoot, |
| bundleStateRoot: source.workspaceRoot, |
| lease: owner.lease, |
| }), |
| /does not name this Storage Root/, |
| ); |
| } finally { |
| await owner?.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| await rm(other.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle whose payload does not hash to what its row claims', async () => { |
| const source = await makeWorkspace('maka-import-tamper-source'); |
| const target = await makeWorkspace('maka-import-tamper-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // An archive digest authenticates the archive, not the state inside it: it |
| // says the bytes arrived as sent, not that a row claiming a hash names a |
| // file that hashes to it. Without validating the hydrated state, this |
| // imports and publishes a reference to content nobody can read back. |
| await writeFile( |
| join(source.workspaceRoot, 'context-offload-values', seeded.relativePath), |
| 'XYZ', |
| ); |
| |
| await assert.rejects(() => importState(target.workspaceRoot, source.workspaceRoot)); |
| |
| // Rejected before the target was touched, not midway through it. |
| const after = openDatabase(target.workspaceRoot, true); |
| try { |
| const present = after |
| .prepare('SELECT COUNT(*) AS count FROM session_metadata WHERE session_id = ?') |
| .get(sessionId) as { count?: unknown }; |
| assert.equal(Number(present.count), 0); |
| } finally { |
| after.close(); |
| } |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses to publish through a payload directory that leaves the Storage Root', async () => { |
| const source = await makeWorkspace('maka-import-escape-source'); |
| const target = await makeWorkspace('maka-import-escape-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // Containment by string comparison says nothing about what the path |
| // resolves to. With the payload directory replaced by a symlink, a lexical |
| // check passes and the payloads land outside the workspace entirely. |
| const { symlink } = await import('node:fs/promises'); |
| const values = join(target.workspaceRoot, 'context-offload-values'); |
| const elsewhere = join(target.root, 'elsewhere'); |
| await mkdir(elsewhere, { recursive: true }); |
| await rm(values, { recursive: true, force: true }); |
| await symlink(elsewhere, values); |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /escapes the Storage Root/, |
| ); |
| assert.deepEqual(await readdir(elsewhere), [], 'nothing was written outside the root'); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('clears the collection candidate of a blob the import references again', async () => { |
| const source = await makeWorkspace('maka-import-gc-source'); |
| const target = await makeWorkspace('maka-import-gc-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ABC'); |
| |
| // The target holds the same content -- payloads are content-addressed, so |
| // this is the ordinary case -- and it was queued for collection while |
| // nothing referenced it. The import references it again. |
| const queue = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| queue.exec(` |
| DELETE FROM context_refs; |
| DELETE FROM context_session_usage; |
| INSERT INTO context_gc_candidates SELECT blob_id, 0 FROM context_blobs; |
| `); |
| } finally { |
| queue.close(); |
| } |
| |
| await importState(target.workspaceRoot, source.workspaceRoot); |
| |
| // Left behind, the next collection fails outright and keeps failing: the |
| // candidate is referenced, which collection treats as corruption. |
| const after = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite'), { |
| readOnly: true, |
| }); |
| try { |
| const stranded = after |
| .prepare(` |
| SELECT COUNT(*) AS count FROM context_gc_candidates |
| WHERE blob_id IN (SELECT blob_id FROM context_refs) |
| `) |
| .get() as { count?: unknown }; |
| assert.equal(Number(stranded.count), 0); |
| } finally { |
| after.close(); |
| } |
| assert.ok(seeded.relativePath.length > 0); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle whose context references a Session it does not carry', async () => { |
| const source = await makeWorkspace('maka-import-foreign-ref-source'); |
| const target = await makeWorkspace('maka-import-foreign-ref-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // An export only keeps refs for the Sessions it carries, but a bundle can |
| // be assembled rather than exported, and its digest still checks out. A |
| // reference owned by a Session that never arrives can never be released: |
| // that happens when its Session is retired. |
| const tamper = new DatabaseSync(join(source.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| // Usage rows move with it. Left inconsistent, the snapshot validator |
| // catches the tampering first and this guard is never reached -- the |
| // test would pass while proving the wrong thing. |
| tamper.prepare('UPDATE context_refs SET session_id = ?').run('a-session-not-in-this-bundle'); |
| tamper |
| .prepare('UPDATE context_session_usage SET session_id = ?') |
| .run('a-session-not-in-this-bundle'); |
| } finally { |
| tamper.close(); |
| } |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /references a Session it does not carry/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle carrying collection state from the workspace it left', async () => { |
| const source = await makeWorkspace('maka-import-stale-gc-source'); |
| const target = await makeWorkspace('maka-import-stale-gc-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // The export empties this queue on its private copy. One that survives is a |
| // decision about a moment in another workspace, and a fresh target adopts |
| // the bundle's database whole -- so the candidate names a blob the target |
| // now references, which collection treats as corruption from then on. |
| const tamper = new DatabaseSync(join(source.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| tamper.exec('INSERT INTO context_gc_candidates SELECT blob_id, 0 FROM context_blobs'); |
| } finally { |
| tamper.close(); |
| } |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /collection state from the workspace it left/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a payload path that is a symlink, even onto matching bytes', async () => { |
| const source = await makeWorkspace('maka-import-payload-link-source'); |
| const target = await makeWorkspace('maka-import-payload-link-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // Matching content read through a link is not the same fact as matching |
| // content at the path: the Context Store refuses to read through one and |
| // reports the payload corrupt. Accepting it here imports a tree that the |
| // Store cannot use. |
| const { symlink } = await import('node:fs/promises'); |
| const destination = join(target.workspaceRoot, 'context-offload-values', seeded.relativePath); |
| const decoy = join(target.root, 'decoy'); |
| await writeFile(decoy, 'ABC'); |
| await mkdir(dirname(destination), { recursive: true }); |
| await symlink(decoy, destination); |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /Context payload already names different content/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('reports a payload path occupied by a directory as a conflict', async () => { |
| const source = await makeWorkspace('maka-import-payload-dir-source'); |
| const target = await makeWorkspace('maka-import-payload-dir-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // `O_NOFOLLOW` answers the symlink; it says nothing about the other things |
| // a path can be. A directory opens fine and only fails at the read, which |
| // surfaces a raw `EISDIR` instead of saying what is actually wrong. |
| await mkdir(join(target.workspaceRoot, 'context-offload-values', seeded.relativePath), { |
| recursive: true, |
| }); |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /Context payload already names different content/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle carrying a payload nothing references', async () => { |
| const source = await makeWorkspace('maka-import-orphan-blob-source'); |
| const target = await makeWorkspace('maka-import-orphan-blob-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // An export drops blobs nothing references. One that survives is quota the |
| // target can never reclaim: collection only ever considers a blob after a |
| // reference releases it, and this one never had a reference to release. |
| const tamper = new DatabaseSync(join(source.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| tamper.exec(` |
| DELETE FROM context_refs; |
| DELETE FROM context_session_usage; |
| `); |
| } finally { |
| tamper.close(); |
| } |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /payload nothing references/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle carrying usage for a Session it does not hold', async () => { |
| const source = await makeWorkspace('maka-import-surplus-usage-source'); |
| const target = await makeWorkspace('maka-import-surplus-usage-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // Surplus in the direction the aggregate comparison cannot see: every real |
| // Session's numbers still add up, and this row is simply extra. A fresh |
| // target adopts it, and the next write for that Session fails against a |
| // count that describes a store it never had. |
| const tamper = new DatabaseSync(join(source.workspaceRoot, 'context-offload.sqlite')); |
| try { |
| tamper.prepare('INSERT INTO context_session_usage VALUES (?, 0, 0)').run('a-ghost-session'); |
| } finally { |
| tamper.close(); |
| } |
| |
| await assert.rejects( |
| () => importState(target.workspaceRoot, source.workspaceRoot), |
| /usage for a Session it does not hold/, |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('never replaces a context database that is already there', async () => { |
| const source = await makeWorkspace('maka-import-no-replace-source'); |
| const target = await makeWorkspace('maka-import-no-replace-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| const targetSession = await createSession(target.workspaceRoot, 'Unrelated'); |
| const kept = await seedContext(target.workspaceRoot, targetSession, 'ZZ'); |
| |
| // Publication creates; it never replaces. What it would replace is a |
| // database a Context Store may hold open, and on POSIX that Store then |
| // keeps writing to an unlinked inode -- writes that survive until the next |
| // restart and are then simply gone. |
| const imported = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.equal(imported.contextRefs, 1); |
| |
| const after = new DatabaseSync(join(target.workspaceRoot, 'context-offload.sqlite'), { |
| readOnly: true, |
| }); |
| try { |
| const rows = after |
| .prepare('SELECT session_id FROM context_refs ORDER BY session_id') |
| .all() as Array<{ session_id?: unknown }>; |
| assert.deepEqual( |
| rows.map((row) => String(row.session_id)).sort(), |
| [sessionId, targetSession].sort(), |
| 'the Session that was already here keeps its reference', |
| ); |
| } finally { |
| after.close(); |
| } |
| const values = join(target.workspaceRoot, 'context-offload-values'); |
| assert.equal(await readFile(join(values, seeded.relativePath), 'utf8'), 'ABC'); |
| assert.equal(await readFile(join(values, kept.relativePath), 'utf8'), 'ZZ'); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('publishes only the payloads the bundle declares', async () => { |
| const source = await makeWorkspace('maka-import-undeclared-source'); |
| const target = await makeWorkspace('maka-import-undeclared-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // Bytes no row names. The validator only ever looks at rows, so a bundle |
| // can be assembled with these and they arrive charged to nothing and |
| // reachable by nothing: usage counts blobs, and collection starts from a |
| // blob whose references were released. |
| const stowaway = join( |
| source.workspaceRoot, |
| 'context-offload-values', |
| 'sha256', |
| 'ff', |
| 'f'.repeat(64), |
| ); |
| await mkdir(dirname(stowaway), { recursive: true }); |
| await writeFile(stowaway, 'NOT DECLARED'); |
| |
| const imported = await importState(target.workspaceRoot, source.workspaceRoot); |
| assert.equal(imported.contextRefs, 1); |
| |
| const values = join(target.workspaceRoot, 'context-offload-values'); |
| assert.equal(await readFile(join(values, seeded.relativePath), 'utf8'), 'ABC'); |
| assert.equal( |
| await readFile(join(values, 'sha256', 'ff', 'f'.repeat(64)), 'utf8').catch(() => undefined), |
| undefined, |
| 'the undeclared payload did not travel', |
| ); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |
| |
| test('refuses a bundle that declares a payload it does not carry', async () => { |
| const source = await makeWorkspace('maka-import-missing-payload-source'); |
| const target = await makeWorkspace('maka-import-missing-payload-target'); |
| try { |
| const sessionId = await createSession(source.workspaceRoot); |
| await seedHistory(source.workspaceRoot, sessionId); |
| const seeded = await seedContext(source.workspaceRoot, sessionId, 'ABC'); |
| await createSession(target.workspaceRoot, 'Unrelated'); |
| |
| // A row whose bytes are absent. Publishing the rest and committing the |
| // Session would leave a reference to content nobody can read back. |
| await rm(join(source.workspaceRoot, 'context-offload-values', seeded.relativePath)); |
| |
| await assert.rejects(() => importState(target.workspaceRoot, source.workspaceRoot)); |
| } finally { |
| await rm(source.root, { recursive: true, force: true }); |
| await rm(target.root, { recursive: true, force: true }); |
| } |
| }); |