blob: ca40284517b04cc05465b74401a151f069654b8d [file]
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, realpath, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import type {
CreateSessionInput,
PermissionMode,
PermissionResponse,
QueueEnqueueOutcome,
SessionEvent,
SessionSummary,
StoredMessage,
UserMessageInput,
UserQuestionResponse,
} from '@maka/core';
import { createMakaSessionDriver } from '../session-driver.js';
import type { RuntimeContinuation, SafeBoundaryContinuationPlan } from '@maka/runtime';
describe('Maka session driver', () => {
test('creates an ask-permission session from the first prompt and streams the turn', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: nextId('turn'),
});
const turn = await driver.preparePrompt('please inspect this workspace');
assert.equal(runtime.sent.length, 0, 'turn ownership must be available before runtime starts');
const events = await collect(turn.events);
assert.equal(driver.getSessionId(), 'session-1');
assert.deepEqual(runtime.created, [
{
cwd: '/repo',
name: 'New Chat',
backend: 'ai-sdk',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
permissionMode: 'ask',
},
]);
assert.deepEqual(runtime.sent, [
{
sessionId: 'session-1',
input: { turnId: 'turn-1', text: 'please inspect this workspace' },
},
]);
assert.deepEqual(
{ sessionId: turn.sessionId, turnId: turn.turnId },
{
sessionId: 'session-1',
turnId: 'turn-1',
},
);
assert.deepEqual(
events.map((event) => event.type),
['text_delta', 'complete'],
);
});
test('new sessions use the default title while displayText keeps the typed prompt', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: nextId('turn'),
});
const typed = '/skill:alpha 帮我整理';
const composed = 'The user explicitly invoked…\n\n<user-message>\n帮我整理\n</user-message>';
const turn = await driver.preparePrompt(typed, { modelText: composed });
await collect(turn.events);
assert.equal(runtime.created[0]?.name, 'New Chat');
assert.deepEqual(runtime.sent[0]?.input, {
turnId: 'turn-1',
text: composed,
displayText: typed,
});
});
test('can still create a bypass session when explicitly requested', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
permissionMode: 'bypass',
newId: nextId('turn'),
});
await collectPrompt(driver, 'ship fast');
assert.equal(runtime.created[0]?.permissionMode, 'bypass');
});
test('uses an updated permission mode for a new session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.setPermissionMode('execute');
await collectPrompt(driver, 'run tests');
assert.equal(runtime.created[0]?.permissionMode, 'execute');
});
test('updates permission mode on an active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
await driver.setPermissionMode('execute');
assert.deepEqual(runtime.permissionModes, [
{
sessionId: 'session-1',
mode: 'execute',
},
]);
});
test('uses an updated orchestration mode for a new session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.setOrchestrationMode?.('swarm');
await collectPrompt(driver, 'analyze in parallel');
assert.equal(driver.getOrchestrationMode?.(), 'swarm');
assert.equal(runtime.created[0]?.orchestrationMode, 'swarm');
});
test('updates orchestration mode on an active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'start');
await driver.setOrchestrationMode?.('swarm');
await driver.setOrchestrationMode?.('default');
assert.deepEqual(runtime.orchestrationModes, [
{ sessionId: 'session-1', mode: 'swarm' },
{ sessionId: 'session-1', mode: 'default' },
]);
assert.equal(driver.getOrchestrationMode?.(), 'default');
});
test('passes a one-turn swarm override as trusted metadata without changing visible text', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: nextId('turn'),
});
await collect(
(
await driver.preparePrompt('inspect runtime, UI, and tests', {
turnOrchestration: { mode: 'swarm', source: 'slash_command' },
})
).events,
);
await collectPrompt(driver, 'summarize normally');
assert.deepEqual(
runtime.sent.map(({ input }) => input),
[
{
turnId: 'turn-1',
text: 'inspect runtime, UI, and tests',
turnOrchestration: { mode: 'swarm', source: 'slash_command' },
},
{ turnId: 'turn-2', text: 'summarize normally' },
],
);
assert.equal(driver.getOrchestrationMode?.(), 'default');
});
test('uses an updated model for a new session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.setModel('claude-opus-4-1');
await collectPrompt(driver, 'run tests');
assert.equal(runtime.created[0]?.model, 'claude-opus-4-1');
});
test('updates model on an active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
await driver.setModel('claude-opus-4-1');
assert.deepEqual(runtime.sessionUpdates, [
{
sessionId: 'session-1',
patch: { model: 'claude-opus-4-1', thinkingLevel: undefined },
},
]);
});
test('switches connection and model together on an active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
await driver.setModel('glm-5.2', 'zai');
// The connection rides in the same updateSession patch, so the next turn
// rebuilds the backend on the new provider.
assert.deepEqual(runtime.sessionUpdates, [
{
sessionId: 'session-1',
patch: { model: 'glm-5.2', thinkingLevel: undefined, llmConnectionSlug: 'zai' },
},
]);
});
test('a same-connection setModel does not churn the connection', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
await driver.setModel('claude-opus-4-1', 'anthropic');
assert.deepEqual(runtime.sessionUpdates, [
{
sessionId: 'session-1',
patch: { model: 'claude-opus-4-1', thinkingLevel: undefined },
},
]);
});
test('creates the next session on a connection chosen before any session exists', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.setModel('glm-5.2', 'zai');
await collectPrompt(driver, 'run tests');
assert.equal(runtime.created[0]?.llmConnectionSlug, 'zai');
assert.equal(runtime.created[0]?.model, 'glm-5.2');
});
test('renames the active session through runtime updateSession', async () => {
const runtime = new RecordingRuntime();
runtime.updatedSessionName = 'Canonical title';
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
const renamed = await driver.renameSession('watcher 根目录事件风暴修复');
assert.equal(renamed, 'Canonical title');
assert.deepEqual(runtime.sessionUpdates, [
{
sessionId: 'session-1',
patch: { name: 'watcher 根目录事件风暴修复' },
},
]);
});
test('rejects rename before a session starts', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await assert.rejects(driver.renameSession('too early'), /before a session starts/);
assert.deepEqual(runtime.sessionUpdates, []);
});
test('moves an active session, warns about dirty old cwd, and leaves prompts unchanged', async () => {
const oldCwd = await mkdtemp(join(tmpdir(), 'maka-move-old-'));
const nextCwd = join(oldCwd, 'worktree-next');
await mkdir(nextCwd);
try {
const runtime = new RecordingRuntime();
const inspected: string[] = [];
const canonicalNextCwd = await realpath(nextCwd);
const driver = createMakaSessionDriver({
runtime,
cwd: oldCwd,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
inspectCwdChanges: async (cwd) => {
inspected.push(cwd);
return true;
},
newId: nextId('turn'),
});
await collectPrompt(driver, 'before move');
const result = await driver.moveSession!(nextCwd);
assert.deepEqual(result, {
previousCwd: oldCwd,
cwd: canonicalNextCwd,
changed: true,
oldCwdDirty: true,
});
assert.deepEqual(inspected, [oldCwd]);
assert.deepEqual(runtime.sessionUpdates.at(-1), {
sessionId: 'session-1',
patch: { cwd: canonicalNextCwd },
});
await collectPrompt(driver, 'after move');
assert.equal(runtime.sent.at(-1)?.input.text, 'after move');
assert.equal(runtime.sent.at(-1)?.input.displayText, undefined);
} finally {
await rm(oldCwd, { recursive: true, force: true });
}
});
test('rejects empty, missing, and non-directory move targets before persistence', async () => {
const oldCwd = await mkdtemp(join(tmpdir(), 'maka-move-validate-'));
const file = join(oldCwd, 'file.txt');
const { writeFile } = await import('node:fs/promises');
await writeFile(file, 'file');
try {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: oldCwd,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'start');
await assert.rejects(driver.moveSession!(''), /cannot be empty/);
await assert.rejects(driver.moveSession!(join(oldCwd, 'missing')), /does not exist/);
await assert.rejects(driver.moveSession!(file), /not a directory/);
assert.deepEqual(runtime.sessionUpdates, []);
} finally {
await rm(oldCwd, { recursive: true, force: true });
}
});
test('uses the moved cwd for a new session without changing its prompt', async () => {
const oldCwd = await mkdtemp(join(tmpdir(), 'maka-move-new-session-'));
const nextCwd = join(oldCwd, 'next');
await mkdir(nextCwd);
try {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: oldCwd,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
inspectCwdChanges: async () => false,
});
await collectPrompt(driver, 'first');
await driver.moveSession!(nextCwd);
driver.startNewSession();
await collectPrompt(driver, 'new session');
assert.equal(runtime.sent.at(-1)?.input.text, 'new session');
} finally {
await rm(oldCwd, { recursive: true, force: true });
}
});
test('switches to an existing session for the next prompt', async () => {
const repo = await mkdtemp(join(tmpdir(), 'maka-switch-cwd-'));
try {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [
{
id: 'session-2',
cwd: repo,
name: 'Existing chat',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: 'anthropic',
connectionLocked: false,
model: 'claude-opus-4-1',
permissionMode: 'execute',
orchestrationMode: 'swarm',
},
];
runtime.sessionMessages.set('session-2', [
storedUserMessage('user-1', 'turn-1', 'previous question'),
storedAssistantMessage('assistant-1', 'turn-1', 'previous answer'),
]);
const driver = createMakaSessionDriver({
runtime,
cwd: repo,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
const summary = await driver.switchSession('session-2');
await collectPrompt(driver, 'continue');
assert.equal(summary.summary.id, 'session-2');
assert.deepEqual(
summary.messages.map((message) => message.id),
['user-1', 'assistant-1'],
);
assert.equal(runtime.created.length, 0);
assert.equal(runtime.sent[0]?.sessionId, 'session-2');
assert.equal(driver.getOrchestrationMode?.(), 'swarm');
driver.startNewSession();
await collectPrompt(driver, 'new session keeps swarm');
assert.equal(runtime.created[0]?.orchestrationMode, 'swarm');
} finally {
await rm(repo, { recursive: true, force: true });
}
});
test('uses a resumed session cwd without injecting a synthetic reminder', async () => {
const oldCwd = await mkdtemp(join(tmpdir(), 'maka-move-persisted-old-'));
const nextCwd = join(oldCwd, 'worktree-next');
await mkdir(nextCwd);
try {
const runtime = new RecordingRuntime();
const canonicalNextCwd = await realpath(nextCwd);
runtime.sessionSummaries = [sessionSummary({ id: 'session-2', cwd: canonicalNextCwd })];
const driver = createMakaSessionDriver({
runtime,
cwd: oldCwd,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.switchSession('session-2');
await collectPrompt(driver, 'after restart');
assert.equal(runtime.sent.at(-1)?.input.text, 'after restart');
assert.equal(runtime.sent.at(-1)?.input.displayText, undefined);
assert.deepEqual(runtime.sessionUpdates, []);
} finally {
await rm(oldCwd, { recursive: true, force: true });
}
});
test('rejects a session summary without a cwd and leaves the active session unchanged', async () => {
const repo = await mkdtemp(join(tmpdir(), 'maka-active-cwd-'));
try {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [{ ...sessionSummary({ id: 'no-cwd' }), cwd: undefined }];
const driver = createMakaSessionDriver({
runtime,
cwd: repo,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'hi');
await assert.rejects(driver.switchSession('no-cwd'), /Session has no working directory/);
await collectPrompt(driver, 'again');
assert.equal(runtime.sent[0]?.sessionId, 'session-1');
assert.equal(runtime.sent[1]?.sessionId, 'session-1');
} finally {
await rm(repo, { recursive: true, force: true });
}
});
test('rejects switching to a session whose cwd no longer exists', async () => {
const missingCwd = await mkdtemp(join(tmpdir(), 'maka-missing-session-cwd-'));
await rm(missingCwd, { recursive: true, force: true });
const runtime = new RecordingRuntime();
const deleted = sessionSummary({ id: 'deleted-worktree', cwd: missingCwd });
runtime.sessionSummaries = [deleted];
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
assert.deepEqual(await driver.getSessionResumeAvailability?.(deleted), {
available: false,
reason: 'Working directory no longer exists',
});
await assert.rejects(
driver.switchSession('deleted-worktree'),
new RegExp(`Session cwd no longer exists: ${escapeRegExp(missingCwd)}`),
);
assert.equal(driver.getSessionId(), null);
});
test('switches across folders and uses the resumed cwd for the next new session', async () => {
const repo = await mkdtemp(join(tmpdir(), 'maka-active-cwd-'));
const elsewhere = await mkdtemp(join(tmpdir(), 'maka-other-cwd-'));
try {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [sessionSummary({ id: 'other-folder', cwd: elsewhere })];
const driver = createMakaSessionDriver({
runtime,
cwd: repo,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'hi');
const resumed = await driver.switchSession('other-folder');
driver.startNewSession();
await collectPrompt(driver, 'new work here');
assert.equal(resumed.summary.cwd, elsewhere);
assert.equal(runtime.sent[0]?.sessionId, 'session-1');
assert.equal(runtime.created[1]?.cwd, elsewhere);
} finally {
await rm(repo, { recursive: true, force: true });
await rm(elsewhere, { recursive: true, force: true });
}
});
test('adopts the resumed connection and model for the next new session', async () => {
const repo = await mkdtemp(join(tmpdir(), 'maka-active-cwd-'));
try {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [
sessionSummary({ id: 'other-conn', cwd: repo, llmConnectionSlug: 'other-connection' }),
];
const driver = createMakaSessionDriver({
runtime,
cwd: repo,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'hi');
await driver.switchSession('other-conn');
driver.startNewSession();
await collectPrompt(driver, 'new work here');
assert.equal(runtime.created[1]?.llmConnectionSlug, 'other-connection');
assert.equal(runtime.created[1]?.model, 'claude-sonnet-4-5');
} finally {
await rm(repo, { recursive: true, force: true });
}
});
test('lists current-cwd sessions before other recent sessions', async () => {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [
sessionSummary({ id: 'other-newer', cwd: '/other', lastMessageAt: 30 }),
sessionSummary({ id: 'cwd-newer', cwd: '/repo', lastMessageAt: 20 }),
sessionSummary({ id: 'cwd-older', cwd: '/repo', lastMessageAt: 10 }),
];
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
const sessions = await driver.listSessions();
assert.deepEqual(
sessions.map((session) => session.id),
['cwd-newer', 'cwd-older', 'other-newer'],
);
});
test('uses the default turn id generator when one is not injected', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'hi');
assert.match(runtime.sent[0]?.input.turnId ?? '', /^[0-9a-f-]{36}$/);
});
test('compacts the active session through the runtime compact API', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: fixedIds('turn-1', 'turn-compact'),
});
await collectPrompt(driver, 'hello');
const events = await collect(driver.compactSession());
assert.deepEqual(runtime.compacted, [
{ sessionId: 'session-1', input: { turnId: 'turn-compact' } },
]);
assert.deepEqual(
runtime.sent.map((item) => item.input.text),
['hello'],
);
assert.deepEqual(
events.map((event) => event.type),
['complete'],
);
});
test('plans and streams a safe-boundary resume for the active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: fixedIds('turn-1'),
});
await collectPrompt(driver, 'hello');
const events = await collect(driver.resumeLatest!());
assert.deepEqual(runtime.resumePlanSessions, ['session-1']);
assert.equal(runtime.resumedContinuations.length, 1);
assert.deepEqual(
events.map((event) => event.type),
['text_complete', 'complete'],
);
});
test('routes permission responses to the active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: nextId('turn'),
});
await collectPrompt(driver, 'run tests');
await driver.respondToPermission({
requestId: 'permission-1',
decision: 'allow',
rememberForTurn: true,
});
assert.deepEqual(runtime.permissionResponses, [
{
sessionId: 'session-1',
response: {
requestId: 'permission-1',
decision: 'allow',
rememberForTurn: true,
},
},
]);
});
test('routes user-question responses to the active session', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
newId: nextId('turn'),
});
await collectPrompt(driver, 'choose');
await driver.respondToUserQuestion?.({ requestId: 'question-1', answers: ['A', null] });
assert.deepEqual(runtime.userQuestionResponses, [
{
sessionId: 'session-1',
response: { requestId: 'question-1', answers: ['A', null] },
},
]);
});
test('lists rewind targets newest-first, one per prompted turn, including the latest', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'first question');
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', ' first question\nmore detail'),
storedAssistantMessage('assistant-1', 'turn-1', 'first answer'),
storedUserMessage('user-2', 'turn-2', 'second question'),
storedAssistantMessage('assistant-2', 'turn-2', 'second answer'),
storedUserMessage('user-3', 'turn-3', 'third question'),
]);
const targets = await driver.listRewindTargets();
// Rewinding resets to *before* a turn, so the latest turn (turn-3) is itself a
// valid target (undo it, edit its prompt, resend). All prompted turns appear
// newest-first, label = first non-empty prompt line.
assert.deepEqual(targets, [
{ turnId: 'turn-3', label: 'third question' },
{ turnId: 'turn-2', label: 'second question' },
{ turnId: 'turn-1', label: 'first question' },
]);
});
test('surfaces the interrupted latest turn as a rewind target', async () => {
// Regression: interrupting a turn leaves its (aborted) turn_state as the last
// message, so the old head-exclusion dropped exactly the turn the user wanted
// to redo. Every prompted turn is now a target regardless of the trailing
// message.
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'first question');
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'first question'),
storedAssistantMessage('assistant-1', 'turn-1', 'first answer'),
storedUserMessage('user-2', 'turn-2', 'interrupted question'),
storedTurnState('state-2', 'turn-2', 'aborted'),
]);
assert.deepEqual(await driver.listRewindTargets(), [
{ turnId: 'turn-2', label: 'interrupted question' },
{ turnId: 'turn-1', label: 'first question' },
]);
});
test('keeps all prompted turns as targets when the head is a non-prompt turn (e.g. compact)', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'first question');
// A /compact turn has no user message, so it never becomes a target itself;
// the prompted turns around it stay listed unchanged.
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'first question'),
storedAssistantMessage('assistant-1', 'turn-1', 'first answer'),
storedUserMessage('user-2', 'turn-2', 'second question'),
storedAssistantMessage('assistant-2', 'turn-2', 'second answer'),
storedContextCompactedNote('note-1', 'turn-compact'),
]);
const targets = await driver.listRewindTargets();
assert.deepEqual(targets, [
{ turnId: 'turn-2', label: 'second question' },
{ turnId: 'turn-1', label: 'first question' },
]);
});
test('keeps the only prompted turn as a target after a compact', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'only question');
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'only question'),
storedAssistantMessage('assistant-1', 'turn-1', 'only answer'),
storedContextCompactedNote('note-1', 'turn-compact'),
]);
assert.deepEqual(await driver.listRewindTargets(), [
{ turnId: 'turn-1', label: 'only question' },
]);
});
test('lists no rewind targets before a session starts, but the sole turn once one exists', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
assert.deepEqual(await driver.listRewindTargets(), []);
await collectPrompt(driver, 'only question');
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'only question'),
storedAssistantMessage('assistant-1', 'turn-1', 'only answer'),
]);
// The single turn is now rewindable: reset to before it (an empty branch) and
// refill its prompt.
assert.deepEqual(await driver.listRewindTargets(), [
{ turnId: 'turn-1', label: 'only question' },
]);
});
test('rewinds by branching before the turn, switching onto the branch, and returning its prompt', async () => {
const repo = await mkdtemp(join(tmpdir(), 'maka-rewind-cwd-'));
try {
const runtime = new RecordingRuntime();
runtime.sessionSummaries = [
sessionSummary({ id: 'session-1', cwd: repo, orchestrationMode: 'swarm' }),
];
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'first question'),
storedAssistantMessage('assistant-1', 'turn-1', 'first answer'),
storedUserMessage('user-2', 'turn-2', 'second question\nwith detail'),
]);
const driver = createMakaSessionDriver({
runtime,
cwd: repo,
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.switchSession('session-1');
const result = await driver.rewindToTurn('turn-2');
// Branches *before* turn-2 (dropping it), and returns turn-2's full prompt
// — the whole text, not the one-line label — for the editor to refill.
assert.deepEqual(runtime.branchedBefore, [
{ sessionId: 'session-1', sourceTurnId: 'turn-2' },
]);
assert.deepEqual(runtime.branched, []);
assert.equal(result.summary.id, 'session-1-branch');
assert.equal(result.prompt, 'second question\nwith detail');
assert.equal(driver.getSessionId(), 'session-1-branch');
assert.equal(driver.getOrchestrationMode?.(), 'swarm');
} finally {
await rm(repo, { recursive: true, force: true });
}
});
test('rejects rewind to a turn with no user prompt', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'first question');
runtime.sessionMessages.set('session-1', [
storedUserMessage('user-1', 'turn-1', 'first question'),
storedContextCompactedNote('note-1', 'turn-compact'),
]);
await assert.rejects(driver.rewindToTurn('turn-compact'), /no user prompt/);
assert.deepEqual(runtime.branchedBefore, []);
});
test('rejects rewind before a session starts', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await assert.rejects(driver.rewindToTurn('turn-1'), /before a session starts/);
assert.deepEqual(runtime.branchedBefore, []);
});
test('startNewSession makes the next prompt create a fresh session, keeping settings', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await driver.setModel('claude-opus-4-1');
await collectPrompt(driver, 'first');
assert.equal(driver.getSessionId(), 'session-1');
driver.startNewSession();
assert.equal(driver.getSessionId(), null);
await collectPrompt(driver, 'second');
// A second createSession call — the prompt started a new session rather than
// reusing the old one — and it kept the current model.
assert.equal(runtime.created.length, 2);
assert.equal(runtime.created[1]?.model, 'claude-opus-4-1');
assert.equal(runtime.created[1]?.name, 'New Chat');
});
test('steer / queueMessage / takePendingFollowup / retractQueued delegate to the runtime', async () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
await collectPrompt(driver, 'run tests');
runtime.steerOutcome = { kind: 'queued' };
assert.deepEqual(driver.steer?.('x'), { kind: 'queued' });
assert.deepEqual(runtime.steered, [{ sessionId: 'session-1', text: 'x' }]);
runtime.queueOutcome = { kind: 'queued' };
assert.deepEqual(driver.queueMessage?.('y'), { kind: 'queued' });
assert.deepEqual(runtime.queued, [{ sessionId: 'session-1', text: 'y' }]);
runtime.followupText = 'a\n\nb';
assert.equal(driver.takePendingFollowup?.(), 'a\n\nb');
assert.deepEqual(runtime.followupDrains, ['session-1']);
runtime.retractText = 'x\n\ny';
assert.equal(driver.retractQueued?.(), 'x\n\ny');
assert.deepEqual(runtime.retracted, ['session-1']);
});
test('steer / queueMessage fall back before any session exists', () => {
const runtime = new RecordingRuntime();
const driver = createMakaSessionDriver({
runtime,
cwd: '/repo',
llmConnectionSlug: 'anthropic',
model: 'claude-sonnet-4-5',
});
assert.deepEqual(driver.steer?.('x'), { kind: 'fallback' });
assert.deepEqual(driver.queueMessage?.('y'), { kind: 'fallback' });
assert.equal(driver.takePendingFollowup?.(), null);
assert.equal(driver.retractQueued?.(), '');
assert.deepEqual(runtime.steered, []);
});
});
class RecordingRuntime {
readonly created: CreateSessionInput[] = [];
readonly sent: Array<{ sessionId: string; input: UserMessageInput }> = [];
readonly compacted: Array<{ sessionId: string; input: { turnId?: string } }> = [];
readonly resumePlanSessions: string[] = [];
readonly resumedContinuations: RuntimeContinuation[] = [];
readonly permissionResponses: Array<{ sessionId: string; response: PermissionResponse }> = [];
readonly userQuestionResponses: Array<{ sessionId: string; response: UserQuestionResponse }> = [];
readonly permissionModes: Array<{ sessionId: string; mode: PermissionMode }> = [];
readonly orchestrationModes: Array<{
sessionId: string;
mode: import('@maka/core/orchestration').OrchestrationMode;
}> = [];
readonly sessionUpdates: Array<{
sessionId: string;
patch: {
cwd?: string;
model?: string;
llmConnectionSlug?: string;
thinkingLevel?: import('@maka/core/model-thinking').ThinkingLevel | undefined;
name?: string;
};
}> = [];
readonly branched: Array<{ sessionId: string; sourceTurnId: string }> = [];
readonly branchedBefore: Array<{ sessionId: string; sourceTurnId: string }> = [];
readonly sessionMessages = new Map<string, StoredMessage[]>();
sessionSummaries: SessionSummary[] = [];
updatedSessionName = 'New Chat';
async createSession(input: CreateSessionInput): Promise<SessionSummary> {
this.created.push(input);
return {
id: 'session-1',
name: input.name ?? 'New Chat',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: input.status ?? 'active',
backend: input.backend,
llmConnectionSlug: input.llmConnectionSlug,
connectionLocked: false,
model: input.model ?? '',
permissionMode: input.permissionMode,
};
}
async *sendMessage(sessionId: string, input: UserMessageInput): AsyncIterable<SessionEvent> {
this.sent.push({ sessionId, input });
yield {
type: 'text_delta',
id: 'event-1',
turnId: input.turnId,
ts: 1,
messageId: 'message-1',
text: 'ok',
};
yield {
type: 'complete',
id: 'event-2',
turnId: input.turnId,
ts: 2,
stopReason: 'end_turn',
};
}
async *compactSession(
sessionId: string,
input: { turnId?: string } = {},
): AsyncIterable<SessionEvent> {
this.compacted.push({ sessionId, input });
yield {
type: 'complete',
id: 'event-compact-complete',
turnId: input.turnId ?? 'turn-compact',
ts: 3,
stopReason: 'end_turn',
};
}
async planLatestAuthoritativeSafeBoundaryContinuation(
sessionId: string,
): Promise<SafeBoundaryContinuationPlan> {
this.resumePlanSessions.push(sessionId);
return {
disposition: 'continue',
rejectionReasons: [],
diagnostics: [],
continuation: {
sessionId,
runId: 'resume-run',
turnId: 'resume-turn',
} as RuntimeContinuation,
};
}
async *resumeSafeBoundaryContinuation(
continuation: RuntimeContinuation,
): AsyncIterable<SessionEvent> {
this.resumedContinuations.push(continuation);
yield {
type: 'text_complete',
id: 'resume-text',
turnId: continuation.turnId,
ts: 4,
messageId: 'resume-message',
text: 'resumed',
};
yield {
type: 'complete',
id: 'resume-complete',
turnId: continuation.turnId,
ts: 5,
stopReason: 'end_turn',
};
}
async stopSession(_sessionId: string): Promise<void> {}
readonly steered: Array<{ sessionId: string; text: string }> = [];
readonly queued: Array<{ sessionId: string; text: string }> = [];
readonly followupDrains: string[] = [];
readonly retracted: string[] = [];
steerOutcome: QueueEnqueueOutcome = { kind: 'queued' };
queueOutcome: QueueEnqueueOutcome = { kind: 'queued' };
followupText: string | null = null;
retractText = '';
steer(sessionId: string, text: string): QueueEnqueueOutcome {
this.steered.push({ sessionId, text });
return this.steerOutcome;
}
queueMessage(sessionId: string, text: string): QueueEnqueueOutcome {
this.queued.push({ sessionId, text });
return this.queueOutcome;
}
drainFollowup(sessionId: string): string | null {
this.followupDrains.push(sessionId);
return this.followupText;
}
retractQueue(sessionId: string): string {
this.retracted.push(sessionId);
return this.retractText;
}
async respondToPermission(sessionId: string, response: PermissionResponse): Promise<void> {
this.permissionResponses.push({ sessionId, response });
}
async respondToUserQuestion(sessionId: string, response: UserQuestionResponse): Promise<void> {
this.userQuestionResponses.push({ sessionId, response });
}
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<SessionSummary> {
this.permissionModes.push({ sessionId, mode });
return {
id: sessionId,
name: 'New Chat',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: 'anthropic',
connectionLocked: false,
model: 'claude-sonnet-4-5',
permissionMode: mode,
};
}
async setOrchestrationMode(
sessionId: string,
mode: import('@maka/core/orchestration').OrchestrationMode,
): Promise<SessionSummary> {
this.orchestrationModes.push({ sessionId, mode });
return {
...sessionSummary({ id: sessionId }),
orchestrationMode: mode,
};
}
async updateSession(
sessionId: string,
patch: {
cwd?: string;
model?: string;
llmConnectionSlug?: string;
thinkingLevel?: import('@maka/core/model-thinking').ThinkingLevel | undefined;
name?: string;
},
): Promise<SessionSummary> {
this.sessionUpdates.push({ sessionId, patch });
return {
id: sessionId,
cwd: patch.cwd ?? '/repo',
name: this.updatedSessionName,
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: patch.llmConnectionSlug ?? 'anthropic',
connectionLocked: false,
model: patch.model ?? 'claude-sonnet-4-5',
permissionMode: 'ask',
};
}
async listSessions(): Promise<SessionSummary[]> {
return this.sessionSummaries;
}
async getMessages(sessionId: string): Promise<StoredMessage[]> {
return this.sessionMessages.get(sessionId) ?? [];
}
async branchFromTurn(
sessionId: string,
input: { sourceTurnId: string; name?: string },
): Promise<SessionSummary> {
this.branched.push({ sessionId, sourceTurnId: input.sourceTurnId });
return this.recordBranch(sessionId);
}
async branchBeforeTurn(
sessionId: string,
input: { sourceTurnId: string; name?: string },
): Promise<SessionSummary> {
this.branchedBefore.push({ sessionId, sourceTurnId: input.sourceTurnId });
return this.recordBranch(sessionId);
}
private recordBranch(sessionId: string): SessionSummary {
// Model a branch by adding a new summary to the list switchSession reads.
const source = this.sessionSummaries.find((session) => session.id === sessionId);
const branch: SessionSummary = {
...(source ?? sessionSummary({ id: sessionId })),
id: `${sessionId}-branch`,
};
this.sessionSummaries = [...this.sessionSummaries, branch];
this.sessionMessages.set(branch.id, this.sessionMessages.get(sessionId) ?? []);
return branch;
}
}
function nextId(prefix: string): () => string {
let count = 0;
return () => `${prefix}-${++count}`;
}
function fixedIds(...ids: string[]): () => string {
let index = 0;
return () => ids[index++] ?? ids[ids.length - 1] ?? 'id';
}
function sessionSummary(overrides: Partial<SessionSummary>): SessionSummary {
return {
id: 'session',
cwd: '/repo',
name: 'Existing chat',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: 'anthropic',
connectionLocked: false,
model: 'claude-sonnet-4-5',
permissionMode: 'ask',
...overrides,
};
}
function storedUserMessage(id: string, turnId: string, text: string): StoredMessage {
return {
type: 'user',
id,
turnId,
ts: 1,
text,
};
}
function storedAssistantMessage(id: string, turnId: string, text: string): StoredMessage {
return {
type: 'assistant',
id,
turnId,
ts: 2,
text,
modelId: 'claude-sonnet-4-5',
};
}
function storedContextCompactedNote(id: string, turnId: string): StoredMessage {
return {
type: 'system_note',
id,
turnId,
ts: 3,
kind: 'context_compacted',
};
}
function storedTurnState(
id: string,
turnId: string,
status: 'completed' | 'aborted' | 'failed',
): StoredMessage {
return {
type: 'turn_state',
id,
turnId,
ts: 4,
status,
partialOutputRetained: false,
};
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
const out: T[] = [];
for await (const item of iterable) out.push(item);
return out;
}
async function collectPrompt(
driver: ReturnType<typeof createMakaSessionDriver>,
prompt: string,
): Promise<SessionEvent[]> {
return collect((await driver.preparePrompt(prompt)).events);
}