Files
makelore/tests/unit/pi-managed-user-entry-fork-real.test.ts

230 lines
8.6 KiB
TypeScript

// @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<Array<{ id: string }>> };
};
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);
});