Skip to content

check-engine-double-contract never discovers a double whose write verb is vi.fn(…)implOf drops a call-expression initializer #8639

Description

@os-zhuang

Observation while working #8570 (PR #8633). Filed unassigned; nobody is on it. Not a defect in that PR — its own double calls assertEngineDeleteDispatch explicitly.

What I measured

check-engine-double-contract's discovery cannot see an engine double whose write verb is wrapped in a call — the vi.fn(...) spelling. implOf (scripts/check-engine-double-contract.mjs:353) returns the function only for a method, a FunctionExpression or an ArrowFunction:

if(ts.isPropertyAssignment(member)){constinit=member.initializer;if(init&&(ts.isFunctionExpression(init)||ts.isArrowFunction(init)))returninit;returnnull;}

delete: vi.fn(async (o, opts) => …) is a CallExpression, so implOf answers null, consider() returns before the sibling and shape tests, and the double is never discovered. It is therefore neither pinned nor carried in scripts/engine-double-contract.baseline.json — it is simply absent from both sides of a ledger that is otherwise reconciled in both directions.

Confirmed on two real files rather than inferred: packages/metadata-protocol/src/protocol.batch-row-driver-text.test.ts and protocol.batch-row-http-status.test.ts each declare an engine double with insert / update / delete / findOne / transaction, all as vi.fn(async …), and both are absent from the gate's pinned listing and from the baseline while the run reports OK — 213 pinned, 133 in the DEBT ledger, 2 exempt.

Population, as an upper bound rather than a verdict: grep -rln "delete: vi\.fn\|update: vi\.fn" --include=*.test.ts packages/ matches 168 files. I did not classify how many of those are engine-shaped (≥2 engine siblings + isEngineVerbShape) versus driver or repository doubles, which is the measurement this card wants.

Why it is worth recording

The gate's own header names this failure mode: "a discovery that silently stops matching makes this script report OK while reading nothing" — which is why it asserts DISCOVERED is non-zero. That invariant catches a scan that breaks entirely; it cannot catch a scan that quietly skips one spelling, and vi.fn is the spelling most reached for when a test wants to assert call counts on the double it just wrote. Each skipped double is exactly what the gate exists to prevent: a fake free to accept a call ObjectQL.delete / ObjectQL.update refuses, on the paths a double was introduced for.

Scope if picked up

Teach implOf to unwrap a call whose sole function argument is the implementation (the vi.fn(fn) / vi.fn(async …) shape), then re-run: newly discovered doubles either pin or land in the baseline as a first measurement of a set the scan could not previously reach — the same act #5629 recorded when it stopped discarding deletes that declare no parameters, and #5480 when the update slice first landed. Both precedents are in the script's own comments and both say plainly that a first measurement is not a raised ratchet. Whoever takes it should say which it is in the entry note, and fix rather than ledger anything cheap to pin.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions