Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/__tests__/zcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,39 @@ describe('ZCode support', () => {
}
});

it('honors an explicitly configured timeout over the per-event ZCode default', async () => {
const home = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-zcode-test-'));
try {
const configPath = path.join(home, '.zcode', 'cli', 'config.json');
const manifestPath = path.join(home, 'managed-hooks.json');
const teamDef: HookDef = {
source: 'team',
key: 'slow-sync',
event: 'Stop',
command: 'slow-team-sync',
timeout: 300,
description: '[teamai:hook:slow-sync] slow team sync',
};

await reconcileHooks(configPath, 'zcode', [teamDef], {
manifestPath,
builtinOverride: { overrides: { 'Hook dispatch stop': { timeout: 240 } } },
});

const cfg = await fse.readJson(configPath);
const stop = cfg.hooks.events.Stop as Array<{ hooks: Array<{ args?: string[]; timeoutMs?: number }> }>;
const team = stop.find((g) => g.hooks[0].args?.[1] === 'slow-team-sync');
const builtin = stop.find((g) => g.hooks[0].args?.[1]?.includes('hook-dispatch stop'));

// hooks.yaml states seconds; the entry is written in milliseconds.
expect(team?.hooks[0].timeoutMs).toBe(300_000);
// A team `builtin.overrides.<key>.timeout` must reach ZCode too.
expect(builtin?.hooks[0].timeoutMs).toBe(240_000);
} finally {
await fse.remove(home);
}
});

it('removeAll preserves a user-disabled hooks.enabled while stripping entries', async () => {
const home = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-zcode-test-'));
try {
Expand Down
10 changes: 8 additions & 2 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ function toZcodeEntry(def: HookDef): ZcodeHookMatcher {
PostToolUse: 30000,
UserPromptSubmit: 60000,
};
// The table is ZCode's DEFAULT, not an override: a timeout the team stated in
// hooks.yaml (per-hook `timeout`, or `builtin.overrides.<key>.timeout`) is the
// one the user asked for and still wins, as it does on every other tool.
// `def.timeout` is in seconds; ZCode entries are in milliseconds.
const timeoutMs =
def.timeout !== undefined ? def.timeout * 1000 : ZCODE_TIMEOUT_MS[def.event] ?? 60000;
const entry: ZcodeHookEntry =
process.platform === 'win32'
? {
Expand All @@ -325,7 +331,7 @@ function toZcodeEntry(def: HookDef): ZcodeHookMatcher {
type: 'process',
command: 'cmd',
args: ['/c', def.command],
timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 60000,
timeoutMs,
}
: {
type: 'process',
Expand All @@ -334,7 +340,7 @@ function toZcodeEntry(def: HookDef): ZcodeHookMatcher {
// so managed-entry detection and the managed-hooks manifest share one
// command representation (the same invariant the Codex format keeps).
args: ['-lc', def.command],
timeoutMs: ZCODE_TIMEOUT_MS[def.event] ?? 60000,
timeoutMs,
};
const group: ZcodeHookMatcher = { hooks: [entry] };
// ZCode's matcher is a case-sensitive regex on the match value; '*' is an
Expand Down
Loading