Skip to content

test(objectql): the "not tenant-scoped" DataEvent pin measures the withheld column instead of claiming it - #16124

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-15688-tenant-pin-comment
Sep 6, 2026
Merged

test(objectql): the "not tenant-scoped" DataEvent pin measures the withheld column instead of claiming it#16124
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-15688-tenant-pin-comment

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #15688

What was wrong

The #14970 block in packages/objectql/src/engine-data-events.test.ts carried a pin whose stated reason was false:

task declares no organization_id, so resolveTenantFieldName finds no column and nothing is published

registerObject injects the kernel organization_id column (TENANT_SCOPE_FIELD_DEF, gated on the injection plan's tenant flag) into every object that does not declare tenancy: { enabled: false }. So on the registered task the resolver answers organization_id, not null. The pin passed — correctly — because the ROW carried no organization. No assertion was wrong; the explanation was, and the next author copying it builds a "not tenant-scoped" fixture out of an object that is walled.

#14970 is not reopened by this and this body carries no closing keyword for it. That block stays correct.

Measured on this base (9b459b791), before writing anything

A throwaway harness in packages/objectql/src, run and then deleted (tree proven back to HEAD):

reading result
registered task columns ["organization_id","created_at","created_by","updated_at","updated_by","owner_id","owning_business_unit_id","id","title","status"]
registered tenancy: { enabled: false } object columns same list minus organization_id
resolveTenantFieldName(registered task) "organization_id"
resolveTenantFieldName(registered opt-out object) null

⇒ the card's core reading is confirmed, in both directions.

The fix — route 2, the durable one

Triage offered a reword or a fixture swap and declined to rule; the swap turns the sentence into an assertion, so that is what this does.

  1. A new audit_note fixture declaring tenancy: { enabled: false } carries the genuinely-unscoped pin. The pin now asserts the premise before using it: the registry WITHHELD organization_id on audit_note, with the registered task as the control that it is otherwise INJECTED. A future edit that moves the injection reddens here instead of leaving a stale sentence.
  2. The old task case is kept as its own pin — no coverage is lost — and described as what it measures: an injection-scoped object whose rows carry no organization, omitting the key on all three actions.
  3. The neighbouring invoice pin's Distinct from the case above clause is repaired in the same pass, named here rather than smuggled in: the pin above it changed, so that clause became false as a direct consequence of this edit. It now names the real distinction — injected column with the row silent, versus author-declared column with the row explicitly null.

Comment only plus one fixture, inside one test file. No runtime change; no tenant-injection behaviour touched; no test skipped, weakened or quarantined.

A claim in the card this PR did NOT inherit

The card carried one reading triage explicitly did not reproduce: "a predicate write on the registered task under an isolated wall had the batch key stamped, precisely because the object IS tenant-scoped once registered." Measured here rather than cited:

  • registered task, posture provider isolated, member context with an active organization, no recorded verdict ⇒ organizationId ABSENT (hasOwnProperty false).
  • the same write with a recorded { kind: 'organization', organizationId: 'org_acme' } verdict ⇒ key stamped org_acme.

So on this tree the batch key follows the recorded Layer 0 verdict alone, never the object's tenant-scoping — bulkEventOrganizationId reads only TenantLayer0VerdictSchema since the mirror was deleted (#15706 / #15813, ruled 2026-09-05). The reading was true of the producer that existed when it was taken and is false of this one. Nothing in the new comments asserts it. The neighbouring half of that sentence does still hold by source reading: security-plugin.ts answers objectHasOrgIdField off the registered field set, so a registered task is walled.

Verification

All at 5cc619435.

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-data-events.test.ts41 passed before, 42 passed after (one pin added).
  • Ablation — removing tenancy: { enabled: false } from the new fixture, i.e. exactly the mistake this card closes. Mutation proven on disk before measuring (anchored count 1 to 0; blob ce432926 vs HEAD blob fc764bb5), then: 1 failed | 41 passed, AssertionError: expected [ 'organization_id', ...(8) ] to not include 'organization_id' on the new pin. Restore proven after: blob back to fc764bb5 and git diff HEAD empty. No rebuild leg is owed — the mutation is in the test file itself and the subject is imported relative within the same package, so nothing resolves through dist/.
  • pnpm --filter @objectstack/objectql typecheck — green, and it really covers this file: the build config excludes **/*.test.ts, but check:test-typecheck runs tsconfig.test.json, tsc --listFiles puts this file in that program, and its exact shrink-only ledger entry (TS6133: 'warn' ... x1) held. 44 file(s) / 242 error(s) / 69 pinned signature(s) unmoved.
  • 46 derived gate families (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reads the change set itself): 44 exit 0. The two others are PREREQUISITE NOT MET (exit 3, NOT MEASURED, not red): check:dual-build-cjs-loads and check:type-check-debt --re-measure both need the whole workspace built. CI builds it; the per-package test-typecheck above is the same file under the same strictness.
  • Full repo ESLinteslint . --no-inline-config --format json: 6091 files, 0 errors, 0 warnings, exit 0. Not a narrowed run.

No changeset — skip-changeset

Measured, not assumed: packages/objectql publishes files: ["dist", "README.md", "CHANGELOG.md"], its tsup entries are src/index.ts and src/core.ts only, and neither entry reaches this file. A *.test.ts cannot enter dist/, so this diff publishes nothing from any released package — which is what the label is for, per AGENTS.md.

Clause-②: no, re-derived from this diff and not inherited: one file, packages/objectql/src/engine-data-events.test.ts, +73 / -10. No schema, no export, no accept/reject behaviour, nothing published moves.

🤖 Generated with Claude Code


Generated by Claude Code

…thheld column instead of claiming it

The `#14970` block's pin `an object that is not tenant-scoped OMITS the key on
all three actions` was written on `task` in the belief that `task` declares no
`organization_id`. `registerObject` INJECTS the kernel column
(`TENANT_SCOPE_FIELD_DEF`, gated on the injection plan's `tenant` flag), so on
the registered `task` `resolveTenantFieldName` answers `organization_id`. The
pin passed — correctly — for a different reason than it stated: the ROW carried
no organization, not the object no column.

Measured on this base: registered `task` carries `organization_id`;
`resolveTenantFieldName` = `organization_id`. An object declaring
`tenancy: { enabled: false }` has the column withheld and resolves `null`.

The sentence becomes an assertion:

  * a new `audit_note` fixture declaring `tenancy: { enabled: false }` carries
    the genuinely-unscoped pin, and asserts the column was WITHHELD (with the
    registered `task` as the control that it is otherwise INJECTED) before
    asserting the omission;
  * the old `task` case is kept as its own pin and described as what it
    measures — an injection-scoped object whose rows carry no organization;
  * the neighbouring `invoice` pin's "distinct from the case above" clause is
    repaired in the same pass, since the pin above it changed.

No runtime change, no assertion weakened, `#14970` not reopened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 6, 2026
@github-actions github-actions Bot added the size/s label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

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 a4816a79d0396d0fd10696cdf95d66e55aef92d3packageMentionDocs.

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

2 participants