// @vitest-environment node import { realpathSync } from 'node:fs'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it } from 'vitest'; import { createCodingConversationStore } from '../../electron/coding-projects/conversation-store'; import { createCodingProjectAgent } from '../../electron/coding-projects/project-config'; import { CodingProjectService } from '../../electron/coding-projects/project-service'; import { createCodingProjectStore, createLocalCodingProject, createMemoryCodingProjectStorage, } from '../../electron/coding-projects/project-store'; import { CodingConversationService } from '../../electron/coding-runtime/conversation-service'; import { PiManagedExtensionHost } from '../../electron/coding-runtime/pi/extension-host'; import { buildPiProviderCatalog, selectPiProviderModel, } from '../../electron/coding-runtime/pi/provider-config'; import { createPiManagedWorkerOpener, PiConversationRuntime, } from '../../electron/coding-runtime/pi/runtime'; import { PiSessionRegistry } from '../../electron/coding-runtime/pi/session-registry'; import { PiWorkerProcess, type PiWorkerProcessOptions } from '../../electron/coding-runtime/pi/worker-process'; import { PiWorkerPool } from '../../electron/coding-runtime/pi/worker-pool'; import type { ProviderAccount } from '../../electron/shared/providers/types'; const roots: string[] = []; const NOW = '2026-08-25T08:00:00.000Z'; const SOURCE_SESSION_ID = 'c67b7c7b-8364-4dcb-a7f7-913316e8d735'; function electronExecutable(): string { const requireFromProject = createRequire(path.resolve('package.json')); return requireFromProject('electron') as string; } afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true, maxRetries: 3, }))); }); describe('real managed Pi user-entry fork', () => { it('forks a persisted Pi 0.84.2 session through the product service and hydrates an independent binding', async () => { const root = realpathSync(await mkdtemp(path.join(tmpdir(), 'makelore-pi-real-fork-'))); roots.push(root); const projectPath = path.join(root, 'project'); const userDataDir = path.join(root, 'user-data'); const projectStore = createCodingProjectStore(createMemoryCodingProjectStorage(), { createId: () => 'project-real-fork', now: () => NOW, }); const { project } = await createLocalCodingProject({ projectPath, now: NOW }, projectStore); const model = { accountId: 'account-real-fork', modelId: 'model-real-fork', thinkingLevel: 'off' as const, }; await createCodingProjectAgent(projectPath, { id: 'agent-real-fork', avatarId: 'avatar-01', roleName: 'Fork verifier', name: 'Fork verifier', model, modelResolution: 'resolved', responsibility: { mission: 'Verify a persisted user-entry fork', owns: [], boundaries: [], collaborators: [], principles: [], }, skillIds: [], }, { now: NOW }); const store = createCodingConversationStore(projectPath, { createId: () => '4ad88774-c373-421b-9c97-5facd368ba66', now: () => NOW, }); const source = await store.create({ agentId: 'agent-real-fork', title: 'Persisted source', model, modelResolution: 'resolved', }); await store.ensureSessionBinding(source.id, async () => ({ piSessionId: SOURCE_SESSION_ID, sessionKey: SOURCE_SESSION_ID, })); const sessionDirectory = path.join( userDataDir, 'coding-runtime', 'pi', 'sessions', project.id, ); await mkdir(sessionDirectory, { recursive: true }); const sessionEntries = [ { type: 'session', version: 3, id: SOURCE_SESSION_ID, timestamp: NOW, cwd: project.path }, { type: 'message', id: 'entry-user-real-fork', parentId: null, timestamp: NOW, message: { role: 'user', content: 'Persisted user fork source', timestamp: 1 }, }, { type: 'message', id: 'entry-assistant-real-fork', parentId: 'entry-user-real-fork', timestamp: NOW, message: { role: 'assistant', content: [{ type: 'text', text: 'Persisted assistant response' }], usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, totalTokens: 2, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: 'stop', timestamp: 2, }, }, ]; await writeFile( path.join(sessionDirectory, `2026-08-25T08-00-00-000Z_${SOURCE_SESSION_ID}.jsonl`), `${sessionEntries.map((entry) => JSON.stringify(entry)).join('\n')}\n`, ); const packageRoot = realpathSync(path.resolve( 'node_modules', '@earendil-works', 'pi-coding-agent', )); const { SessionManager } = await import(pathToFileURL( path.join(packageRoot, 'dist', 'core', 'session-manager.js'), ).href) as { SessionManager: { list(cwd: string, sessionDir: string): Promise> }; }; expect(await SessionManager.list(project.path, sessionDirectory)).toEqual([ expect.objectContaining({ id: SOURCE_SESSION_ID }), ]); const account: ProviderAccount = { id: model.accountId, vendorId: 'custom', label: 'Local fork proof account', authMode: 'local', apiProtocol: 'openai-completions', baseUrl: 'http://127.0.0.1:9/v1', model: model.modelId, enabled: true, isDefault: true, createdAt: NOW, updatedAt: NOW, }; const providerInput = { accounts: [account], modelSummaries: [] }; const registry = new PiSessionRegistry({ projectStore }); const extensionHost = new PiManagedExtensionHost(); const processOptions: PiWorkerProcessOptions[] = []; const pool = new PiWorkerPool({ maxIdle: 2, openWorker: createPiManagedWorkerOpener({ registry, executablePath: electronExecutable(), cliPath: path.join(packageRoot, 'dist', 'cli.js'), userDataDir, bundledSkillsDir: path.resolve('resources/coding-skills'), extensionHost, loadProviderInput: async () => providerInput, resolveCredential: async () => null, createProcess: (options) => { processOptions.push(options); return new PiWorkerProcess(options); }, }), }); const runtime = new PiConversationRuntime({ pool, registry, extensionHost, resolveModel: async (candidate) => selectPiProviderModel( buildPiProviderCatalog(providerInput), candidate, ), }); const service = new CodingConversationService( new CodingProjectService(projectStore), runtime, ); try { const sourceSnapshot = await service.getSnapshot(source.id); expect(processOptions[0]?.sessionDir).toBe(sessionDirectory); expect(processOptions[0]?.cwd).toBe(project.path); expect(sourceSnapshot.nodes).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: 'message', role: 'user', sourceEntryId: 'entry-user-real-fork', status: 'complete', }), expect.objectContaining({ kind: 'message', role: 'assistant', sourceEntryId: 'entry-assistant-real-fork', status: 'complete', }), ])); const forked = await service.fork(source.id, 'entry-user-real-fork'); const [sourceAfterFork, forkedSnapshot, sourceBinding, forkedBinding] = await Promise.all([ service.getSnapshot(source.id), service.getSnapshot(forked.id), store.get(source.id), store.get(forked.id), ]); expect(forked.agentId).toBe(source.agentId); expect(forkedSnapshot).toMatchObject({ conversation: { id: forked.id, agentId: source.agentId }, nodes: [], run: { status: 'idle' }, worker: { status: 'ready' }, }); expect(sourceAfterFork.nodes).toEqual(sourceSnapshot.nodes); expect(sourceBinding).toMatchObject({ piSessionId: SOURCE_SESSION_ID, sessionKey: SOURCE_SESSION_ID, }); expect(forkedBinding?.piSessionId).toBeTruthy(); expect(forkedBinding?.sessionKey).toBeTruthy(); expect(forkedBinding?.piSessionId).not.toBe(sourceBinding?.piSessionId); expect(forkedBinding?.sessionKey).not.toBe(sourceBinding?.sessionKey); expect(runtime.getDiagnostics().workers).toHaveLength(2); } finally { await runtime.shutdown(); await extensionHost.close(); } expect(runtime.getDiagnostics().workers).toHaveLength(0); }, 30_000); });