From 58c0d5344479c16d722c2302cb4ff0d2c461e8e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 10:48:34 +0000 Subject: [PATCH 1/2] =?UTF-8?q?test(qa):=20assertArmed=20=E2=80=94=20fixtu?= =?UTF-8?q?res=20prove=20the=20control=20under=20test=20is=20engaged=20(#8?= =?UTF-8?q?074)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qa/dogfood/test/armed.dogfood.test.ts | 373 +++++++++++++++++ packages/qa/dogfood/test/armed.ts | 374 ++++++++++++++++++ 2 files changed, 747 insertions(+) create mode 100644 packages/qa/dogfood/test/armed.dogfood.test.ts create mode 100644 packages/qa/dogfood/test/armed.ts diff --git a/packages/qa/dogfood/test/armed.dogfood.test.ts b/packages/qa/dogfood/test/armed.dogfood.test.ts new file mode 100644 index 0000000000..d84a006516 --- /dev/null +++ b/packages/qa/dogfood/test/armed.dogfood.test.ts @@ -0,0 +1,373 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#8074] The arming guard is SHOWN TO FIRE — in both directions, on real boots. +// +// This file exists because of what it guards. `assertArmed` is a mechanism for +// catching assertions that cannot fail; a version of it that could not itself +// fail would be the defect wearing the fix's clothes, and it would be invisible +// in exactly the same way (green, quiet, reported as tested). +// +// So every probe is measured twice, and the disarmed halves are not synthetic: +// +// principalArmed DISARMED on an org-LESS boot — a live reproduction of +// #8023, where a fresh sign-up holds `['everyone']` and the +// `org_member`-gated write floor never applies +// ARMED on `bootStack(..., { orgContext: true })` +// authSettingArmed DISARMED on the default auth config — a live reproduction +// of #8049, where `passwordHistoryCount` is 0/undefined and +// the reuse control has nothing to reject against +// ARMED after the `applyConfigPatch` the fixture uses +// seededArmed DISARMED on a permission set that does not exist +// ARMED on `member_default`, which does +// armedWhen DISARMED / ARMED on both sides of its own predicate +// +// The `member_default` case is not decoration: it is the control that proves +// the seeded-row probe CAN find something, so its negative reading is a real +// negative rather than a probe that never matches anything. +// +// Two positive controls guard the "it throws" cases from the cheapest possible +// fake — an `assertArmed` that always threw would satisfy every rejection +// assertion here, so an all-armed declaration is asserted to RESOLVE. +// +// Boots two stacks, uses custom boot options and mutates auth config, so it +// stays out of `SHARED_SHOWCASE` (see `vitest.config.ts`). + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import showcaseStack from '@objectstack/example-showcase'; +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { + assertArmed, + armedWhen, + authSettingArmed, + principalArmed, + resolveAuthzFor, + seededArmed, + type ArmingProbe, +} from './armed.js'; + +/** The control #8023's fixture measures, named the way that card names it. */ +const WRITE_FLOOR = 'the wildcard row-level write floor (`owner_only_writes`)'; +const WRITE_FLOOR_DISARM = + 'an org-less boot: no organization ⇒ no `sys_member` row ⇒ the principal never holds ' + + '`org_member` ⇒ the positions-gated floor never applies. Boot with `orgContext: true`.'; + +/** The control #8049's fixture measures. */ +const REUSE_CONTROL = "ADR-0069 D1's password-reuse rejection"; +const REUSE_DISARM = + '`passwordHistoryCount` defaults to 0 (off): no history is recorded, so a reuse assertion ' + + 'has nothing to reject against. Arm it through `applyConfigPatch`.'; + +describe('[#8074] assertArmed: the guard against assertions that cannot fail', () => { + /** The org-LESS stack — #8023's and #8049's disarmed shapes, booted for real. */ + let orgless: VerifyStack; + /** The org-BOUND stack — the same probe, armed. */ + let orgbound: VerifyStack; + + let orglessMember: string; + let orgboundMember: string; + + beforeAll(async () => { + orgless = await bootStack(showcaseStack, {}); + orgbound = await bootStack(showcaseStack, { orgContext: true }); + + // The first user is the seeded dev admin; a fresh sign-up is the plain + // member #8023's fixture measures. + await orgless.signIn(); + await orgbound.signIn(); + orglessMember = await orgless.signUp('armed-orgless@verify.test'); + orgboundMember = await orgbound.signUp('armed-orgbound@verify.test'); + }, 300_000); + + afterAll(async () => { + await orgless?.stop?.(); + await orgbound?.stop?.(); + }); + + // ── the fixture's own integrity ─────────────────────────────────────────── + // + // Every reading below is worthless if the two stacks are not actually the two + // shapes they are named for, so that is asserted before anything is measured. + + it('[integrity] the two stacks really are the org-less and org-bound shapes', async () => { + const less = await resolveAuthzFor(orgless, orglessMember); + const bound = await resolveAuthzFor(orgbound, orgboundMember); + expect(less.userId, 'the org-less member resolved as a real principal').toBeTruthy(); + expect(bound.userId, 'the org-bound member resolved as a real principal').toBeTruthy(); + expect(less.positions, 'org-less: a fresh sign-up holds only the everyone anchor') + .not.toContain('org_member'); + expect(bound.positions, 'org-bound: the membership reconciler bound the sign-up') + .toContain('org_member'); + }); + + // ── principalArmed — instance 1, both directions ────────────────────────── + + describe('principalArmed reproduces #8023 and reports it', () => { + const probeOn = (stack: VerifyStack, token: string): ArmingProbe => + principalArmed({ + stack, + token, + who: 'the plain member', + positions: ['org_member'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }); + + it('reports DISARMED on an org-less boot, and names what it saw', async () => { + const verdict = await probeOn(orgless, orglessMember).read(); + expect(verdict.armed, 'org-less: the floor is outside this principal’s domain').toBe(false); + expect(verdict.observed).toContain('missing positions'); + expect(verdict.observed).toContain('org_member'); + }); + + it('reports ARMED on `orgContext: true` — the same probe, the other direction', async () => { + const verdict = await probeOn(orgbound, orgboundMember).read(); + expect(verdict.armed, 'org-bound: the principal is inside the floor’s domain').toBe(true); + expect(verdict.observed).toContain('org_member'); + }); + + it('assertArmed REJECTS the disarmed stack, naming the control and the default', async () => { + await expect(assertArmed([probeOn(orgless, orglessMember)])).rejects.toThrow( + /this fixture is DISARMED/, + ); + const err = await assertArmed([probeOn(orgless, orglessMember)]).catch((e: Error) => e); + expect(String(err)).toContain(WRITE_FLOOR); + // The remedy, not just the symptom — the sentence the next author needs. + expect(String(err)).toContain('orgContext: true'); + }); + + it('[positive control] assertArmed RESOLVES on the armed stack', async () => { + // Without this, every rejection assertion above would be satisfied by an + // `assertArmed` that simply always threw. + await expect(assertArmed([probeOn(orgbound, orgboundMember)])).resolves.toBeUndefined(); + }); + + it('a principal that resolves to nothing is DISARMED, not skipped', async () => { + const verdict = await principalArmed({ + stack: orgbound, + token: 'not-a-token-8074', + who: 'a bogus credential', + positions: ['org_member'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }).read(); + expect(verdict.armed).toBe(false); + expect(verdict.observed).toContain('missing positions'); + }); + }); + + // ── authSettingArmed — instance 2, both directions ──────────────────────── + + describe('authSettingArmed reproduces #8049 and reports it', () => { + const probe = (stack: VerifyStack): ArmingProbe => + authSettingArmed({ + stack, + setting: 'passwordHistoryCount', + armed: (v) => Number(v) >= 1, + control: REUSE_CONTROL, + disarmedBy: REUSE_DISARM, + }); + + it('reads DISARMED by default and ARMED after the patch the fixture uses', async () => { + // Read the default FIRST, then arm, then restore — so this case is + // atomic and cannot depend on the order vitest runs the file in. + const before = await probe(orgless).read(); + expect(before.armed, 'the platform default leaves the reuse control off').toBe(false); + expect(before.observed).toContain('passwordHistoryCount='); + + const auth = await orgless.kernel.getServiceAsync('auth'); + try { + auth.applyConfigPatch({ passwordHistoryCount: 3 }); + const after = await probe(orgless).read(); + expect(after.armed, 'the same seam the settings service writes arms it').toBe(true); + expect(after.observed).toContain('passwordHistoryCount=3'); + await expect(assertArmed([probe(orgless)])).resolves.toBeUndefined(); + } finally { + auth.applyConfigPatch({ passwordHistoryCount: undefined }); + } + + const restored = await probe(orgless).read(); + expect(restored.armed, 'and the restore really disarmed it again').toBe(false); + }); + + it('assertArmed REJECTS the default config, naming the 0 default', async () => { + const err = await assertArmed([probe(orgless)]).catch((e: Error) => e); + expect(String(err)).toContain(REUSE_CONTROL); + expect(String(err)).toContain('defaults to 0'); + }); + + it('a stack with no readable auth config is DISARMED whatever the predicate says', async () => { + // A predicate that would call `undefined` armed must not be able to turn + // "the service is gone" into a pass. + const verdict = await authSettingArmed({ + stack: { kernel: { getServiceAsync: async () => undefined } } as unknown as VerifyStack, + setting: 'passwordHistoryCount', + armed: (v) => v === undefined, + control: REUSE_CONTROL, + disarmedBy: REUSE_DISARM, + }).read(); + expect(verdict.armed).toBe(false); + expect(verdict.observed).toContain("no 'auth' service resolved"); + }); + }); + + // ── seededArmed — the row a control rides on ────────────────────────────── + + describe('seededArmed', () => { + it('[control] finds a set that really seeded — so its negative reading is a real negative', async () => { + const verdict = await seededArmed({ + stack: orgless, + object: 'sys_permission_set', + where: { name: 'member_default' }, + control: 'the platform baseline permission set', + disarmedBy: 'a permission set that failed to seed grants the principal nothing', + }).read(); + expect(verdict.armed).toBe(true); + expect(verdict.observed).toContain('is seeded'); + }); + + it('reports DISARMED for a set that does not exist', async () => { + const verdict = await seededArmed({ + stack: orgless, + object: 'sys_permission_set', + where: { name: 'no_such_set_8074' }, + control: 'an app-declared permission set', + disarmedBy: 'a set that failed to seed grants the principal nothing', + }).read(); + expect(verdict.armed).toBe(false); + expect(verdict.observed).toContain('never seeded'); + }); + + it('reports DISARMED when the row exists but fails its own predicate', async () => { + const verdict = await seededArmed({ + stack: orgless, + object: 'sys_permission_set', + where: { name: 'member_default' }, + armed: () => false, + control: 'a seeded row that must also carry something', + disarmedBy: 'the row seeded but not the part the control rides on', + }).read(); + expect(verdict.armed).toBe(false); + expect(verdict.observed).toContain('fails its own arming predicate'); + }); + }); + + // ── armedWhen — the general probe, both directions ──────────────────────── + + describe('armedWhen', () => { + const generic = (armed: (v: number) => boolean) => + armedWhen({ + control: 'a generic control', + disarmedBy: 'a generic default', + observe: () => 7, + armed, + describe: (v) => `observed ${v}`, + }); + + it('is ARMED when its predicate holds and DISARMED when it does not', async () => { + expect((await generic((v) => v === 7).read()).armed).toBe(true); + expect((await generic((v) => v === 8).read()).armed).toBe(false); + }); + + it('treats a non-boolean predicate result as DISARMED', async () => { + // "not proven armed" is the safe direction for an ambiguous reading. + const sloppy = armedWhen({ + control: 'a control judged by a truthy non-boolean', + disarmedBy: 'a predicate that does not return a real boolean', + observe: () => 7, + armed: ((v: number) => v) as unknown as (v: number) => boolean, + }); + expect((await sloppy.read()).armed).toBe(false); + }); + }); + + // ── the guard's own vacuity refusals ────────────────────────────────────── + + describe('the guard refuses its own vacuous spellings', () => { + it('assertArmed([]) throws — an empty declaration would certify everything', async () => { + await expect(assertArmed([])).rejects.toThrow(/arming declaration is EMPTY/); + }); + + it('principalArmed with nothing required throws at CONSTRUCTION', () => { + expect(() => + principalArmed({ + stack: orgless, + token: orglessMember, + who: 'nobody in particular', + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }), + ).toThrow(/can never fail/); + }); + + it('seededArmed with an empty `where` throws at CONSTRUCTION', () => { + expect(() => + seededArmed({ + stack: orgless, + object: 'sys_permission_set', + where: {}, + control: 'anything', + disarmedBy: 'anything', + }), + ).toThrow(/matches any row/); + }); + + it('a probe whose read THROWS counts as disarmed, never as fine', async () => { + const broken: ArmingProbe = { + control: 'a control whose probe cannot read the stack', + disarmedBy: 'an unreadable precondition', + read: async () => { + throw new Error('the service went away'); + }, + }; + const err = await assertArmed([broken]).catch((e: Error) => e); + expect(String(err)).toContain('the arming probe itself threw'); + expect(String(err)).toContain('the service went away'); + }); + + it('names EVERY disarmed control, not just the first', async () => { + const err = await assertArmed([ + principalArmed({ + stack: orgless, + token: orglessMember, + who: 'the plain member', + positions: ['org_member'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }), + authSettingArmed({ + stack: orgless, + setting: 'passwordHistoryCount', + armed: (v) => Number(v) >= 1, + control: REUSE_CONTROL, + disarmedBy: REUSE_DISARM, + }), + ]).catch((e: Error) => e); + expect(String(err)).toContain(WRITE_FLOOR); + expect(String(err)).toContain(REUSE_CONTROL); + expect(String(err)).toContain('2 of 2 control(s)'); + }); + + it('[positive control] a fully armed multi-probe declaration RESOLVES', async () => { + // The counterweight to every rejection case above. + await expect( + assertArmed([ + principalArmed({ + stack: orgbound, + token: orgboundMember, + who: 'the org-bound member', + positions: ['org_member'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }), + seededArmed({ + stack: orgbound, + object: 'sys_permission_set', + where: { name: 'member_default' }, + control: 'the platform baseline permission set', + disarmedBy: 'a set that failed to seed grants the principal nothing', + }), + ]), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/qa/dogfood/test/armed.ts b/packages/qa/dogfood/test/armed.ts new file mode 100644 index 0000000000..9d212a9e29 --- /dev/null +++ b/packages/qa/dogfood/test/armed.ts @@ -0,0 +1,374 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#8074] `assertArmed` — a fixture proves the control it measures is ENGAGED +// before it measures anything. +// +// ── the defect class ────────────────────────────────────────────────────── +// +// Three independent instances landed in one shift, each with a DIFFERENT +// mechanism, and the common factor is not "someone wrote a careless test" — +// it is that a PLATFORM DEFAULT turned the assertion into a tautology while +// the harness kept reporting success: +// +// #8023 the harness booted with no organization, so the principal never +// held `org_member`, so the positions-gated wildcard write floor +// never applied. The first fixture PASSED against the very build +// whose defect the card had already reproduced over HTTP. +// #8049 `passwordHistoryCount` defaults to 0 (off), so no history is +// recorded, so "a reused password is refused" had nothing to reject +// against and would have certified a control never exercised. +// #7809 the package tsconfig excludes the test layer, so a type-level pin +// was never evaluated by anything the author ran. +// +// None was caught by reading the test. All three were caught by a developer +// deliberately trying to make their own assertion fail. +// +// ── what this module does, stated narrowly ──────────────────────────────── +// +// It makes the silent precondition an EXPLICIT, MACHINE-CHECKED declaration +// read off the live booted stack: the fixture names the control it is about +// to measure and the default that would disarm it, and the harness refuses to +// proceed unless the control is observably engaged. +// +// ⛔ It reaches instances 1 and 2 — RUNTIME disarms, observable from inside a +// booted stack. It does NOT reach instance 3, and is deliberately built so it +// cannot read as though it does. That gap is reasoned, not overlooked: +// +// - #7809's assertion was evaluated by `tsc`, not by vitest, and what +// disarmed it lived in `tsconfig.json`. No runtime probe against a booted +// stack can observe an assertion that the runtime never sees. +// - the coverage half of that question is already owned repo-wide by +// `scripts/check-type-check-coverage.mjs` (its TEST_DEBT ledger lifts a +// package's test-layer exclusion, compiles it, and ratchets the count); +// rebuilding a package-local copy here would be duplicate machinery, not +// new coverage. +// - the remaining SIGNAL hole in that gate — `PINS_CHECKED` recognising +// only `@ts-expect-error`, so an assignability-const pin in a hidden test +// layer is invisible to the mechanism whose job is to notice phantom pins +// — is filed as #8113 and is not this module's to close. +// - measured for this package rather than assumed: `@objectstack/dogfood` +// appears in neither the DEBT nor the TEST_DEBT ledger, its tsconfig +// `include` is `["src/**/*", "test/**/*"]` with no test-shaped `exclude`, +// and it declares the `typecheck` script the turbo graph runs. Instance +// 3's mechanism is absent from the surface this module covers. +// +// ── three design decisions, each load-bearing ───────────────────────────── +// +// 1. CALL IT FROM `beforeAll`, NOT FROM AN `it()`. +// A disarmed fixture must produce ZERO green cells. The hand-rolled +// `[integrity]` idiom this module generalises (`owd-public-read-write- +// write-floor`, `authored-row-write-scope`, `bulk-widener-probe`) is an +// ordinary case, so a disarm reddens ONE cell and every sibling assertion +// still prints green — and #8023's harm was precisely a green cell in a +// 124-cell matrix re-drive. Throwing from `beforeAll` fails the file. +// +// 2. AN UNREADABLE PROBE COUNTS AS DISARMED. +// A probe whose read throws returns "not proven armed", never "fine". The +// opposite default reproduces the defect class one level up: a guard that +// passes when it cannot see is a guard that passes. +// +// 3. AN EMPTY DECLARATION IS REFUSED, AND SO IS A PROBE THAT CANNOT FAIL. +// `assertArmed([])` throws; `principalArmed` with nothing to require and +// `seededArmed` with an empty `where` throw at construction. An +// "assert the control is armed" helper that is itself vacuous is this +// card's own defect wearing the fix's clothes, so the vacuous spellings +// are unavailable rather than discouraged. +// +// Every probe below is shown to fire IN BOTH DIRECTIONS against real boots in +// `armed.dogfood.test.ts` — including live reproductions of #8023's org-less +// disarm and #8049's `passwordHistoryCount: 0` disarm. A guard for vacuous +// assertions that was never shown to fail would be the joke it exists to stop. + +import { resolveAuthzContext } from '@objectstack/core'; +import type { VerifyStack } from '@objectstack/verify'; + +/** The system read context every probe uses — probes observe, never mutate. */ +const SYS = { isSystem: true } as const; + +/** One probe's reading of the live stack. */ +export interface ArmingVerdict { + /** `true` only when the control was OBSERVED engaged. Never a default. */ + readonly armed: boolean; + /** + * What was actually read, in the words the failure message needs. This is + * the half that turns "something is off" into "the org bind never happened", + * so it is required rather than optional. + */ + readonly observed: string; +} + +/** A named precondition, read off the live stack. */ +export interface ArmingProbe { + /** The control this arms, named the way its card names it. */ + readonly control: string; + /** + * The DEFAULT that would disarm it — the sentence the next author needs in + * order to fix the fixture, not merely to know it is broken. + */ + readonly disarmedBy: string; + read(): Promise; +} + +function render(value: unknown): string { + if (typeof value === 'string') return JSON.stringify(value); + if (value === undefined) return 'undefined'; + try { + return JSON.stringify(value) ?? String(value); + } catch { + return String(value); + } +} + +function reason(err: unknown): string { + return err instanceof Error ? `${err.name}: ${err.message}` : String(err); +} + +/** + * Assert every named control is engaged against the live stack, or refuse to + * let the fixture measure anything. + * + * Call from `beforeAll` — see decision 1 in this file's header. + */ +export async function assertArmed(probes: readonly ArmingProbe[]): Promise { + if (!Array.isArray(probes) || probes.length === 0) { + throw new Error( + 'assertArmed(): the arming declaration is EMPTY, which asserts nothing. An empty ' + + 'declaration would certify every fixture that forgot to write one — the exact defect ' + + 'class this helper exists to close (#8074). Name at least one control, or do not call ' + + 'assertArmed at all and say in the fixture header why no precondition applies.', + ); + } + + const disarmed: string[] = []; + for (const probe of probes) { + let verdict: ArmingVerdict; + try { + verdict = await probe.read(); + } catch (err) { + // Decision 2: unreadable is DISARMED, never "fine". + verdict = { armed: false, observed: `the arming probe itself threw — ${reason(err)}` }; + } + if (verdict.armed !== true) { + disarmed.push( + ` • control: ${probe.control}\n` + + ` observed: ${verdict.observed}\n` + + ` disarmed by: ${probe.disarmedBy}`, + ); + } + } + + if (disarmed.length > 0) { + throw new Error( + `[#8074] this fixture is DISARMED: ${disarmed.length} of ${probes.length} control(s) it ` + + 'measures are not engaged on the booted stack, so its assertions would pass without ' + + 'testing anything.\n\n' + + `${disarmed.join('\n\n')}\n\n` + + 'Nothing below this point is evidence. Arm the control (or drop the assertions that ' + + 'depend on it) — do not weaken the declaration.', + ); + } +} + +/** + * The general probe: observe some live value, judge it, and say what was seen. + * + * `armed` must return a REAL boolean; any other return counts as disarmed, + * because the safe direction for an ambiguous reading is "not proven armed". + */ +export function armedWhen(spec: { + control: string; + disarmedBy: string; + observe: () => T | Promise; + armed: (observed: T) => boolean; + describe?: (observed: T) => string; +}): ArmingProbe { + return { + control: spec.control, + disarmedBy: spec.disarmedBy, + async read(): Promise { + const observed = await spec.observe(); + return { + armed: spec.armed(observed) === true, + observed: spec.describe ? spec.describe(observed) : render(observed), + }; + }, + }; +} + +/** + * Resolve the authorization context for a bearer token the SAME way the REST + * entry point does — never a hand-built principal, which would arm the probe + * with the fixture's own assumption instead of the runtime's answer. + * + * Exported because three fixtures hand-roll this dance today. + */ +export async function resolveAuthzFor(stack: VerifyStack, token: string): Promise<{ + userId?: string; + positions: string[]; + permissions: string[]; +}> { + const ql = await stack.kernel.getServiceAsync('objectql'); + const authService = await stack.kernel.getServiceAsync('auth'); + let api: any = authService?.api; + if (!api && typeof authService?.getApi === 'function') api = await authService.getApi(); + const headers = new Headers({ authorization: `Bearer ${token}` }); + const ctx = await resolveAuthzContext({ + ql, + headers, + getSession: async (h: any) => api?.getSession?.({ headers: h }), + }); + return { + userId: ctx?.userId, + positions: Array.isArray(ctx?.positions) ? [...ctx.positions] : [], + permissions: Array.isArray(ctx?.permissions) ? [...ctx.permissions] : [], + }; +} + +/** + * A principal is inside the domain the policy under test is gated to. + * + * This is #8023's precondition. The wildcard row-level write floor is gated to + * `positions: ['org_member']`, which a principal holds only through a + * `sys_member` row; an org-less boot hands a fresh sign-up `['everyone']`, the + * floor never applies, and a real 403 defect records as a passing cell. + */ +export function principalArmed(spec: { + stack: VerifyStack; + /** A bearer token — resolved through the real request path. */ + token: string; + /** Who this is, for the failure message (e.g. `'bob (edit:true persona)'`). */ + who: string; + /** Positions the principal must hold for the control to apply. */ + positions?: readonly string[]; + /** Permission sets the principal must hold for the control to apply. */ + permissions?: readonly string[]; + control: string; + disarmedBy: string; +}): ArmingProbe { + const wantPositions = spec.positions ?? []; + const wantPermissions = spec.permissions ?? []; + if (wantPositions.length === 0 && wantPermissions.length === 0) { + // Decision 3: a probe with nothing to require can never report disarmed. + throw new Error( + `principalArmed(${spec.who}): no positions and no permissions required, so this probe ` + + 'can never fail. Name what the control is gated to, or use `armedWhen` for a fact this ' + + 'shape cannot express.', + ); + } + return { + control: spec.control, + disarmedBy: spec.disarmedBy, + async read(): Promise { + const ctx = await resolveAuthzFor(spec.stack, spec.token); + const missingPositions = wantPositions.filter((p) => !ctx.positions.includes(p)); + const missingPermissions = wantPermissions.filter((p) => !ctx.permissions.includes(p)); + const armed = missingPositions.length === 0 && missingPermissions.length === 0; + const seen = + `${spec.who} resolved as userId=${render(ctx.userId)} ` + + `positions=${render(ctx.positions)} permissions=${render(ctx.permissions)}`; + if (armed) return { armed: true, observed: seen }; + const missing = [ + missingPositions.length ? `missing positions ${render(missingPositions)}` : '', + missingPermissions.length ? `missing permissions ${render(missingPermissions)}` : '', + ] + .filter(Boolean) + .join('; '); + return { armed: false, observed: `${missing} — ${seen}` }; + }, + }; +} + +/** + * A settings-backed auth control is switched ON in the live manager. + * + * This is #8049's precondition, and the same shape covers every fixture that + * arms policy through `applyConfigPatch` (`two-factor-lockout`, + * `oidc-authorize-env-gate`, `bearer-lane-password-change`): all three patch + * and then TRUST the patch, so a key that is renamed, clamped or dropped would + * silently return them to the vacuous state their headers warn about. + * + * A missing `auth` service or an unreadable config is DISARMED regardless of + * the predicate — otherwise a predicate like `(v) => v === undefined` would + * read "the service is gone" as armed. + */ +export function authSettingArmed(spec: { + stack: VerifyStack; + /** Key on the auth manager's effective options, e.g. `passwordHistoryCount`. */ + setting: string; + armed: (value: unknown) => boolean; + control: string; + disarmedBy: string; + describe?: (value: unknown) => string; +}): ArmingProbe { + return { + control: spec.control, + disarmedBy: spec.disarmedBy, + async read(): Promise { + const auth = await spec.stack.kernel.getServiceAsync('auth'); + const config = auth?.config; + if (!config || typeof config !== 'object') { + return { + armed: false, + observed: auth + ? "the 'auth' service exposes no readable config, so no setting can be proven armed" + : "no 'auth' service resolved on this stack", + }; + } + const value = config[spec.setting]; + return { + armed: spec.armed(value) === true, + observed: `auth config ${spec.setting}=${ + spec.describe ? spec.describe(value) : render(value) + }`, + }; + }, + }; +} + +/** + * The row a control is carried by actually seeded. + * + * A permission set, policy or position that failed to seed disarms everything + * downstream of it just as thoroughly as a wrong default: the fixture binds a + * set that does not exist, the principal holds nothing, and every refusal it + * then measures comes from somewhere else. + */ +export function seededArmed(spec: { + stack: VerifyStack; + object: string; + /** Non-empty by construction: `{}` matches any row and proves nothing. */ + where: Record; + control: string; + disarmedBy: string; + /** Optional extra verdict on the row that was found. */ + armed?: (row: any) => boolean; +}): ArmingProbe { + if (!spec.where || Object.keys(spec.where).length === 0) { + // Decision 3: an empty `where` matches the first row of the table. + throw new Error( + `seededArmed(${spec.object}): an empty \`where\` matches any row, so this probe would be ` + + 'armed by the table being non-empty. Name the row the control is carried by.', + ); + } + return { + control: spec.control, + disarmedBy: spec.disarmedBy, + async read(): Promise { + const ql = await spec.stack.kernel.getServiceAsync('objectql'); + const row = await ql?.findOne?.(spec.object, { where: spec.where, context: { ...SYS } }); + if (!row) { + return { + armed: false, + observed: `no ${spec.object} row matches ${render(spec.where)} — it never seeded`, + }; + } + const extra = spec.armed ? spec.armed(row) === true : true; + return { + armed: extra, + observed: extra + ? `${spec.object} row ${render(spec.where)} is seeded (id=${render(row.id)})` + : `${spec.object} row ${render(spec.where)} exists but fails its own arming predicate`, + }; + }, + }; +} From f4c6902b00b972b502c130d22b8dcde3e50645bd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 11:02:48 +0000 Subject: [PATCH 2/2] test(qa): retrofit assertArmed into the five patch-and-trust fixtures (#8074) --- ...earer-lane-password-change.dogfood.test.ts | 19 ++++++++ .../oidc-authorize-env-gate.dogfood.test.ts | 19 ++++++++ ...lic-read-write-write-floor.dogfood.test.ts | 43 +++++++++++++++++++ .../test/two-factor-lockout.dogfood.test.ts | 27 ++++++++++++ 4 files changed, 108 insertions(+) diff --git a/packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts b/packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts index e98a109f39..0e0dc1fab3 100644 --- a/packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts +++ b/packages/qa/dogfood/test/bearer-lane-password-change.dogfood.test.ts @@ -79,6 +79,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import showcaseStack from '@objectstack/example-showcase'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { assertArmed, authSettingArmed } from './armed.js'; const SYS = { context: { isSystem: true } }; @@ -144,6 +145,24 @@ describe('#8049: /auth/change-password clears the force-change flag and enforces const auth = await stack.kernel.getServiceAsync('auth'); auth.applyConfigPatch({ passwordHistoryCount: HISTORY_COUNT }); + // [#8074] …and READ IT BACK. Patching and trusting the patch is the same + // bet the org-less fixture of #8023 lost: if this key is ever renamed, + // clamped or dropped, `applyConfigPatch` still returns quietly and every + // reuse assertion below silently returns to the vacuous state the header + // warns about. From `beforeAll`, so a disarm leaves no green cells. + await assertArmed([ + authSettingArmed({ + stack, + setting: 'passwordHistoryCount', + armed: (v) => Number(v) >= 1, + control: "ADR-0069 D1's password-reuse rejection (and its history append)", + disarmedBy: + '`passwordHistoryCount` defaults to 0 (off): nothing is recorded, so "a reused ' + + 'password is refused" has nothing to reject against and certifies a control that ' + + 'was never exercised. Arm it through `applyConfigPatch`.', + }), + ]); + adminToken = await stack.signIn(); }, 180_000); diff --git a/packages/qa/dogfood/test/oidc-authorize-env-gate.dogfood.test.ts b/packages/qa/dogfood/test/oidc-authorize-env-gate.dogfood.test.ts index 55bccf92a3..a7ecd4b278 100644 --- a/packages/qa/dogfood/test/oidc-authorize-env-gate.dogfood.test.ts +++ b/packages/qa/dogfood/test/oidc-authorize-env-gate.dogfood.test.ts @@ -65,6 +65,7 @@ import { createHash } from 'node:crypto'; import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import showcaseStack from '@objectstack/example-showcase'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { assertArmed, authSettingArmed } from './armed.js'; // Must be on before the AuthPlugin builds its plugin list (kernel.use during // bootStack), or /oauth2/authorize is not mounted and every assertion below @@ -186,6 +187,24 @@ describe('#8102: the D5.1 /oauth2/authorize env-access gate runs on every creden }, }); + // [#8074] The comment above says it in prose; this reads it back off the + // live manager. `this.config.oidcAuthorizeGate` is consulted only when it + // is truthy, so an unset gate means the branch under test never executes + // and "the request was refused" cannot be told from "there was no gate". + await assertArmed([ + authSettingArmed({ + stack, + setting: 'oidcAuthorizeGate', + armed: (v) => typeof v === 'function', + control: 'the cloud control-plane OIDC authorize gate', + disarmedBy: + '`oidcAuthorizeGate` is unset in open editions, so the hook never runs and every ' + + 'assertion in this file passes on the broken build. Install one through ' + + '`applyConfigPatch`.', + describe: (v) => (typeof v === 'function' ? 'a function (installed)' : String(v)), + }), + ]); + const user = (await ql.find( 'sys_user', { where: { email: ADMIN_EMAIL }, limit: 1 }, diff --git a/packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.ts b/packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.ts index 46e91148fc..ab13cb6297 100644 --- a/packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.ts +++ b/packages/qa/dogfood/test/owd-public-read-write-write-floor.dogfood.test.ts @@ -53,6 +53,20 @@ import { bootStack, type VerifyStack } from '@objectstack/verify'; import { resolveAuthzContext } from '@objectstack/core'; import { BUILTIN_OPERATION_MESSAGES } from '@objectstack/spec/system'; import { SecurityPlugin, securityDefaultPermissionSets } from '@objectstack/plugin-security'; +import { assertArmed, principalArmed } from './armed.js'; + +/** + * [#8074] The control every case below measures, and the default that silences + * it. The first fixture written for #8023 was org-less and PASSED against the + * known-broken build; `assertArmed` in `beforeAll` is what now makes that + * impossible rather than merely documented. + */ +const WRITE_FLOOR = + "the platform's wildcard row-level write floor (`owner_only_writes`, positions ['org_member'])"; +const WRITE_FLOOR_DISARM = + 'an org-less harness: no organization ⇒ no `sys_member` row ⇒ a fresh sign-up holds only ' + + "`['everyone']` ⇒ the positions-gated floor never applies and every case here passes on the " + + 'broken build. `orgContext: true` in the boot options above is what arms it.'; // ── the three objects, identical but for the OWD ─────────────────────────── @@ -230,6 +244,35 @@ describe('[#8023] a public_read_write OWD opens row-level writes (and nothing el await bindSet(mallyId, 'owdw_viewer'); await bindSet(carolId, 'owdw_scoped'); + // [#8074] The precondition the header above records in prose, now read off + // the live stack and enforced BEFORE anything is measured. It is asserted + // here rather than in an `it()` on purpose: a disarmed fixture must produce + // ZERO green cells, and #8023's harm was exactly one green cell in a + // 124-cell matrix re-drive. The `[integrity]` case below keeps its own copy + // of the positions check — it is redundant with this gate by design, since + // it also proves the facts this gate cannot (who created which row, and + // that the three objects differ only by their OWD). + await assertArmed([ + principalArmed({ + stack, + token: bobToken, + who: 'bob (the edit:true persona)', + positions: ['org_member'], + permissions: ['owdw_editor'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }), + principalArmed({ + stack, + token: carolToken, + who: 'carol (the #7792 select-narrowed persona)', + positions: ['org_member'], + permissions: ['owdw_scoped'], + control: WRITE_FLOOR, + disarmedBy: WRITE_FLOOR_DISARM, + }), + ]); + // Rows are created over HTTP by ALICE so `created_by` is genuinely hers — // a system-context seed would stamp no creator and the floor under test // would never engage. diff --git a/packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts b/packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts index 9fef24b0ce..313ef73ce5 100644 --- a/packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts +++ b/packages/qa/dogfood/test/two-factor-lockout.dogfood.test.ts @@ -47,6 +47,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { createHmac } from 'node:crypto'; import showcaseStack from '@objectstack/example-showcase'; import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { assertArmed, authSettingArmed } from './armed.js'; const SYS = { context: { isSystem: true } }; const ADMIN_PASSWORD = 'admin123'; @@ -138,6 +139,32 @@ describe('#3624 follow-up: better-auth 2FA lockout counts wrong codes', () => { lockoutDurationMinutes: LOCKOUT_DURATION_MINUTES, }); + // [#8074] Read the policy back off the live manager. `lockoutThreshold: 0` + // is the default and it disables lockout entirely (`recordFailedLogin` + // returns early on `Number(threshold) || 0`), so a patch that silently did + // not land would leave every "the account locks" assertion below measuring + // an account that can never lock — green, and meaningless. + await assertArmed([ + authSettingArmed({ + stack, + setting: 'lockoutThreshold', + armed: (v) => Number(v) === LOCKOUT_THRESHOLD, + control: 'the #3690 account-lockout policy the second factor must honour', + disarmedBy: + '`lockoutThreshold` defaults to 0, which disables lockout outright — the counter ' + + 'never trips and no code path under test runs. Arm it through `applyConfigPatch`.', + }), + authSettingArmed({ + stack, + setting: 'lockoutDurationMinutes', + armed: (v) => Number(v) === LOCKOUT_DURATION_MINUTES, + control: 'the lockout window this file asserts `locked_until` against', + disarmedBy: + 'an unset duration makes the stamped window whatever the default is, so the ' + + 'assertion on `locked_until` would be measuring a number nobody chose.', + }), + ]); + const token = await stack.signIn(); const me = await (await stack.apiAs(token, 'GET', '/auth/get-session')).json() as any; adminEmail = me?.user?.email;