fix(node): mirror Claude async rewake metadata

This commit is contained in:
ZacharyZhang-NY committed 2026-07-20 08:53:23 -04:00
1 parent 2a4e62f0a3
commit 45f6ee076b
4 files changed
+46 -3

No files matched your search

+17 -1
View File
@@ -22,7 +22,17 @@ const HOOK_EVENTS = new Set([
const COMMON_HANDLER_KEYS = ['if', 'once', 'statusMessage', 'timeout'];
const HANDLER_KEYS: Readonly<Record<string, readonly string[]>> = {
command: [...COMMON_HANDLER_KEYS, 'args', 'async', 'asyncRewake', 'command', 'shell', 'type'],
command: [
...COMMON_HANDLER_KEYS,
'args',
'async',
'asyncRewake',
'command',
'rewakeMessage',
'rewakeSummary',
'shell',
'type',
],
prompt: [...COMMON_HANDLER_KEYS, 'continueOnBlock', 'model', 'prompt', 'type'],
agent: [...COMMON_HANDLER_KEYS, 'model', 'prompt', 'type'],
http: [...COMMON_HANDLER_KEYS, 'allowedEnvVars', 'headers', 'type', 'url'],
@@ -99,6 +109,10 @@ function optionalString(value: JsonObject, field: string, label: string): void {
}
}
function optionalNonEmptyString(value: JsonObject, field: string, label: string): void {
if (value[field] !== undefined) requireNonEmptyString(value[field], field, label);
}
function optionalBoolean(value: JsonObject, field: string, label: string): void {
if (value[field] !== undefined && typeof value[field] !== 'boolean') {
throw new Error(`${label} field "${field}" must be a boolean`);
@@ -138,6 +152,8 @@ function validateHandler(value: unknown, event: string, group: number, index: nu
if (value.args !== undefined) validateStringArray(value.args, 'args', label);
optionalBoolean(value, 'async', label);
optionalBoolean(value, 'asyncRewake', label);
optionalNonEmptyString(value, 'rewakeMessage', label);
optionalNonEmptyString(value, 'rewakeSummary', label);
if (value.shell !== undefined && value.shell !== 'bash' && value.shell !== 'powershell') {
throw new Error(`${label} field "shell" must be "bash" or "powershell"`);
}
+16 -1
View File
@@ -10,7 +10,22 @@ const claudeBinary = process.env.ELYDORA_CLAUDE_BINARY;
test('official Claude Code accepts the installed user hook contract', {
skip: claudeBinary ? false : 'set ELYDORA_CLAUDE_BINARY to the official Claude Code executable',
}, async () => {
const fixture = await createFixture();
const fixture = await createFixture({
settings: {
hooks: {
Stop: [{
hooks: [{
type: 'command',
command: process.execPath,
args: ['--version'],
asyncRewake: true,
rewakeMessage: 'Background validation failed',
rewakeSummary: 'Validation feedback',
}],
}],
},
},
});
try {
assert.equal((await fixture.install()).code, 0);
const environment = {
+12
View File
@@ -76,6 +76,15 @@ test('Claude installs one exact managed triple and preserves user settings', asy
matcher: 'Bash',
hooks: [{ type: 'command', command: 'existing-command', timeout: 5 }],
}],
Stop: [{
hooks: [{
type: 'command',
command: 'background-check',
asyncRewake: true,
rewakeMessage: 'Background validation failed',
rewakeSummary: 'Validation feedback',
}],
}],
},
};
const fixture = await createFixture({ settings: existing });
@@ -87,6 +96,7 @@ test('Claude installs one exact managed triple and preserves user settings', asy
assert.equal(installed.settings.model, 'sonnet');
assert.deepEqual(installed.settings.hooks.Notification, existing.hooks.Notification);
assert.deepEqual(installed.settings.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]);
assert.deepEqual(installed.settings.hooks.Stop, existing.hooks.Stop);
assertManagedTriple(installed.settings, fixture);
const second = await fixture.install();
@@ -166,6 +176,8 @@ test('Claude rejects invalid official hook shapes before writes', async (t) => {
['command value', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: '' }] }] } }), /non-empty string/i],
['argument type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', args: [1] }] }] } }), /array of strings/i],
['timeout zero', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: 0 }] }] } }), /positive finite number/i],
['empty rewake message', JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'x', rewakeMessage: '' }] }] } }), /rewakeMessage.*non-empty string/i],
['empty rewake summary', JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'x', rewakeSummary: '' }] }] } }), /rewakeSummary.*non-empty string/i],
['headers type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'http', url: 'https://example.test', headers: { A: 1 } }] }] } }), /map names to strings/i],
['input type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'mcp_tool', server: 's', tool: 't', input: [] }] }] } }), /input.*must be an object/i],
];