Skip to content

Commit e51d0ca

Browse files
baozhoutaoclaude
andauthored
test(objectql): 把 now 单一时刻 pin 从值相等改成按构造断言 (#5896) (#6658)
`engine.test.ts` 的 `pins \`now\` once per find (#1979)` 用值相等钉 「一次 find 只取一个 `new Date()`」,而回归形态是逐次求值各取一次时钟 —— 同毫秒内的两个 Date 值相等、对象不同,故该断言只在三次求值恰好跨毫秒 边界时才红。实测(复刻 PR #5894 探针 B):整文件跑 10 次里它绿了 3 次, 单进程 200 次 find 里 145 次三值全等 —— 按运气报警。 改为对 `ExpressionEngine.evaluate` 收到的上下文插桩:先钉求值次数(3 = 1 formula 字段 × 3 行,防止空调用列表下的空绿),再按对象同一性断言三次 求值拿到同一个 `now`。值相等保留为「调用方可见的症状」,但不再是报警来源。 保留 `#1979` 出处标记(issue 处置选项 1:加强而非退休)。 test-only:未触碰任何生产文件。 Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We Co-authored-by: Claude <noreply@anthropic.com>
1 parent d13f627 commit e51d0ca

1 file changed

Lines changed: 26 additions & 3 deletions

File tree

packages/objectql/src/engine.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, onTestFinished } from 'vitest';
22
import { ObjectQL } from './engine';
3+
import { ExpressionEngine } from '@objectstack/formula';
34
import { SchemaRegistry } from './registry';
45
import type { IDataDriver } from '@objectstack/spec/contracts';
56

@@ -1929,6 +1930,19 @@ describe('ObjectQL Engine', () => {
19291930
});
19301931

19311932
it('pins `now` once per find so every row sees the same instant (#1979)', async () => {
1933+
// Asserted by CONSTRUCTION rather than by value (#5896). The regression
1934+
// this guards is a per-evaluation `new Date()`, and two such reads
1935+
// inside the same millisecond are equal in value while being distinct
1936+
// objects — so a value comparison only fails when the three
1937+
// evaluations happen to straddle a millisecond boundary. Measured
1938+
// against that exact regression, the value form passed through it in
1939+
// 3 of 10 full-file runs (and in 145 of 200 finds within one warm
1940+
// process): it reported by luck. Spying on the eval context pins the
1941+
// mechanism instead — ONE clock read, handed to every evaluation by
1942+
// identity — which fails whatever the millisecond happens to be.
1943+
const evaluate = vi.spyOn(ExpressionEngine, 'evaluate');
1944+
onTestFinished(() => { evaluate.mockRestore(); });
1945+
19321946
vi.mocked(SchemaRegistry.getObject).mockReturnValue({
19331947
name: 'ping',
19341948
fields: {
@@ -1946,8 +1960,17 @@ describe('ObjectQL Engine', () => {
19461960

19471961
const result = await engine.find('ping', { fields: ['id', 'ts'] } as any);
19481962

1949-
// Determinism: a single operation snapshots one `now`, shared across
1950-
// every row — not a fresh wall-clock read per evaluation.
1963+
// 1 formula field × 3 rows: the evaluations the identity claim is over.
1964+
// Without this count the claim below could pass vacuously on an empty
1965+
// call list.
1966+
expect(evaluate).toHaveBeenCalledTimes(3);
1967+
const nows = (evaluate.mock.calls as unknown as Array<[unknown, { now?: Date }]>)
1968+
.map(([, ctx]) => ctx.now);
1969+
expect(nows[0]).toBeInstanceOf(Date);
1970+
expect(nows.every((n) => n === nows[0])).toBe(true);
1971+
1972+
// …and the consequence a caller can see. Kept as the caller-visible
1973+
// symptom, but it is no longer what makes this test report.
19511974
expect(result[0].ts).toEqual(result[1].ts);
19521975
expect(result[1].ts).toEqual(result[2].ts);
19531976
});

0 commit comments

Comments
 (0)