noggin

Cross-instance sync coverage

The hardest noggin bugs are the ones where two views of the same noggin disagree. They live at the intersection of two axes:

This page maps every (provider × topology) cell to the test that pins its behaviour. A cell marked — with a reason isn't a gap: it's a combination that's structurally impossible (e.g. there's no "two processes" case for memory://, because each process has its own in-memory store).

When you add a new provider or change a notification mechanism, update this table along with the code. CI doesn't enforce it yet, but a stale row here is a strong signal that something in the contract has drifted.

Matrix

Topologyfile://memory://localstorage://
Same handle
(baseline: my writes are visible to me)
provider-parity.test.mjs (file://)provider-parity.test.mjs (memory://)provider-parity.test.mjs (localstorage://)
Two handles, one process
(engine dedupe + refcount)
multi-instance.test.mjs (file://)multi-instance.test.mjs (memory://)multi-instance.test.mjs (localstorage:// same tab)
Two handles via RPC
(noggin.changed fan-out)
server-adapter.test.ts (two RPC servers on one file)RemoteNoggin.test.ts (rebases when another client mutates)— (RPC is server-side; localStorage only exists in a browser)
Two processes, one machine
(file lock + fs.watch)
concurrency.test.mjs (CLI processes) + two-windows-sync.spec.ts (VS Code dev hosts)— (memory is per-process; cross-process sharing is impossible by construction)— (localStorage is per-origin-per-process; processes don't share)
Two browser tabs
(DOM storage event)
— (file:// is a Node-side provider)— (memory is per-process)playground.spec.ts (cross-window sync) + external-change-cause.test.mjs (cross-window via Node shim)

Contract tests that span all providers

Beyond the cells above, these tests pin the contract that every provider must honour:

ContractTest
Empty-diff apply does not fire onDidChangeempty-diff.test.mjs
Dispose is idempotent; peer survives sibling disposedispose-semantics.test.mjs
ChangeEvent payload is readonly ItemChange[] (no wrapping)external-change-cause.test.mjs
Apply queue serializes within one handlemulti-instance.test.mjs + provider-parity.test.mjs
Two opens of the same URL share state (URL dedupe)multi-instance.test.mjs + dispose-semantics.test.mjs
Refcount: backend torn down only after last handle disposesdispose-semantics.test.mjs

Edge-case tests

These are narrower than the matrix cells — each pins a specific failure mode that doesn't fit neatly into "(provider, topology)" but has bitten us or could bite us:

Edge caseTest
fs.watch rapid-write race + identical-bytes rewritefile-watch-race.test.mjs
RemoteNoggin rolls back on server-side apply failureRemoteNoggin.test.ts (server-side apply failure)
RPC subscribe / unsubscribe / resubscribe yields fresh idserver-adapter.test.ts (resubscribe)
External file write fires noggin.changed over RPCserver-adapter.test.ts (watch: true default)
Unrelated storage keys do not fire onDidChangeexternal-change-cause.test.mjs

Notes on the topology axis

When a new provider lands

  1. Add a row of cells for the new scheme in the Matrix above.
  2. Add the provider to provider-parity.test.mjs's FACTORIES array so the same-handle baseline runs.
  3. Decide which topologies are structurally applicable. Skip the ones that aren't and document the reason in the cell. Don't leave a cell empty without a reason — empty cells look like gaps.
  4. Add multi-instance and dispose coverage to multi-instance.test.mjs and dispose-semantics.test.mjs.
  5. If the provider supports a notification mechanism (file watching, pub/sub, etc.), add an external-mutation test to external-change-cause.test.mjs.

The matrix is descriptive, not generative — there's no single test file that loops over every cell. Each cell links to where the relevant test actually lives. The reason is pragmatic: the test runners differ (node:test, vitest, Playwright) and the fixtures differ too. Forcing one uniform shape over all of them costs more than the visibility benefit returns. See the adversarial critique in the session log for the full reasoning.