Files
nanoclaw/skills-engine/__tests__/customize.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

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();
});
});