|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// [#8074] The arming guard is SHOWN TO FIRE — in both directions, on real boots. |
| 4 | +// |
| 5 | +// This file exists because of what it guards. `assertArmed` is a mechanism for |
| 6 | +// catching assertions that cannot fail; a version of it that could not itself |
| 7 | +// fail would be the defect wearing the fix's clothes, and it would be invisible |
| 8 | +// in exactly the same way (green, quiet, reported as tested). |
| 9 | +// |
| 10 | +// So every probe is measured twice, and the disarmed halves are not synthetic: |
| 11 | +// |
| 12 | +// principalArmed DISARMED on an org-LESS boot — a live reproduction of |
| 13 | +// #8023, where a fresh sign-up holds `['everyone']` and the |
| 14 | +// `org_member`-gated write floor never applies |
| 15 | +// ARMED on `bootStack(..., { orgContext: true })` |
| 16 | +// authSettingArmed DISARMED on the default auth config — a live reproduction |
| 17 | +// of #8049, where `passwordHistoryCount` is 0/undefined and |
| 18 | +// the reuse control has nothing to reject against |
| 19 | +// ARMED after the `applyConfigPatch` the fixture uses |
| 20 | +// seededArmed DISARMED on a permission set that does not exist |
| 21 | +// ARMED on `member_default`, which does |
| 22 | +// armedWhen DISARMED / ARMED on both sides of its own predicate |
| 23 | +// |
| 24 | +// The `member_default` case is not decoration: it is the control that proves |
| 25 | +// the seeded-row probe CAN find something, so its negative reading is a real |
| 26 | +// negative rather than a probe that never matches anything. |
| 27 | +// |
| 28 | +// Two positive controls guard the "it throws" cases from the cheapest possible |
| 29 | +// fake — an `assertArmed` that always threw would satisfy every rejection |
| 30 | +// assertion here, so an all-armed declaration is asserted to RESOLVE. |
| 31 | +// |
| 32 | +// Boots two stacks, uses custom boot options and mutates auth config, so it |
| 33 | +// stays out of `SHARED_SHOWCASE` (see `vitest.config.ts`). |
| 34 | + |
| 35 | +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
| 36 | +import showcaseStack from '@objectstack/example-showcase'; |
| 37 | +import { bootStack, type VerifyStack } from '@objectstack/verify'; |
| 38 | +import { |
| 39 | + assertArmed, |
| 40 | + armedWhen, |
| 41 | + authSettingArmed, |
| 42 | + principalArmed, |
| 43 | + resolveAuthzFor, |
| 44 | + seededArmed, |
| 45 | + type ArmingProbe, |
| 46 | +} from './armed.js'; |
| 47 | + |
| 48 | +/** The control #8023's fixture measures, named the way that card names it. */ |
| 49 | +const WRITE_FLOOR = 'the wildcard row-level write floor (`owner_only_writes`)'; |
| 50 | +const WRITE_FLOOR_DISARM = |
| 51 | + 'an org-less boot: no organization ⇒ no `sys_member` row ⇒ the principal never holds ' + |
| 52 | + '`org_member` ⇒ the positions-gated floor never applies. Boot with `orgContext: true`.'; |
| 53 | + |
| 54 | +/** The control #8049's fixture measures. */ |
| 55 | +const REUSE_CONTROL = "ADR-0069 D1's password-reuse rejection"; |
| 56 | +const REUSE_DISARM = |
| 57 | + '`passwordHistoryCount` defaults to 0 (off): no history is recorded, so a reuse assertion ' + |
| 58 | + 'has nothing to reject against. Arm it through `applyConfigPatch`.'; |
| 59 | + |
| 60 | +describe('[#8074] assertArmed: the guard against assertions that cannot fail', () => { |
| 61 | + /** The org-LESS stack — #8023's and #8049's disarmed shapes, booted for real. */ |
| 62 | + let orgless: VerifyStack; |
| 63 | + /** The org-BOUND stack — the same probe, armed. */ |
| 64 | + let orgbound: VerifyStack; |
| 65 | + |
| 66 | + let orglessMember: string; |
| 67 | + let orgboundMember: string; |
| 68 | + |
| 69 | + beforeAll(async () => { |
| 70 | + orgless = await bootStack(showcaseStack, {}); |
| 71 | + orgbound = await bootStack(showcaseStack, { orgContext: true }); |
| 72 | + |
| 73 | + // The first user is the seeded dev admin; a fresh sign-up is the plain |
| 74 | + // member #8023's fixture measures. |
| 75 | + await orgless.signIn(); |
| 76 | + await orgbound.signIn(); |
| 77 | + orglessMember = await orgless.signUp('armed-orgless@verify.test'); |
| 78 | + orgboundMember = await orgbound.signUp('armed-orgbound@verify.test'); |
| 79 | + }, 300_000); |
| 80 | + |
| 81 | + afterAll(async () => { |
| 82 | + await orgless?.stop?.(); |
| 83 | + await orgbound?.stop?.(); |
| 84 | + }); |
| 85 | + |
| 86 | + // ── the fixture's own integrity ─────────────────────────────────────────── |
| 87 | + // |
| 88 | + // Every reading below is worthless if the two stacks are not actually the two |
| 89 | + // shapes they are named for, so that is asserted before anything is measured. |
| 90 | + |
| 91 | + it('[integrity] the two stacks really are the org-less and org-bound shapes', async () => { |
| 92 | + const less = await resolveAuthzFor(orgless, orglessMember); |
| 93 | + const bound = await resolveAuthzFor(orgbound, orgboundMember); |
| 94 | + expect(less.userId, 'the org-less member resolved as a real principal').toBeTruthy(); |
| 95 | + expect(bound.userId, 'the org-bound member resolved as a real principal').toBeTruthy(); |
| 96 | + expect(less.positions, 'org-less: a fresh sign-up holds only the everyone anchor') |
| 97 | + .not.toContain('org_member'); |
| 98 | + expect(bound.positions, 'org-bound: the membership reconciler bound the sign-up') |
| 99 | + .toContain('org_member'); |
| 100 | + }); |
| 101 | + |
| 102 | + // ── principalArmed — instance 1, both directions ────────────────────────── |
| 103 | + |
| 104 | + describe('principalArmed reproduces #8023 and reports it', () => { |
| 105 | + const probeOn = (stack: VerifyStack, token: string): ArmingProbe => |
| 106 | + principalArmed({ |
| 107 | + stack, |
| 108 | + token, |
| 109 | + who: 'the plain member', |
| 110 | + positions: ['org_member'], |
| 111 | + control: WRITE_FLOOR, |
| 112 | + disarmedBy: WRITE_FLOOR_DISARM, |
| 113 | + }); |
| 114 | + |
| 115 | + it('reports DISARMED on an org-less boot, and names what it saw', async () => { |
| 116 | + const verdict = await probeOn(orgless, orglessMember).read(); |
| 117 | + expect(verdict.armed, 'org-less: the floor is outside this principal’s domain').toBe(false); |
| 118 | + expect(verdict.observed).toContain('missing positions'); |
| 119 | + expect(verdict.observed).toContain('org_member'); |
| 120 | + }); |
| 121 | + |
| 122 | + it('reports ARMED on `orgContext: true` — the same probe, the other direction', async () => { |
| 123 | + const verdict = await probeOn(orgbound, orgboundMember).read(); |
| 124 | + expect(verdict.armed, 'org-bound: the principal is inside the floor’s domain').toBe(true); |
| 125 | + expect(verdict.observed).toContain('org_member'); |
| 126 | + }); |
| 127 | + |
| 128 | + it('assertArmed REJECTS the disarmed stack, naming the control and the default', async () => { |
| 129 | + await expect(assertArmed([probeOn(orgless, orglessMember)])).rejects.toThrow( |
| 130 | + /this fixture is DISARMED/, |
| 131 | + ); |
| 132 | + const err = await assertArmed([probeOn(orgless, orglessMember)]).catch((e: Error) => e); |
| 133 | + expect(String(err)).toContain(WRITE_FLOOR); |
| 134 | + // The remedy, not just the symptom — the sentence the next author needs. |
| 135 | + expect(String(err)).toContain('orgContext: true'); |
| 136 | + }); |
| 137 | + |
| 138 | + it('[positive control] assertArmed RESOLVES on the armed stack', async () => { |
| 139 | + // Without this, every rejection assertion above would be satisfied by an |
| 140 | + // `assertArmed` that simply always threw. |
| 141 | + await expect(assertArmed([probeOn(orgbound, orgboundMember)])).resolves.toBeUndefined(); |
| 142 | + }); |
| 143 | + |
| 144 | + it('a principal that resolves to nothing is DISARMED, not skipped', async () => { |
| 145 | + const verdict = await principalArmed({ |
| 146 | + stack: orgbound, |
| 147 | + token: 'not-a-token-8074', |
| 148 | + who: 'a bogus credential', |
| 149 | + positions: ['org_member'], |
| 150 | + control: WRITE_FLOOR, |
| 151 | + disarmedBy: WRITE_FLOOR_DISARM, |
| 152 | + }).read(); |
| 153 | + expect(verdict.armed).toBe(false); |
| 154 | + expect(verdict.observed).toContain('missing positions'); |
| 155 | + }); |
| 156 | + }); |
| 157 | + |
| 158 | + // ── authSettingArmed — instance 2, both directions ──────────────────────── |
| 159 | + |
| 160 | + describe('authSettingArmed reproduces #8049 and reports it', () => { |
| 161 | + const probe = (stack: VerifyStack): ArmingProbe => |
| 162 | + authSettingArmed({ |
| 163 | + stack, |
| 164 | + setting: 'passwordHistoryCount', |
| 165 | + armed: (v) => Number(v) >= 1, |
| 166 | + control: REUSE_CONTROL, |
| 167 | + disarmedBy: REUSE_DISARM, |
| 168 | + }); |
| 169 | + |
| 170 | + it('reads DISARMED by default and ARMED after the patch the fixture uses', async () => { |
| 171 | + // Read the default FIRST, then arm, then restore — so this case is |
| 172 | + // atomic and cannot depend on the order vitest runs the file in. |
| 173 | + const before = await probe(orgless).read(); |
| 174 | + expect(before.armed, 'the platform default leaves the reuse control off').toBe(false); |
| 175 | + expect(before.observed).toContain('passwordHistoryCount='); |
| 176 | + |
| 177 | + const auth = await orgless.kernel.getServiceAsync<any>('auth'); |
| 178 | + try { |
| 179 | + auth.applyConfigPatch({ passwordHistoryCount: 3 }); |
| 180 | + const after = await probe(orgless).read(); |
| 181 | + expect(after.armed, 'the same seam the settings service writes arms it').toBe(true); |
| 182 | + expect(after.observed).toContain('passwordHistoryCount=3'); |
| 183 | + await expect(assertArmed([probe(orgless)])).resolves.toBeUndefined(); |
| 184 | + } finally { |
| 185 | + auth.applyConfigPatch({ passwordHistoryCount: undefined }); |
| 186 | + } |
| 187 | + |
| 188 | + const restored = await probe(orgless).read(); |
| 189 | + expect(restored.armed, 'and the restore really disarmed it again').toBe(false); |
| 190 | + }); |
| 191 | + |
| 192 | + it('assertArmed REJECTS the default config, naming the 0 default', async () => { |
| 193 | + const err = await assertArmed([probe(orgless)]).catch((e: Error) => e); |
| 194 | + expect(String(err)).toContain(REUSE_CONTROL); |
| 195 | + expect(String(err)).toContain('defaults to 0'); |
| 196 | + }); |
| 197 | + |
| 198 | + it('a stack with no readable auth config is DISARMED whatever the predicate says', async () => { |
| 199 | + // A predicate that would call `undefined` armed must not be able to turn |
| 200 | + // "the service is gone" into a pass. |
| 201 | + const verdict = await authSettingArmed({ |
| 202 | + stack: { kernel: { getServiceAsync: async () => undefined } } as unknown as VerifyStack, |
| 203 | + setting: 'passwordHistoryCount', |
| 204 | + armed: (v) => v === undefined, |
| 205 | + control: REUSE_CONTROL, |
| 206 | + disarmedBy: REUSE_DISARM, |
| 207 | + }).read(); |
| 208 | + expect(verdict.armed).toBe(false); |
| 209 | + expect(verdict.observed).toContain("no 'auth' service resolved"); |
| 210 | + }); |
| 211 | + }); |
| 212 | + |
| 213 | + // ── seededArmed — the row a control rides on ────────────────────────────── |
| 214 | + |
| 215 | + describe('seededArmed', () => { |
| 216 | + it('[control] finds a set that really seeded — so its negative reading is a real negative', async () => { |
| 217 | + const verdict = await seededArmed({ |
| 218 | + stack: orgless, |
| 219 | + object: 'sys_permission_set', |
| 220 | + where: { name: 'member_default' }, |
| 221 | + control: 'the platform baseline permission set', |
| 222 | + disarmedBy: 'a permission set that failed to seed grants the principal nothing', |
| 223 | + }).read(); |
| 224 | + expect(verdict.armed).toBe(true); |
| 225 | + expect(verdict.observed).toContain('is seeded'); |
| 226 | + }); |
| 227 | + |
| 228 | + it('reports DISARMED for a set that does not exist', async () => { |
| 229 | + const verdict = await seededArmed({ |
| 230 | + stack: orgless, |
| 231 | + object: 'sys_permission_set', |
| 232 | + where: { name: 'no_such_set_8074' }, |
| 233 | + control: 'an app-declared permission set', |
| 234 | + disarmedBy: 'a set that failed to seed grants the principal nothing', |
| 235 | + }).read(); |
| 236 | + expect(verdict.armed).toBe(false); |
| 237 | + expect(verdict.observed).toContain('never seeded'); |
| 238 | + }); |
| 239 | + |
| 240 | + it('reports DISARMED when the row exists but fails its own predicate', async () => { |
| 241 | + const verdict = await seededArmed({ |
| 242 | + stack: orgless, |
| 243 | + object: 'sys_permission_set', |
| 244 | + where: { name: 'member_default' }, |
| 245 | + armed: () => false, |
| 246 | + control: 'a seeded row that must also carry something', |
| 247 | + disarmedBy: 'the row seeded but not the part the control rides on', |
| 248 | + }).read(); |
| 249 | + expect(verdict.armed).toBe(false); |
| 250 | + expect(verdict.observed).toContain('fails its own arming predicate'); |
| 251 | + }); |
| 252 | + }); |
| 253 | + |
| 254 | + // ── armedWhen — the general probe, both directions ──────────────────────── |
| 255 | + |
| 256 | + describe('armedWhen', () => { |
| 257 | + const generic = (armed: (v: number) => boolean) => |
| 258 | + armedWhen<number>({ |
| 259 | + control: 'a generic control', |
| 260 | + disarmedBy: 'a generic default', |
| 261 | + observe: () => 7, |
| 262 | + armed, |
| 263 | + describe: (v) => `observed ${v}`, |
| 264 | + }); |
| 265 | + |
| 266 | + it('is ARMED when its predicate holds and DISARMED when it does not', async () => { |
| 267 | + expect((await generic((v) => v === 7).read()).armed).toBe(true); |
| 268 | + expect((await generic((v) => v === 8).read()).armed).toBe(false); |
| 269 | + }); |
| 270 | + |
| 271 | + it('treats a non-boolean predicate result as DISARMED', async () => { |
| 272 | + // "not proven armed" is the safe direction for an ambiguous reading. |
| 273 | + const sloppy = armedWhen<number>({ |
| 274 | + control: 'a control judged by a truthy non-boolean', |
| 275 | + disarmedBy: 'a predicate that does not return a real boolean', |
| 276 | + observe: () => 7, |
| 277 | + armed: ((v: number) => v) as unknown as (v: number) => boolean, |
| 278 | + }); |
| 279 | + expect((await sloppy.read()).armed).toBe(false); |
| 280 | + }); |
| 281 | + }); |
| 282 | + |
| 283 | + // ── the guard's own vacuity refusals ────────────────────────────────────── |
| 284 | + |
| 285 | + describe('the guard refuses its own vacuous spellings', () => { |
| 286 | + it('assertArmed([]) throws — an empty declaration would certify everything', async () => { |
| 287 | + await expect(assertArmed([])).rejects.toThrow(/arming declaration is EMPTY/); |
| 288 | + }); |
| 289 | + |
| 290 | + it('principalArmed with nothing required throws at CONSTRUCTION', () => { |
| 291 | + expect(() => |
| 292 | + principalArmed({ |
| 293 | + stack: orgless, |
| 294 | + token: orglessMember, |
| 295 | + who: 'nobody in particular', |
| 296 | + control: WRITE_FLOOR, |
| 297 | + disarmedBy: WRITE_FLOOR_DISARM, |
| 298 | + }), |
| 299 | + ).toThrow(/can never fail/); |
| 300 | + }); |
| 301 | + |
| 302 | + it('seededArmed with an empty `where` throws at CONSTRUCTION', () => { |
| 303 | + expect(() => |
| 304 | + seededArmed({ |
| 305 | + stack: orgless, |
| 306 | + object: 'sys_permission_set', |
| 307 | + where: {}, |
| 308 | + control: 'anything', |
| 309 | + disarmedBy: 'anything', |
| 310 | + }), |
| 311 | + ).toThrow(/matches any row/); |
| 312 | + }); |
| 313 | + |
| 314 | + it('a probe whose read THROWS counts as disarmed, never as fine', async () => { |
| 315 | + const broken: ArmingProbe = { |
| 316 | + control: 'a control whose probe cannot read the stack', |
| 317 | + disarmedBy: 'an unreadable precondition', |
| 318 | + read: async () => { |
| 319 | + throw new Error('the service went away'); |
| 320 | + }, |
| 321 | + }; |
| 322 | + const err = await assertArmed([broken]).catch((e: Error) => e); |
| 323 | + expect(String(err)).toContain('the arming probe itself threw'); |
| 324 | + expect(String(err)).toContain('the service went away'); |
| 325 | + }); |
| 326 | + |
| 327 | + it('names EVERY disarmed control, not just the first', async () => { |
| 328 | + const err = await assertArmed([ |
| 329 | + principalArmed({ |
| 330 | + stack: orgless, |
| 331 | + token: orglessMember, |
| 332 | + who: 'the plain member', |
| 333 | + positions: ['org_member'], |
| 334 | + control: WRITE_FLOOR, |
| 335 | + disarmedBy: WRITE_FLOOR_DISARM, |
| 336 | + }), |
| 337 | + authSettingArmed({ |
| 338 | + stack: orgless, |
| 339 | + setting: 'passwordHistoryCount', |
| 340 | + armed: (v) => Number(v) >= 1, |
| 341 | + control: REUSE_CONTROL, |
| 342 | + disarmedBy: REUSE_DISARM, |
| 343 | + }), |
| 344 | + ]).catch((e: Error) => e); |
| 345 | + expect(String(err)).toContain(WRITE_FLOOR); |
| 346 | + expect(String(err)).toContain(REUSE_CONTROL); |
| 347 | + expect(String(err)).toContain('2 of 2 control(s)'); |
| 348 | + }); |
| 349 | + |
| 350 | + it('[positive control] a fully armed multi-probe declaration RESOLVES', async () => { |
| 351 | + // The counterweight to every rejection case above. |
| 352 | + await expect( |
| 353 | + assertArmed([ |
| 354 | + principalArmed({ |
| 355 | + stack: orgbound, |
| 356 | + token: orgboundMember, |
| 357 | + who: 'the org-bound member', |
| 358 | + positions: ['org_member'], |
| 359 | + control: WRITE_FLOOR, |
| 360 | + disarmedBy: WRITE_FLOOR_DISARM, |
| 361 | + }), |
| 362 | + seededArmed({ |
| 363 | + stack: orgbound, |
| 364 | + object: 'sys_permission_set', |
| 365 | + where: { name: 'member_default' }, |
| 366 | + control: 'the platform baseline permission set', |
| 367 | + disarmedBy: 'a set that failed to seed grants the principal nothing', |
| 368 | + }), |
| 369 | + ]), |
| 370 | + ).resolves.toBeUndefined(); |
| 371 | + }); |
| 372 | + }); |
| 373 | +}); |
0 commit comments