maestro/src/scripts/migrate-config.test.ts
oss-sync 2044f0a2c4
Some checks failed
CI / build-and-test (push) Failing after 7m0s
sync: update from private repo (91d8d79c)
2026-07-09 23:57:26 +00:00

146 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 },
});
});
});