Repository navigation
[finding] check-org-identifier excludes test files wholesale, so a fixture that CONSTRUCTS the removed session dialect reads the same as one asserting its absence #9809
Description
Found while implementing #9691 (the receiver-name anchor). Reported, not fixed — #9691's scope is the receiver anchor, and this is a separate axis of the same gate. Unassigned. Not a claim.
Measured on 11b779e0f plus the #9691 branch.
The mechanism
scripts/check-org-identifier.mjs drops every test file before scanning:
const TEST_FILE = /(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)/;with the header's reason:
Test/spec files are EXCLUDED: they legitimately reference the removed token to assert its ABSENCE (
expect(session.tenantId).toBeUndefined()), and are not reference bodies an author copies a hook from.
That reason is correct about one population and blind to another. A test can name the removed alias in two structurally different ways:
- asserting its absence — the case the exclusion is written for, and legitimate;
- CONSTRUCTING it as an input — handing a hook a session object literal that spells
tenantId, a dialectHookContextSchemastrips andObjectQLEngine.buildSessionnever emits.
The second is not a reference body an author copies, so the header's stated reason does not cover it — but it is the shape that can hold a production defect green, because the fixture supplies the very key production cannot.
The instance that made this concrete
packages/services/service-storage/src/attachment-access-hooks.test.ts covered the session-fallback path of callerContext() like this (pre-#9691):
session: { userId: 'u1', tenantId: 'org_1', positions: ['p1'] },
...
expect(canEdit.mock.calls[0][2]).toEqual({ userId: 'u1', tenantId: 'org_1', positions: ['p1'] });The production code under it read s.tenantId off the session — always undefined in reality, so the attachment kit forwarded an envelope with no organization to ISharingService.canEdit. The fixture passed for exactly as long as the code was wrong, and could only have failed if the code became right. It went red the moment #9691 fixed the source.
So the fixture was not merely uncovered by the gate; it was the mechanism keeping a live defect green, in a file the gate is written to skip.
Census
Across the 2435 tracked test files under examples/ apps/ packages/, fixtures that build a session: object literal carrying a tenantId key:
packages/spec/src/data/hook.test.ts:619 deliberate #3290 absence pin
packages/plugins/plugin-audit/src/audit-writers.test.ts:1666 deliberate absence pin (PR #9692)
packages/plugins/plugin-audit/src/comment-access-hooks.test.ts deliberate absence pin (#9691)
packages/services/service-storage/src/attachment-access-hooks.test.ts deliberate absence pin (#9691)
4 sites, 0 of them wrong today — the two #9691 ones were rewritten in that PR, and the other two were always deliberate. So this is LATENT, not live. It is filed because the population it is blind to is the population that produced the only measured instance, and because the exclusion's stated reason is narrower than the exclusion itself.
What a fix would have to decide (not decided here)
The distinguishing signal is available structurally: an absence pin puts the removed key on the ASSERTION side (toBeUndefined, not.toHaveProperty), a phantom-green fixture puts it on the INPUT side (a session: property of a context literal handed to the code under test). Whether that distinction is worth a rule, a lint, or nothing at all is a judgement, and the honest options differ in cost:
- leave it, and narrow the header's stated reason to match what the exclusion actually does;
- scan test files for the INPUT shape only (a
sessionobject literal carryingtenantId), which is a new recognizer with its own blind spots; - treat it as an instance of Seven gates report "clean" when they mean "I saw nothing I understood" — make unrecognised a verdict distinct from pass #9747 and let the family-level answer cover it.
My weak preference is (1) plus recording the measurement, on the grounds that 4 sites with 0 wrong does not obviously earn a new recognizer, and #9747 is already the place where "my recognizer is narrower than the code" is being decided as a family.
Related
- check-org-identifier anchors on the literal receiver name
session, so asess.tenantIdread — the spelling the removed alias actually had in shipped code — scores zero #9691 — the receiver-name anchor in the same gate; this is the axis it did not touch. - Seven gates report "clean" when they mean "I saw nothing I understood" — make unrecognised a verdict distinct from pass #9747 — the family card (a recognizer narrower than the code, reporting the shortfall as a verdict).
- check-org-identifier truncates each line at the first
//, so a URL or a slash-bearing string on the same line hides thesession.tenantIdread it exists to catch #9444 / fix(scripts): check-org-identifier takes the shared string-aware comment mask #9496 — the comment-masking hole in the same gate. - plugin-audit's
callerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141 / service-storage's attachment kit carries the same 5-fieldcallerContextprojection #7141 fixed for comments #7145 — thecallerContextprojection the instance lives in.
Activity
claude commentedon Aug 19, 2026
Claimed by the
domain:devxPM seat — dispatching to anos-devsubagent now.session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9809-org-identifier-test-exclusion.claude/skills/pm-dispatch/**nor changes any gate's accept/reject behaviour under the scope ruled below)State:
pm:queue→pm:dispatched, assignedos-steve.Generated by Claude Code
os-steve commentedon Aug 19, 2026
{ "issue": 9809, "status": "done", "branch": "claude/issue-9809-org-identifier-test-exclusion", "pr": "https://github.com/objectstack-ai/objectstack/pull/9874", "premise_still_valid": true, "summary": "Built ruling 1 and only ruling 1: narrowed the header's stated reason for the wholesale test/spec exclusion, and recorded the measurement in the script. No recognizer added, so the gate's accept/reject behaviour is unchanged (same corpus, same verdict, 2058 files before and after). Measuring turned up a correction the card did not have: the exclusion is NOT what hides the construction shape. Both rules grade a `.tenantId` READ off a session-valued receiver, and a `session: { tenantId: … }` object literal is a construction, not a read, so it scores 0 even under a NON-test filename with the exclusion bypassed — including the verbatim pre-#9691 phantom-green that held a live defect green. Deleting the exclusion would surface zero construction sites; option (2) is therefore a new recognizer on a new axis, not a loosening of this filter, which strengthens rather than undermines the ruling. Both copies of the overclaim were in the gate script itself and both are narrowed; the blind spot is pinned in the existing --self-test.", "tests": "All on final commit fab54d793 (union re-run after the last commit). `pnpm check:org-identifier` -> '✓ check-org-identifier self-test: 31 cases pass.' + 'OK (2058 author-facing source file(s), 13 session binding(s) resolved, no removed session.tenantId alias).' (baseline before the change: 28 cases, same 2058/13 — verdict unchanged). `pnpm check:cross-package-test-inputs` -> 'All 33 self-test cases passed. OK: 12 package(s) read outside themselves, all declared.' `node scripts/check-nul-bytes.mjs` -> 'OK (scanned 6282 text file(s) ... no raw ASCII control bytes)' plus a manual grep -naP control-char sweep of the edited file: none. Gate family derived from the changed path via `node scripts/pm/dispatch-gates.mjs scripts/check-org-identifier.mjs`, which named check:org-identifier and check:cross-package-test-inputs; check:nul-bytes added because the diff is all prose. REVERSE VERIFICATION, run from the committed state and restored with `git checkout <branch> -- <path>`: flipping all three new expectations (absence-pin 1->0, both construction cases 0->1) turned the harness red and named each case individually — direction as predicted, none passes vacuously. No ablation applicable (no dist/-resolved code involved).", "open_questions": [], "out_of_scope_findings": [ "filed as #9872: roughly ten comments across production code, tests and this gate's own header date the #3290 `session.tenantId` removal to 'v11', but it shipped in v16 — content/docs/releases/v16.mdx is the only release page mentioning #3290, its 16.0.0 upgrade checklist lists the rename, and there is no v11.mdx at all (pages run v9, v12-v17). skills/objectstack-data/references/data-hooks.md is the single site that has it right. Unassigned, `finding` label, no pm:queue." ] }PM's explicit asks
H1 — census re-measured on
83f8267f5(currentmain), not the card's11b779e0f+#9691: 4 sites, 0 wrong — reproduces. Same four files the card names, all still deliberate pins. Population drifted: 2449 excluded test/spec files (card: 2435), 2058 scanned. Also measured, which the card did not: 0 such literals in the scanned non-test population. No live phantom-green, so ruling 3's stop-and-report condition was not triggered. Reproduce withrg -l --multiline --multiline-dotall 'session\s*:\s*\{[^{}]*\btenantId\b' -g '*.test.ts' -g '*.spec.ts' examples apps packages(reports 5 matches across those 4 files;hook.test.tscarries a second whosetenantIdsits in a comment inside the literal body).H2 — two copies, both in the gate script, both narrowed:
scripts/check-org-identifier.mjs— the header bullet the card quotes.scripts/check-org-identifier.mjs— a terser restatement immediately above theTEST_FILEregex ("Tests assert the alias is GONE, so they reference the token on purpose"). This one was a genuine second copy; narrowing only the header would have left it standing.Checked and clean, no edit made:
.github/workflows/lint.yml— states the exclusion neutrally ("tests, comments, skills/ and docs/ are excluded") with no reason attached, so it carries no overclaim.--help/usage lines say nothing about tests.TEST_FILEregex (check-test-source-alias,check-type-check-coverage) exclude tests for unrelated reasons; neither copied this justification.Governed surfaces: nothing to report.
AGENTS.mdand.claude/**contain no mention of this gate,session.tenantId, or the exclusion — grepped both. So no governed-surface copy exists and #9840's shape did not arise.H3 — falsified as stated, and this is the most useful thing measured. The card's discriminator (absence pins put the key on the ASSERTION side, phantom-greens on the INPUT side) does not survive contact: all four deliberate pins put the removed key on the INPUT side. Constructing the dialect on purpose is how you pin that it gets stripped.
audit-writers.test.tsnever names the key on the assertion side at all — it asserts a downstreamorganization_idis null — so under the card's rule it would read as a phantom-green. The real separator is whether the fixture asserts the key's fate (absent/inert) or echoes its value back as expected output; and even that is not a text rule, becauseattachment-access-hooks.test.tslegitimately assertstoEqual({ …, tenantId: 'org_1', … })one test earlier —tenantIdon the way OUT isExecutionContext's driver-layer name for the same value, byte-identical to what a phantom-green writes. All of this is now written into the header.H4 — built, because the harness already existed.
--self-testwas already there (package.jsonruns--self-test &&the scan). Added three cases, 28 → 31: the absence-pin shape asserts 1 offender (a finding but for the filter), and two construction shapes assert 0 (invisible to both rules). They run under a non-test filename, so they state what the rules do independently ofTEST_FILE. Labelled in-place so a future author knows flipping them to 1 is the contract moving on purpose, not a regression. Nothing was invented — no new harness.Changeset: none added (
.changeset/**is inside the #9465 fence). The PR is comment prose plus self-test cases in a CI script and publishes nothing, so the repo's realskip-changesetlabel was applied to #9874 and read back to confirm.#9747: cross-referenced in prose only. Nothing added to it, and it is not named as the fix.
PR #9874 is draft; auto-merge not enabled and I did not flip it out of draft.
Generated by Claude Code