Skip to content

Commit 70a6df2

Browse files
committed
test(objectql): update the two condition-gate pins fail loud invalidates (#4775)
`hook-binder.test.ts` pinned the OLD uncompilable behaviour and explicitly accepted either outcome ("ignored at compile time (handler runs) or evaluated false (skipped) … just assert we didn't crash"). That latitude was the defect: "condition ignored" deleted the gate, so a hook declared to run conditionally ran on every write. It now asserts the rejection, the `uncompilable` reason and that the handler never ran. `hook-metrics.test.ts` used a BARE `name == "skipme"` condition. Hook conditions are `record`-scoped, so the bare identifier resolved to nothing and the expression faulted on every call — the "skip" the test asserted came from the old swallow (fault → warn → false), not from a condition that answered NO. This is precisely the never-actually-enforced condition #4775 was expected to flush out. Rewritten as `record.name == "skipme"`, which expresses the same intent and is actually evaluated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
1 parent dd7a961 commit 70a6df2

2 files changed

Lines changed: 28 additions & 10 deletions

File tree

packages/objectql/src/hook-binder.test.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { describe, it, expect, vi } from 'vitest';
44
import { ObjectQL } from './engine.js';
55
import { bindHooksToEngine } from './hook-binder.js';
6-
import { wrapDeclarativeHook } from './hook-wrappers.js';
6+
import { wrapDeclarativeHook, HookConditionError } from './hook-wrappers.js';
77
import type { Hook, HookContext } from '@objectstack/spec/data';
88

99
function makeEngine() {
@@ -377,21 +377,33 @@ describe('wrapDeclarativeHook', () => {
377377
expect(calls).toEqual(['done']); // awaited despite async=true
378378
});
379379

380-
it('logs and treats invalid condition formulas as skipping', async () => {
381-
const warn = vi.fn();
380+
it('rejects the operation when the condition formula does not compile', async () => {
381+
// [#4775] This test used to accept EITHER outcome ("ignored at compile time
382+
// (handler runs) or evaluated false (skipped) … just assert we didn't
383+
// crash"). That latitude was the defect: "condition ignored" DELETED the
384+
// gate, so a hook declared to run conditionally ran on every write, and the
385+
// only trace was a `warn`. A condition that cannot compile can never be
386+
// evaluated, so the hook can neither run nor be skipped honestly — the
387+
// operation is rejected instead, naming the hook.
388+
const error = vi.fn();
382389
const calls: string[] = [];
383390
const meta: Hook = {
384391
name: 'badcond', object: 'a', events: ['beforeInsert'], priority: 100,
385392
condition: '(((not valid syntax',
386393
handler: () => { calls.push('ran'); },
387394
};
388395
const wrapped = wrapDeclarativeHook(meta, meta.handler as any, {
389-
logger: { debug: () => {}, info: () => {}, warn, error: () => {} },
396+
logger: { debug: () => {}, info: () => {}, warn: () => {}, error },
390397
});
391-
await wrapped(makeCtx());
392-
expect(warn).toHaveBeenCalled();
393-
// Either ignored at compile time (handler runs) or evaluated false
394-
// (skipped). Both are valid; just assert we didn't crash.
395-
expect(calls.length === 0 || calls[0] === 'ran').toBe(true);
398+
399+
const err = await wrapped(makeCtx()).then(() => null, (e) => e);
400+
401+
expect(err).toBeInstanceOf(HookConditionError);
402+
expect(err.reason).toBe('uncompilable');
403+
expect(err.message).toContain("Hook 'badcond'");
404+
expect(calls).toEqual([]);
405+
// Reported at bind time too, at error level — an operator sees the broken
406+
// hook before the first write trips over it.
407+
expect(error).toHaveBeenCalled();
396408
});
397409
});

packages/objectql/src/hook-metrics.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ describe('hook metrics', () => {
8282
object: 'account',
8383
events: ['beforeInsert'],
8484
priority: 100,
85-
condition: 'name == "skipme"',
85+
// [#4775] Was a bare `name == "skipme"`. Hook conditions are
86+
// `record`-scoped, so the bare identifier resolved to nothing and
87+
// the expression FAULTED on every call — the "skip" this test
88+
// asserted came from the old swallow (fault → warn → false), not
89+
// from a condition that answered NO. `record.name` is the same
90+
// intent, actually evaluated: 'acme' != 'skipme' ⇒ FALSE ⇒ skip.
91+
condition: 'record.name == "skipme"',
8692
handler: async () => { /* noop */ },
8793
};
8894
bindHooksToEngine(engine, [hook], { packageId: 'p', metrics });

0 commit comments

Comments
 (0)