The ObjectStackAdapter advisory chain had both ends pinned and its middle
link pinned by nothing: the producer suite asserts the event is emitted into
a sink it builds itself, the renderer suite asserts the message is built over
an event it writes by hand, and nothing asserted the two are connected.
Measured on the 45 suites naming AdapterProvider plus the producer and
renderer suites (633 tests): cutting the a.onSaveAdvisory subscription left
all 45 GREEN; cutting the emitSaveAdvisories call inside it left all 45
GREEN; cutting the unsubscribe left all 45 GREEN. 38 of the 41 AdapterProvider
suites mock the module away and, before this file, exactly 0 imported the
real one.
This pin mounts the real provider with no adapter prop -- passing one makes
the effect return early, before the subscription is installed -- drives a
real save through the adapter the provider built, and asserts the finding
arrives at the sink. It goes red at each of the three cut points.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Fixes#7116
The
ObjectStackAdapteradvisory chain had both ends pinned and its middle link pinned by nothing. This adds one pin that asserts the CONNECTION.Precondition — the card was filed as grep-measured, so it was re-measured by ablation
The card said so itself, and it was right to. Its grep reading reproduced, with the denominator moved by one since filing:
AdapterProviderdvisorexpectA sharper census explains the blind spot, and it is starker than the sibling card's: 38 of the 41 mock the module away with
vi.mock, and exactly 0 imported the realAdapterProvider. Control for that zero, same query shape againstsaveAdvisoryToast: 2 hits, so the zero is a reading and not a dead query.The ablation that settles it
Baseline: the 41 suites naming
AdapterProviderplus the producer and renderer suites — 45 files / 633 tests, green. Then, three separate cuts, each proven on disk by blob-hash change plus a marker count, each restored by state (git diff HEADempty,git diff --cachedempty, blob hash back):a.onSaveAdvisorysubscription, outrightemitSaveAdvisoriescall inside itPredicted green before running, on the strength of the 0-real-imports census; observed green. The gap is real, and it has three independently cuttable points rather than the one the card assumed.
It is NOT covered transitively by the sibling pin
Measured, not assumed. With this channel's seam cut (H1), the pin that landed as
7e2e6f42cstays green, 3/3. They are sibling channels: that one isMetadataClient, minted per component, sink riding the factory config; this one isObjectStackAdapter, the long-lived shared instance whose sink is a subscribe/unsubscribe registration wired once by the provider (built by #4237).The pin
packages/app-shell/src/providers/AdapterProvider.advisorySink.test.tsx— three cases: the save renders the finding, a clean save says nothing (with a control that the write really travelled), and the subscription is released on unmount.The load-bearing design point: the provider must build its own adapter.
AdapterProvidertakes an optionaladapterprop, and passing it makes the effect return early — before the subscription is installed. A test that hands in a ready-made adapter is green against all three ablations above and sees nothing. So nothing is passed; the provider runs its realinit().Real: the provider and its effect, the
ObjectStackAdapterit constructs, that adapter's save interceptor, the real SDK client and its response parsing,createAuthenticatedFetch,withSettleSignal, the registration and its unsubscribe, the i18ntread through the provider's ref,readSaveAdvisories, andemitSaveAdvisories. Stubbed, and only these two:sonner(the terminal sink, imported as a module binding so it cannot be handed over) andglobalThis.fetch(the server).Red-first evidence
The pin goes red at each cut point, each for the right reason — and H3 discriminates, failing only its unmount case:
expected [] to have a length of 1expected [] to have a length of 1expected "vi.fn()" to not be called at all, but actually been called 1 timesAblation validity: this lane is source-resolved — the root vitest config aliases every
@object-ui/*to that package'ssrc, so nodistrebuild stands between the mutation and the run, and a stale build cannot manufacture a false green here.Scope
Deliberately not a bigger test at either end — both ends are already covered, and adding to either would grow the suite without closing the hole. The tier, per-finding formatting, door verbs and empty-list drop stay the renderer suite's; response-shape filtering,
modederivation and listener isolation stay the producer suite's. No production code changed.Verification, at
ad32812detype-checkfor app-shell: exit 0, 0 errors — re-run after building the dependency closure, since the first attempt failed only on unresolved workspace modules and was NOT MEASURED rather than redcheck-changeset-presence: exit 0. Its verdict was quoted rather than predicted — it demanded a declaration, and names an empty frontmatter as "a pass, not a workaround" for a change that releases nothing, which is what the changeset carrieschangeset:check(fixed + no-major),check-changeset-overwrite,check-control-bytes,lint:coverage,type-check:coverage: all exit 0Declared narrowing: the repo-wide lint scan is CI's and was not run here. Evidence the narrowing excludes nothing — population read from eslint's own config rather than guessed (1038 files under
packages/app-shell, counted from--format json, 0 errors), the new file confirmed present in that population, and no type-aware linting configured (noprojectService, noparserOptions.project; control: the config file reads backrules10 times,export defaultonce), so this diff cannot move the verdict of any untouched file.Generated by Claude Code