|
31 | 31 | * the legacy `usr_system` service row (`SystemUserId.SYSTEM` — no longer |
32 | 32 | * provisioned, but present in every DB an older runtime created). |
33 | 33 | * |
| 34 | + * ## Two populations, held apart on purpose |
| 35 | + * |
| 36 | + * {@link CORPUS} is the REACHABLE one: every entry is a shape a `sys_user` |
| 37 | + * read can really return, so a failure there is a live defect. |
| 38 | + * {@link NON_OBJECT_CORPUS} is the unreachable one — truthy non-objects, which |
| 39 | + * no real read yields. It was originally left out of this file because the two |
| 40 | + * owners genuinely disagreed on it and it would have failed; [#12515] closed |
| 41 | + * that disagreement by giving plugin-security the same `typeof` guard |
| 42 | + * `isHumanUserRow` already had, which is what made the class pinnable. The two |
| 43 | + * stay in separate arrays so the arrays keep saying different things: a red in |
| 44 | + * `CORPUS` means a reachable answer moved, a red in `NON_OBJECT_CORPUS` means |
| 45 | + * the fail-closed guard was dropped. |
| 46 | + * |
34 | 47 | * ## Why the pin lives in plugin-auth and not in plugin-security |
35 | 48 | * |
36 | 49 | * Reaching both predicates from one test is a package-boundary problem, and |
@@ -184,6 +197,59 @@ const CORPUS: { name: string; row: unknown }[] = [ |
184 | 197 | { name: 'an undefined row', row: undefined }, |
185 | 198 | ]; |
186 | 199 |
|
| 200 | +/** |
| 201 | + * The NON-OBJECT input class — held separately from {@link CORPUS} on purpose. |
| 202 | + * |
| 203 | + * ## Why it is a second array and not four more corpus entries |
| 204 | + * |
| 205 | + * `CORPUS`'s contract is that every entry is a shape a `sys_user` read can |
| 206 | + * really return, and these are not: a real read yields plain objects, measured |
| 207 | + * against a real `SqlDriver` over the shipped `SysUser` declaration. Filing |
| 208 | + * them into `CORPUS` would quietly falsify that promise and blur the one |
| 209 | + * distinction that decides how a failure here should be read. |
| 210 | + * |
| 211 | + * ## Why it is pinned at all, given it is unreachable |
| 212 | + * |
| 213 | + * This class is the gap the original pin deliberately left: it was excluded |
| 214 | + * because at the time it would have FAILED, not because it was uninteresting. |
| 215 | + * The two owners genuinely disagreed on it — `isHumanUserRow` requires |
| 216 | + * `typeof row === 'object'` and answered `false`, while plugin-security's |
| 217 | + * hand-spelled copy ran a bare truthiness check whose two property comparisons |
| 218 | + * are both `undefined` on a non-object and therefore both pass, answering |
| 219 | + * `true`. That direction fails OPEN on the copy that performs the |
| 220 | + * platform-admin promotion. |
| 221 | + * |
| 222 | + * Unreachable-today would be a fine reason to shrug if the asymmetry had a |
| 223 | + * scheduled end. It does not: consolidating the predicate into a package both |
| 224 | + * plugins depend on stays declined (it would widen a published surface), so |
| 225 | + * nothing is going to delete this divergence on its own. The guard closed it |
| 226 | + * instead, and this group is what stops it coming back — if a refactor ever |
| 227 | + * makes a non-object row reachable, or if the guard is dropped as noise, these |
| 228 | + * cases are the only mechanism that says so. Without them the pin sits green |
| 229 | + * through exactly the edit that reopens the hole. |
| 230 | + * |
| 231 | + * Both owners must answer NON-HUMAN here. That is the fail-closed direction, |
| 232 | + * and for a promotion predicate the safe answer to malformed input is "no". |
| 233 | + */ |
| 234 | +const NON_OBJECT_CORPUS: { name: string; row: unknown }[] = [ |
| 235 | + { |
| 236 | + name: 'a bare id STRING where a row was expected', |
| 237 | + row: 'usr_alice', |
| 238 | + }, |
| 239 | + { |
| 240 | + name: "the SYSTEM account's own id as a bare string — fail-open would promote the service account", |
| 241 | + row: SystemUserId.SYSTEM, |
| 242 | + }, |
| 243 | + { name: 'a number', row: 42 }, |
| 244 | + { name: 'the boolean true', row: true }, |
| 245 | + { |
| 246 | + name: 'a function — truthy, and every property read on it is undefined', |
| 247 | + row: () => 'not a row', |
| 248 | + }, |
| 249 | + { name: 'the number zero — falsy, so the decision already agreed', row: 0 }, |
| 250 | + { name: 'an empty string — falsy, so the decision already agreed', row: '' }, |
| 251 | +]; |
| 252 | + |
187 | 253 | describe('human-user predicate agreement — plugin-security `isHumanUser` vs plugin-auth `isHumanUserRow`', () => { |
188 | 254 | const saved: Record<string, string | undefined> = {}; |
189 | 255 | const PINNED_ENV = ['OS_TENANCY_POSTURE', 'OS_PLATFORM_OWNER_EMAIL']; |
@@ -239,6 +305,56 @@ describe('human-user predicate agreement — plugin-security `isHumanUser` vs pl |
239 | 305 | expect(CORPUS.map(({ row }) => isHumanUserRow(row)).some((v) => !v)).toBe(true); |
240 | 306 | }); |
241 | 307 |
|
| 308 | + describe('the non-object input class — unreachable today, and fail-CLOSED on both sides', () => { |
| 309 | + for (const { name, row } of NON_OBJECT_CORPUS) { |
| 310 | + it(`agrees on ${name}`, async () => { |
| 311 | + const authSays = isHumanUserRow(row); |
| 312 | + const security = await securityVerdict(row); |
| 313 | + |
| 314 | + // Stated as an absolute, not just as agreement: two predicates could |
| 315 | + // agree by both failing OPEN, which is the outcome this group exists |
| 316 | + // to forbid. `isHumanUserRow` is asserted false first so a regression |
| 317 | + // in the OWNER cannot be laundered into "well, they still agree". |
| 318 | + expect( |
| 319 | + authSays, |
| 320 | + `plugin-auth isHumanUserRow must answer NON-HUMAN for a non-object row.\n` + |
| 321 | + ` row: ${String(row)} (typeof ${typeof row})`, |
| 322 | + ).toBe(false); |
| 323 | + |
| 324 | + expect( |
| 325 | + security.human, |
| 326 | + `plugin-security and plugin-auth disagree on a NON-OBJECT row — the security\n` + |
| 327 | + `copy is failing OPEN on malformed input, and it is the copy that PERFORMS\n` + |
| 328 | + `platform-admin promotion.\n` + |
| 329 | + ` row: ${String(row)} (typeof ${typeof row})\n` + |
| 330 | + ` plugin-auth isHumanUserRow -> ${authSays}\n` + |
| 331 | + ` plugin-security isHumanUser -> ${security.human} (reason: ${security.reason ?? 'none'})\n` + |
| 332 | + `The fix is the \`typeof\` guard in bootstrap-platform-admin.ts, mirroring\n` + |
| 333 | + `isHumanUserRow — not a relaxation of this expectation.`, |
| 334 | + ).toBe(false); |
| 335 | + |
| 336 | + // Same anti-vacuity guard the reachable corpus uses: only the human |
| 337 | + // filter reaches `no_users`, so this proves the negative came from the |
| 338 | + // predicate rather than from a harness that broke earlier. |
| 339 | + expect(security.reason, 'negative verdict did not come from the human filter').toBe( |
| 340 | + 'no_users', |
| 341 | + ); |
| 342 | + }); |
| 343 | + } |
| 344 | + |
| 345 | + it('anti-vacuity: this group really carries truthy non-objects, not just falsy ones', () => { |
| 346 | + // A falsy row is non-human on both sides even with the guard removed, so |
| 347 | + // a group that had quietly lost its truthy members would keep passing |
| 348 | + // through the very regression it is here to catch. |
| 349 | + const truthyNonObjects = NON_OBJECT_CORPUS.filter( |
| 350 | + ({ row }) => Boolean(row) && typeof row !== 'object', |
| 351 | + ); |
| 352 | + expect(truthyNonObjects.length, 'no truthy non-object rows left in the group').toBeGreaterThan( |
| 353 | + 0, |
| 354 | + ); |
| 355 | + }); |
| 356 | + }); |
| 357 | + |
242 | 358 | it('the legacy usr_system row alone leaves the install with NO admin and awaiting a human', async () => { |
243 | 359 | // The card's harm model, stated as an outcome rather than a predicate call: |
244 | 360 | // a DB carrying only the legacy service row must be "no humans yet" on BOTH |
|
0 commit comments