|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [#10101] Unit pins for the SHARED platform-row organization resolver — the |
| 5 | + * cloud#1395 Option A ruling's artifact ("A platform row's organization is the |
| 6 | + * SUBJECT record's organization; actor context is the fallback, never the |
| 7 | + * primary"), promoted here from plugin-audit so audit stamping, the |
| 8 | + * approval-row writer and the automation-run recorder share ONE precedence. |
| 9 | + * |
| 10 | + * The four-limb precedence is pinned per limb, and the `sys_api_key` |
| 11 | + * divergence is pinned by name: `tenancy.organizationField` answers "which |
| 12 | + * column says who this row is ABOUT", `tenantField`/`organization_id` answers |
| 13 | + * "what is this object WALLED by", and the two DELIBERATELY diverge for |
| 14 | + * credential tables (#8287). Flattening that divergence — resolving the stamp |
| 15 | + * from the wall, or walling from the stamp — is the two-tables-disagree |
| 16 | + * pathology this promotion exists to end. |
| 17 | + */ |
| 18 | + |
| 19 | +import { describe, it, expect, vi } from 'vitest'; |
| 20 | + |
| 21 | +import { |
| 22 | + createFieldPresenceProbe, |
| 23 | + createRecordOrganizationResolver, |
| 24 | + resolveRecordOrganizationField, |
| 25 | +} from './record-organization.js'; |
| 26 | + |
| 27 | +/** Minimal engine double: `getSchema` over a name → definition map. */ |
| 28 | +function engineOf(defs: Record<string, any>) { |
| 29 | + return { |
| 30 | + getSchema: vi.fn((name: string) => defs[name]), |
| 31 | + }; |
| 32 | +} |
| 33 | + |
| 34 | +const hasFieldOf = (def: any) => (field: string) => |
| 35 | + def?.fields != null && Object.prototype.hasOwnProperty.call(def.fields, field); |
| 36 | + |
| 37 | +describe('resolveRecordOrganizationField — the four-limb precedence', () => { |
| 38 | + it('limb 0: a declared `tenancy.organizationField` wins over everything, the ADR-0066 opt-out included (sys_api_key)', () => { |
| 39 | + // The shipped divergent case: an UNWALLED credential table |
| 40 | + // (`enabled: false`) whose rows are still ABOUT one organization, under a |
| 41 | + // column that deliberately is NOT the tenant column. |
| 42 | + const def = { |
| 43 | + name: 'sys_api_key', |
| 44 | + tenancy: { enabled: false, organizationField: 'active_organization_id' }, |
| 45 | + fields: { id: {}, name: {}, user_id: {}, active_organization_id: {}, revoked: {} }, |
| 46 | + }; |
| 47 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBe('active_organization_id'); |
| 48 | + }); |
| 49 | + |
| 50 | + it('limb 0 guard (#5315): a declared organizationField naming a MISSING column falls through, never resolves to nothing', () => { |
| 51 | + // Missing column + disabled tenancy → limb 1 answers null (not the |
| 52 | + // phantom name, and not organization_id either). |
| 53 | + const def = { |
| 54 | + name: 'sys_api_key', |
| 55 | + tenancy: { enabled: false, organizationField: 'active_organization_id' }, |
| 56 | + fields: { id: {}, organization_id: {} }, |
| 57 | + }; |
| 58 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBeNull(); |
| 59 | + }); |
| 60 | + |
| 61 | + it('limb 1: `tenancy.enabled === false` WITHOUT an organizationField resolves null even when an org FK exists (ADR-0066)', () => { |
| 62 | + // The sys_sso_provider shape: platform-global, keeps an optional org FK, |
| 63 | + // explicitly not tenant-scoped. Stamping from the FK would hide a global |
| 64 | + // object's platform rows from the platform admin who acted. |
| 65 | + const def = { |
| 66 | + name: 'sys_sso_provider', |
| 67 | + tenancy: { enabled: false }, |
| 68 | + fields: { id: {}, organization_id: {} }, |
| 69 | + }; |
| 70 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBeNull(); |
| 71 | + }); |
| 72 | + |
| 73 | + it('limb 2: a declared `tenancy.tenantField` answers when present', () => { |
| 74 | + const def = { |
| 75 | + name: 'ws_doc', |
| 76 | + tenancy: { enabled: true, tenantField: 'workspace_id' }, |
| 77 | + fields: { id: {}, workspace_id: {}, organization_id: {} }, |
| 78 | + }; |
| 79 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBe('workspace_id'); |
| 80 | + }); |
| 81 | + |
| 82 | + it('limb 3: the canonical injected `organization_id` when nothing is declared', () => { |
| 83 | + const def = { name: 'crm_deal', fields: { id: {}, organization_id: {} } }; |
| 84 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBe('organization_id'); |
| 85 | + }); |
| 86 | + |
| 87 | + it('limb 4: no organization of its own → null (single-tenant shape)', () => { |
| 88 | + const def = { name: 'crm_deal', fields: { id: {}, amount: {} } }; |
| 89 | + expect(resolveRecordOrganizationField(def, hasFieldOf(def))).toBeNull(); |
| 90 | + expect(resolveRecordOrganizationField(undefined, () => true)).toBeNull(); |
| 91 | + expect(resolveRecordOrganizationField(null, () => true)).toBeNull(); |
| 92 | + }); |
| 93 | +}); |
| 94 | + |
| 95 | +describe('createFieldPresenceProbe', () => { |
| 96 | + it('answers from the registered schema, map and array field shapes alike, memoized per object', () => { |
| 97 | + const engine = engineOf({ |
| 98 | + map_obj: { fields: { id: {}, organization_id: {} } }, |
| 99 | + arr_obj: { fields: [{ name: 'id' }, { name: 'organization_id' }] }, |
| 100 | + }); |
| 101 | + const has = createFieldPresenceProbe(engine); |
| 102 | + expect(has('map_obj', 'organization_id')).toBe(true); |
| 103 | + expect(has('arr_obj', 'organization_id')).toBe(true); |
| 104 | + expect(has('map_obj', 'missing')).toBe(false); |
| 105 | + expect(has('nowhere', 'organization_id')).toBe(false); |
| 106 | + has('map_obj', 'id'); |
| 107 | + // one getSchema per object, not per question |
| 108 | + expect(engine.getSchema.mock.calls.filter(([n]) => n === 'map_obj')).toHaveLength(1); |
| 109 | + }); |
| 110 | + |
| 111 | + it('an engine with no getSchema reports every field absent (skip-the-stamp posture, never a throw)', () => { |
| 112 | + const has = createFieldPresenceProbe({}); |
| 113 | + expect(has('anything', 'organization_id')).toBe(false); |
| 114 | + }); |
| 115 | +}); |
| 116 | + |
| 117 | +describe('createRecordOrganizationResolver — the writers’ memoized face', () => { |
| 118 | + it('organizationOf reads the resolved column off the first candidate record that carries a non-empty value', () => { |
| 119 | + const engine = engineOf({ crm_deal: { fields: { id: {}, organization_id: {} } } }); |
| 120 | + const r = createRecordOrganizationResolver(engine); |
| 121 | + expect(r.organizationFieldFor('crm_deal')).toBe('organization_id'); |
| 122 | + expect(r.organizationOf('crm_deal', { id: 'd1', organization_id: 'org_A' })).toBe('org_A'); |
| 123 | + // precedence across candidates: first non-empty wins (live record before |
| 124 | + // trigger snapshot, result before prior state — the callers' order) |
| 125 | + expect( |
| 126 | + r.organizationOf('crm_deal', { id: 'd1', organization_id: '' }, { id: 'd1', organization_id: 'org_B' }), |
| 127 | + ).toBe('org_B'); |
| 128 | + expect(r.organizationOf('crm_deal', undefined, null, { id: 'd1' })).toBeNull(); |
| 129 | + }); |
| 130 | + |
| 131 | + it('pins the sys_api_key divergence end to end: the stamp column is active_organization_id, never the wall', () => { |
| 132 | + const engine = engineOf({ |
| 133 | + sys_api_key: { |
| 134 | + tenancy: { enabled: false, organizationField: 'active_organization_id' }, |
| 135 | + fields: { id: {}, name: {}, user_id: {}, active_organization_id: {}, revoked: {} }, |
| 136 | + }, |
| 137 | + }); |
| 138 | + const r = createRecordOrganizationResolver(engine); |
| 139 | + expect(r.organizationFieldFor('sys_api_key')).toBe('active_organization_id'); |
| 140 | + expect( |
| 141 | + r.organizationOf('sys_api_key', { id: 'k1', active_organization_id: 'org_key' }), |
| 142 | + ).toBe('org_key'); |
| 143 | + // A record carrying an `organization_id` VALUE anyway (defensive noise) |
| 144 | + // still stamps from the DECLARED column, not the canonical spelling. |
| 145 | + expect( |
| 146 | + r.organizationOf('sys_api_key', { id: 'k1', organization_id: 'org_wrong', active_organization_id: 'org_key' }), |
| 147 | + ).toBe('org_key'); |
| 148 | + }); |
| 149 | + |
| 150 | + it('degrades to null — the acting-context fallback signal — on a getSchema-less double, a throwing getSchema, and an unknown object', () => { |
| 151 | + expect(createRecordOrganizationResolver({}).organizationOf('crm_deal', { organization_id: 'org_A' })).toBeNull(); |
| 152 | + const throwing = { getSchema: () => { throw new Error('not booted'); } }; |
| 153 | + expect(createRecordOrganizationResolver(throwing).organizationOf('crm_deal', { organization_id: 'org_A' })).toBeNull(); |
| 154 | + const empty = engineOf({}); |
| 155 | + expect(createRecordOrganizationResolver(empty).organizationOf('crm_deal', { organization_id: 'org_A' })).toBeNull(); |
| 156 | + }); |
| 157 | + |
| 158 | + it('memoizes the column per object (one schema read for N writes)', () => { |
| 159 | + const engine = engineOf({ crm_deal: { fields: { id: {}, organization_id: {} } } }); |
| 160 | + const r = createRecordOrganizationResolver(engine); |
| 161 | + r.organizationOf('crm_deal', { organization_id: 'a' }); |
| 162 | + r.organizationOf('crm_deal', { organization_id: 'b' }); |
| 163 | + r.organizationFieldFor('crm_deal'); |
| 164 | + // one call from the probe's field-set read + one from the column |
| 165 | + // resolution — and no growth with further questions |
| 166 | + const calls = engine.getSchema.mock.calls.filter(([n]) => n === 'crm_deal').length; |
| 167 | + r.organizationOf('crm_deal', { organization_id: 'c' }); |
| 168 | + expect(engine.getSchema.mock.calls.filter(([n]) => n === 'crm_deal').length).toBe(calls); |
| 169 | + expect(calls).toBeLessThanOrEqual(2); |
| 170 | + }); |
| 171 | +}); |
0 commit comments