Files
TORCH/test/fleet/provider-updates.test.mjs
T

139 lines
8.4 KiB
JavaScript

import assert from 'node:assert/strict';
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { managedProvider, planProviderUpdates, providerStore, updateProviders } from '../../src/adapters/updates.mjs';
import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs';
import { execFileSync } from 'node:child_process';
import { inspectRepository } from '../../src/kernel/git.mjs';
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
import { proposeDomains } from '../../src/kernel/domains.mjs';
import { installProject } from '../../src/kernel/install.mjs';
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
import { runCli } from '../../src/cli.mjs';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-provider-update-'));
const env = { ...process.env, XDG_DATA_HOME: root };
const calls = [];
let version = '1.2.3';
let failInstall = false;
const runner = (command, args, options) => {
calls.push({ command, args, options });
if (args[0] === 'view') return { status: 0, stdout: JSON.stringify(version) };
if (args[0] === 'install') {
if (failInstall) return { status: 1, stderr: 'offline' };
const pkg = args.at(-1);
const name = pkg.startsWith('@openai/') ? 'codex' : pkg.startsWith('@anthropic-ai/') ? 'claude' : 'pi';
const prefix = args[args.indexOf('--prefix') + 1];
mkdirSync(join(prefix, 'node_modules', '.bin'), { recursive: true });
const path = join(prefix, 'node_modules', '.bin', name);
writeFileSync(path, `#!/bin/sh\n# ${version}\n`);
chmodSync(path, 0o755);
return { status: 0, stdout: 'installed' };
}
return { status: 0, stdout: args[0] === '--version' ? `provider ${version}`
: args.includes('exec') ? '{"type":"thread.started","thread_id":"provider-update-fixture-session"}\n{"type":"turn.completed"}\n' : 'help' };
};
return { env, calls, runner, next: v => { version = v; }, breakInstall: () => { failInstall = true; } };
}
test('SCN-provider-updates: all built-ins resolve official latest, verify and launch the managed binary', () => {
const f = fixture();
const plan = planProviderUpdates(['codex', 'claude', 'pi'], f);
assert.equal(f.calls.length, 0);
assert.equal(plan.modifiesGlobalInstallations, false);
assert.throws(() => updateProviders(['codex'], f), { code: 'APPROVAL_REQUIRED' });
assert.equal(existsSync(providerStore(f.env)), false);
const result = updateProviders(['codex', 'claude', 'pi'], { ...f, authorized: true });
assert.deepEqual(result.results.map(r => r.outcome), ['updated', 'updated', 'updated']);
const registry = createRuntimeAdapterRegistry({ env: f.env });
for (const name of ['codex', 'claude', 'pi']) {
const selected = managedProvider(name, f);
assert.equal(registry.get(name).executable, selected.executable);
assert.equal(selected.version, '1.2.3');
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--version'), true);
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--help'), true);
}
for (const call of f.calls.filter(c => c.command === 'npm')) {
assert.equal(call.args.includes('-g'), false);
assert.equal(call.args.includes('--registry'), true);
assert.equal(call.options.timeout > 0, true);
}
assert.equal(f.calls.find(c => c.args.at(-1) === '@earendil-works/[email protected]').args.includes('--ignore-scripts'), true);
const count = f.calls.length;
assert.equal(updateProviders(['codex'], { ...f, authorized: true }).results[0].outcome, 'fresh');
assert.equal(f.calls.length, count, 'fresh startup performs no network/install calls');
});
test('SCN-provider-update-cli: opted-in startup updates selected provider, dry runs stay offline, failed refresh cannot launch', async () => {
const f = fixture();
const root = join(f.env.XDG_DATA_HOME, 'repo');
mkdirSync(root);
const git = args => execFileSync('git', ['-C', root, ...args], { stdio: 'pipe' });
git(['init', '-b', 'main']); git(['config', 'user.email', '[email protected]']); git(['config', 'user.name', 'Fixture']);
writeFileSync(join(root, 'README.md'), '# Test project\n');
git(['add', '.']); git(['commit', '-m', 'fixture']);
const repository = inspectRepository(root);
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
installProject({ repository, proposal, env: f.env, runtimes: ['codex'] });
const stdout = process.stdout.write;
const outputs = [];
process.stdout.write = chunk => { outputs.push(String(chunk)); return true; };
try {
const run = args => runCli(args, { cwd: root, env: f.env, spawn: f.runner });
assert.equal(await run(['runtimes', 'update-policy', '--mode', 'auto', '--yes', '--json']), 0);
const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml')));
assert.deepEqual(config.runtimes.codex.updatePolicy, { mode: 'auto', max_age_hours: 24 });
git(['add', '.torch']); git(['commit', '-m', 'config']);
createWorktrees({ repository: inspectRepository(root), parentOverride: join(f.env.XDG_DATA_HOME, 'worktrees') });
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--dry-run', '--json']), 0);
assert.equal(f.calls.length, 0, 'dry-run never downloads or launches');
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--yes', '--json']), 0);
const selected = managedProvider('codex', f);
const launches = f.calls.filter(c => c.args.includes('exec'));
assert.equal(launches.length, 1);
assert.equal(launches[0].command, selected.executable);
assert.equal(f.calls.some(c => c.args.at(-1)?.startsWith('@anthropic-ai/')), false);
const pointer = join(providerStore(f.env), 'codex.json');
const stale = JSON.parse(readFileSync(pointer)); stale.checkedAt = '2000-01-01T00:00:00Z';
writeFileSync(pointer, JSON.stringify(stale)); f.next('1.3.0'); f.breakInstall();
assert.equal(await run(['up', '--only', 'session-manager', '--yes', '--json']), 2);
assert.equal(f.calls.filter(c => c.args.includes('exec')).length, 1, 'failed refresh must not launch the old provider silently');
assert.equal(outputs.some(s => s.includes('RUNTIME_UPDATE_FAILED')), true);
} finally { process.stdout.write = stdout; }
});
test('SCN-provider-updates-recovery: failed updates retain last activation; tampering and concurrent updates stop', () => {
const f = fixture();
updateProviders(['codex'], { ...f, authorized: true });
const active = managedProvider('codex', f);
f.next('1.3.0'); f.breakInstall();
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true, force: true }), { code: 'RUNTIME_UPDATE_FAILED' });
assert.equal(managedProvider('codex', f).executable, active.executable);
assert.equal(managedProvider('codex', f).version, '1.2.3');
mkdirSync(join(providerStore(f.env), 'update.lock'));
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_BUSY' });
writeFileSync(active.executable, 'tampered');
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
});
test('SCN-provider-update-safety: arbitrary packages, unsafe registry versions, path escape and symlink stores fail closed', () => {
const f = fixture();
assert.throws(() => planProviderUpdates(['untrusted-command'], f), { code: 'RUNTIME_UPDATE_FAILED' });
f.next('1.2.3;touch /tmp/evil');
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_FAILED' });
assert.equal(f.calls.some(c => c.args[0] === 'install'), false);
f.next('1.2.3'); updateProviders(['codex'], { ...f, authorized: true });
const path = join(providerStore(f.env), 'codex.json');
const record = JSON.parse(readFileSync(path));
record.prefix = tmpdir(); writeFileSync(path, JSON.stringify(record));
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
const other = fixture();
mkdirSync(join(other.env.XDG_DATA_HOME, 'torch'));
symlinkSync(tmpdir(), providerStore(other.env));
assert.throws(() => planProviderUpdates(['pi'], other), { code: 'RUNTIME_UPDATE_FAILED' });
});