108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
|
||
|
|
import {
|
||
|
|
loadState,
|
||
|
|
saveState,
|
||
|
|
emptyState,
|
||
|
|
sha256Of,
|
||
|
|
hasFileBeenModified,
|
||
|
|
detectModifiedFiles,
|
||
|
|
type FileState,
|
||
|
|
} from '../../src/utils/skills-state.js';
|
||
|
|
|
||
|
|
describe('skills-state', () => {
|
||
|
|
let tmp: string;
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
tmp = await mkdtemp(join(tmpdir(), 'mcpctl-skills-state-'));
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await rm(tmp, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('sha256Of', () => {
|
||
|
|
it('is deterministic and prefixed', () => {
|
||
|
|
expect(sha256Of('hello')).toMatch(/^sha256:[0-9a-f]{64}$/);
|
||
|
|
expect(sha256Of('hello')).toBe(sha256Of('hello'));
|
||
|
|
expect(sha256Of('hello')).not.toBe(sha256Of('hellp'));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('load / save', () => {
|
||
|
|
it('returns empty state when file does not exist', async () => {
|
||
|
|
const state = await loadState(join(tmp, 'no-such.json'));
|
||
|
|
expect(state.skills).toEqual({});
|
||
|
|
expect(state.lastSync).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('round-trips state', async () => {
|
||
|
|
const path = join(tmp, 'state.json');
|
||
|
|
const state = emptyState();
|
||
|
|
state.lastSync = '2026-05-07T00:00:00.000Z';
|
||
|
|
state.lastSyncProject = 'demo';
|
||
|
|
state.skills['my-skill'] = {
|
||
|
|
id: 'cuid-x',
|
||
|
|
semver: '0.1.0',
|
||
|
|
contentHash: sha256Of('body'),
|
||
|
|
scope: 'global',
|
||
|
|
installDir: '/tmp/foo',
|
||
|
|
files: { 'SKILL.md': { sha256: sha256Of('hi'), size: 2 } },
|
||
|
|
postInstallHash: null,
|
||
|
|
lastSyncedAt: '2026-05-07T00:00:00.000Z',
|
||
|
|
};
|
||
|
|
await saveState(state, path);
|
||
|
|
const loaded = await loadState(path);
|
||
|
|
expect(loaded).toEqual(state);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('starts fresh on schema-version drift', async () => {
|
||
|
|
const path = join(tmp, 'state.json');
|
||
|
|
await writeFile(path, JSON.stringify({ schemaVersion: 99, skills: { x: {} } }));
|
||
|
|
const state = await loadState(path);
|
||
|
|
expect(state.schemaVersion).toBe(1);
|
||
|
|
expect(state.skills).toEqual({});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('hasFileBeenModified', () => {
|
||
|
|
it('false when content matches recorded hash + size', async () => {
|
||
|
|
const dir = join(tmp, 'sk');
|
||
|
|
await mkdir(dir);
|
||
|
|
await writeFile(join(dir, 'SKILL.md'), 'hello');
|
||
|
|
const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
|
||
|
|
expect(await hasFileBeenModified(dir, 'SKILL.md', recorded)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('true when content differs', async () => {
|
||
|
|
const dir = join(tmp, 'sk');
|
||
|
|
await mkdir(dir);
|
||
|
|
await writeFile(join(dir, 'SKILL.md'), 'edited');
|
||
|
|
const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
|
||
|
|
expect(await hasFileBeenModified(dir, 'SKILL.md', recorded)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('true when file is missing', async () => {
|
||
|
|
const recorded: FileState = { sha256: sha256Of('hello'), size: 5 };
|
||
|
|
expect(await hasFileBeenModified(tmp, 'missing.md', recorded)).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('detectModifiedFiles', () => {
|
||
|
|
it('returns the list of edited paths', async () => {
|
||
|
|
const dir = join(tmp, 'sk');
|
||
|
|
await mkdir(dir);
|
||
|
|
await writeFile(join(dir, 'SKILL.md'), 'pristine');
|
||
|
|
await writeFile(join(dir, 'extra.md'), 'edited');
|
||
|
|
const result = await detectModifiedFiles(dir, {
|
||
|
|
'SKILL.md': { sha256: sha256Of('pristine'), size: 8 },
|
||
|
|
'extra.md': { sha256: sha256Of('original'), size: 8 },
|
||
|
|
});
|
||
|
|
expect(result).toEqual(['extra.md']);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|