Skip to content

Commit 510a4bd

Browse files
test(objectql): pin the temporal-comparand door fixture's clock with fake timers (#8937) (#8939)
* test(objectql): de-calendar the temporal-comparand door fixture (#8937) The [#8690] door suite pinned its clock at 2026-08-15T09:00:00.000Z, threaded it as `{ context: { now } }`, and asserted the `{30_days_ago}` floor equalled that instant minus 30 days. The assertion held only while the real date and the fixture date agreed: at 2026-08-16T00:00Z it went red on every branch at once, with no code change, blocking the merge queue for everyone. The clock it threaded does not exist. `now` is declared neither on `ExecutionContextSchema` (@objectstack/spec) nor on `ExecutionContextLike` (@objectstack/core), and nothing on the engine read path reads it -- `filterTokenContextFrom` takes an explicit `now` argument the engine never passes, so `resolveFilterTokens` falls back to the process clock. The `as never` casts on those calls were the tell. Every temporal expectation is now derived rather than written down: the fixture seeds from the real clock (38-in / 13-out holds at any wall time, with two days of margin), and the floor is compared against what the platform's own resolver yields, bracketing the engine call so a UTC-midnight crossing is absorbed exactly rather than tolerated. Two added cases keep that comparison honest: one shows the resolver is genuinely clock-sensitive (so agreement with it is a real statement, not two constants matching), and one records as a tested fact that an injected `context.now` is inert today. Whether the engine SHOULD expose a declared, injectable clock is #8937's remaining half -- a public-contract question left open here, not assumed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt * test(objectql): pin the clock with fake timers, matching sibling suites (#8937) Supersedes the derive-from-real-clock approach in the previous commit with this package's established convention for pinning a clock in an engine test: `vi.useFakeTimers({ toFake: ['Date'] })` + `vi.setSystemTime(PINNED_NOW)`, as used by engine-cel-default-temporal-shape and the three engine-autonumber-* suites. `toFake: ['Date']` keeps real timers, so the engine's async paths are untouched. This is strictly better here. The engine resolves `{30_days_ago}` against the PROCESS clock, so pinning that clock makes the original readable assertion (floor === '2026-07-16') deterministic forever instead of replacing it with a bracketed comparison against a resolver-derived value. The written dates are honest again: they are what the code under test actually sees. The two companion cases are kept and simplified against the pinned clock: one shows the resolver genuinely tracks the instant it is handed (so the pin is not two constants agreeing), and one records as a tested fact that an injected `context.now` is inert today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 716ac9b commit 510a4bd

1 file changed

Lines changed: 113 additions & 3 deletions

File tree

‎packages/objectql/src/engine-temporal-comparand-door.test.ts‎

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,53 @@
1212
* refusal pin with no positive control cannot show the gate is discriminating
1313
* rather than refusing everything, and the two drifting apart into separate
1414
* cases is how that guarantee gets lost.
15+
*
16+
* ## [#8937] The clock is pinned at the PROCESS clock, not through the context
17+
*
18+
* This suite used to fix its clock at `2026-08-15T09:00:00.000Z` and thread it
19+
* through the engine as `{ context: { now } as never }`, then assert the
20+
* `{30_days_ago}` floor equalled that instant minus 30 days. That held only
21+
* while the REAL date and the fixture date agreed: at 2026-08-16T00:00Z it went
22+
* red on every branch at once, with no code change, and misattributed itself to
23+
* whatever PR happened to be open.
24+
*
25+
* The mechanism was a clock the engine does not offer. `{30_days_ago}` is
26+
* resolved by `resolveFilterTokens` from an instant the engine never supplies,
27+
* so the resolver falls back to the PROCESS clock — `context.now` was never
28+
* read, and is not declared on `ExecutionContext` at all (neither the spec
29+
* schema nor `ExecutionContextLike` in `@objectstack/core` carries it; the
30+
* `as never` casts were TypeScript already saying so). Whether the engine
31+
* SHOULD expose a declared, injectable clock is #8937's open half — a
32+
* public-contract question this file must not assume either way.
33+
*
34+
* So the fixture now pins the clock the engine ACTUALLY reads, with fake
35+
* timers, exactly as this package's sibling temporal suites already do
36+
* (`engine-cel-default-temporal-shape`, the three `engine-autonumber-*`
37+
* files): `vi.useFakeTimers({ toFake: ['Date'] })` — Date only, so the engine's
38+
* async paths are untouched — plus `vi.setSystemTime(PINNED_NOW)`. Every
39+
* temporal expectation below is then deterministic forever, and the written
40+
* dates are honest: they are what the code under test really sees.
41+
*
42+
* Two companion cases keep that honest rather than merely green: one shows the
43+
* resolver genuinely tracks the clock it is given (so the pin above is not two
44+
* constants agreeing), and one records as a TESTED fact that an injected
45+
* `context.now` is inert today — the trap that armed this file in the first
46+
* place, now stated instead of silent.
1547
*/
1648

17-
import { describe, it, expect, beforeEach } from 'vitest';
49+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
50+
import { resolveFilterTokens } from '@objectstack/core';
1851
import { ObjectQL } from './engine.js';
1952

53+
/**
54+
* The `{30_days_ago}` floor the platform's own resolver yields for `instant`.
55+
* Used by the companion cases to show the resolver is clock-SENSITIVE.
56+
*/
57+
function resolvedFloorAt(instant: Date): string {
58+
const out = resolveFilterTokens({ $gte: '{30_days_ago}' }, { now: instant });
59+
return out.$gte as string;
60+
}
61+
2062
/** Days back from `now`, as the canonical UTC instant the store holds. */
2163
function daysAgoIso(now: Date, days: number): string {
2264
return new Date(now.getTime() - days * 86_400_000).toISOString();
@@ -101,9 +143,17 @@ function makeRecordingDriver() {
101143
describe('[#8690] the temporal-comparand door at the engine collection point', () => {
102144
let engine: ObjectQL;
103145
let reads: SeenRead[];
146+
/**
147+
* [#8937] Pinned as the PROCESS clock below, which is the clock the engine
148+
* actually resolves `{30_days_ago}` against — so this date is deterministic,
149+
* not a wager on what day the suite runs.
150+
*/
104151
const now = new Date('2026-08-15T09:00:00.000Z');
105152

106153
beforeEach(async () => {
154+
// Date only: the engine's async paths must keep real timers.
155+
vi.useFakeTimers({ toFake: ['Date'] });
156+
vi.setSystemTime(now);
107157
const rec = makeRecordingDriver();
108158
reads = rec.reads;
109159
engine = new ObjectQL();
@@ -126,14 +176,18 @@ describe('[#8690] the temporal-comparand door at the engine collection point', (
126176
reads.length = 0;
127177
});
128178

179+
afterEach(() => { vi.useRealTimers(); });
180+
129181
const refusalOf = async (p: Promise<unknown>) =>
130182
p.then(() => null, (e: any) => e as Error & { code?: string; status?: number });
131183

132184
it('refuses the card\'s comparands with code AND status, while the positive control still returns 38', async () => {
133185
// ── the defect's own cells ──────────────────────────────────────────────
134186
for (const comparand of ['last_30_days', 'not-a-date-at-all', 'last_7_days', 'last_90_days']) {
187+
// No context is threaded: the refusal precedes token resolution entirely,
188+
// so no clock — injected or otherwise — can participate in this verdict.
135189
const err = await refusalOf(
136-
engine.find('support_case', { where: { created_date: { $gte: comparand } } }, { context: { now } as never }),
190+
engine.find('support_case', { where: { created_date: { $gte: comparand } } }),
137191
);
138192
expect(err, `${comparand} must be refused, not answered with an empty chart`).not.toBeNull();
139193
// The reverse-verification requirement: BOTH halves of the envelope.
@@ -150,11 +204,14 @@ describe('[#8690] the temporal-comparand door at the engine collection point', (
150204
// ── the POSITIVE CONTROL, in this same test by ruling ───────────────────
151205
// Without it the four refusals above are equally consistent with a gate
152206
// that refuses everything.
207+
// [#8937] Deterministic because the PROCESS clock is pinned to `now` — the
208+
// clock the engine really resolves against — rather than injected through a
209+
// context key nothing reads.
153210
const floor = new Date(now.getTime() - 30 * 86_400_000).toISOString().slice(0, 10);
211+
expect(floor).toBe('2026-07-16');
154212
const inWindow = await engine.find(
155213
'support_case',
156214
{ where: { created_date: { $gte: '{30_days_ago}' } } },
157-
{ context: { now } as never },
158215
);
159216
expect(inWindow).toHaveLength(38);
160217
// The token really resolved — the door let the platform's own spelling
@@ -164,6 +221,59 @@ describe('[#8690] the temporal-comparand door at the engine collection point', (
164221
expect(reads[reads.length - 1].ast.where.created_date.$gte).toBe(floor);
165222
});
166223

224+
/**
225+
* [#8937] The discriminating half of the floor assertion above.
226+
*
227+
* A pinned clock plus a written-down floor would also pass if the resolver
228+
* ignored its instant entirely and returned a constant. These pins show the
229+
* floor genuinely tracks the instant handed to it, so the positive control
230+
* above is a statement about a live clock rather than two frozen values
231+
* agreeing. Written dates here are the resolver's INPUTS, so nothing in this
232+
* case can rot on a calendar date.
233+
*/
234+
it('resolves {30_days_ago} against the instant it is given, not a constant', () => {
235+
const a = new Date('2026-03-10T12:00:00.000Z');
236+
const b = new Date('2026-03-15T12:00:00.000Z');
237+
// Two instants five days apart yield two DIFFERENT floors, five days apart.
238+
expect(resolvedFloorAt(a)).toBe('2026-02-08');
239+
expect(resolvedFloorAt(b)).toBe('2026-02-13');
240+
// A month/year boundary, and the leap-free February this floor crosses.
241+
expect(resolvedFloorAt(new Date('2026-01-05T00:00:00.000Z'))).toBe('2025-12-06');
242+
// With no instant supplied the resolver falls back to the process clock —
243+
// the path the engine actually takes. Deterministic here because that clock
244+
// is pinned, which is the whole point of pinning it.
245+
expect(resolveFilterTokens({ $gte: '{30_days_ago}' }, {}).$gte).toBe('2026-07-16');
246+
});
247+
248+
/**
249+
* [#8937] A CHARACTERIZATION pin, not an endorsement.
250+
*
251+
* `now` is not declared on `ExecutionContext` (neither the spec schema nor
252+
* `ExecutionContextLike` in `@objectstack/core` carries it), and nothing on
253+
* the engine's read path reads it — `filterTokenContextFrom` takes an
254+
* explicit `now` argument the engine never passes. An injected `context.now`
255+
* is therefore inert today, which is exactly what let a date-armed fixture
256+
* look like it was pinning a clock.
257+
*
258+
* Recorded here so the trap is a stated, tested fact instead of a silent one.
259+
* If #8937's open half lands — the engine gaining a declared, injectable
260+
* clock — this pin SHOULD go red: delete it in that PR, deliberately.
261+
*/
262+
it('does not honour an injected context.now today — the engine reads the process clock', async () => {
263+
// Five years off: were it honoured, the floor would land in 2020.
264+
const injected = new Date('2020-06-01T00:00:00.000Z');
265+
await engine.find(
266+
'support_case',
267+
{ where: { created_date: { $gte: '{30_days_ago}' } } },
268+
{ context: { now: injected } as never },
269+
);
270+
271+
const boundFloor = reads[reads.length - 1].ast.where.created_date.$gte;
272+
expect(boundFloor).not.toBe(resolvedFloorAt(injected));
273+
// It tracked the pinned PROCESS clock instead — the real source.
274+
expect(boundFloor).toBe('2026-07-16');
275+
});
276+
167277
it('refuses on both doors — the lowered object form and the authored array sugar', async () => {
168278
const object = await refusalOf(
169279
engine.find('support_case', { where: { created_date: { $gte: 'last_30_days' } } }),

0 commit comments

Comments
 (0)