test(triggers,plugins,services): the authz false-green sweep — five in-lane fixtures now provision the resolver's tables - #18067
Conversation
…cts two fixtures drive Both fixtures boot a real ObjectKernel over a real better-sqlite3 SqlDriver and register only their own test objects. Every write/decision then drives core's `resolveUserAuthzGrants`, whose five reads were refused by the driver because the tables were never provisioned. `tryFind` classifies a missing table as "not provisioned" and answers `[]`, so nothing went red: both suites resolved grants from reads that never happened. The refusals were invisible to a shared-log sweep because #11081 withholds and asserts them through `captureExpectedReadRefusals`. Registering the objects locally — with only the columns the reading path touches, so no dependency edge onto plugin-auth or plugin-security is added — makes the reads SUCCEED, which is the outcome that channel's own contract names. The declared-absent lists shrink to the tables that are still genuinely unprovisioned. No product code changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
…jects three more fixtures drive The same defect the previous commit closed, in the three sites the repo-wide sweep found outside the two the withheld-noise channel was hiding: each boots a real ObjectKernel over a real better-sqlite3 SqlDriver, registers only its own test object, and then drives core's `resolveUserAuthzGrants` against tables that were never provisioned. Measured at 2f1a6f6, per file with --maxWorkers=1: before-update-flow-payload-reach 10, reentrant-start-condition 10, paused-run-visibility 5 — five tables each, no line of it visible as a failure because `tryFind` answers `[]` for a missing table. Registered locally with only the columns the reading path touches, so no dependency edge onto plugin-auth or plugin-security is added. No product code changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
…thz-false-green-sweep
… seam `objectql.registry` is typed `EngineSchemaRegistryView` here, which declares no `registerObject`; the file already carries a structural `TestObjectRegistry` for exactly that and casts to it two lines up. tsc --noEmit TS2339, fixed by using the same cast. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
📓 Docs Drift CheckNothing 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
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract review — Verdict: PASSServed-tier: claude-opus-5 Head reviewed:
Clause ② — re-derived from the DELIVERED diff, ⛔ not carried from the claim
⭐ The dev's own discriminating control is the right shape and I record it because a negative needs one: after a build, every identifier this diff introduces greps to 0 files in each
The card's hard prohibition, checked directlyThe card said: ⛔ "Do not silence, filter or re-level the log line. The count must fall because the read SUCCEEDS." Measured on the diff: 0 touches to No dependency edge added, as the card required: 0 Landing state
Noted, not blocking
Generated by Claude Code |
Fixes #17985
Clause-②: no
The card handed this lane a static pointer — 73 test files outside
plugin-authnaming
resolveUserAuthzGrants/resolveAuthzContext— and asked for a measurement.Here is the measurement, the instrument's own blind spot (which it has, and which the
pointer could never have shown), the five in-lane sites it found, and the out-of-lane
ones this lane reports rather than touches.
The population, and how it was derived
Reproduced at the branch point
2f1a6f696, same greps the card used:*.test.tsoutsideplugin-authnaming the resolverplugin-auth*.ts, not just tests)*.test.tsoutsideplugin-authconstructingAuthManagerThe card's method is "run the candidate packages' suites". That was done — all 18 —
and then extended to every other package this lane owns, because a candidate list built
from a grep cannot name a fixture that drives the resolver without mentioning it (and
one of the five sites below is exactly that). 44 package suites in total, each a full
vitest run --maxWorkers=2, ~34k tests, all green.The instrument has a blind spot, and it is the one that mattered
Counting
refused a read on '...'in the shared log under-reads. Four fixtures routetheir refusals through
captureExpectedReadRefusals(#10629 / #11081), which withholdsthe driver line and asserts the count instead. Against those, a grep of the log reads
a clean zero while every read is still being refused.
A complete census of that channel: 24
captureExpectedReadRefusalscall sites in therepo; 4 of them declare the authz resolver's tables as expected-absent. Two are in
this lane, two are not. Both of this lane's two are real sites of the class, and neither
would have appeared in a log-grep sweep.
That is not a defect in #10629 — the channel names this outcome itself:
How a "site" is told apart from any other refused read
resolve-authz-context.tsissues eight reads with fixed filter shapes and limits, so arefusal is attributed to the resolver by the
(table, filter, limit)triple in the loggedstatement —
sys_position ... where name in (...) limit 200is the resolver's; thewhere name = ? limit 1on the same table is not.The discriminator earns its keep:
plugin-security's suite emits 585 refused reads and0 of them are resolver-class (they are
sys_permission_set/sys_position/sys_capabilityreads by name, a different fail-soft path), whileclient's 264contain 227 that are.
Controls — so the zeros mean something
plugin-auth's two known fixturesat
2f1a6f696: 7 lines,sys_user_position2 /sys_user_permission_set3 /sys_position2 — byte-for-byte the baseline PR test(plugin-auth): register the authz objects two sign-in fixtures drive #17982 measured independently. Theinstrument detects the class when the class is there.
plugin-security's 585 and 227 out ofclient's 264 — it is not "any refusal".refusalscounter, not through the log.The five in-lane sites — before and after
Before is at
2f1a6f696; after is this branch. Per file,--maxWorkers=1.triggers/trigger-record-change/src/record-change-integration.test.tsplugins/plugin-approvals/src/status-mirror-cascade.integration.test.tstriggers/trigger-record-change/src/before-update-flow-payload-reach.test.tstriggers/trigger-record-change/src/reentrant-start-condition.test.tsservices/service-automation/src/paused-run-visibility.test.ts121 refused authz reads, every one of them a grant resolution that answered
[]becausethe table was missing rather than because the state was empty.
Actual output. The two withheld ones, read off the capture's own counter by a one-off
console.logofnoise.refusals(injected, run, restored):The three visible ones, counted from the log:
Whole-suite, after:
plugin-approvals45 files / 738 tests / 32 refusals, 0 resolver-class ·trigger-record-change10 / 101 / 3, 0 resolver-class ·service-automation134 / 1581 / 8,0 resolver-class. All
sys_organizationand onesys_metadata_activation— theprobeInstallOrganizationsclass, not this card's.The count falls because the read SUCCEEDS
⛔ No log line is silenced, filtered or re-levelled: the diff is five test files, zero
product code. The declared-absent lists shrink — the tables leave them because the
fixture now provisions them, and
sys_organization/sys_approval_delegationstaydeclared because those probes are still genuinely unprovisioned.
The positive proof, PR #17982's shape: a one-off, non-committed assertion that seeds a
sys_user_positionrow and then asks the resolver for that principal's grants. Before thefix it cannot even reach the table; after it, the row comes back and the resolver's answer
carries it.
oneoff_rolereachedgrants.positionsonly because the resolver'ssys_user_positionread returned a real row — the leg that answered
[]before.Both legs are anchored on
HEAD(which carries the implementation), restored withgit checkout HEAD -- ..., and verified bygit hash-objectagainst the HEAD blob plus anempty
git diff HEAD:Each injection was proved to have landed on disk before the run (marker count grepped, not
inferred from the editor's exit code), and every mutation ran under a
trap ... EXIT INT TERM.The remedy, and the edge it does not add
Each fixture declares the objects locally, with only the columns the reading path
touches, so ⛔ no dependency edge onto
plugin-authorplugin-securityis added — thefind-envelope-limb-removal.test.tsprecedent PR #17982 applied:sys_useridfilter;emailis the RLS owner-email fallbacksys_membersys_user_positionsys_user_permission_setsys_positionisRowActive, tenant scope⛔
sys_position_permission_setandsys_permission_setare deliberately absent: theresolver reaches them only once a
sys_positionrow resolves and a permission-set id iscollected, and none of these fixtures seeds either. The ADR-0091 validity columns are absent
for the reason
sys_memberlacks them today —isGrantActivereads an absent bound asunbounded, so declaring them would change no verdict.
Out of lane — REPORTED, not touched
🔴 The population is repo-wide by construction and this lane owns part of it. These are
measured and left alone for routing.
@objectstack/clientdomain:cli@objectstack/runtimedomain:cliclient, per file — these seven account for 100% of the package's 227, so there is noeighth site in it:
runtime, by file:src/notifications.hono.integration.test.tsandsrc/notification-schema-conformance.integration.test.ts, each declaring['sys_user','sys_member','sys_user_position','sys_user_permission_set','sys_position','sys_setting']as
ABSENT_AUTHZ_TABLES.Every other swept package reads 0 resolver-class, including
core,spec,rest,mcp,dogfood,verify,cloud-connection,plugin-hono-server,organizations,plugin-security,plugin-sharingand all 16services/*, 4connectors/*and the othertwo
triggers/*.Checks
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom the delivered change set at this head — see the report comment on sweep: do harnesses outside plugin-auth drive resolveUserAuthzGrants against a narrower object set? — the #17897 class is a latent FALSE GREEN, not just log noise #17985 for the verdict line.
check:type-check-debtfirst answered exit 3 (PREREQUISITE NOT MET, heap OOM), which isnot a pass; re-run under the
--max-old-space-size=6144ceiling its own script pins, itexits 0 with a real verdict (
5 ledger entries re-measured, none above its recorded number).pnpm --filter @objectstack/plugin-approvals --filter @objectstack/trigger-record-change --filter @objectstack/service-automation typecheck— exit 0, all threeDone.tracked files matching
eslint.config.mjs's own**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minus its only global
ignores(NEVER_LINTED); 5 files linted, counted from--format json, 0 errors / 0 warnings; and the invariance that makes the narrowing ameasurement rather than a skipped run — this repo's config never enables type-aware
linting (no
parserOptions.project, no typed@typescript-eslintrules, stated andmeasured with a positive control at
eslint.config.mjs:327), so no edit in this diff canmove the verdict on a file it does not contain. The full-repo scan is CI's.
grep -naPcontrol-character scan over all five edited files — no matches.No changeset — measured, not assumed
All three packages ship
files[] = ["dist","README.md","CHANGELOG.md"]. After a build,grepping the shipped path for every symbol this diff introduces:
Nothing published moves, so this carries
skip-changesetrather than a changeset.Clause-② re-determination from the delivered diff:
no. The diff is five*.test.tsfiles. It adds no exported symbol reachable from any published entry (measured above: every
introduced identifier is absent from all three
dist/trees while a real published symbolis present in four files of each), and no new key on an already-published payload — the
object literals it adds are fixture-local
consts consumed only byregistry.registerObjectinside the same file.
Acceptance notes
Observations from the sweep, noted and not filed — none is in this card's class, and
each already has a home or no one to hand it to:
plugin-securityemits 585 refused reads, 0 resolver-class:sys_permission_set(471),
sys_organization(81),sys_position(24),sys_capability(7),sys_audience_binding_suggestion(2). Thesys_*ones are reads by name from adifferent fail-soft path; 14 more are a deliberate connection-abort fixture. Carrier: the
domain:servicesseat already owns the file surface, but no PR in flight touches it.probeInstallOrganizations/ seed-loader / metadata classes are the bulk of what isleft repo-wide (
rest362sys_metadata,dogfood126sys_migration,verify22sys_migration, andsys_organizationacross ~10 packages). That population is [finding] 15 more PASSING@objectstack/runtimetests print the samerefused a read onfeature — 134 lines after #10380 quiets its two, led bynotifications.hono.integration.test.ts(52) #10629's,not this card's.
trigger-record-change/src/record-change-integration.test.tsemits one refusal onsys_metadata_activationthat itscaptureExpectedReadRefusalslist does not declare —unchanged by this diff, present in both legs.
service-automation/src/notify-zero-delivery-visibility.integration.test.tsexits 1 whenrun as a single file under
--maxWorkers=1and passes in the package run. Pre-existing,untouched by this diff. Carrier: none identified.
Generated by Claude Code