Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/storage/src/__tests__/session-metadata-maintenance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import type { CreateSessionInput } from '@maka/core';
import {
backupSessionMetadataDatabase,
exportLegacySessionTree,
SESSION_METADATA_EXPORT_FORMAT,
} from '../session-metadata-maintenance.js';
import {
createLegacyFileSessionStore,
createSessionStore,
SQLITE_SESSION_METADATA_DATABASE_NAME,
} from '../session-store.js';
import { createSqliteSessionMetadataStore } from '../sqlite-session-metadata-store.js';

describe('session metadata migration maintenance', () => {
test('exports a legacy-compatible tree and creates an online SQLite backup', async () => {
const container = await mkdtemp(join(tmpdir(), 'maka-session-metadata-maintenance-'));
const workspaceRoot = join(container, 'workspace');
const exportRoot = join(container, 'legacy-export');
const backupPath = join(container, 'backups', 'sessions.sqlite');
const store = createSessionStore(workspaceRoot);
try {
const created = await store.create(makeInput());
await store.appendMessage(created.id, {
type: 'user',
id: 'user-1',
turnId: 'turn-1',
ts: 10,
text: 'portable transcript',
});
await store.rename(created.id, 'Canonical SQLite title');

const exported = await exportLegacySessionTree({
workspaceRoot,
destinationRoot: exportRoot,
now: () => 123,
});
assert.equal(exported.sessionsExported, 1);

const legacy = createLegacyFileSessionStore(exportRoot);
assert.equal((await legacy.readHeader(created.id)).name, 'Canonical SQLite title');
assert.equal((await legacy.readMessages(created.id))[0]?.type, 'user');

const manifest = JSON.parse(await readFile(exported.manifestPath, 'utf8')) as {
format: string;
exportedAt: number;
sessions: Array<{ header: { id: string; name: string } }>;
};
assert.equal(manifest.format, SESSION_METADATA_EXPORT_FORMAT);
assert.equal(manifest.exportedAt, 123);
assert.deepEqual(
manifest.sessions.map((record) => record.header.id),
[created.id],
);
assert.equal(manifest.sessions[0]?.header.name, 'Canonical SQLite title');

const backup = await backupSessionMetadataDatabase({
workspaceRoot,
destinationPath: backupPath,
});
assert.equal(backup.destinationPath, backupPath);
assert.ok(backup.pagesCopied > 0);
const restored = createSqliteSessionMetadataStore(backupPath);
try {
assert.equal((await restored.read(created.id)).header.name, 'Canonical SQLite title');
} finally {
restored.close();
}

const liveTranscript = await readFile(
join(workspaceRoot, 'sessions', created.id, 'session.jsonl'),
'utf8',
);
assert.equal(JSON.parse(liveTranscript.split('\n')[0]!).type, 'session_transcript');
await assert.rejects(
() => exportLegacySessionTree({ workspaceRoot, destinationRoot: exportRoot }),
/already exists/,
);
await assert.rejects(
() => backupSessionMetadataDatabase({ workspaceRoot, destinationPath: backupPath }),
/already exists/,
);
} finally {
store.close?.();
await rm(container, { recursive: true, force: true });
}
});

test('refuses to export over the live workspace', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-session-metadata-export-overlap-'));
const store = createSessionStore(root);
try {
await store.create(makeInput());
await assert.rejects(
() => exportLegacySessionTree({ workspaceRoot: root, destinationRoot: root }),
/overlaps/,
);
await assert.rejects(
() =>
backupSessionMetadataDatabase({
workspaceRoot: root,
destinationPath: join(root, SQLITE_SESSION_METADATA_DATABASE_NAME),
}),
/differ from the source/,
);
} finally {
store.close?.();
await rm(root, { recursive: true, force: true });
}
});
});

function makeInput(): CreateSessionInput {
return {
cwd: '/tmp/cwd',
backend: 'fake',
llmConnectionSlug: 'fake',
model: 'fake-model',
permissionMode: 'ask',
name: 'Initial title',
};
}
42 changes: 42 additions & 0 deletions packages/storage/src/__tests__/settings-store-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import type { SessionHeader, StoredMessage } from '@maka/core/session';
import { createSessionStore } from '../session-store.js';
import { createSettingsStore } from '../settings-store.js';

function makeHeader(overrides: Partial<SessionHeader> = {}): SessionHeader {
Expand Down Expand Up @@ -44,6 +45,47 @@ async function seedSession(
}

describe('SettingsStore.usageStats request logs', () => {
it('uses canonical SQLite metadata for transcript-marker sessions', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-sqlite-'));
const sessions = createSessionStore(workspaceRoot);
try {
const header = await sessions.create({
cwd: '/tmp/maka-workspace',
backend: 'ai-sdk',
llmConnectionSlug: 'sqlite-provider',
model: 'sqlite-default-model',
permissionMode: 'ask',
});
await sessions.appendMessages(header.id, [
{
type: 'assistant',
id: 'assistant-1',
turnId: 'turn-1',
ts: 10,
text: 'tracked',
modelId: 'sqlite-runtime-model',
},
{
type: 'token_usage',
id: 'usage-1',
turnId: 'turn-1',
ts: 20,
input: 8,
output: 2,
},
]);

const stats = await createSettingsStore(workspaceRoot).usageStats('all');

assert.equal(stats.summary.totalTokens, 10);
assert.equal(stats.logs[0]?.provider, 'sqlite-provider');
assert.equal(stats.logs[0]?.model, 'sqlite-runtime-model');
} finally {
sessions.close?.();
await rm(workspaceRoot, { recursive: true, force: true });
}
});

it('includes tool invocation rows without inflating model usage totals', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-settings-usage-'));
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ describe('SqliteSessionMetadataStore', () => {
await store.create(fullHeader());
assert.equal(await store.remove('session-1'), true);
assert.equal(await store.remove('session-1'), false);
assert.equal(await store.has('session-1'), false);
assert.equal(await store.isTombstoned('session-1'), true);
assert.deepEqual(await store.list({ labelSlug: 'alpha' }), []);
assert.deepEqual(
await store.importEntries([
Expand Down
42 changes: 40 additions & 2 deletions packages/storage/src/__tests__/sqlite-session-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,28 @@ describe('default SQLite session metadata store', () => {
assert.equal((await store.readMessages(created.id))[0]?.type, 'user');

const transcriptPath = join(root, 'sessions', created.id, 'session.jsonl');
const [legacyHeader, message] = (await readFile(transcriptPath, 'utf8'))
const [marker, message] = (await readFile(transcriptPath, 'utf8'))
.trim()
.split('\n')
.map((line) => JSON.parse(line) as Record<string, unknown>);
assert.equal(legacyHeader?.name, 'Initial title');
assert.deepEqual(marker, {
type: 'session_transcript',
sessionId: created.id,
schemaVersion: 1,
});
assert.equal('name' in (marker ?? {}), false);
assert.equal(message?.type, 'user');
await stat(join(root, SQLITE_SESSION_METADATA_DATABASE_NAME));
await assert.rejects(() => stat(join(root, 'runtime.sqlite')), { code: 'ENOENT' });

store.close?.();
const reopened = createSessionStore(root);
try {
assert.equal((await reopened.readHeader(created.id)).name, 'SQLite title');
assert.equal((await reopened.readMessages(created.id)).length, 1);
} finally {
reopened.close?.();
}
} finally {
store.close?.();
await rm(root, { recursive: true, force: true });
Expand Down Expand Up @@ -149,6 +163,30 @@ describe('default SQLite session metadata store', () => {
await rm(root, { recursive: true, force: true });
}
});

test('fails closed when a transcript marker has no canonical SQLite metadata', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-default-session-orphan-marker-'));
const sessionId = 'orphan-session';
const sessionDir = join(root, 'sessions', sessionId);
await mkdir(sessionDir, { recursive: true });
await writeFile(
join(sessionDir, 'session.jsonl'),
`${JSON.stringify({
type: 'session_transcript',
sessionId,
schemaVersion: 1,
})}\n`,
'utf8',
);

const store = createSessionStore(root);
try {
await assert.rejects(() => store.list(), /has no SQLite metadata/);
} finally {
store.close?.();
await rm(root, { recursive: true, force: true });
}
});
});

function makeInput(overrides: Partial<CreateSessionInput> = {}): CreateSessionInput {
Expand Down
2 changes: 2 additions & 0 deletions packages/storage/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './session-store.js';
export * from './session-transcript.js';
export * from './sqlite-session-metadata-store.js';
export * from './session-metadata-transfer.js';
export * from './session-metadata-maintenance.js';
export * from './agent-run-store.js';
export * from './shell-run-store.js';
export * from './connection-store.js';
Expand Down
Loading
Loading