Skip to content

test(rest): enumerate metadata write doors on every mount base, not just /api/v1 - #12445

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-11473-scoped-meta-mount-coverage
Aug 26, 2026
Merged

test(rest): enumerate metadata write doors on every mount base, not just /api/v1#12445
os-litant merged 2 commits into
mainfrom
claude/issue-11473-scoped-meta-mount-coverage

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#11473

Test-only. No runtime change. packages/rest/src/rest-server.ts is read, never edited — it is fenced by PR #12421.

The scope question the card asks first, and the answer

The card does not ask for a fixture; it asks whether one is worth having:

Whether this is worth a fixture at all is a scope-discipline judgement: it protects a configuration the framework ships as off-by-default, and no evidence was gathered here about whether any real deployment turns it on.

Measured on this checkout, enableProjectScoping: true appears in 22 places. Every one is either a test, a doc-comment example, or a published doc:

wherewhat it is
12 sites across 7 packages/rest test filesin-repo composition tests that already boot the scoped mount
3 sites across 2 packages/client test files (2 real scoped boots, 1 docstring), plus a docstring in packages/client/src/index.tsthe shipped SDK's project(id) factory exists for scoped servers
3 sites in packages/spec/src/compose-stacks-key-loss.test.tsspec-level composition tests
packages/cli/src/utils/merge-boot-config.test.tspasses it in to assert the boot builder overrides it to false
packages/cli/src/commands/serve.ts (comment)the CLI reads api.enableProjectScoping and forwards it to REST + dispatcher
content/docs/api/environment-routing.mdxa published docs page instructing users to set it, inside an os:check snippet

Zero non-test, non-doc sites turn it on. So the flag is genuinely off-by-default here, and the standalone boot path forces it off on purpose — createStandaloneStack() returns api: { enableProjectScoping: false, projectResolution: 'none' } and mergeBootConfig lets the boot builder win that key ("scoping is not the author's call on a standalone host"). A host-shaped config (isHostConfigshouldBootWithLibrary false) still reaches the plugin with the author's value intact, and serve.ts says outright that "Cloud / multi-environment boot modes live in a separate distribution", which is the distribution where this is on.

That measurement decides which of the card's two resolutions to take, and it decides against the more expensive one:

  • Resolution 1 — a new scoping-on dogfood fixture — is NOT taken. A permanent, real-boot fixture is a standing cost every future contributor pays, and the surface it would protect has no measured in-repo deployment. That is exactly what startup-stage scope discipline says not to buy.
  • Resolution 2 — make the enumeration base-agnostic — IS taken, because it is not a fixture for a config at all. It repairs an instrument that is already in the suite and whose declared claim is currently false.

The defect

#8919's file declares that "the metadata write doors are a CLOSED, enumerated set", and earns that by DERIVING the door list from the composed server's own route table rather than from a hand-written list. The derivation filtered the route table on a literal /api/v1/meta prefix.

RestServer.registerRoutes calls registerForBase(...)once per base. registerMetadataEndpoints is one of the registrars it calls, so with api.enableProjectScoping on, the whole door set is mounted a second time under /api/v1/environments/:environmentId. No scoped door path starts with /api/v1/meta.

The filter's blind spot was therefore precisely the population the filter exists to enumerate — and it was invisible from inside the assertion, because the only boot in the file was an unscoped one, for which the filter is complete.

Measured, doors derived by the old prefix filter vs doors mounted, on the three compositions the server can be configured into:

compositionmountedderived by prefix filter
default (enableProjectScoping absent → false)55
enableProjectScoping: true, projectResolution: 'auto'105
enableProjectScoping: true, projectResolution: 'required'50

The required row is the sharp one: registerForBase is called with the scoped base and nothing else, so a closed-set claim was being asserted over the empty set while five real write doors were live.

The change

One file, packages/rest/src/meta-write-door-capability-enumeration.test.ts:

  1. The derivation matches the meta path segment instead of an /api/v1/meta prefix, and the expectation is built per mounted base, so the closed-set claim is checked against every mount the composition brings up. The matcher is a regular expression that accepts meta when it is preceded by a slash or the start of the path and followed by a slash or the end of the path.
  2. boot() takes an optional composition, so the two scoping-on route tables can be enumerated.
  3. One routing probe on the scoped mount — anonymous, capability-less, capable — driving all five doors through their scoped paths.

Why the probe is three cases and not a second copy of the matrix

registerMetadataEndpoints swaps in a guarded registrar, calls registerMetadataEndpointsInner(basePath) inside the swap, and restores it in a finally. Both passes run that same body, and the per-door capability gates live in that same body. There is no seam at which the two mounts could carry different gates — so duplicating fifteen refusal cases onto the scoped base would buy repetition, not coverage. What is not structural is whether a request addressed to a scoped path is routed at all, and that is what the probe measures. Measured on the scoped mount, all five doors: anonymous → 401 with the flat anonymous-deny envelope, protocol call count 0; authenticated without capability → 403 FORBIDDEN, protocol call count 0; manage_metadata holder → 200, protocol call count 1. The capable control is what makes the two refusals readable as decisions rather than as an unrouted path.

Bounded in-place correction, named because it is not what the card asked for

The DOORS docstring said "The six metadata write doors". The table has held five since #12195 retired the compound-name save — that card removed the entry and the comment right below it explains the removal, but the count in the sentence above was not touched. The docstring now says the count is the table's own length and records why. Evidence is the table itself and #12195's own note inside it; no behaviour is involved.

LATENT, not live

enableProjectScoping defaults to false (rest-server.ts: api.enableProjectScoping ?? false), so a default deployment mounts none of the scoped doors and an anonymous probe of one 404s with ENDPOINT_NOT_FOUND. The filer's own words: "a coverage observation, not a measured hole." This PR closes a coverage gap in an anti-drift assertion. It does not close a reachable hole, and nothing here should be read as one.

Verification

Gate union run at d5e52b309, the final commit on this branch.

Ablation — the pin fails when the thing it guards is removed. META_SEGMENT was reverted to the pre-change prefix form, the mutation was confirmed on disk by occurrence counts (removed-text 1 → 0, injected-text 0 → 1) and a changed git hash-object, and the run went red in the predicted direction:

FAIL > 'auto' mounts BOTH bases, and both are enumerated
FAIL > 'required' mounts ONLY the scoped base ... AssertionError: expected [] to deeply equal [ ...(5) ]
Tests 2 failed | 48 passed (50)

The 48 that stayed green include the original #8919 default-boot assertion — which is the whole point: the prefix filter was complete for that boot, so the old assertion could not see its own blind spot. Restore was git checkout HEAD -- against the file spelled as an absolute path under an EXIT INT TERM trap, proven byte-identical to the HEAD blob and git diff HEAD empty. No dist/ is involved: the file under test imports ./rest-server.js, resolved to source by vitest.

Green run, same file, unmutated: Test Files 1 passed (1) · Tests 50 passed (50).

whatverdict
pnpm --filter '@objectstack/rest^...' buildexit 0
pnpm --filter @objectstack/rest testTest Files 147 passed (147) · Tests 2345 passed (2345)
pnpm --filter @objectstack/rest typecheckexit 0 — src only; this package's tsconfig.json excludes **/*.test.ts, so it says nothing about the edited file
tests-included tsc --noEmit --listFiles over that packageedited file present in the program (1 hit; control rest-server.ts 1 hit; a term known absent 0 hits). 0 errors attributed to the edited file; program total 155, equal to the recorded TEST_DEBT['@objectstack/rest'] = 155, so the shrink-only ratchet is unmoved
pnpm lint (eslint . --no-inline-config, whole repo)exit 0
pnpm check:nul-bytesOK (scanned 6881 text file(s) ... no raw ASCII control bytes)
pnpm check:cross-package-test-inputsOK: 18 package(s) read outside themselves, all declared
pnpm check:test-source-aliasOK — 72 packages with tests scanned
pnpm check:engine-double-contractOK — 416 pinned, 133 in the DEBT ledger, 2 exempt
pnpm check:where-matcher303 matcher(s) discovered, 303 answer the combinator battery correctly or refuse it loudly
pnpm check:type-check-coverage, check:query-options-erasure, check:slot-lookup, check:type-source-resolution, check:published-files, check:page-declaration-shape, check:dispatcher-error-vocabularyexit 0
node scripts/check-ci-filter-parity.mjs, check-comment-mask-adoption.mjs, check-plugin-teardown-shape.mjsexit 0

Families re-derived in this worktree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack over the actual changed file, not from the dispatch list. check:type-check-debt --re-measure was not run locally (it needs the whole workspace closure built); the only ledger entry a packages/rest test edit can move is @objectstack/rest, and the row above measures it at the recorded number with zero errors in the edited file.

No changeset: this diff adds no user-visible behaviour and ships nothing — test files are not published. skip-changeset.


Generated by Claude Code

The #8919 anti-drift assertion derives the metadata write-door list from
the composed server's own route table, but filtered it on a literal
`/api/v1/meta` prefix. `RestServer.registerRoutes` calls `registerForBase`
once per base, so with `api.enableProjectScoping` on the same doors are
also mounted at `/api/v1/environments/:environmentId/meta/...`, which no
`/api/v1/meta` prefix can match. Because the only boot in the file was an
unscoped one, the filter was complete for that boot and the blind spot was
invisible from inside the assertion it narrowed.
Match the `meta` path segment instead and build the expectation per mounted
base, then boot the two other compositions the server can be configured
into. Measured before the change, doors derived vs doors mounted:
default 5/5, `auto` 5/10, `required` 0/5 — under `required` the closed-set
claim was being asserted over the empty set.
Adds one routing probe on the scoped mount (anonymous, capability-less,
capable) so the scoped boot actually routes a request rather than booting
and measuring nothing. The refusals themselves are structurally shared:
`registerMetadataEndpoints` wraps the registrar around
`registerMetadataEndpointsInner(basePath)`, which is the same body on both
passes, so there is no seam at which the two mounts could diverge.
Test-only; no runtime change. `rest-server.ts` is read, not edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8515954fb6dd4102e120b59ce6410f94600d5710packageMentionDocs.

@os-litantos-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review August 26, 2026 04:09
@os-litant
os-litant added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit bb920eeAug 26, 2026
37 checks passed
@os-litant
os-litant deleted the claude/issue-11473-scoped-meta-mount-coverage branch August 26, 2026 04:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The environment-scoped /meta mount is a second, entirely unpinned copy of every metadata write door

2 participants

@os-litant@claude