Repository navigation
test(plugin-security): the explain/enforce parity table shares a read-only principal rig per posture source - #23748
Conversation
…-only principal rig per posture source The 60 principal rows only read (explain, the explained user's own find, the resolved sets, a system read of an explained filter), so the rows of one posture source now share one rig, booted by its first row and torn down after the table. Rows that can write (RLS, sharing) still boot their own. Every rig's teardown also disables the AsyncLocalStorage instances its engine and plugin own: on Node 22 each one that served a run() stays on a process-wide async_hooks list that every later promise creation walks, and neither destroy() disables them. No it/describe/expect line changes. Claude-Session: https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ Co-authored-by: Claude <noreply@anthropic.com>
…st-budget-plugin-security
📓 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): |
席位核对
Verdict: ACCEPT at head
Generated by Claude Code |
Refs #23184 (plugin-security rows: explain-enforce-parity, position-write-through)
Clause-②: no (test wall time only; no published schema, behaviour or export moves)
A partial landing for the
@objectstack/plugin-securityslice of the test-budget card. The diff is tests-only: one file,packages/plugins/plugin-security/src/explain-enforce-parity.test.ts, +62 / -8. No production file, option, constant, timeout or export changes, and noplugin-securitysource module (the C2/C3 epic's territory) is touched.Rows this answers
src/explain-enforce-parity.test.ts38332198724)src/position-write-through.test.ts38267594064)mainin150da61aa5(#22962, the stage-8 test half), and the module it tested,position-write-through.ts, left in5c9bfb0d3f(#23302, C3 stage 7b). Both are ancestors oforigin/main(merge-base --is-ancestorexit 0). No change is owed; the next refresh PR's list should not name it.A correction to the claim: it says C3 stage 7b deleted the test file. That commit deleted the module; the test file had already gone one commit earlier in the series, in
150da61aa5.Cause, measured
A temporary per-phase probe of the base file (deleted, never committed) showed where the wall went:
bootPrincipalrig, about 84 ms per boot:syncSchemasof 11 objects (2.5 s across the 60 boots), 21 seed inserts (1.8 s), and the administrator's authz context (0.5 s). The rows' own faces cost about 28 ms each. The 33 RLS rows and 5 sharing rows cost about 8-12 ms each.ci.ymlpins),AsyncLocalStorageruns onasync_hooks. A storage that has served onerun()stays on a process-wide list untildisable(), and every later promise creation walks that list.ObjectQLholds three storages per engine andSecurityPluginone per plugin, and neitherdestroy()disables them. So each of the 98 torn-down rigs left its storages behind, and each later row walked all of them. A CPU profile of an intermediate build showed_propagate(node:internal/async_local_storage/async_hooks) as the top self-time frame.TEST_BUDGET_CLASSES, and the cost could be cut without weakening any row.Each mechanism, measured alone (module line, ms): base 7790/7967/7374; storages released only 3572/4054; shared principal rig only 2350/2522; both 1609/1724.
The change
withPrincipal/sharedPrincipalRig). The source's first row boots the rig, and anafterAlltears the 4 rigs down. Every principal row only reads: explain, the explained user's own find through the real resolver and middleware, the permission sets enforcement hands the user, and a system read of an explained filter. The RLS and sharing rows can write (by-id update, delete, insert), so they still boot their own rig per row.AsyncLocalStorageinstances its engine and plugin own (releaseAsyncContexts). It runs after the rig's last row is judged, on objects nothing uses again.Nothing weakened
Static lines:
it(1/1,describe(1/1,expect(19/19, byte-identical and in order between base and head (cmpexit 0). One hook line is added: the shared rigs'afterAll.Runtime assertions per case (
expect.getState().assertionCallsin temporary instrumented base and head copies, deleted): identical per case and in order, 335 over 98 cases, no zero-assertion case, every casepass.Isolation, per principal row (a temporary probe over the head file, deleted). For all 60 principal rows:
readFilterapplied as a system read.60/60 on every reading.
A leak would be loud (a one-off mutation via
scripts/ablation-replace.mjs, never committed): the removed user'ssetsface was made to write asys_memberrow into the shared rig. 15 rows went red, 13 of them later rows of the same rig (isolated,groupandorg-scoping). Restore was proven: blob378d595c80equal to HEAD, andgit diff HEADempty.Order independence: head under
--sequence.shuffle.testswith seeds 11 / 2026 / 23184: 98/98 passed each time.No row moved from the real stack to a stub. No timeout, budget or project changed. Nothing was deleted.
Wall time
Vitest default-reporter module line (hooks plus cases, collection excluded; the number
test-core.logcarries). Command:pnpm exec vitest run --maxWorkers=2 FILE --reporter=default --reporter=json, inpackages/plugins/plugin-security, underos-verify-lock, on a 4-vCPU box.534ca13eb5, head5cad7b98dd5cad7b98dd(load 1.1-1.6)db5121f2d6(load 1.7-2.3, other agents' work beside it)Verification (at final head
db5121f2d6, closure rebuilt after the merge)pnpm --filter @objectstack/plugin-security run typecheck:VERDICT command-exit 0.check:test-typecheckOK, 0 files in the debt ledger;tsc -p tsconfig.test.json --listFilesOnlylists the file.pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2: Test Files 186 passed (186), Tests 3714 passed, 45 skipped (in other files),VERDICT command-exit 0.eslint --no-inline-configon the file: exit 0. Control-byte self-scan: 0 hits.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatdb5121f2d6: 1 path, 54 commands, identical to the seat's lead. Each was run separately, exit captured before any pipe.check:dual-build-cjs-loadsandcheck:i18nexited 3 (PREREQUISITE NOT MET, workspace not built) and exited 0 afterturbo run build --filter=!@objectstack/docs(75/75).dispatch-gates --ran: 54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN.Acceptance notes
ObjectQL.destroy()andSecurityPlugin.destroy()leave theirAsyncLocalStorageinstances enabled. On Node 22 every one of them stays on the process-wideasync_hookslist that each later promise creation walks. In this file that was about half the wall (7.8 s to 3.6-4.1 s with the release alone). Any suite that builds many engines in one process pays the same. A production process builds one engine, and no public door was measured giving a wrong answer, so no defect class applies. Ifdestroy()later disables them,releaseAsyncContextsbecomes a no-op and can go.Generated by Claude Code