Files
ponytail/tests/hooks.test.js
T
DietrichGebertandClaude Opus 4.8 248a30b40b test: simplify hooks temp-dir cleanup to a one-line exit handler (#221)
#213 guarded cleanup with a flag + named function + process.once. But
fs.rmSync with force:true already no-ops on a missing path, so the guard
and the explicit end-of-file call are unnecessary. Collapse to a single
process.on('exit') handler.

Refs #204

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:29:09 +02:00

162 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const root = path.join(__dirname, '..');
function run(script, env, input = '') {
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
env: { ...process.env, ...env },
input,
encoding: 'utf8',
});
}
// Keep the base env clean so the default-dir checks are deterministic; the
// CLAUDE_CONFIG_DIR case sets it explicitly.
delete process.env.CLAUDE_CONFIG_DIR;
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
// Runs on normal exit and on assertion-throw exit; force makes it idempotent.
process.on('exit', () => fs.rmSync(temp, { recursive: true, force: true }));
const home = path.join(temp, 'home');
const pluginData = path.join(temp, 'plugin-data');
fs.mkdirSync(home, { recursive: true });
// USERPROFILE alongside HOME: os.homedir() reads USERPROFILE on Windows, HOME on POSIX.
const codexEnv = {
HOME: home,
USERPROFILE: home,
PLUGIN_DATA: pluginData,
PONYTAIL_DEFAULT_MODE: 'ultra',
};
const codexState = path.join(pluginData, '.ponytail-active');
let result = run('ponytail-activate.js', codexEnv);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(codexState, 'utf8'), 'ultra');
let output = JSON.parse(result.stdout);
assert.equal(output.systemMessage, 'PONYTAIL:ULTRA');
assert.match(
output.hookSpecificOutput.additionalContext,
/PONYTAIL MODE ACTIVE — level: ultra/,
);
result = run(
'ponytail-mode-tracker.js',
codexEnv,
JSON.stringify({ prompt: '@ponytail lite' }),
);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite');
output = JSON.parse(result.stdout);
assert.equal(output.systemMessage, 'PONYTAIL:LITE');
result = run(
'ponytail-mode-tracker.js',
codexEnv,
JSON.stringify({ prompt: 'normal mode' }),
);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.existsSync(codexState), false);
output = JSON.parse(result.stdout);
assert.equal(output.systemMessage, 'PONYTAIL:OFF');
// A request that merely mentions "normal mode" must not deactivate ponytail.
result = run('ponytail-mode-tracker.js', codexEnv, JSON.stringify({ prompt: '@ponytail lite' }));
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite');
result = run(
'ponytail-mode-tracker.js',
codexEnv,
JSON.stringify({ prompt: 'add a normal mode toggle next to dark mode' }),
);
assert.equal(result.status, 0, result.stderr);
assert.equal(
fs.readFileSync(codexState, 'utf8'),
'lite',
'incidental "normal mode" in a request must not turn ponytail off',
);
const claudeEnv = {
HOME: home,
USERPROFILE: home,
PONYTAIL_DEFAULT_MODE: 'full',
};
delete claudeEnv.PLUGIN_DATA;
result = run('ponytail-activate.js', claudeEnv);
assert.equal(result.status, 0, result.stderr);
assert.equal(
fs.readFileSync(path.join(home, '.claude', '.ponytail-active'), 'utf8'),
'full',
);
// CLAUDE_CONFIG_DIR overrides ~/.claude for the flag file (issue #34).
const home2 = path.join(temp, 'home2');
fs.mkdirSync(home2, { recursive: true });
const customConfigDir = path.join(temp, 'custom-claude');
result = run('ponytail-activate.js', {
HOME: home2,
USERPROFILE: home2,
CLAUDE_CONFIG_DIR: customConfigDir,
PONYTAIL_DEFAULT_MODE: 'lite',
});
assert.equal(result.status, 0, result.stderr);
assert.equal(
fs.readFileSync(path.join(customConfigDir, '.ponytail-active'), 'utf8'),
'lite',
);
assert.equal(
fs.existsSync(path.join(home2, '.claude', '.ponytail-active')),
false,
'flag must not land in ~/.claude when CLAUDE_CONFIG_DIR is set',
);
const copilotData = path.join(temp, 'copilot-data');
const codexData = path.join(temp, 'codex-data-shadow');
result = run('ponytail-activate.js', {
HOME: home,
USERPROFILE: home,
COPILOT_PLUGIN_DATA: copilotData,
PLUGIN_DATA: codexData,
PONYTAIL_DEFAULT_MODE: 'full',
});
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(copilotData, '.ponytail-active'), 'utf8'), 'full');
assert.equal(
fs.existsSync(path.join(codexData, '.ponytail-active')),
false,
'copilot hooks must not write mode state to codex PLUGIN_DATA',
);
output = JSON.parse(result.stdout);
assert.match(output.additionalContext, /PONYTAIL MODE ACTIVE — level: full/);
result = run(
'ponytail-mode-tracker.js',
{
HOME: home,
USERPROFILE: home,
COPILOT_PLUGIN_DATA: copilotData,
PLUGIN_DATA: codexData,
},
JSON.stringify({ prompt: '/ponytail ultra' }),
);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(copilotData, '.ponytail-active'), 'utf8'), 'ultra');
assert.equal(
fs.existsSync(path.join(codexData, '.ponytail-active')),
false,
'copilot mode tracker must keep codex PLUGIN_DATA untouched',
);
output = JSON.parse(result.stdout);
assert.deepEqual(output, {});
console.log('hook compatibility checks passed');