import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtemp, rm, readFile, writeFile, mkdir } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { installManagedSessionHook, removeManagedSessionHook, MARKER_KEY } from '../../src/utils/sessionhook.js'; describe('sessionhook', () => { let tmp: string; beforeEach(async () => { tmp = await mkdtemp(join(tmpdir(), 'mcpctl-sessionhook-')); }); afterEach(async () => { await rm(tmp, { recursive: true, force: true }); }); it('creates settings.json from scratch when missing', async () => { const path = join(tmp, 'settings.json'); const result = await installManagedSessionHook('mcpctl skills sync --quiet', path); expect(result.updated).toBe(true); const settings = JSON.parse(await readFile(path, 'utf-8')); expect(settings.hooks.SessionStart).toHaveLength(1); const entry = settings.hooks.SessionStart[0].hooks[0]; expect(entry.command).toBe('mcpctl skills sync --quiet'); expect(entry[MARKER_KEY]).toBe(true); }); it('is idempotent — re-running does not add duplicates', async () => { const path = join(tmp, 'settings.json'); await installManagedSessionHook('mcpctl skills sync --quiet', path); const second = await installManagedSessionHook('mcpctl skills sync --quiet', path); expect(second.updated).toBe(false); const settings = JSON.parse(await readFile(path, 'utf-8')); const entries = settings.hooks.SessionStart.flatMap((g: { hooks: unknown[] }) => g.hooks); const managed = entries.filter((e: Record) => e[MARKER_KEY] === true); expect(managed).toHaveLength(1); }); it('updates the command in place when it changes', async () => { const path = join(tmp, 'settings.json'); await installManagedSessionHook('mcpctl skills sync', path); const updated = await installManagedSessionHook('mcpctl skills sync --quiet', path); expect(updated.updated).toBe(true); const settings = JSON.parse(await readFile(path, 'utf-8')); const managed = settings.hooks.SessionStart .flatMap((g: { hooks: unknown[] }) => g.hooks) .find((e: Record) => e[MARKER_KEY] === true); expect(managed.command).toBe('mcpctl skills sync --quiet'); }); it('preserves non-managed hooks', async () => { const path = join(tmp, 'settings.json'); await mkdir(tmp, { recursive: true }); await writeFile(path, JSON.stringify({ hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'echo user-hook' }] }], }, })); await installManagedSessionHook('mcpctl skills sync --quiet', path); const settings = JSON.parse(await readFile(path, 'utf-8')); const all = settings.hooks.SessionStart.flatMap((g: { hooks: unknown[] }) => g.hooks); expect(all).toHaveLength(2); expect(all.find((e: Record) => e.command === 'echo user-hook')).toBeDefined(); expect(all.find((e: Record) => e[MARKER_KEY] === true)).toBeDefined(); }); it('remove drops the managed entry but keeps user hooks', async () => { const path = join(tmp, 'settings.json'); await writeFile(path, JSON.stringify({ hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'echo user' }] }], }, })); await installManagedSessionHook('mcpctl skills sync --quiet', path); const removed = await removeManagedSessionHook(path); expect(removed.removed).toBe(true); const settings = JSON.parse(await readFile(path, 'utf-8')); const all = settings.hooks.SessionStart.flatMap((g: { hooks: unknown[] }) => g.hooks); expect(all).toHaveLength(1); expect(all[0].command).toBe('echo user'); }); it('remove is a no-op when no managed entry exists', async () => { const path = join(tmp, 'settings.json'); const result = await removeManagedSessionHook(path); expect(result.removed).toBe(false); }); it('survives empty settings.json', async () => { const path = join(tmp, 'settings.json'); await writeFile(path, ''); await installManagedSessionHook('mcpctl skills sync --quiet', path); const settings = JSON.parse(await readFile(path, 'utf-8')); expect(settings.hooks.SessionStart).toHaveLength(1); }); it('strips line comments before parsing', async () => { const path = join(tmp, 'settings.json'); await writeFile(path, '// a leading comment\n{\n "hooks": {}\n}\n'); await installManagedSessionHook('mcpctl skills sync --quiet', path); const settings = JSON.parse(await readFile(path, 'utf-8')); expect(settings.hooks.SessionStart).toHaveLength(1); }); });