test(metadata): pin the watcher ordering guarantee itself, not get()'s microtask depth (#6043) - #6547
Merged
Conversation
`register-notifies-watchers.test.ts` 的「announces AFTER the write lands」 用例,通过在回调里 `await manager.get(...)`、并在 `await register(...)` 之后读取变量来断言顺序。`notifyWatchers` 以 `void callback(event)` 派发、 从不 await 回调,所以这个写法实际测的是 `get()` 有几个 await 帧,而不是 它声称的顺序保证。 实测两个方向都成立: * 把 `get()` 换成语义等价的 `(await this.getDiagnosed(...)).data` 委托写法 ——纯等价重构——用例立刻变红(#5840 因此放弃该写法,保留三行重复); * 把广播挪到 registry 写入之后、realtime 发布与 writable loader 保存之前 ——真实违反 `register()` 文档的顺序——用例 15/15 全绿。 改为按 issue 的 (b) 方案:在回调里**同步**读 registry,断言广播那一刻写已 落地;并保留 (a) 方案覆盖「订阅者重读看得见新 body」——捕获订阅者的 promise 并由测试 await,而不是假定它已结算。新增 overwrite 变体,使早广播的失败信息 直接读作 V1/V2,而不是与「promise 未结算」同形的 undefined。 同步复核 #6051 留在 `get()` 上的 TSDoc:该常驻理由已过期,改为陈述现状 (仅注释改动,不重构 `get()`)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KDU3qAuJyajAQm3GkUXdfA
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 8, 2026 04:19
This was referenced Aug 8, 2026
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.
Fixes #6043
What this changes
packages/metadata/src/register-notifies-watchers.test.tshad a case named"announces AFTER the write lands, so a subscriber that re-reads sees the new body".
It asserted an ordering guarantee by awaiting
get()inside the watcher callbackand reading a variable back after
await register(...)resolved.notifyWatchersLocaldispatches withvoid callback(event)and never awaits itshandlers, so
readBackwas only ever populated becauseget()'sawaithop counthappened to be smaller than the microtasks
await register(...)yields. The case waspinning this method's microtask depth, not the ordering it named.
Per the triage ruling, this implements option (b) — synchronously read the registry
inside the callback and assert the write has already landed at broadcast time — and
keeps option (a) alongside it so the "a subscriber that re-reads sees the new body"
claim stays covered without frame-dependence (the subscriber's promise is captured and
awaited by the test instead of assumed settled).
One case became three:
hasthe entry andholds the new body. Entirely synchronous, so no consumer method's
awaithops canmove the verdict.
announcement came too early" and "the promise had not settled" surface as
undefined;with a distinct pre-write body the early announcement fails as
V1whereV2wasrequired, and names the defect on sight.
get()— the (a) half, awaited rather than raced.Verification — both failure modes of the old case, measured
Direction was decided before running each, per the reverse-verification discipline.
1. Pure equivalent refactor of
get()must stay GREEN (the old false positive)Applying the delegation form #5840 abandoned —
return (await this.getDiagnosed(type, name)).data;—semantics unchanged, one extra async frame.
Old case, before this PR — RED:
New cases, with this PR — GREEN, and so is the whole package:
Predicted green: (b) never calls
get()at all, and (a) awaits the captured promise.2. Broadcast moved before the write lands must go RED (the regression it guards)
Hoisting the
notifyWatchers(...)block above the registry write inregister():All three go red, and the overwrite case names the defect in its message.
3. Correction to the issue's stated false-negative
The issue predicted the old case would stay green when the broadcast moved before the
write. Measured, it goes red — but for the wrong reason: the subscriber misses the
registry, falls through to the loaders, and reports the same bare
undefinedat the sameline as the equivalent-refactor case above. The old assertion carries one bit where the
defect has two, so its red could not distinguish a real ordering regression from a
harmless refactor.
The genuine false negative sits one notch finer. Hoisting the announcement above the
realtime publish and the writable-loader save loop — still a violation of the ordering
register()documents — leaves the old case fully green:All sabotage was reverted before committing; the committed diff to
metadata-manager.tsis comment-only.
The
get()TSDoc notePR #6051 left a standing note on
get()recording the delegation as unsafe. That reasonis now stale — the test it cited no longer observes the frame count — so the note is
rewritten to state current reality, as the ruling directs.
get()itself is notrefactored here (production change, out of scope for this card).
The delegation now looks viable: the whole
@objectstack/metadatasuite was re-measuredon the delegating version and stayed green (519/519). Offered as a follow-up suggestion,
not taken — and the note records the caveat that
get()'s callers outside this packagewere never surveyed for timing sensitivity, only this package's tests.
Scope
Option (c) from the issue — a "drained" observability point on
notifyWatchers— is outof scope and not attempted.
Only the registry half of
register()'s ordering claim is pinned.register()documentsthat the announcement follows the write into the registry and every writable loader,
but this fixture's
MemoryLoaderdeclaresmemory:andregister()persists todatasource:loaders only, so no loader in this file is ever written to and the secondhalf is not observable from it. Recorded in the file header and filed separately as a
finding; it needs a writable-datasource fixture, not another assertion.
Changeset
None — this is a test-only change plus a comment-only touch, so it releases nothing. The
PM will apply the
skip-changesetlabel at acceptance.Generated by Claude Code