Files
TORCH/test/fleet/console-schedule-launcher.test.mjs

195 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { createConsoleServer } from '../../src/console/server.mjs';
import { openControlPlane } from '../../src/control-plane/service.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 { ScheduleLauncherService } from '../../src/schedules/launcher.mjs';
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'torch-console-schedule-launcher-'));
const env = {
...process.env,
XDG_DATA_HOME: mkdtempSync(join(tmpdir(), 'torch-console-schedule-state-')),
XDG_CONFIG_HOME: mkdtempSync(join(tmpdir(), 'torch-console-schedule-config-')),
};
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) });
proposal.schedules = [
{
id: 'manager-check-in', title: 'Session manager check-in', owner: 'session-manager',
lifetime: 'system', trigger: { type: 'interval', seconds: 900 }, behavior: 'coordination',
action: { type: 'manager-check-in', manager_id: 'session-manager' },
required_authority: ['owner'], retry: { max_attempts: 1 },
failure_recipient: 'session-manager', source_of_truth: 'active organization graph',
},
{
id: 'authorized-integration-drain', title: 'Authorized integration drain', owner: 'owner',
lifetime: 'system', trigger: { type: 'interval', seconds: 60 }, behavior: 'mutating',
action: { type: 'integration-drain', landing_authority_id: 'session-manager', limit: 20 },
required_authority: ['owner'], retry: { max_attempts: 1 },
failure_recipient: 'session-manager', source_of_truth: 'owner-approved integration policy',
},
];
proposal.review = {
status: 'approved', reviewedAt: '2026-09-28T00:00:00Z', reviewedBy: 'fixture-owner', notes: [],
};
installProject({ repository, proposal, env, projectId: 'console-schedule-fixture' });
return { root, env };
}
async function request(origin, path, payload) {
return fetch(`${origin}${path}`, {
method: 'POST', headers: {
origin, 'content-type': 'application/json', 'sec-fetch-site': 'same-origin',
}, body: JSON.stringify(payload),
});
}
test('SCN-console-schedule-launcher: the owner reviews exact timer effects before a single audited install or refresh', async () => {
const context = fixture();
const systemdCalls = [];
const executor = (file, args) => {
systemdCalls.push({ file, args });
return { status: 0, stdout: '', stderr: '' };
};
const server = createConsoleServer({
repositoryRoot: context.root, env: context.env, scheduleLauncherExecutor: executor,
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
try {
const initial = await fetch(`${origin}/api/snapshot`);
const initialSnapshot = await initial.json();
assert.equal(initialSnapshot.scheduleLauncher.installed, false);
const unitDirectory = join(context.env.XDG_CONFIG_HOME, 'systemd', 'user');
assert.equal(existsSync(unitDirectory), false);
const crossOrigin = await fetch(`${origin}/api/schedule-launcher/preview`, {
method: 'POST', headers: { origin: 'https://untrusted.example', 'content-type': 'application/json' },
body: JSON.stringify({ action: 'install' }),
});
assert.equal(crossOrigin.status, 403);
assert.deepEqual(systemdCalls, []);
const firstPreviewResponse = await request(origin, '/api/schedule-launcher/preview', { action: 'install' });
assert.equal(firstPreviewResponse.status, 200);
const firstPreview = await firstPreviewResponse.json();
assert.equal(firstPreview.plan.canProceed, true);
assert.equal(firstPreview.plan.mutationPerformed, false);
assert.equal(firstPreview.plan.systemSchedules.length, 2);
assert.equal(firstPreview.plan.systemSchedules.find((schedule) => schedule.id === 'authorized-integration-drain').behavior, 'mutating');
assert.deepEqual(firstPreview.plan.files.map((file) => file.name), [
firstPreview.plan.serviceName, firstPreview.plan.timerName,
]);
assert.match(firstPreview.plan.effect, /enables and starts the timer/);
assert.equal(existsSync(unitDirectory), false, 'preview must not create unit files');
assert.deepEqual(systemdCalls, [], 'preview must not call systemd');
const configPath = join(context.root, '.torch', 'torch.yaml');
const config = JSON.parse(readFileSync(configPath, 'utf8'));
config.schedules[0].title = 'Changed after preview';
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
const staleConfirmation = await request(origin, '/api/schedule-launcher', {
action: 'install', token: firstPreview.token, planHash: firstPreview.planHash,
});
assert.equal(staleConfirmation.status, 409);
assert.equal((await staleConfirmation.json()).error, 'SCHEDULE_LAUNCHER_PREVIEW_STALE');
assert.deepEqual(systemdCalls, []);
assert.equal(existsSync(unitDirectory), false);
const previewResponse = await request(origin, '/api/schedule-launcher/preview', { action: 'install' });
assert.equal(previewResponse.status, 200);
const preview = await previewResponse.json();
assert.equal(preview.plan.canProceed, true);
const confirmationPayload = {
action: 'install', token: preview.token, planHash: preview.planHash,
};
const confirmed = await request(origin, '/api/schedule-launcher', confirmationPayload);
assert.equal(confirmed.status, 200);
const result = await confirmed.json();
assert.equal(result.mutationPerformed, true);
assert.equal(result.auditEventId.length > 0, true);
assert.deepEqual(systemdCalls.map((call) => call.args), [
['--user', 'daemon-reload'],
['--user', 'enable', '--now', preview.plan.timerName],
]);
assert.equal(existsSync(join(unitDirectory, preview.plan.serviceName)), true);
assert.equal(existsSync(join(unitDirectory, preview.plan.timerName)), true);
const replay = await request(origin, '/api/schedule-launcher', confirmationPayload);
assert.equal(replay.status, 200);
assert.equal((await replay.json()).auditEventId, result.auditEventId);
assert.equal(systemdCalls.length, 2, 'replay cannot repeat systemd mutation');
const controlPlane = openControlPlane({ repositoryRoot: context.root, env: context.env });
try {
const audits = controlPlane.database.prepare(
"SELECT * FROM audit_events WHERE operation = 'schedule.launcher.install'",
).all();
assert.equal(audits.length, 1);
} finally { controlPlane.close(); }
const refreshed = await fetch(`${origin}/api/snapshot`);
const snapshot = await refreshed.json();
assert.equal(snapshot.scheduleLauncher.installed, true);
assert.equal(snapshot.scheduleLauncher.stale, false);
const updatedConfig = JSON.parse(readFileSync(configPath, 'utf8'));
updatedConfig.schedules[0].title = 'Changed after installation';
writeFileSync(configPath, `${JSON.stringify(updatedConfig, null, 2)}\n`);
const reconcilePreviewResponse = await request(origin, '/api/schedule-launcher/preview', { action: 'reconcile' });
assert.equal(reconcilePreviewResponse.status, 200);
const reconcilePreview = await reconcilePreviewResponse.json();
assert.equal(reconcilePreview.plan.canProceed, true);
assert.equal(reconcilePreview.plan.stale, true);
assert.equal(reconcilePreview.plan.updates.some((entry) => entry.changed), true);
const reconcilePayload = {
action: 'reconcile', token: reconcilePreview.token, planHash: reconcilePreview.planHash,
};
const reconciled = await request(origin, '/api/schedule-launcher', reconcilePayload);
assert.equal(reconciled.status, 200);
assert.equal((await reconciled.json()).mutationPerformed, true);
assert.equal(systemdCalls.length, 4);
const reconcileReplay = await request(origin, '/api/schedule-launcher', reconcilePayload);
assert.equal(reconcileReplay.status, 200);
assert.equal(systemdCalls.length, 4, 'replayed refresh cannot repeat systemd mutation');
const finalSnapshot = await fetch(`${origin}/api/snapshot`);
assert.equal((await finalSnapshot.json()).scheduleLauncher.stale, false);
const auditControl = openControlPlane({ repositoryRoot: context.root, env: context.env });
try {
const audits = auditControl.database.prepare(
"SELECT operation FROM audit_events WHERE operation LIKE 'schedule.launcher.%' ORDER BY rowid",
).all();
assert.deepEqual(audits.map((event) => event.operation), [
'schedule.launcher.install', 'schedule.launcher.reconcile',
]);
} finally { auditControl.close(); }
assert.match(readFileSync(new URL('../../site/console.html', import.meta.url), 'utf8'), /data-schedule-launcher-confirm/);
const consoleScript = readFileSync(new URL('../../site/console.js', import.meta.url), 'utf8');
const consoleServer = readFileSync(new URL('../../src/console/server.mjs', import.meta.url), 'utf8');
assert.match(consoleScript, /\/api\/schedule-launcher\/preview/);
assert.match(consoleServer, /SCHEDULE_LAUNCHER_PREVIEW_STALE/);
} finally {
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
const direct = new ScheduleLauncherService({
repositoryRoot: context.root, env: context.env,
executor: () => { throw new Error('expected-digest check must precede systemd'); },
});
assert.throws(() => direct.reconcile({ expectedDigest: 'out-of-date' }), (error) =>
error.code === 'SCHEDULE_LAUNCHER_PLAN_STALE');
});