107 lines
4.6 KiB
TypeScript
107 lines
4.6 KiB
TypeScript
|
|
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<string, unknown>) => 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<string, unknown>) => 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<string, unknown>) => e.command === 'echo user-hook')).toBeDefined();
|
||
|
|
expect(all.find((e: Record<string, unknown>) => 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);
|
||
|
|
});
|
||
|
|
});
|