230 lines
8.6 KiB
TypeScript
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);
|
|
});
|