Files
boocode/apps/control/src/services/__tests__/ssh-config.test.ts
indifferentketchup b18de2a331 chore: snapshot working tree - pty_exited notifications + in-flight inference WIP
feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean).

wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes.

openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
2026-06-14 12:48:47 +00:00

235 lines
9.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
validateLlamaConfig,
computeDiff,
backupFilename,
applyRemoteConfig,
healthWait,
type SshExec,
type ExecResult,
} from '../ssh-config.js';
// A minimal subset of the llama-swap config schema sufficient for these tests:
// top-level object with a required non-empty `models` object.
const SCHEMA = {
type: 'object',
required: ['models'],
properties: {
models: {
type: 'object',
minProperties: 1,
additionalProperties: {
type: 'object',
properties: { cmd: { type: 'string' } },
},
},
},
} as const;
const VALID_YAML = `models:\n m1:\n cmd: "llama-server -m m1.gguf"\n`;
describe('validateLlamaConfig', () => {
it('accepts a valid config', () => {
const r = validateLlamaConfig(VALID_YAML, SCHEMA);
expect(r.valid).toBe(true);
expect(r.errors).toEqual([]);
});
it('rejects broken YAML with a parse error', () => {
const r = validateLlamaConfig('models:\n m1:\n cmd: "x\n : :', SCHEMA);
expect(r.valid).toBe(false);
expect(r.errors[0]).toMatch(/YAML parse error/);
});
it('rejects a config missing required models', () => {
const r = validateLlamaConfig('healthCheckTimeout: 30\n', SCHEMA);
expect(r.valid).toBe(false);
expect(r.errors.join(' ')).toMatch(/models/);
});
it('rejects a non-mapping document', () => {
const r = validateLlamaConfig('- just\n- a\n- list\n', SCHEMA);
expect(r.valid).toBe(false);
});
});
describe('computeDiff', () => {
it('returns empty for identical text', () => {
expect(computeDiff('a\nb\n', 'a\nb\n')).toBe('');
});
it('marks changed lines with -/+', () => {
const d = computeDiff('a\nb\nc\n', 'a\nX\nc\n');
expect(d).toContain('- b');
expect(d).toContain('+ X');
});
});
describe('backupFilename', () => {
it('produces a timestamped path', () => {
const name = backupFilename('/etc/llama/config.yaml', new Date('2026-06-12T03:04:05.678Z'));
expect(name).toBe('/etc/llama/config.yaml.bak-20260612T030405Z');
});
});
// ─── apply pipeline failure paths ────────────────────────────────────────────
function makeExec(handlers: Record<string, ExecResult>): { exec: SshExec; calls: string[] } {
const calls: string[] = [];
const exec: SshExec = async (_t, command) => {
calls.push(command);
for (const [pattern, result] of Object.entries(handlers)) {
if (command.includes(pattern)) return result;
}
return { code: 0, stdout: '', stderr: '' };
};
return { exec, calls };
}
const target = { host: 'h', user: 'u', keyPath: '/k' };
const okFetcher = (async () => new Response('{}', { status: 200 })) as unknown as typeof fetch;
describe('applyRemoteConfig', () => {
it('aborts at validate for an invalid config and never touches the host', async () => {
const { exec, calls } = makeExec({});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart', newConfig: 'not: valid: yaml: here:::',
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher,
});
expect(r.ok).toBe(false);
expect(r.step).toBe('validate');
expect(calls).toHaveLength(0);
});
it('aborts at validate when the host config is unreadable', async () => {
const { exec } = makeExec({ "cat '": { code: 1, stdout: '', stderr: 'no such file' } });
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher,
});
expect(r.ok).toBe(false);
expect(r.step).toBe('validate');
expect(r.error).toMatch(/read current failed/);
});
it('backs up BEFORE write and aborts on write failure (backup retained)', async () => {
const { exec, calls } = makeExec({
"cat '": { code: 0, stdout: 'models:\n old: {}\n', stderr: '' }, // read current
'cp ': { code: 0, stdout: '', stderr: '' }, // backup
'cat >': { code: 1, stdout: '', stderr: 'disk full' }, // write fails
});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher,
now: new Date('2026-06-12T00:00:00Z'),
});
expect(r.ok).toBe(false);
expect(r.step).toBe('write');
expect(r.backupPath).toBe('/c.yaml.bak-20260612T000000Z');
// backup (cp) must precede write (cat >)
const cpIdx = calls.findIndex((c) => c.startsWith('cp '));
const writeIdx = calls.findIndex((c) => c.startsWith('cat >'));
expect(cpIdx).toBeGreaterThanOrEqual(0);
expect(writeIdx).toBeGreaterThan(cpIdx);
});
it('aborts at restart on restart failure', async () => {
const { exec } = makeExec({
"cat '": { code: 0, stdout: 'models:\n old: {}\n', stderr: '' },
'cp ': { code: 0, stdout: '', stderr: '' },
'cat >': { code: 0, stdout: '', stderr: '' },
restart: { code: 1, stdout: '', stderr: 'service not found' },
});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart-svc', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher,
});
expect(r.ok).toBe(false);
expect(r.step).toBe('restart');
});
it('aborts at health when the service never comes back', async () => {
const { exec } = makeExec({
"cat '": { code: 0, stdout: 'models:\n old: {}\n', stderr: '' },
'cp ': { code: 0, stdout: '', stderr: '' },
'cat >': { code: 0, stdout: '', stderr: '' },
'restart-svc': { code: 0, stdout: '', stderr: '' },
});
const downFetcher = (async () => { throw new Error('refused'); }) as unknown as typeof fetch;
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart-svc', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: downFetcher,
healthAttempts: 2, healthDelayMs: 1,
});
expect(r.ok).toBe(false);
expect(r.step).toBe('health');
});
it('succeeds through the full pipeline', async () => {
const { exec } = makeExec({
"cat '": { code: 0, stdout: 'models:\n old: {}\n', stderr: '' },
'cp ': { code: 0, stdout: '', stderr: '' },
'cat >': { code: 0, stdout: '', stderr: '' },
'restart-svc': { code: 0, stdout: '', stderr: '' },
});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'restart-svc', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher,
healthAttempts: 1, healthDelayMs: 1,
});
expect(r.ok).toBe(true);
expect(r.step).toBe('done');
expect(r.backupPath).toBeDefined();
});
});
describe('healthWait', () => {
it('returns true on first OK', async () => {
const ok = await healthWait('http://h', okFetcher, 3, 1);
expect(ok).toBe(true);
});
it('returns false after exhausting attempts', async () => {
const downFetcher = (async () => new Response('', { status: 503 })) as unknown as typeof fetch;
const ok = await healthWait('http://h', downFetcher, 2, 1);
expect(ok).toBe(false);
});
});
// ─── wrapper mode (forced-command verbs) ─────────────────────────────────────
describe('applyRemoteConfig wrapper mode', () => {
it('sends verbs (not raw shell) and reads the backup path from the backup verb', async () => {
const { exec, calls } = makeExec({
read: { code: 0, stdout: 'models:\n old: {}\n', stderr: '' },
backup: { code: 0, stdout: '/c.yaml.bak-WRAP\n', stderr: '' },
write: { code: 0, stdout: '', stderr: '' },
restart: { code: 0, stdout: '', stderr: '' },
});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'ignored-in-wrapper', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher, mode: 'wrapper',
healthAttempts: 1, healthDelayMs: 1,
});
expect(r.ok).toBe(true);
// backup path comes from the wrapper's stdout, not a client-computed name
expect(r.backupPath).toBe('/c.yaml.bak-WRAP');
// verbs only — no cat/cp/cat > shell commands
expect(calls).toEqual(['read', 'backup', 'write', 'restart']);
expect(calls.some((c) => c.includes('cat') || c.includes('cp '))).toBe(false);
});
it('aborts at write when the wrapper write verb fails (backup retained)', async () => {
const { exec } = makeExec({
read: { code: 0, stdout: 'old\n', stderr: '' },
backup: { code: 0, stdout: '/c.yaml.bak-WRAP\n', stderr: '' },
write: { code: 1, stdout: '', stderr: 'denied' },
});
const r = await applyRemoteConfig({
target, configPath: '/c.yaml', restartCmd: 'x', newConfig: VALID_YAML,
schema: SCHEMA, baseUrl: 'http://h', exec, fetcher: okFetcher, mode: 'wrapper',
});
expect(r.ok).toBe(false);
expect(r.step).toBe('write');
expect(r.backupPath).toBe('/c.yaml.bak-WRAP');
});
});