|
38 | 38 | import { describe, it, expect, vi, afterEach } from 'vitest'; |
39 | 39 | import type { ExecutionContext } from '@objectstack/spec/kernel'; |
40 | 40 | import { ObjectQL } from './engine.js'; |
41 | | -import { resolveSystemWriteOrganization } from './tenancy/system-write-organization.js'; |
| 41 | +import { |
| 42 | + resolveSystemWriteOrganization, |
| 43 | + isSystemWriteOrganizationRequiredError, |
| 44 | + SystemWriteOrganizationRequiredError, |
| 45 | + SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE, |
| 46 | +} from './tenancy/system-write-organization.js'; |
42 | 47 |
|
43 | 48 | const ORG_ID = 'org_msokm9oaz0cal87q'; |
44 | 49 | const SECOND_ORG_ID = 'org_second'; |
@@ -371,3 +376,135 @@ describe('#8844 the exclusions — populations the refusal must not touch', () = |
371 | 376 | expect(observed.filter((c) => c.object === 'sys_organization')).toEqual([]); |
372 | 377 | }); |
373 | 378 | }); |
| 379 | + |
| 380 | + |
| 381 | +// ── [#14936] The published recognizer ──────────────────────────────────────── |
| 382 | +// |
| 383 | +// The card's measurement, taken from a real consumer package loading |
| 384 | +// `@objectstack/objectql` through each of the two realms its own `exports` |
| 385 | +// declares (`import` -> `dist/index.mjs`, `require` -> `dist/index.js`): |
| 386 | +// |
| 387 | +// SAME CLASS IDENTITY (A === B): false |
| 388 | +// instA instanceof A (same realm): true |
| 389 | +// instA instanceof B (CROSS-REALM): false |
| 390 | +// code compare survives the split: true |
| 391 | +// |
| 392 | +// So a consumer had exactly two options and both were bad: `instanceof`, which |
| 393 | +// is unsound across that split and fails SILENTLY, or re-spelling |
| 394 | +// `'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'`, which the provenance gate counts |
| 395 | +// as a stamp site in the consumer's own package and which can drift from what |
| 396 | +// the engine throws. These pins cover the third option this card publishes. |
| 397 | +// |
| 398 | +// Each case below carries its DISCRIMINATING CONTROL, for the reason this |
| 399 | +// file's header already states: a suite that only asserted "the recognizer |
| 400 | +// says true" would stay green if the recognizer were `() => true`, and one |
| 401 | +// that only asserted the same-realm instance would stay green if the |
| 402 | +// recognizer were `instanceof`-based - which is the very defect being fixed. |
| 403 | + |
| 404 | +/** |
| 405 | + * What a SECOND copy of this module produces: structurally the refusal, |
| 406 | + * nominally a different class. This is the CJS build's class arriving at a |
| 407 | + * consumer holding the ESM one (or the reverse) - the exact shape the card's |
| 408 | + * cross-realm measurement found, reproduced here without needing two builds. |
| 409 | + */ |
| 410 | +class SystemWriteOrganizationRequiredErrorOtherRealmCopy extends Error { |
| 411 | + readonly code = 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' as const; |
| 412 | + readonly status = 500; |
| 413 | + constructor() { |
| 414 | + super('refused by the other realm\'s copy of this module'); |
| 415 | + this.name = 'SystemWriteOrganizationRequiredError'; |
| 416 | + } |
| 417 | +} |
| 418 | + |
| 419 | +describe('#14936 the published recognizer for the org-less system-write refusal', () => { |
| 420 | + it('the published constant IS the code the thrown refusal carries, at its 500 status', () => { |
| 421 | + const err = new SystemWriteOrganizationRequiredError('dispatch_order', 'isolated', 'walled-posture'); |
| 422 | + expect(err.code).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE); |
| 423 | + // ADR-0112 envelope: `code` AND `status`. Asserting the throw alone would |
| 424 | + // stay green against an unrelated failure, and would not notice the status |
| 425 | + // moving off 500 - which #8844 ruled deliberately. |
| 426 | + expect(err.status).toBe(500); |
| 427 | + // The wire string, spelled once here on purpose: this is the TEST layer, |
| 428 | + // which `check:error-code-provenance` does not scan, so pinning it costs no |
| 429 | + // stamp site while making a silent rename of the constant impossible to |
| 430 | + // pass off as "still the same code". |
| 431 | + expect(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE).toBe('ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'); |
| 432 | + }); |
| 433 | + |
| 434 | + it('recognises the refusal the engine actually throws', () => { |
| 435 | + const err = new SystemWriteOrganizationRequiredError( |
| 436 | + 'dispatch_order', 'single', 'ambiguous-organization', 2, |
| 437 | + ); |
| 438 | + expect(isSystemWriteOrganizationRequiredError(err)).toBe(true); |
| 439 | + }); |
| 440 | + |
| 441 | + it("recognises the OTHER realm's copy - the exact case `instanceof` gets wrong", () => { |
| 442 | + const fromOtherRealm = new SystemWriteOrganizationRequiredErrorOtherRealmCopy(); |
| 443 | + // THE CONTROL, and the whole point of the card. Without this line the |
| 444 | + // assertion below would pass just as happily against an `instanceof` |
| 445 | + // implementation, i.e. against the defect. |
| 446 | + expect(fromOtherRealm instanceof SystemWriteOrganizationRequiredError).toBe(false); |
| 447 | + expect(isSystemWriteOrganizationRequiredError(fromOtherRealm)).toBe(true); |
| 448 | + }); |
| 449 | + |
| 450 | + it('recognises a transport-rebuilt envelope, which is WHY it does not narrow to the class', () => { |
| 451 | + // #5437 withholds the prose from the wire and keeps the machine-readable |
| 452 | + // `code`, so what a consumer catches downstream of a transport can be an |
| 453 | + // envelope carrying the code and nothing else. |
| 454 | + const wireEnvelope: Record<string, unknown> = { |
| 455 | + code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED', status: 500, |
| 456 | + }; |
| 457 | + expect(isSystemWriteOrganizationRequiredError(wireEnvelope)).toBe(true); |
| 458 | + // ...and it carries none of the class's own members. A type guard |
| 459 | + // (`err is SystemWriteOrganizationRequiredError`) would promise these, |
| 460 | + // turning a sound check into an unsound assertion one layer down - which |
| 461 | + // is why the predicate returns `boolean`. |
| 462 | + expect(wireEnvelope.object).toBeUndefined(); |
| 463 | + expect(wireEnvelope.posture).toBeUndefined(); |
| 464 | + expect(wireEnvelope.reason).toBeUndefined(); |
| 465 | + }); |
| 466 | + |
| 467 | + it.each([ |
| 468 | + ['null', null], |
| 469 | + ['undefined', undefined], |
| 470 | + ['a bare string carrying the code', 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'], |
| 471 | + ['an Error with no code at all', new Error('boom')], |
| 472 | + ['a DIFFERENT engine refusal', Object.assign(new Error('dup'), { code: 'DUPLICATE_RECORD' })], |
| 473 | + ['a prefix lookalike', Object.assign(new Error('x'), { code: 'ERR_SYSTEM_WRITE_ORGANIZATION' })], |
| 474 | + ['a suffix lookalike', Object.assign(new Error('x'), { code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED_V2' })], |
| 475 | + // DERIVED from the constant, never spelled. A lowercase literal in a `code` |
| 476 | + // position is a real `check:error-code-casing` finding (ADR-0112 D1), and the |
| 477 | + // gate cannot tell a negative fixture from a real emission - it classified this |
| 478 | + // very site as `(emission)`. Deriving it is not an opt-out: the gate's own |
| 479 | + // output says a code value with NO literal at the position is out of reach for |
| 480 | + // its patterns BY CONSTRUCTION. It also makes the fixture track the constant |
| 481 | + // instead of restating it - the same argument this card makes about consumers |
| 482 | + // re-spelling literals, applied to its own test. |
| 483 | + ['the code in the wrong case', Object.assign(new Error('x'), { |
| 484 | + code: SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE.toLowerCase(), |
| 485 | + })], |
| 486 | + ])('refuses %s', (_label, value) => { |
| 487 | + expect(isSystemWriteOrganizationRequiredError(value)).toBe(false); |
| 488 | + }); |
| 489 | + |
| 490 | + it('keeps `code` a LITERAL type, which is what the cross-package consumer types itself from', () => { |
| 491 | + // `plugin-sharing/src/sharing-rule-service.ts` declares its own constant as |
| 492 | + // `SystemWriteOrganizationRequiredError['code']`. Had this refactor widened |
| 493 | + // the class field to `string`, that consumer would keep COMPILING while |
| 494 | + // silently losing the drift protection it asked for - so the widening is |
| 495 | + // pinned as a TYPE error rather than a value assertion. This file carries no |
| 496 | + // `test-typecheck-debt.json` entry, so a new error here is red on arrival. |
| 497 | + const pinned: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' = |
| 498 | + new SystemWriteOrganizationRequiredError('dispatch_order', 'isolated', 'walled-posture').code; |
| 499 | + expect(pinned).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE); |
| 500 | + }); |
| 501 | + |
| 502 | + it('publishes both names from the package BARREL, not only from the module', async () => { |
| 503 | + // The card's landing surface is "the module plus that package's index.ts |
| 504 | + // export" - a consumer reaches these by bare specifier, so an export that |
| 505 | + // exists only on the deep module is not the affordance that was asked for. |
| 506 | + const barrel = await import('./index.js'); |
| 507 | + expect(barrel.SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE); |
| 508 | + expect(barrel.isSystemWriteOrganizationRequiredError).toBe(isSystemWriteOrganizationRequiredError); |
| 509 | + }); |
| 510 | +}); |
0 commit comments