Skip to content

test(objectql): pin the temporal-comparand door fixture's clock with fake timers (#8937) - #8939

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-8937-temporal-door-context-now
Aug 16, 2026
Merged

test(objectql): pin the temporal-comparand door fixture's clock with fake timers (#8937)#8939
os-project-manager merged 2 commits into
mainfrom
claude/issue-8937-temporal-door-context-now

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Part of #8937

Stops the bleeding on the p0: from 2026-08-16T00:00Z engine-temporal-comparand-door.test.ts failed 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:

  • now is declared on neither ExecutionContextSchema (@objectstack/spec, kernel/execution-context.zod.ts) nor ExecutionContextLike (@objectstack/core, utils/filter-tokens.ts).
  • Nothing on the engine read path reads it. filterTokenContextFrom(execCtx, now?) takes an explicit now argument, and engine.resolveWhereTokens never passes one — so resolveFilterTokens falls back to new Date().
  • The as never casts 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 "declares now among its context". That docblock sentence reads:

packages/core's resolveFilterTokens is field-AGNOSTIC by construction — its context is now / timezone / userId / orgId ...

"its" is resolveFilterTokens's context — FilterTokenResolutionContext. The field names settle it: that list ends in orgId, which is FilterTokenResolutionContext; ExecutionContextLike spells the same concept tenantId, and filterTokenContextFrom is precisely the bridge between the two types. So honouring context.now would 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-shape and the three engine-autonumber-* suites:

vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(now);

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. The as never context 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:

  1. 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.
  2. 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 injected now five 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 keyword

Whether the engine should expose a declared, injectable clock is a public-contract question: it needs now on ExecutionContextSchema in packages/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 of context.now exist, and the wire path delete options.context unconditionally, 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.

  • Target file: 10 passed (10) — was 1 failed / 7 passed. Green today (2026-08-16) while asserting a 2026-07-16 floor is itself the proof the process clock is overridden; under the old code today's real clock produced 2026-07-17.
  • Full @objectstack/objectql suite: 211 files / 3697 tests passed, up from 210 passed + 1 failed / 3694. tsc --noEmit clean.
  • TZ=Asia/Tokyo (already the next local calendar day) — 10/10.
  • Gate union re-derived from the changed path via 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, and check:type-check-debt --re-measure (33 ledger entries, none above its recorded number) on a fully built workspace closure.
  • No runtime code changed, so there is no consumer sweep to run — the diff is one test file. skip-changeset applies and is on the PR.

One process note worth recording: while measuring limb 1 I built @objectstack/core with a probe, reverted the source, and re-ran — and the test went green on the probe's stale dist/, which would have certified the opposite conclusion. A source-level revert is not enough when the consumer resolves from dist/; the rebuild is part of the revert.

Generated by Claude Code

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
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 16, 2026 12:50am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@github-actions github-actions Bot added the tests label Aug 16, 2026
@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 16, 2026 — with Claude
…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 os-project-manager changed the title test(objectql): de-calendar the temporal-comparand door fixture (#8937) test(objectql): pin the temporal-comparand door fixture's clock with fake timers (#8937) Aug 16, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 16, 2026 01:05
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit 510a4bd Aug 16, 2026
31 checks passed
@os-project-manager
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants