test(objectql): pin the temporal-comparand door fixture's clock with fake timers (#8937) - #8939
Merged
os-project-manager merged 2 commits intoAug 16, 2026
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
…es (#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
os-project-manager
marked this pull request as ready for review
August 16, 2026 01:05
os-project-manager
deleted the
claude/issue-8937-temporal-door-context-now
branch
August 16, 2026 01:18
os-project-manager
pushed a commit
that referenced
this pull request
Aug 16, 2026
…8939 relay) gen:migration-registry, spec build, gen:export-origins, gen:api-surface, gen:spec-changes, gen:upgrade-guide, gen:openapi restore. check:generated: all 13 artifacts up to date. Survival asserted on the merged tree, one hit each in the regenerated registry: engine-dotted-filter-refused (this branch), driver-sql-unresolvable-where-column-refused (#8927), filter-preset-ordering-comparand-refused (#8935), identity-api-key-schema-retired (#8932) - plus implementation bodies: sql-driver.ts INVALID_FIELD refusal + envelope test (#8927), isDateRangePresetName (#8935), ApiKeySchema still absent (#8932), classifyDottedFilterHead at both doors (this branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8937
Stops the bleeding on the p0: from 2026-08-16T00:00Z
engine-temporal-comparand-door.test.tsfailed deterministically on every branch, blocking the merge queue for everyone. It deliberately does not decide the card's other half, so it does not close it — see "What this PR leaves open".What was actually wrong
The suite pinned its clock at
2026-08-15T09:00:00.000Z, threaded it through the engine as{ context: { now } as never }, and asserted the{30_days_ago}floor equalled that instant minus 30 days. That held only while the real date and the fixture date agreed. It armed itself to fail the next day.The clock it threaded does not exist:
nowis declared on neitherExecutionContextSchema(@objectstack/spec,kernel/execution-context.zod.ts) norExecutionContextLike(@objectstack/core,utils/filter-tokens.ts).filterTokenContextFrom(execCtx, now?)takes an explicitnowargument, andengine.resolveWhereTokensnever passes one — soresolveFilterTokensfalls back tonew Date().as nevercasts on those two call sites were the tell: TypeScript was already refusing the key.The issue proposed limb 1 ("honour the injected
context.now") on the grounds that the door's docblock "declaresnowamong its context". That docblock sentence reads:"its" is
resolveFilterTokens's context —FilterTokenResolutionContext. The field names settle it: that list ends inorgId, which isFilterTokenResolutionContext;ExecutionContextLikespells the same concepttenantId, andfilterTokenContextFromis precisely the bridge between the two types. So honouringcontext.nowwould add a caller-injectable clock to the engine's public execution envelope, not restore a declared one.What this PR does
It pins the clock the engine actually reads — the process clock — using this package's established convention for exactly this, as used by
engine-cel-default-temporal-shapeand the threeengine-autonumber-*suites:toFake: ['Date']leaves real timers alone, so the engine's async paths are untouched. With that in place the original readable assertion (floor === '2026-07-16') is deterministic forever, and the written dates are honest again: they are what the code under test really sees. Theas nevercontext injection is gone from the positive control and from the refusal loop, where no clock can participate anyway.Two companion cases keep the pin honest rather than merely green:
resolves {30_days_ago} against the instant it is given, not a constant— a pinned clock plus a written-down floor would also pass if the resolver ignored its instant and returned a constant. This shows two instants five days apart yield two floors five days apart, plus a month/year-boundary case. Its dates are resolver inputs, so nothing in it can rot.does not honour an injected context.now today— a characterization pin, not an endorsement. It records the trap as a stated, tested fact instead of a silent one: an injectednowfive years off is ignored and the floor tracks the pinned process clock. If the engine ever gains a declared injectable clock this pin should go red — the docblock says to delete it in that PR, deliberately.An earlier commit on this branch derived everything from the real clock with bracketed comparisons; it worked, but the fake-timer form matches local precedent, keeps the readable assertions, and needs no bracketing, so it supersedes that.
What this PR leaves open — why
Part of, not a closing keywordWhether the engine should expose a declared, injectable clock is a public-contract question: it needs
nowonExecutionContextSchemainpackages/spec(which this card's dispatch scoped out), and it adds a new caller-settable surface to every data operation. I measured that it would work and that it would break no existing caller — zero producers ofcontext.nowexist, and the wire pathdelete options.contextunconditionally, so no client could reach it. But "it works and breaks nothing" is not authority to widen a public contract, so it is escalated rather than guessed. Full three-axis analysis in the report on #8937.Because that half is undecided, merging this must not close the card — hence
Part of.Verification
All at commit
34c19e6.@objectstack/objectqlsuite: 211 files / 3697 tests passed, up from 210 passed + 1 failed / 3694.tsc --noEmitclean.TZ=Asia/Tokyo(already the next local calendar day) — 10/10.scripts/pm/dispatch-gates.mjs, all green:check-nul-bytes,check:durability-log-level,check-engine-split-ratio,check:query-options-erasure,check:type-check-coverage, andcheck:type-check-debt --re-measure(33 ledger entries, none above its recorded number) on a fully built workspace closure.skip-changesetapplies and is on the PR.One process note worth recording: while measuring limb 1 I built
@objectstack/corewith a probe, reverted the source, and re-ran — and the test went green on the probe's staledist/, which would have certified the opposite conclusion. A source-level revert is not enough when the consumer resolves fromdist/; the rebuild is part of the revert.Generated by Claude Code