|
3 | 3 | HookEvent, |
4 | 4 | HookSchema, |
5 | 5 | HookContextSchema, |
| 6 | + defineHook, |
6 | 7 | type Hook, |
7 | 8 | type HookContext, |
8 | 9 | } from './hook.zod'; |
@@ -754,3 +755,53 @@ describe('HookSchema - condition property', () => { |
754 | 755 | expect(hook.condition).toBeUndefined(); |
755 | 756 | }); |
756 | 757 | }); |
| 758 | + |
| 759 | +// ============================================================================ |
| 760 | +// defineHook factory (#4269) |
| 761 | +// ============================================================================ |
| 762 | + |
| 763 | +describe('defineHook (#4269)', () => { |
| 764 | + const config: Hook = { |
| 765 | + name: 'order_guard', |
| 766 | + object: 'order', |
| 767 | + events: ['beforeUpdate'], |
| 768 | + handler: 'guardStatus', |
| 769 | + condition: 'record.amount > 1000', |
| 770 | + }; |
| 771 | + |
| 772 | + it('drift guard: factory output IS the schema parse output', () => { |
| 773 | + // The factory must stay a pure `HookSchema.parse` — if it ever grows its |
| 774 | + // own normalization, the two authoring paths (convention scan vs |
| 775 | + // `defineStack({ hooks })` binding) fork into different artifact shapes. |
| 776 | + expect(defineHook(config)).toEqual(HookSchema.parse(config)); |
| 777 | + }); |
| 778 | + |
| 779 | + it('materializes defaults and CEL shorthand (input shape → resolved shape)', () => { |
| 780 | + const resolved = defineHook({ ...config, retryPolicy: {} }); |
| 781 | + expect(resolved.priority).toBe(100); |
| 782 | + expect(resolved.async).toBe(false); |
| 783 | + expect(resolved.onError).toBe('abort'); |
| 784 | + expect(resolved.retryPolicy).toEqual({ maxRetries: 3, backoffMs: 1000 }); |
| 785 | + expect(resolved.condition).toEqual({ dialect: 'cel', source: 'record.amount > 1000' }); |
| 786 | + }); |
| 787 | + |
| 788 | + it('passes an inline function handler through by reference', () => { |
| 789 | + const fn = async () => {}; |
| 790 | + const resolved = defineHook({ ...config, handler: fn }); |
| 791 | + expect(resolved.handler).toBe(fn); |
| 792 | + }); |
| 793 | + |
| 794 | + it('re-parsing factory output is idempotent (bind-time double validation is safe)', () => { |
| 795 | + const resolved = defineHook(config); |
| 796 | + expect(HookSchema.parse(resolved)).toEqual(resolved); |
| 797 | + }); |
| 798 | + |
| 799 | + it("hard-fails at authoring time with the schema's own guidance, not a second dialect", () => { |
| 800 | + expect(() => defineHook({ |
| 801 | + ...config, |
| 802 | + // @ts-expect-error — `enabled` is not a hook key (#4207 guidance) |
| 803 | + enabled: true, |
| 804 | + })).toThrow(/no on\/off switch/); |
| 805 | + expect(() => defineHook({ ...config, name: 'NotSnakeCase' })).toThrow(); |
| 806 | + }); |
| 807 | +}); |
0 commit comments