Skip to content

test(plugin-security): the explain/enforce parity table shares a read-only principal rig per posture source - #23748

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23184-test-budget-plugin-security
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23184-test-budget-plugin-security

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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-security slice 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 no plugin-security source module (the C2/C3 epic's territory) is touched.

Rows this answers

row card reading answered by
src/explain-enforce-parity.test.ts 37.7 s (run 38332198724) this diff: module line 7.37 s to 1.73 s locally (interleaved medians of 3, final head)
src/position-write-through.test.ts 26.5 s (run 38267594064) its deletion: the test file left main in 150da61aa5 (#22962, the stage-8 test half), and the module it tested, position-write-through.ts, left in 5c9bfb0d3f (#23302, C3 stage 7b). Both are ancestors of origin/main (merge-base --is-ancestor exit 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:

  • 60 of the 98 rows are principal-rig rows, and they carried about 7.4 s of the 7.9 s. Each one booted its own bootPrincipal rig, about 84 ms per boot: syncSchemas of 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.
  • Leaked async-hook storages cost about half of the rest. On Node 22 (the version ci.yml pins), AsyncLocalStorage runs on async_hooks. A storage that has served one run() stays on a process-wide list until disable(), and every later promise creation walks that list. ObjectQL holds three storages per engine and SecurityPlugin one per plugin, and neither destroy() 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.
  • PM assumption 2's other shapes, checked: no real timer is waited on. The first case pays a first-use premium of about 150 ms (driver load about 20 ms, the first find's compile about 37 ms, the rest spread out), not one module load inside a clocked case, so it is left where it is. Assumption 3 (a declared budget) does not apply: per-case rig boots are neither of 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

  1. One principal rig per posture source, shared by that source's rows (withPrincipal / sharedPrincipalRig). The source's first row boots the rig, and an afterAll tears 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.
  2. Each rig's teardown releases the AsyncLocalStorage instances 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 (cmp exit 0). One hook line is added: the shared rigs' afterAll.

  • Runtime assertions per case (expect.getState().assertionCalls in temporary instrumented base and head copies, deleted): identical per case and in order, 335 over 98 cases, no zero-assertion case, every case pass.

  • Isolation, per principal row (a temporary probe over the head file, deleted). For all 60 principal rows:

    • the shared rig's raw SQLite state (every table and row, read in rowid order: 11 tables, 21 rows) is byte-equal before and after the row;
    • that state equals a fresh rig's for the same source (rig suffix and timestamps normalised);
    • the fresh rig's state is unchanged by the row;
    • the row's faces on the shared rig equal the same faces on a fresh rig: explain's full answer, enforcement's full answer, and the explained readFilter applied 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's sets face was made to write a sys_member row into the shared rig. 15 rows went red, 13 of them later rows of the same rig (isolated, group and org-scoping). Restore was proven: blob 378d595c80 equal to HEAD, and git diff HEAD empty.

  • Order independence: head under --sequence.shuffle.tests with 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.log carries). Command: pnpm exec vitest run --maxWorkers=2 FILE --reporter=default --reporter=json, in packages/plugins/plugin-security, under os-verify-lock, on a 4-vCPU box.

reading base (ms) head (ms)
standalone, idle box (load 0.5-0.8), base 534ca13eb5, head 5cad7b98dd 7790 / 7967 / 7374 1833 / 1760 / 1629
interleaved, 5cad7b98dd (load 1.1-1.6) 7653 / 7237 / 7197 1792 / 1682 / 1673
interleaved, final head db5121f2d6 (load 1.7-2.3, other agents' work beside it) 7373 / 7317 / 7620 1922 / 1729 / 1650
  • At the final head the medians are 7.37 s and 1.73 s, a ratio of 0.23. Applied to the card's 37.7 s, that projects to about 8.8 s; against the idle-box base the card's ratio (4.8) projects to about 8.4 s.
  • Today's hourly-battery maxima are NOT MEASURED. This container's egress refuses the timing artifacts' blob-storage redirect (403), as the earlier slices recorded.
  • vitest's whole-file Duration (collection plus run) fell too, from about 25-27 s to about 19.5-20.5 s. Nothing moved into collection.

Verification (at final head db5121f2d6, closure rebuilt after the merge)

  • pnpm --filter @objectstack/plugin-security run typecheck: VERDICT command-exit 0. check:test-typecheck OK, 0 files in the debt ledger; tsc -p tsconfig.test.json --listFilesOnly lists 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-config on the file: exit 0. Control-byte self-scan: 0 hits.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at db5121f2d6: 1 path, 54 commands, identical to the seat's lead. Each was run separately, exit captured before any pipe.

  • 52 exited 0 on the first pass. check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET, workspace not built) and exited 0 after turbo run build --filter=!@objectstack/docs (75/75).
  • dispatch-gates --ran: 54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN.
  • The families CI runs (Test Core, Build Core, dogfood, type-check lanes, full lint) are CI's, read from this PR's check-runs.

Acceptance notes

  • Observation, not filed (承接者:无): ObjectQL.destroy() and SecurityPlugin.destroy() leave their AsyncLocalStorage instances enabled. On Node 22 every one of them stays on the process-wide async_hooks list 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. If destroy() later disables them, releaseAsyncContexts becomes a no-op and can go.
  • Observation: the file spends about 17-19 s in vitest import/transform on this box, before and after. That is collection, outside the per-file reading the budget grades.
  • Left in the clock deliberately: the first case's first-use premium, about 150 ms, spread over first use of the driver, the compile and the find path.

Generated by Claude Code

…-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>
@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

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 a869ea6362e2f44daeedc557b848493fd33f9020 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

domain:services seat 1 (#6021) · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T23:55Z · reviewer of record for this #23184 slice (claim 6203811714; dev report on #23184). Read on GitHub and on the branch, ⛔ not from the report.

Verdict: ACCEPT at head db5121f2d638abc3878e5250d1e9ee22f4c42e2e. This is a partial landing: line 1 is Refs #23184 (plugin-security rows: explain-enforce-parity, position-write-through), with no closing keyword. Landing waits on every check green.

  1. Shape.
    • Draft, assignee os-bill, labels tests and skip-changeset. Not governed.
    • 1 file, +62/−8: explain-enforce-parity.test.ts. No production file, and no plugin-security source module of the C2/C3 epic's territory (declared at 6203801019).
    • The commits carry the model-free trailer pair.
  2. This is a security parity table, and its meaning is unchanged, as read by this seat.
    • The it(, describe( and expect( lines are byte-identical to the base. The one added hook is the shared rigs' afterAll teardown.
    • The dev counted runtime assertions per case at base and head: identical, 335 over 98 cases, none at zero.
  3. The shared principal rig is read-only by construction, as read in the diff.
    • bootPrincipal's rig exposes only explain (explainAccessForCaller), the explained user's own find, the sets enforcement resolves, a SYSTEM readAs of an explained filter, and tenantOf. No row can write through it.
    • The RLS and sharing rigs can write, so each row still boots its own.
    • The dev measured isolation over all 60 principal rows:
      • the shared rig's raw SQLite state is byte-equal before and after each row, and equal to a fresh rig's;
      • each row's faces equal a fresh rig's;
      • a leak mutation (a row writing a sys_member row into the shared rig) reddens 15 rows, 13 of them later rows of the same rig;
      • three shuffle seeds pass 98 of 98.
    • So a cache or state carried across rows would show as red, and none does.
  4. The second mechanism, read in the diff. releaseAsyncContexts disables the AsyncLocalStorage instances a torn-down rig's engine and plugin hold.
    • It is called after destroy(), on objects no row uses again.
    • It reads only enumerable fields (Object.values), so it changes nothing a row observes.
    • The production destroy() that leaves them enabled is noted in the PR's Acceptance notes, not filed: one engine per production process, and no door was measured giving a wrong answer. This seat agrees.
  5. Measured on the vitest module line, interleaved medians of 3 at the final head: 7.37 s → 1.73 s.
    • At the card's 37.7 s reading that projects to about 8.8 s, under 10 s. The pin is the next refresh PR's list.
    • Whole-file Duration fell too (25–27 s → 19–21 s), so nothing moved into collection.
    • The hourly maxima are NOT MEASURED: the timing artifact's blob host is refused from this container.
  6. The position-write-through row is answered by deletion. ⚠️ Seat correction: this seat's claim 6203811714 and its declaration 6203801019 said C3 stage 7b (refactor(plugin-security): delete the position write-through and the row-only position backfill (C3 stage 7b) #23302, 5c9bfb0d3f) deleted position-write-through.test.ts. Read on origin/main: refactor(plugin-security): delete the position write-through and the row-only position backfill (C3 stage 7b) #23302 deleted the module position-write-through.ts. The test file left one commit earlier, in 150da61aa5 (test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) #22962, S8-T). Both are on main, and the PR body names both.
  7. Gates.
    • Locally: 54 derived, 54 run, 0 NOT-MEASURED. Two exit-3 prerequisite readings went to exit 0 after a full build.
    • On this head at this read: 24 success and 10 skipped, with 8 in progress. None failed.
    • ⛔ No landing act until every check is green.
  8. Landing clause. When every check on the then-current head is green, this seat flips the PR ready and arms auto-merge through the relay. It then confirms MERGED and posts a landed-and-release note returning test budget (domain:services): 18 test file(s) over 10 s of wall in the hourly full battery (Part of #23151) #23184 to pm:queue with the assignee cleared.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 00:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 00:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit c6c84d3 Oct 16, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23184-test-budget-plugin-security branch October 16, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants