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']); }); }); });