Files
nanoclaw/skills-engine/__tests__/run-migrations.test.ts
Gabi Simons 11c201088b refactor: CI optimization, logging improvements, and codebase formatting (#456)
* fix(db): remove unique constraint on folder to support multi-channel agents

* ci: implement automated skill drift detection and self-healing PRs

* fix: align registration logic with Gavriel's feedback and fix build/test issues from Daniel Mi

* style: conform to prettier standards for CI validation

* test: fix branch naming inconsistency in CI (master vs main)

* fix(ci): robust module resolution by removing file extensions in scripts

* refactor(ci): simplify skill validation by removing redundant combination tests

* style: conform skills-engine to prettier, unify logging in index.ts and cleanup unused imports

* refactor: extract multi-channel DB changes to separate branch

Move channel column, folder suffix logic, and related migrations
to feat/multi-channel-db-v2 for independent review. This PR now
contains only CI/CD optimizations, Prettier formatting, and
logging improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 23:13:36 +02:00

236 lines
6.5 KiB
TypeScript

import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, createTempDir } from './test-helpers.js';
describe('run-migrations', () => {
let tmpDir: string;
let newCoreDir: string;
const scriptPath = path.resolve('scripts/run-migrations.ts');
const tsxBin = path.resolve('node_modules/.bin/tsx');
beforeEach(() => {
tmpDir = createTempDir();
newCoreDir = path.join(tmpDir, 'new-core');
fs.mkdirSync(newCoreDir, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
function createMigration(version: string, code: string): void {
const migDir = path.join(newCoreDir, 'migrations', version);
fs.mkdirSync(migDir, { recursive: true });
fs.writeFileSync(path.join(migDir, 'index.ts'), code);
}
function runMigrations(
from: string,
to: string,
): { stdout: string; exitCode: number } {
try {
const stdout = execFileSync(tsxBin, [scriptPath, from, to, newCoreDir], {
cwd: tmpDir,
encoding: 'utf-8',
stdio: 'pipe',
timeout: 30_000,
});
return { stdout, exitCode: 0 };
} catch (err: any) {
return { stdout: err.stdout ?? '', exitCode: err.status ?? 1 };
}
}
it('outputs empty results when no migrations directory exists', () => {
const { stdout, exitCode } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(exitCode).toBe(0);
expect(result.migrationsRun).toBe(0);
expect(result.results).toEqual([]);
});
it('outputs empty results when migrations dir exists but is empty', () => {
fs.mkdirSync(path.join(newCoreDir, 'migrations'), { recursive: true });
const { stdout, exitCode } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(exitCode).toBe(0);
expect(result.migrationsRun).toBe(0);
});
it('runs migrations in the correct version range', () => {
// Create a marker file when the migration runs
createMigration(
'1.1.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-1.1.0'), 'done');
`,
);
createMigration(
'1.2.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-1.2.0'), 'done');
`,
);
// This one should NOT run (outside range)
createMigration(
'2.1.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-2.1.0'), 'done');
`,
);
const { stdout, exitCode } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(exitCode).toBe(0);
expect(result.migrationsRun).toBe(2);
expect(result.results[0].version).toBe('1.1.0');
expect(result.results[0].success).toBe(true);
expect(result.results[1].version).toBe('1.2.0');
expect(result.results[1].success).toBe(true);
// Verify the migrations actually ran
expect(fs.existsSync(path.join(tmpDir, 'migrated-1.1.0'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'migrated-1.2.0'))).toBe(true);
// 2.1.0 is outside range
expect(fs.existsSync(path.join(tmpDir, 'migrated-2.1.0'))).toBe(false);
});
it('excludes the from-version (only runs > from)', () => {
createMigration(
'1.0.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-1.0.0'), 'done');
`,
);
createMigration(
'1.1.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-1.1.0'), 'done');
`,
);
const { stdout } = runMigrations('1.0.0', '1.1.0');
const result = JSON.parse(stdout);
expect(result.migrationsRun).toBe(1);
expect(result.results[0].version).toBe('1.1.0');
// 1.0.0 should NOT have run
expect(fs.existsSync(path.join(tmpDir, 'migrated-1.0.0'))).toBe(false);
});
it('includes the to-version (<= to)', () => {
createMigration(
'2.0.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-2.0.0'), 'done');
`,
);
const { stdout } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(result.migrationsRun).toBe(1);
expect(result.results[0].version).toBe('2.0.0');
expect(result.results[0].success).toBe(true);
});
it('runs migrations in semver ascending order', () => {
// Create them in non-sorted order
for (const v of ['1.3.0', '1.1.0', '1.2.0']) {
createMigration(
v,
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
const log = path.join(root, 'migration-order.log');
const existing = fs.existsSync(log) ? fs.readFileSync(log, 'utf-8') : '';
fs.writeFileSync(log, existing + '${v}\\n');
`,
);
}
const { stdout } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(result.migrationsRun).toBe(3);
expect(result.results.map((r: any) => r.version)).toEqual([
'1.1.0',
'1.2.0',
'1.3.0',
]);
// Verify execution order from the log file
const log = fs.readFileSync(
path.join(tmpDir, 'migration-order.log'),
'utf-8',
);
expect(log.trim()).toBe('1.1.0\n1.2.0\n1.3.0');
});
it('reports failure and exits non-zero when a migration throws', () => {
createMigration(
'1.1.0',
`throw new Error('migration failed intentionally');`,
);
const { stdout, exitCode } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(exitCode).toBe(1);
expect(result.migrationsRun).toBe(1);
expect(result.results[0].success).toBe(false);
expect(result.results[0].error).toBeDefined();
});
it('ignores non-semver directories in migrations/', () => {
fs.mkdirSync(path.join(newCoreDir, 'migrations', 'README'), {
recursive: true,
});
fs.mkdirSync(path.join(newCoreDir, 'migrations', 'utils'), {
recursive: true,
});
createMigration(
'1.1.0',
`
import fs from 'fs';
import path from 'path';
const root = process.argv[2];
fs.writeFileSync(path.join(root, 'migrated-1.1.0'), 'done');
`,
);
const { stdout, exitCode } = runMigrations('1.0.0', '2.0.0');
const result = JSON.parse(stdout);
expect(exitCode).toBe(0);
expect(result.migrationsRun).toBe(1);
expect(result.results[0].version).toBe('1.1.0');
});
});