146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
/**
|
||
* Smoke tests for the migrate-config CLI. We invoke the bash wrapper as a
|
||
* subprocess so the test exercises the same code path operators will use.
|
||
*
|
||
* The bash wrapper auto-picks dist/scripts/migrate-config.js if it exists,
|
||
* else falls back to tsx — both are tested transparently because we don't
|
||
* care which runtime ran, only that the output is correct.
|
||
*/
|
||
import { afterEach, describe, expect, it } from 'vitest';
|
||
import { transformCamel } from './migrate-config.js';
|
||
import { spawnSync } from 'child_process';
|
||
import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
||
import { tmpdir } from 'os';
|
||
import { join, dirname } from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const REPO_ROOT = join(__dirname, '..', '..'); // src/scripts → repo root
|
||
const SCRIPT = join(REPO_ROOT, 'scripts', 'migrate-config.sh');
|
||
const FIXTURES = join(__dirname, '..', '__fixtures__', 'config-migration');
|
||
|
||
function runCli(args: string[]): { stdout: string; stderr: string; status: number } {
|
||
const r = spawnSync('bash', [SCRIPT, ...args], { encoding: 'utf-8', cwd: REPO_ROOT });
|
||
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', status: r.status ?? -1 };
|
||
}
|
||
|
||
describe('migrate-config CLI', () => {
|
||
let tempDir = '';
|
||
|
||
afterEach(() => {
|
||
if (tempDir) {
|
||
rmSync(tempDir, { recursive: true, force: true });
|
||
tempDir = '';
|
||
}
|
||
});
|
||
|
||
it('--help exits 0 and prints usage', () => {
|
||
const r = runCli(['--help']);
|
||
expect(r.status).toBe(0);
|
||
expect(r.stdout).toContain('Usage: migrate-config');
|
||
});
|
||
|
||
it('unknown flag exits 3', () => {
|
||
const r = runCli(['--nope']);
|
||
expect(r.status).toBe(3);
|
||
expect(r.stderr).toMatch(/unknown flag/);
|
||
});
|
||
|
||
it('missing config file exits 1', () => {
|
||
tempDir = mkdtempSync(join(tmpdir(), 'migrate-config-'));
|
||
const r = runCli(['--config', join(tempDir, 'nope.yaml')]);
|
||
expect(r.status).toBe(1);
|
||
expect(r.stderr).toMatch(/config file not found/);
|
||
});
|
||
|
||
it('dry-run on v1 fixture prints v2 YAML to stdout and does not modify the file', () => {
|
||
tempDir = mkdtempSync(join(tmpdir(), 'migrate-config-'));
|
||
const target = join(tempDir, 'config.yaml');
|
||
copyFileSync(join(FIXTURES, 'v1-single-ollama.yaml'), target);
|
||
const before = readFileSync(target, 'utf-8');
|
||
|
||
const r = runCli(['--dry-run', '--config', target]);
|
||
expect(r.status).toBe(0);
|
||
expect(r.stdout).toMatch(/config_version: 2/);
|
||
expect(r.stdout).toMatch(/connection_type: direct/);
|
||
// Dry run does NOT touch the source file.
|
||
expect(readFileSync(target, 'utf-8')).toBe(before);
|
||
expect(readdirSync(tempDir).filter(f => f.includes('.bak-'))).toHaveLength(0);
|
||
});
|
||
|
||
it('in-place rewrite produces v2 file and a timestamped backup', () => {
|
||
tempDir = mkdtempSync(join(tmpdir(), 'migrate-config-'));
|
||
const target = join(tempDir, 'config.yaml');
|
||
copyFileSync(join(FIXTURES, 'v1-single-ollama.yaml'), target);
|
||
|
||
const r = runCli(['--config', target]);
|
||
expect(r.status).toBe(0);
|
||
|
||
const migrated = readFileSync(target, 'utf-8');
|
||
expect(migrated).toMatch(/^config_version: 2/m);
|
||
expect(migrated).toMatch(/storage:/);
|
||
|
||
// Exactly one backup file in the same directory.
|
||
const backups = readdirSync(tempDir).filter(f => f.startsWith('config.yaml.bak-'));
|
||
expect(backups).toHaveLength(1);
|
||
|
||
// Backup matches the original input.
|
||
const original = readFileSync(join(FIXTURES, 'v1-single-ollama.yaml'), 'utf-8');
|
||
expect(readFileSync(join(tempDir, backups[0]!), 'utf-8')).toBe(original);
|
||
});
|
||
|
||
it('already-v2 file → no-op + exit 0', () => {
|
||
tempDir = mkdtempSync(join(tmpdir(), 'migrate-config-'));
|
||
const target = join(tempDir, 'config.yaml');
|
||
writeFileSync(
|
||
target,
|
||
[
|
||
'config_version: 2',
|
||
'llm:',
|
||
' workers:',
|
||
' - id: w1',
|
||
' connection_type: direct',
|
||
' endpoint: http://localhost:11434/v1',
|
||
' model: qwen3:32b',
|
||
' roles: [auto]',
|
||
' max_concurrency: 1',
|
||
' enabled: true',
|
||
'',
|
||
].join('\n'),
|
||
);
|
||
const before = readFileSync(target, 'utf-8');
|
||
const r = runCli(['--config', target]);
|
||
expect(r.status).toBe(0);
|
||
expect(r.stdout).toMatch(/already up to date/);
|
||
// No rewrite (no backup either).
|
||
expect(readFileSync(target, 'utf-8')).toBe(before);
|
||
expect(readdirSync(tempDir).filter(f => f.includes('.bak-'))).toHaveLength(0);
|
||
});
|
||
|
||
it('config_version: 99 exits 2 with the typo-guard message', () => {
|
||
tempDir = mkdtempSync(join(tmpdir(), 'migrate-config-'));
|
||
const target = join(tempDir, 'config.yaml');
|
||
writeFileSync(target, 'config_version: 99\nprovider:\n model: x\n');
|
||
const r = runCli(['--config', target]);
|
||
expect(r.status).toBe(2);
|
||
expect(r.stderr).toMatch(/config_version=99/);
|
||
});
|
||
});
|
||
|
||
describe('transformCamel', () => {
|
||
it('extra_body の中身のキーは camelCase 変換せず素通しする(src/config.ts transformKeys と同じ不変条件)', () => {
|
||
const input = {
|
||
provider: {
|
||
workers: [
|
||
{ id: 'w1', extra_body: { reasoning_effort: 'max', chat_template_kwargs: { enable_thinking: true } } },
|
||
],
|
||
},
|
||
};
|
||
const out = transformCamel(input) as any;
|
||
expect(out.provider.workers[0].extraBody).toEqual({
|
||
reasoning_effort: 'max',
|
||
chat_template_kwargs: { enable_thinking: true },
|
||
});
|
||
});
|
||
});
|