* 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>
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import {
|
|
isCustomizeActive,
|
|
startCustomize,
|
|
commitCustomize,
|
|
abortCustomize,
|
|
} from '../customize.js';
|
|
import { CUSTOM_DIR } from '../constants.js';
|
|
import {
|
|
createTempDir,
|
|
setupNanoclawDir,
|
|
createMinimalState,
|
|
cleanup,
|
|
writeState,
|
|
} from './test-helpers.js';
|
|
import {
|
|
readState,
|
|
recordSkillApplication,
|
|
computeFileHash,
|
|
} from '../state.js';
|
|
|
|
describe('customize', () => {
|
|
let tmpDir: string;
|
|
const originalCwd = process.cwd();
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir();
|
|
setupNanoclawDir(tmpDir);
|
|
createMinimalState(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, CUSTOM_DIR), { recursive: true });
|
|
process.chdir(tmpDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(originalCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
it('startCustomize creates pending.yaml and isCustomizeActive returns true', () => {
|
|
// Need at least one applied skill with file_hashes for snapshot
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
expect(isCustomizeActive()).toBe(false);
|
|
startCustomize('test customization');
|
|
expect(isCustomizeActive()).toBe(true);
|
|
|
|
const pendingPath = path.join(tmpDir, CUSTOM_DIR, 'pending.yaml');
|
|
expect(fs.existsSync(pendingPath)).toBe(true);
|
|
});
|
|
|
|
it('abortCustomize removes pending.yaml', () => {
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
startCustomize('test');
|
|
expect(isCustomizeActive()).toBe(true);
|
|
|
|
abortCustomize();
|
|
expect(isCustomizeActive()).toBe(false);
|
|
});
|
|
|
|
it('commitCustomize with no changes clears pending', () => {
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
startCustomize('no-op');
|
|
commitCustomize();
|
|
|
|
expect(isCustomizeActive()).toBe(false);
|
|
});
|
|
|
|
it('commitCustomize with changes creates patch and records in state', () => {
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
startCustomize('add feature');
|
|
|
|
// Modify the tracked file
|
|
fs.writeFileSync(trackedFile, 'export const x = 2;\nexport const y = 3;');
|
|
|
|
commitCustomize();
|
|
|
|
expect(isCustomizeActive()).toBe(false);
|
|
const state = readState();
|
|
expect(state.custom_modifications).toBeDefined();
|
|
expect(state.custom_modifications!.length).toBeGreaterThan(0);
|
|
expect(state.custom_modifications![0].description).toBe('add feature');
|
|
});
|
|
|
|
it('commitCustomize throws descriptive error on diff failure', () => {
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
startCustomize('diff-error test');
|
|
|
|
// Modify the tracked file
|
|
fs.writeFileSync(trackedFile, 'export const x = 2;');
|
|
|
|
// Make the base file a directory to cause diff to exit with code 2
|
|
const baseFilePath = path.join(
|
|
tmpDir,
|
|
'.nanoclaw',
|
|
'base',
|
|
'src',
|
|
'app.ts',
|
|
);
|
|
fs.mkdirSync(baseFilePath, { recursive: true });
|
|
|
|
expect(() => commitCustomize()).toThrow(/diff error/i);
|
|
});
|
|
|
|
it('startCustomize while active throws', () => {
|
|
const trackedFile = path.join(tmpDir, 'src', 'app.ts');
|
|
fs.mkdirSync(path.dirname(trackedFile), { recursive: true });
|
|
fs.writeFileSync(trackedFile, 'export const x = 1;');
|
|
recordSkillApplication('test-skill', '1.0.0', {
|
|
'src/app.ts': computeFileHash(trackedFile),
|
|
});
|
|
|
|
startCustomize('first');
|
|
expect(() => startCustomize('second')).toThrow();
|
|
});
|
|
});
|