Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,27 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
* ONLY read refusals whose log frames may be withheld here.
*
* `beforeEach` boots a kernel with no datasource and attaches sqlite late, then
* syncs exactly four objects. Every approval decision below therefore probes
* the six authz tables `resolveUserAuthzGrants` reads plus
* `sys_approval_delegation`, which `ApprovalService.lookupActiveDelegation`
* reads best-effort on each decision. All are fail-soft, so the reads are
* EXPECTED — but the driver and the engine each log the fault on the way out.
* syncs its objects. Every approval decision below probes `sys_organization`
* (`ObjectQL.probeInstallOrganizations`) and `sys_approval_delegation`, which
* `ApprovalService.lookupActiveDelegation` reads best-effort on each decision.
* Both are fail-soft, so the reads are EXPECTED — but the driver and the engine
* each log the fault on the way out.
*
* ⛔ MEASURED, not copied from the probers' source: a run of this file at
* `logger: { level: 'info' }` emitted 25 `refused a read on '<t>'` driver lines
* and 25 matching `ERROR Find operation failed` engine frames — sys_user 5 /
* sys_member 4 / sys_position 4 / sys_user_position 4 /
* sys_user_permission_set 4 / sys_organization 2 / sys_approval_delegation 2.
* ⚠️ [#17985] The five authz tables `resolveUserAuthzGrants` reads USED TO BE on
* this list. They are not any more, because this fixture now provisions them
* (see `authzResolverObjects` below) — which is the outcome this channel's own
* contract names: "a table that started resolving means the fixture now
* provisions it". Being fail-soft, their refusal was not just noise: `tryFind`
* answers `[]` for a missing table, so every approval decision here resolved
* its grants from reads that never happened. ⛔ The count fell because the
* reads SUCCEED, not because anything was quietened.
*
* ⛔ MEASURED, not copied from the probers' source. At `225197cdb`-era HEAD,
* before this fixture provisioned anything, a run emitted 25 `refused a read on
* '<t>'` driver lines and 25 matching engine frames — sys_user 5 / sys_member 4
* / sys_position 4 / sys_user_position 4 / sys_user_permission_set 4 /
* sys_organization 2 / sys_approval_delegation 2. The 21 authz ones are gone;
* the remaining two channels are the ones declared below.
*
* ## Why a capture instead of the blanket `silent` this replaces
*
Expand All @@ -87,15 +97,102 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
* happen. A capture nobody asserts is a mute.
*/
const EXPECTED_ABSENT_PROBE_TABLES = [
'sys_user',
'sys_member',
'sys_user_position',
'sys_user_permission_set',
'sys_position',
'sys_organization',
'sys_approval_delegation',
] as const;

/**
* ⚠️ [#17985] The authz objects a real approval decision makes core's
* `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
* `@objectstack/plugin-security`; they are spelled LOCALLY here, with only the
* columns that resolver reads, so this fixture adds no dependency edge onto
* either package (the `find-envelope-limb-removal.test.ts` /
* `signup-existing-address-refusal.test.ts` precedent, applied by PR #17982 to
* the two `plugin-auth` fixtures that carried the same defect).
*
* Columns, and why each one is here — every other column of the real objects is
* deliberately absent, because no read on this path touches it:
* `sys_user` id (filter), email (the RLS owner-email fallback)
* `sys_member` user_id / organization_id (filters), role
* `sys_user_position` user_id (filter), position, organization_id
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
* `sys_position` name (filter), id, active (`isRowActive`),
* organization_id (the driver's tenant scope)
*
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
* resolver reaches them only once a `sys_position` row resolves and a
* permission-set id is collected, and these cases seed neither — registering
* them would add fixture surface no read in this file touches. The ADR-0091
* validity columns are absent for the reason `sys_member` lacks them today:
* `isGrantActive` reads an absent bound as unbounded, so declaring them would
* change no verdict here.
*/
const authzResolverObjects = [
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_user',
label: 'User',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
email: { name: 'email', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_member',
label: 'Member',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
role: { name: 'role', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_position',
label: 'User Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
position: { name: 'position', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_permission_set',
label: 'User Permission Set',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
permission_set_id: { name: 'permission_set_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_position',
label: 'Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
name: { name: 'name', type: 'text' as const },
active: { name: 'active', type: 'boolean' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
] as const;

/** [#11081] Shared by both kernels this file boots; asserted once in `afterAll`. */
const noise = captureExpectedReadRefusals([...EXPECTED_ABSENT_PROBE_TABLES]);

Expand Down Expand Up @@ -207,6 +304,12 @@ describe('an approval decision cascades as the deciding user (#3783)', () => {
for (const def of [opportunity, SysApprovalRequest, SysApprovalAction, SysApprovalApprover]) {
objectql.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
// [#17985] The authz resolver's own reads, so a decision resolves its
// grants from reads that HAPPEN rather than from `tryFind`'s empty answer
// for a missing table.
for (const o of authzResolverObjects) {
objectql.registry.registerObject(o.def as any, o.owner);
}
// Real DDL for all four objects — including the three sys_approval_* tables
// the ApprovalService writes through.
await objectql.syncSchemas();
Expand Down
102 changes: 102 additions & 0 deletions packages/services/service-automation/src/paused-run-visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,103 @@ describe('#8050 merging a third source: no duplicates, and a stated precedence',
* `where: { status: 'paused' }` scan the DB-backed store actually issues. That
* is where "works against a Map, empty against a table" would hide.
*/
/**
* ⚠️ [#17985] The authz objects a write here makes core's
* `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
* `@objectstack/plugin-security`; they are spelled LOCALLY, with only the
* columns that resolver reads, so this fixture adds no dependency edge onto
* either package (the `find-envelope-limb-removal.test.ts` precedent, applied
* by PR #17982 to the two `plugin-auth` fixtures with the same defect).
*
* Without them the driver REFUSED every one of those reads — measured at
* `2f1a6f696`: 5 `refused a read on` driver lines, one per table.
* `tryFind` classifies a missing table as "not provisioned" and answers `[]`,
* so nothing went red: the assertions below passed over grant reads that never
* happened. ⛔ Registering the tables is what makes those reads SUCCEED; the
* fix is not to quieten the line.
*
* Columns, and why each is here — every other column of the real objects is
* deliberately absent, because no read on this path touches it:
* `sys_user` id (filter), email (the RLS owner-email fallback)
* `sys_member` user_id / organization_id (filters), role
* `sys_user_position` user_id (filter), position, organization_id
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
* `sys_position` name (filter), id, active (`isRowActive`),
* organization_id (the driver's tenant scope)
*
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
* resolver reaches them only once a `sys_position` row resolves and a
* permission-set id is collected, and nothing here seeds either. The ADR-0091
* validity columns are absent for the reason `sys_member` lacks them today:
* `isGrantActive` reads an absent bound as unbounded, so declaring them would
* change no verdict here.
*/
const authzResolverObjects = [
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_user',
label: 'User',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
email: { name: 'email', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_member',
label: 'Member',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
role: { name: 'role', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_position',
label: 'User Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
position: { name: 'position', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_permission_set',
label: 'User Permission Set',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
permission_set_id: { name: 'permission_set_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_position',
label: 'Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
name: { name: 'name', type: 'text' as const },
active: { name: 'active', type: 'boolean' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
] as const;

describe('#8050 cold boot over the same sqlite file (full stack)', () => {
let dir: string | undefined;
const kernels: ObjectKernel[] = [];
Expand Down Expand Up @@ -444,6 +541,11 @@ describe('#8050 cold boot over the same sqlite file (full stack)', () => {
});
await driver.connect();
ql.registerDriver(driver, true);
// [#17985] The authz resolver's own reads, registered before the sync so
// they are provisioned rather than refused.
for (const o of authzResolverObjects) {
(ql as any).registry.registerObject(o.def as never, o.owner);
}
await ql.syncSchemas();

const automation = kernel.getService<AutomationEngine>('automation');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,104 @@ function makeDriver(): any {
* top-level key. That is the falsification candidate, and it cannot be tried
* on a payload of scalars.
*/
/**
* ⚠️ [#17985] The authz objects a write here makes core's
* `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
* `@objectstack/plugin-security`; they are spelled LOCALLY, with only the
* columns that resolver reads, so this fixture adds no dependency edge onto
* either package (the `find-envelope-limb-removal.test.ts` precedent, applied
* by PR #17982 to the two `plugin-auth` fixtures with the same defect).
*
* Without them the driver REFUSED every one of those reads — measured at
* `2f1a6f696`: 10 `refused a read on` driver lines, two per table across this
* file's one real-sqlite case.
* `tryFind` classifies a missing table as "not provisioned" and answers `[]`,
* so nothing went red: the assertions below passed over grant reads that never
* happened. ⛔ Registering the tables is what makes those reads SUCCEED; the
* fix is not to quieten the line.
*
* Columns, and why each is here — every other column of the real objects is
* deliberately absent, because no read on this path touches it:
* `sys_user` id (filter), email (the RLS owner-email fallback)
* `sys_member` user_id / organization_id (filters), role
* `sys_user_position` user_id (filter), position, organization_id
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
* `sys_position` name (filter), id, active (`isRowActive`),
* organization_id (the driver's tenant scope)
*
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
* resolver reaches them only once a `sys_position` row resolves and a
* permission-set id is collected, and nothing here seeds either. The ADR-0091
* validity columns are absent for the reason `sys_member` lacks them today:
* `isGrantActive` reads an absent bound as unbounded, so declaring them would
* change no verdict here.
*/
const authzResolverObjects = [
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_user',
label: 'User',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
email: { name: 'email', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-auth',
def: {
name: 'sys_member',
label: 'Member',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
role: { name: 'role', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_position',
label: 'User Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
position: { name: 'position', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_user_permission_set',
label: 'User Permission Set',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
user_id: { name: 'user_id', type: 'text' as const },
permission_set_id: { name: 'permission_set_id', type: 'text' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
{
owner: '@objectstack/plugin-security',
def: {
name: 'sys_position',
label: 'Position',
fields: {
id: { name: 'id', type: 'text' as const, primaryKey: true },
name: { name: 'name', type: 'text' as const },
active: { name: 'active', type: 'boolean' as const },
organization_id: { name: 'organization_id', type: 'text' as const },
},
},
},
] as const;

function registerObjects(registry: TestObjectRegistry, object: string): void {
registry.registerObject({
name: object, label: object,
Expand Down Expand Up @@ -793,6 +891,11 @@ describe('[#15356/#14744] S5 on the real SQL driver — the mutation reaches no
await driver.connect();
objectql.registerDriver(driver, true);
registerObjects(objectql.registry as unknown as TestObjectRegistry, 'sq');
// [#17985] The authz resolver's own reads, registered before the sync so
// they are provisioned rather than refused.
for (const o of authzResolverObjects) {
(objectql.registry as unknown as TestObjectRegistry).registerObject(o.def as never, o.owner);
}
await objectql.syncSchemas();

objectql.registerFunction('sq_mutate_nested', async (args: any) => {
Expand Down
Loading
Loading