Skip to content

Commit c56115b

Browse files
fix(dogfood): the audit-capability fixture's grant writes carry their holder's organization (#24805)
Refs #24780 Clause-②: no **This PR repairs row 1 of the card (`Dogfood Regression Gate (1/3)`). Row 2 (`Console Pin Gate`) belongs to #24749 and is not touched.** The card stays open for the PM to settle after this lands. ## What changed One file: `packages/qa/dogfood/test/audit-log-audit-capability.dogfood.test.ts`. Its two `sys_user_permission_set` grant inserts ran in system context with no organization. Each now carries its holder's organization on the execution context (`{ isSystem: true, tenantId: ORG }`): - **The walled case** (`[#21260] the ledger audit capability is bounded by its holder's organization (scope: org)`, `multiTenant: 'posture-only'`, so the `isolated` posture): the grant to the first owner carries `org.a`, the organization that owner created earlier in the same `beforeAll`. - **The first describe** (a `single` boot): the `grant` helper reads the holder's one membership in `sys_member` and carries that organization (the Default Organization on that boot). The helper asserts exactly one non-empty membership. Without that check, an empty id would fall back to derivation and pass without testing anything. No assertion of any case changed. `resolveSystemInsertOrganization` and the walled lift stay as they are. **Why `tenantId` on the context rather than `organization_id` on the row:** the refusal names the context form first ("pass it on the execution context (`{ context: { isSystem: true, tenantId: ORGANIZATION_ID } }`), or set organization_id on the record itself"). It is also the form #24667 used for its own two walled fixtures (`builtin-positions.boot.test.ts`, `grant-holder-membership-refusal.test.ts`). **The file's other system write**, the bulk `sys_audit_log` insert in the first describe, is not this shape and is unchanged. The ledger declares `systemFields: { tenant: false }` (ADR-0131 D7: no organization column), so `resolveTenantFieldName` answers null and the rule does not apply to that object. Its `single` boot would derive an organization anyway. The file makes no other `ql.insert` / update / delete calls. ## Evidence All runs used `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/audit-log-audit-capability.dogfood.test.ts`, after building the `@objectstack/dogfood^...` closure at the same commit. | tree | result | |---|---| | `f7005cbd8` (the parent of #24667's `fc34d000fc`) | `Test Files 1 passed (1)` · `Tests 13 passed (13)` | | `fc34d000fc` (#24667, the walled lift) | `Test Files 1 failed (1)` · `Tests 10 passed \| 3 skipped (13)`, `SystemWriteOrganizationRequiredError: Insert on 'sys_user_permission_set' was REFUSED … this install runs the 'isolated' tenancy posture` | | `74f78159a` (this branch's base, `main` before the fix) | the same red: `1 failed`, `10 passed \| 3 skipped`, the same refusal, at `_ObjectQL.resolveSystemInsertOrganization` via `plugin-sharing` / `plugin-security` middleware | | `187a247d0` (this head: the fix plus `main` at `46bb5e3e5`) | `Test Files 1 passed (1)` · `Tests 13 passed (13)` | The bisect pair confirms #24667 as the cause. The case's own subject is unchanged: the holder case still reads `2` rows about its own organization's deleted record and `0` about the other's. That reading needs the grant (now stamped `org.a`) to be honoured for the holder. The file runs in exactly one vitest project, `isolated` (it is not in `SHARED_SHOWCASE`). The gate runs `pnpm turbo run test --filter=@objectstack/dogfood` with `OS_TEST_SHARD=k/3`. The boot log shows the file's two boots at posture `single` and posture `isolated`. No boot in this file uses `group`. ## Gates - Local, on `187a247d0`: the 55 commands of `node scripts/pm/dispatch-gates.mjs --commands` (narrowed to this diff) all exit 0. `--ran` reconciles them as `55 derived, 55 run, 0 NOT-MEASURED, 0 UNRUN`. `pnpm lint` (`eslint . --no-inline-config`): exit 0, no findings. - `pnpm --filter @objectstack/dogfood run typecheck`: exit 0. The tsc program lists this file (`--listFilesOnly`, 1 hit). - `Dogfood Regression Gate` does not run on `pull_request` or `merge_group`, so its first CI reading of this change is `main`'s push battery after the merge. The local red-then-green above is the pre-merge reading. ## Changeset `skip-changeset`: the diff is one test file in `@objectstack/dogfood`, a `private: true` package that publishes nothing. No published package's `files[]` changes. ## Acceptance notes - The dispatch expected `group` and `single` runs of the same lines. Measured: the vitest projects are `shared-showcase` and `isolated`, and the file's boots are `single` (first describe) and `isolated` (second). There is no `group` boot here. Both grant lines answer under the boot they run in: `10 passed` before the lift and `13 passed` after this fix. --- _Generated by [Claude Code](https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab98901 commit c56115b

1 file changed

Lines changed: 19 additions & 2 deletions

File tree

‎packages/qa/dogfood/test/audit-log-audit-capability.dogfood.test.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,23 @@ describe('[#21260] the ledger audit capability exempts its holder from the paren
180180
token.admin = await stack.signIn();
181181
const userIdOf = async (email: string) =>
182182
String((await ql.findOne('sys_user', { where: { email }, context: { ...SYS } }))?.id ?? '');
183+
// A grant is a system write on a tenant-scoped object, so it carries its
184+
// holder's organization the way a session write does (ADR-0131 D9): the one
185+
// organization the holder is a member of, the Default Organization here.
186+
const organizationOf = async (userId: string) => {
187+
const memberships: Row[] = await ql.find('sys_member', { where: { user_id: userId }, context: { ...SYS } });
188+
const organizations = memberships.map((m) => String(m.organization_id ?? ''));
189+
expect(organizations, `the organizations user ${userId} is a member of`).toHaveLength(1);
190+
expect(organizations[0], `the organization of user ${userId}`).toBeTruthy();
191+
return organizations[0];
192+
};
183193
const grant = async (email: string, setName: string) => {
184194
const set = await catalogPermissionSet(ql, setName);
185195
expect(set, `fixture permission set ${setName} served by the security catalog`).toBeTruthy();
186-
await ql.insert('sys_user_permission_set', { user_id: await userIdOf(email), permission_set: set.name }, { context: { ...SYS } });
196+
const userId = await userIdOf(email);
197+
await ql.insert('sys_user_permission_set', { user_id: userId, permission_set: set.name }, {
198+
context: { ...SYS, tenantId: await organizationOf(userId) },
199+
});
187200
};
188201
for (const who of ['holder', 'reader', 'writer'] as const) {
189202
const email = `alc-${who}@verify.test`;
@@ -429,7 +442,11 @@ describe('[#21260] the ledger audit capability is bounded by its holder’s orga
429442
}
430443
const userA = await ql.findOne('sys_user', { where: { email: 'alc-org-a@verify.test' }, context: { ...SYS } });
431444
expect(await catalogPermissionSet(ql, capOnly.name), 'fixture permission set served by the security catalog').toBeTruthy();
432-
await ql.insert('sys_user_permission_set', { user_id: userA.id, permission_set: capOnly.name }, { context: { ...SYS } });
445+
// The grant carries its holder's organization: under the wall no
446+
// organization is derivable, so a system write names its own (ADR-0131 D9).
447+
await ql.insert('sys_user_permission_set', { user_id: userA.id, permission_set: capOnly.name }, {
448+
context: { ...SYS, tenantId: org.a },
449+
});
433450
for (const who of ['a', 'b'] as const) {
434451
const res = await stack.apiAs(tok[who], 'POST', `/data/${OBJ}`, { name: `gone ${who}` });
435452
expect(res.status).toBeLessThan(300);

0 commit comments

Comments
 (0)