Files
TORCH/test/fleet/pi-adapter.test.mjs

213 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync, spawn, spawnSync } from 'node:child_process';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join } from 'node:path';
import test from 'node:test';
import { createPiAdapter } from '../../src/adapters/pi.mjs';
import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs';
import { profileCapabilityIssues } from '../../src/adapters/runtime.mjs';
import { describeTorchTools, TORCH_MCP_TOOL_NAMES } from '../../src/mcp/tools.mjs';
import { createTorchPiExtension } from '../../src/adapters/pi/extension.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { openControlPlane } from '../../src/control-plane/service.mjs';
import { planFleetUp } from '../../src/runtime/lifecycle.mjs';
const MCP_SERVER = new URL('../../src/mcp/server.mjs', import.meta.url).pathname;
function fixture({ withWorktrees = false } = {}) {
const root = mkdtempSync(join(tmpdir(), 'torch-pi-'));
const env = { ...process.env, XDG_DATA_HOME: join(tmpdir(), `${basename(root)}-data`) };
execFileSync('git', ['init', '-b', 'main', root]);
execFileSync('git', ['-C', root, 'config', 'user.email', '[email protected]']);
execFileSync('git', ['-C', root, 'config', 'user.name', 'TORCH Test']);
writeFileSync(join(root, 'app.js'), 'export const app = true;\n');
execFileSync('git', ['-C', root, 'add', '.']);
execFileSync('git', ['-C', root, 'commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
const worker = proposal.domains[0];
worker.runtime = 'pi';
worker.model = 'openai/test-model';
worker.reasoning = 'high';
proposal.review = {
status: 'approved', reviewedAt: '2026-09-28T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
installProject({ repository, proposal, env, projectId: 'pi-fixture', runtimes: ['claude', 'codex', 'pi'] });
if (withWorktrees) {
execFileSync('git', ['-C', root, 'add', '.torch']);
execFileSync('git', ['-C', root, 'commit', '-m', 'install TORCH fixture']);
createWorktrees({
repository: inspectRepository(root),
parentOverride: join(tmpdir(), `${basename(root)}-worktrees`),
});
}
return { root, env, worker: worker.id };
}
test('SCN-pi-adapter: Pi profiles produce isolated resumable-turn plans with an explicit required model', () => {
const adapter = createPiAdapter({ idFactory: () => 'pi-session-123' });
const integration = adapter.configure({
repositoryRoot: '/project', areaId: 'worker', mcpEntry: '/torch/src/mcp/server.mjs',
stateRoot: '/private/torch/project-state',
});
assert.equal(integration.mutationPerformed, false);
assert.equal(integration.integration.type, 'pi-mcp-extension');
assert.equal(integration.integration.sessionDir, '/private/torch/project-state/sessions/pi/worker');
const start = adapter.createSession({
areaId: 'worker', title: 'Worker', worktree: '/worktree', promptFile: '/private/prompts/worker.md',
firstMessage: 'Begin.', model: 'openai/test-model', reasoning: 'high', integration: integration.integration,
});
assert.equal(start.runtimeSessionId, 'pi-session-123');
assert.equal(start.requiresRuntimeIdCapture, false);
assert.equal(start.completionState, 'idle');
assert.equal(start.launch.command, 'pi');
assert.equal(start.launch.cwd, '/worktree');
assert.equal(start.launch.args.includes('--no-extensions'), true);
assert.equal(start.launch.args.includes('--no-context-files'), true);
assert.equal(start.launch.args.includes('--no-approve'), true);
assert.equal(start.launch.args.includes('/private/torch/project-state/sessions/pi/worker'), true);
assert.equal(start.launch.args.includes('openai/test-model'), true);
assert.deepEqual(start.launch.args.slice(start.launch.args.indexOf('--thinking'), start.launch.args.indexOf('--thinking') + 2), ['--thinking', 'high']);
const resumed = adapter.resumeSession({
areaId: 'worker', runtimeSessionId: start.runtimeSessionId, worktree: '/worktree',
promptFile: '/private/prompts/worker-current.md',
message: 'Continue from durable TORCH state.', model: 'openai/test-model', reasoning: 'medium',
integration: integration.integration,
});
assert.equal(resumed.runtimeSessionId, start.runtimeSessionId);
assert.equal(resumed.launch.args.includes('--session-id'), true);
assert.equal(resumed.launch.args.includes('Continue from durable TORCH state.'), true);
assert.deepEqual(resumed.launch.args.slice(resumed.launch.args.indexOf('--append-system-prompt'), resumed.launch.args.indexOf('--append-system-prompt') + 2), [
'--append-system-prompt', '/private/prompts/worker-current.md',
]);
assert.throws(() => adapter.createSession({
areaId: 'worker', worktree: '/worktree', promptFile: '/prompt', firstMessage: 'Begin.',
model: 'openai/test-model', integration: {},
}), (error) => error.code === 'RUNTIME_CONFIGURATION_INVALID');
assert.throws(() => adapter.createSession({
areaId: 'worker', worktree: '/worktree', promptFile: '/prompt', firstMessage: 'Begin.',
model: 'openai/test-model', reasoning: 'ultra', integration: integration.integration,
}), (error) => error.code === 'RUNTIME_PROFILE_INVALID');
assert.deepEqual(adapter.capabilities.processModel, 'resumable-turn');
assert.throws(() => adapter.stopSession(), (error) => error.code === 'RUNTIME_CAPABILITY_UNSUPPORTED');
assert.equal(createRuntimeAdapterRegistry().has('pi'), true);
assert.deepEqual(adapter.capabilities.modelRequired, true);
assert.deepEqual(profileCapabilityIssues(adapter, { model: null, reasoning: 'high' }), [
{ field: 'model', capability: 'modelSelection', required: true },
]);
});
test('SCN-pi-lifecycle-plan: installed fleet planning passes identity-bound Pi integration without starting it', () => {
const context = fixture({ withWorktrees: true });
const control = openControlPlane({ repositoryRoot: context.root, env: context.env });
try {
const registry = createRuntimeAdapterRegistry({ env: context.env });
const plan = planFleetUp({
repositoryRoot: context.root, controlPlane: control, adapters: registry.toMap(), fresh: true,
});
assert.equal(plan.canProceed, true, JSON.stringify(plan.blockers));
const action = plan.actions.find((candidate) => candidate.areaId === context.worker);
assert.equal(action.runtime, 'pi');
assert.equal(action.profile.model, 'openai/test-model');
assert.equal(action.profile.reasoning, 'high');
assert.equal(action.integration.type, 'pi-mcp-extension');
assert.equal(action.integration.areaId, context.worker);
assert.equal(action.integration.repositoryRoot, context.root);
assert.equal(action.launch.args.includes(`--torch-area`), true);
assert.equal(action.launch.args.includes(context.worker), true);
assert.equal(action.launch.args.includes('openai/test-model'), true);
assert.equal(action.requiresRuntimeIdCapture, false);
assert.equal(action.completionState, 'idle');
assert.equal(plan.mutationPerformed, false);
assert.equal(control.identity(context.worker).runtimeSessionId, null);
} finally {
control.close();
}
});
test('SCN-pi-mcp-bridge: Pi exposes every identity-bound TORCH MCP tool without project extension discovery', async () => {
const context = fixture();
const tools = [];
const flags = new Map([
['torch-root', context.root], ['torch-area', context.worker],
['torch-mcp-entry', MCP_SERVER], ['torch-node', process.execPath],
]);
const pi = {
registerFlag: (name, options) => flags.set(`registered:${name}`, options),
getFlag: (name) => flags.get(name),
registerTool: (tool) => tools.push(tool),
};
const result = createTorchPiExtension(pi, {
Type: { Unsafe: (schema) => schema },
spawnProcess: (command, args, options) => spawn(command, args, { ...options, env: context.env }),
});
assert.deepEqual(result.registered, TORCH_MCP_TOOL_NAMES);
assert.deepEqual(tools.map((tool) => tool.name), TORCH_MCP_TOOL_NAMES);
assert.equal(describeTorchTools().length, TORCH_MCP_TOOL_NAMES.length);
assert.equal(flags.get('registered:torch-area').type, 'string');
const identityTool = tools.find((tool) => tool.name === 'torch_identity');
const identityResult = await identityTool.execute('call-1', {}, undefined, undefined, {});
const identity = JSON.parse(identityResult.content[0].text);
assert.equal(identity.areaId, context.worker);
assert.equal(identity.runtime, 'pi');
await assert.rejects(
identityTool.execute('call-2', { area_id: 'session-manager' }, undefined, undefined, {}),
/cannot act as session-manager/i,
);
const messageTool = tools.find((tool) => tool.name === 'torch_send_message');
const messageResult = await messageTool.execute('call-3', {
recipient: 'session-manager', body: 'Pi MCP bridge verified.',
}, undefined, undefined, {});
assert.match(messageResult.content[0].text, /message/i);
});
test('SCN-pi-installed-extension-startup: an installed Pi loads the bundled bridge offline without a provider turn', (t) => {
const version = spawnSync('pi', ['--version'], { encoding: 'utf8', timeout: 5_000 });
if (version.error?.code === 'ENOENT') {
t.skip('Pi CLI is not installed in this environment');
return;
}
assert.equal(version.status, 0, version.stderr);
const context = fixture();
const privateConfig = join(context.root, '.pi-smoke-config');
const privateSessions = join(context.root, '.pi-smoke-sessions');
const result = spawnSync('pi', [
'--no-session', '--mode', 'rpc', '--no-extensions', '--no-context-files', '--no-approve', '--offline',
'--extension', new URL('../../src/adapters/pi/extension.mjs', import.meta.url).pathname,
'--torch-root', context.root,
'--torch-area', context.worker,
'--torch-mcp-entry', MCP_SERVER,
'--torch-node', process.execPath,
'--session-dir', privateSessions,
], {
cwd: context.root,
env: {
...context.env,
PI_OFFLINE: '1',
PI_TELEMETRY: '0',
PI_CODING_AGENT_DIR: privateConfig,
PI_CODING_AGENT_SESSION_DIR: privateSessions,
},
input: '{"type":"get_state","id":"torch-extension-smoke"}\n',
encoding: 'utf8',
timeout: 15_000,
});
assert.equal(result.error, undefined, result.error?.message);
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
const events = result.stdout.trim().split('\n').map((line) => JSON.parse(line));
assert.equal(events.some((event) => event.type === 'extension_error'), false, result.stdout);
assert.equal(events.some((event) => event.type === 'response'
&& event.id === 'torch-extension-smoke' && event.command === 'get_state' && event.success), true, result.stdout);
});