Skip to content

Commit 717a6fe

Browse files
committed
test: add pre/before hook tests for runtime
Add tests for pre-hook execution in buildContext phase: - pre hook runs and captures output - before hook as alias for pre - pre takes precedence over before - pre hook output prepended to body - post/after hooks stored for execution phase - hooks excluded from CLI args
1 parent d19c822 commit 717a6fe

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

src/runtime.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,106 @@ Hello`);
143143
expect(context.frontmatter).toEqual({});
144144
expect(context.rawBody).toBe("Just body");
145145
});
146+
147+
it("runs pre hook and captures output", async () => {
148+
const filePath = join(tempDir, "prehook.claude.md");
149+
await writeFile(filePath, `---
150+
pre: echo "pre-hook output"
151+
---
152+
Body content`);
153+
154+
const runtime = createRuntime();
155+
const resolved = await runtime.resolve(filePath);
156+
const context = await runtime.buildContext(resolved);
157+
158+
expect(context.preHookOutput).toBe("pre-hook output\n");
159+
});
160+
161+
it("runs before hook (alias for pre)", async () => {
162+
const filePath = join(tempDir, "beforehook.claude.md");
163+
await writeFile(filePath, `---
164+
before: echo "before-hook output"
165+
---
166+
Body content`);
167+
168+
const runtime = createRuntime();
169+
const resolved = await runtime.resolve(filePath);
170+
const context = await runtime.buildContext(resolved);
171+
172+
expect(context.preHookOutput).toBe("before-hook output\n");
173+
});
174+
175+
it("pre hook takes precedence over before", async () => {
176+
const filePath = join(tempDir, "prebefore.claude.md");
177+
await writeFile(filePath, `---
178+
pre: echo "pre wins"
179+
before: echo "before loses"
180+
---
181+
Body content`);
182+
183+
const runtime = createRuntime();
184+
const resolved = await runtime.resolve(filePath);
185+
const context = await runtime.buildContext(resolved);
186+
187+
expect(context.preHookOutput).toBe("pre wins\n");
188+
});
189+
190+
it("throws error when pre hook fails", async () => {
191+
const filePath = join(tempDir, "badhook.claude.md");
192+
await writeFile(filePath, `---
193+
pre: exit 1
194+
---
195+
Body content`);
196+
197+
const runtime = createRuntime();
198+
const resolved = await runtime.resolve(filePath);
199+
200+
await expect(runtime.buildContext(resolved)).rejects.toThrow("Pre hook failed");
201+
});
202+
203+
it("captures post hook command for later", async () => {
204+
const filePath = join(tempDir, "posthook.claude.md");
205+
await writeFile(filePath, `---
206+
post: echo "done"
207+
---
208+
Body content`);
209+
210+
const runtime = createRuntime();
211+
const resolved = await runtime.resolve(filePath);
212+
const context = await runtime.buildContext(resolved);
213+
214+
expect(context.postHookCommand).toBe("echo \"done\"");
215+
});
216+
217+
it("after hook is alias for post", async () => {
218+
const filePath = join(tempDir, "afterhook.claude.md");
219+
await writeFile(filePath, `---
220+
after: echo "after done"
221+
---
222+
Body content`);
223+
224+
const runtime = createRuntime();
225+
const resolved = await runtime.resolve(filePath);
226+
const context = await runtime.buildContext(resolved);
227+
228+
expect(context.postHookCommand).toBe("echo \"after done\"");
229+
});
230+
231+
it("pre hook uses env vars from frontmatter", async () => {
232+
const filePath = join(tempDir, "hookenv.claude.md");
233+
await writeFile(filePath, `---
234+
env:
235+
MY_VAR: hello
236+
pre: echo "$MY_VAR world"
237+
---
238+
Body`);
239+
240+
const runtime = createRuntime();
241+
const resolved = await runtime.resolve(filePath);
242+
const context = await runtime.buildContext(resolved);
243+
244+
expect(context.preHookOutput).toBe("hello world\n");
245+
});
146246
});
147247

148248
describe("Template Phase", () => {
@@ -324,6 +424,30 @@ Test prompt`);
324424
// Cleanup should not throw for local files
325425
await expect(runtime.cleanup()).resolves.toBeUndefined();
326426
});
427+
428+
it("prepends pre hook output to body in dry run", async () => {
429+
const filePath = join(tempDir, "preoutput.claude.md");
430+
await writeFile(filePath, `---
431+
pre: echo "HOOK OUTPUT"
432+
---
433+
Body content`);
434+
435+
const runtime = createRuntime();
436+
const resolved = await runtime.resolve(filePath);
437+
const context = await runtime.buildContext(resolved);
438+
const processed = await runtime.processTemplate(context);
439+
440+
// Verify pre-hook output is captured
441+
expect(context.preHookOutput).toBe("HOOK OUTPUT\n");
442+
443+
// Simulate what execute does with pre-hook output
444+
let finalBody = processed.body;
445+
if (context.preHookOutput) {
446+
finalBody = `${context.preHookOutput.trim()}\n\n${finalBody}`;
447+
}
448+
449+
expect(finalBody).toBe("HOOK OUTPUT\n\nBody content");
450+
});
327451
});
328452

329453
describe("createRuntime factory", () => {

0 commit comments

Comments
 (0)