Files
Elydora-Node-SDK/test/grok-plugin.test.mjs
T

304 lines
13 KiB
JavaScript

import assert from 'node:assert/strict';
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
assertManagedHandler,
cliPath,
createFixture,
legacyCommand,
managedHandler,
readGrokConfig,
registryModuleUrl,
runNode,
runPlugin,
} from '../test-support/grok-test-helpers.mjs';
async function assertMissing(filePath) {
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
}
function assertManagedTriple(config) {
assertManagedHandler(managedHandler(config, 'PreToolUse'));
assertManagedHandler(managedHandler(config, 'PostToolUse'));
assertManagedHandler(managedHandler(config, 'PostToolUseFailure'));
assert.equal(
managedHandler(config, 'PostToolUse').command,
managedHandler(config, 'PostToolUseFailure').command,
);
}
test('Grok Build is registered in the SDK and CLI', async () => {
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
assert.deepEqual(SUPPORTED_AGENTS.get('grok'), {
name: 'Grok Build',
configDir: '~/.grok/hooks',
configFile: 'elydora-audit.json',
});
const fixture = await createFixture();
try {
const result = await runNode(['--no-warnings', cliPath, 'status'], {
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
GROK_HOME: fixture.grokHome,
}, fixture.projectDir);
assert.equal(result.code, 0, result.stderr);
assert.match(result.stdout, /Grok Build \(grok\)/);
} finally {
await fixture.close();
}
});
test('Grok installs an exact managed triple and preserves valid user hooks', async () => {
const existing = {
schemaVersion: 1,
hooks: {
SessionStart: [{
hooks: [{ type: 'http', url: 'https://example.test/hook', timeout: 0 }],
label: 'keep group metadata',
}],
SessionEnd: [{
hooks: [{ type: 'command', command: 'session-end-command' }],
}],
Stop: [{
hooks: [{ type: 'command', command: 'stop-command' }],
}],
UserPromptSubmit: [{
hooks: [{ type: 'command', command: 'prompt-command' }],
}],
Notification: [{
matcher: 'permission_prompt|idle_prompt',
hooks: [{ type: 'command', command: 'notification-command' }],
}],
PreToolUse: [{
matcher: 'Bash|run_terminal_command',
hooks: [{ type: 'command', command: 'existing-command', timeout: 5 }],
}],
},
};
const source = `${JSON.stringify(existing, null, 2)}\n`;
const fixture = await createFixture({ config: source });
try {
const first = await fixture.install();
assert.equal(first.code, 0, first.stderr);
assert.match(first.stdout, /PostToolUseFailure hooks installed/i);
const firstDocument = await readGrokConfig(fixture.configPath);
assert.equal(firstDocument.config.schemaVersion, 1);
assert.deepEqual(firstDocument.config.hooks.SessionStart, existing.hooks.SessionStart);
assert.deepEqual(firstDocument.config.hooks.SessionEnd, existing.hooks.SessionEnd);
assert.deepEqual(firstDocument.config.hooks.Stop, existing.hooks.Stop);
assert.deepEqual(
firstDocument.config.hooks.UserPromptSubmit,
existing.hooks.UserPromptSubmit,
);
assert.deepEqual(firstDocument.config.hooks.Notification, existing.hooks.Notification);
assert.deepEqual(firstDocument.config.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]);
assert.equal(firstDocument.config.hooks.PreToolUse.length, 2);
assertManagedTriple(firstDocument.config);
const second = await fixture.install();
assert.equal(second.code, 0, second.stderr);
assert.equal(await readFile(fixture.configPath, 'utf-8'), firstDocument.raw);
await assertMissing(path.join(fixture.homeDir, '.grok', 'hooks', 'elydora-audit.json'));
await assertMissing(path.join(fixture.homeDir, '.claude', 'settings.json'));
await assertMissing(path.join(fixture.homeDir, '.cursor', 'hooks.json'));
} finally {
await fixture.close();
}
});
test('missing GROK_HOME uses the default and an empty override fails safely', async () => {
const fixture = await createFixture({ explicitGrokHome: false });
try {
assert.equal((await fixture.install()).code, 0);
fixture.grokHomeOverride = '';
const status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /GROK_HOME is empty.*unset.*absolute home/i);
const install = await fixture.install();
assert.equal(install.code, 1);
assert.match(install.stderr, /GROK_HOME is empty.*unset.*absolute home/i);
await assertMissing(path.join(fixture.projectDir, 'hooks', 'elydora-audit.json'));
} finally {
await fixture.close();
}
});
test('Grok parses the hook file before creating runtime files', async () => {
const fixture = await createFixture({ config: '{ malformed' });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, /parse Grok user hooks/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), '{ malformed');
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
test('Grok preserves an official null handler environment', async () => {
const existing = {
hooks: {
PreToolUse: [{
hooks: [{ type: 'command', command: 'existing-command', env: null }],
}],
},
};
const fixture = await createFixture({ config: JSON.stringify(existing) });
try {
const result = await fixture.install();
assert.equal(result.code, 0, result.stderr);
const { config } = await readGrokConfig(fixture.configPath);
assert.deepEqual(config.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]);
} finally {
await fixture.close();
}
});
test('Grok rejects inactive native hook shapes before writes', async (t) => {
const cases = [
['duplicate field', '{"hooks":{},"hooks":{}}', /duplicate field "hooks"/i],
['hooks shape', JSON.stringify({ hooks: null }), /field "hooks" must be an object/i],
['event shape', JSON.stringify({ hooks: { PreToolUse: null } }), /must be an array/i],
['group shape', JSON.stringify({ hooks: { PreToolUse: [null] } }), /group.*must be an object/i],
['matcher shape', JSON.stringify({ hooks: { PreToolUse: [{ matcher: 1, hooks: [] }] } }), /matcher must be a string/i],
['SessionStart matcher', JSON.stringify({ hooks: { SessionStart: [{ matcher: 'startup', hooks: [] }] } }), /cannot declare a matcher/i],
['SessionEnd matcher', JSON.stringify({ hooks: { SessionEnd: [{ matcher: 'logout', hooks: [] }] } }), /cannot declare a matcher/i],
['Stop matcher', JSON.stringify({ hooks: { Stop: [{ matcher: 'ignored', hooks: [] }] } }), /cannot declare a matcher/i],
['UserPromptSubmit matcher', JSON.stringify({ hooks: { UserPromptSubmit: [{ matcher: 'ignored', hooks: [] }] } }), /cannot declare a matcher/i],
['handler shape', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [null] }] } }), /handler.*must be an object/i],
['handler type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'file' }] }] } }), /unsupported type/i],
['command value', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: '' }] }] } }), /non-empty command/i],
['http value', JSON.stringify({ hooks: { PostToolUse: [{ hooks: [{ type: 'http', url: '' }] }] } }), /non-empty url/i],
['negative timeout', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: -1 }] }] } }), /non-negative integer/i],
['fraction timeout', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: 1.5 }] }] } }), /non-negative integer/i],
['environment shape', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', env: { A: 1 } }] }] } }), /env must map names to strings/i],
];
for (const [name, config, pattern] of cases) {
await t.test(name, async () => {
const fixture = await createFixture({ config });
try {
const result = await fixture.install();
assert.equal(result.code, 1);
assert.match(result.stderr, pattern);
assert.equal(await readFile(fixture.configPath, 'utf-8'), config);
await assertMissing(fixture.agentDir);
} finally {
await fixture.close();
}
});
}
});
test('Grok migrates exact legacy commands and preserves ownership lookalikes', async () => {
const fixture = await createFixture();
const lookalike = `${legacyCommand(fixture.guardScriptPath)} --inspect`;
const legacy = {
hooks: {
PreToolUse: [
{ hooks: [{ type: 'command', command: legacyCommand(fixture.guardScriptPath), timeout: 10 }] },
{ hooks: [{ type: 'command', command: lookalike, timeout: 10 }] },
],
PostToolUse: [{ hooks: [{
type: 'command',
command: legacyCommand(fixture.hookScriptPath),
timeout: 10,
}] }],
},
};
try {
await mkdir(path.dirname(fixture.configPath), { recursive: true });
await writeFile(fixture.configPath, JSON.stringify(legacy, null, 2));
const install = await fixture.install();
assert.equal(install.code, 0, install.stderr);
let { config } = await readGrokConfig(fixture.configPath);
assertManagedTriple(config);
assert(config.hooks.PreToolUse.some((group) => group.hooks[0].command === lookalike));
const managedGroup = config.hooks.PreToolUse.at(-1);
managedGroup.hooks.push({ type: 'command', command: 'user-command', timeout: 10 });
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
const uninstall = await runPlugin(fixture, 'uninstall', 'agent-1');
assert.equal(uninstall.code, 0, uninstall.stderr);
config = (await readGrokConfig(fixture.configPath)).config;
assert.deepEqual(config.hooks.PreToolUse.flatMap((group) => group.hooks), [
{ type: 'command', command: lookalike, timeout: 10 },
{ type: 'command', command: 'user-command', timeout: 10 },
]);
assert.equal(config.hooks.PostToolUse, undefined);
assert.equal(config.hooks.PostToolUseFailure, undefined);
} finally {
await fixture.close();
}
});
test('Grok uninstall preserves user config and removes a fully managed file', async () => {
const userSource = `${JSON.stringify({ owner: 'user', hooks: { Notification: [] } }, null, 2)}\n`;
const userFixture = await createFixture({ config: userSource });
try {
assert.equal((await userFixture.install()).code, 0);
const result = await runPlugin(userFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
assert.deepEqual((await readGrokConfig(userFixture.configPath)).config, {
owner: 'user',
hooks: { Notification: [] },
});
} finally {
await userFixture.close();
}
const managedFixture = await createFixture();
try {
assert.equal((await managedFixture.install()).code, 0);
const result = await runPlugin(managedFixture, 'uninstall', 'agent-1');
assert.equal(result.code, 0, result.stderr);
await assertMissing(managedFixture.configPath);
} finally {
await managedFixture.close();
}
});
test('Grok status requires one complete triple, exact identity, and private key', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
let status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, true);
let { config } = await readGrokConfig(fixture.configPath);
delete config.hooks.PostToolUseFailure;
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
config = (await readGrokConfig(fixture.configPath)).config;
config.hooks.PostToolUseFailure.push(config.hooks.PostToolUseFailure.at(-1));
await writeFile(fixture.configPath, JSON.stringify(config, null, 2));
status = await runPlugin(fixture, 'status', null);
assert.equal(JSON.parse(status.stdout).installed, false);
assert.equal((await fixture.install()).code, 0);
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
status = await runPlugin(fixture, 'status', null);
assert.equal(status.code, 1);
assert.match(status.stderr, /private key is invalid/i);
} finally {
await fixture.close();
}
});
test('Grok installation leaves no transaction files', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
for (const directory of [fixture.agentDir, path.dirname(fixture.configPath)]) {
const names = await readdir(directory);
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
} finally {
await fixture.close();
}
});