Decorator Contract Helpers
The corpus's discipline depends on three packages — runtime, analyzer, codegen — agreeing on the same decorator metadata shape. If the runtime stamps __entity and the analyzer checks for __Entity, the system is broken. @frenchexdev/ddd-testing is the small shared helper that prevents the drift by giving every test suite a single function to verify the contract: assertDecoratorContract.
What ddd-testing Reifies
The pattern is a test-time contract validator. A decorator is supposed to stamp specific fields onto its target class. The test verifies the stamp. If the runtime's stamp shape changes, every dependent package's test fails — the change is visible at the next build.
The helper takes a decorated class instance and a DecoratorContractSpec declaring the expected stamps: readonly string[] (the field names that must exist on the target) and an optional registers: (target) => boolean predicate (for decorators that also register the class in a side registry). It returns an AssertResult with satisfied: boolean, missing: readonly string[], extra: readonly string[] — meaningful enough for a test framework to assert on.
The Runtime: ddd-testing
decorator-contract.ts is the entire surface:
export interface DecoratorContractSpec {
readonly stamps: readonly string[];
readonly registers?: (target: unknown) => boolean;
}
export interface AssertResult {
readonly satisfied: boolean;
readonly missing: readonly string[];
readonly extra: readonly string[];
}
export function assertDecoratorContract(target: object, spec: DecoratorContractSpec): AssertResult {
/* presence-check the stamps, run the optional registers predicate, return the result */
}export interface DecoratorContractSpec {
readonly stamps: readonly string[];
readonly registers?: (target: unknown) => boolean;
}
export interface AssertResult {
readonly satisfied: boolean;
readonly missing: readonly string[];
readonly extra: readonly string[];
}
export function assertDecoratorContract(target: object, spec: DecoratorContractSpec): AssertResult {
/* presence-check the stamps, run the optional registers predicate, return the result */
}A typical test verifying that @Entity({ id: 'CustomerId' }) produces the expected metadata:
import { Entity } from '@frenchexdev/ddd-entity';
import { assertDecoratorContract } from '@frenchexdev/ddd-testing';
@Entity({ id: 'CustomerId' })
class TestCustomer {}
const result = assertDecoratorContract(TestCustomer as object, {
stamps: ['__entity', '__entityMetadata'],
});
expect(result.satisfied).toBe(true);
expect(result.missing).toEqual([]);import { Entity } from '@frenchexdev/ddd-entity';
import { assertDecoratorContract } from '@frenchexdev/ddd-testing';
@Entity({ id: 'CustomerId' })
class TestCustomer {}
const result = assertDecoratorContract(TestCustomer as object, {
stamps: ['__entity', '__entityMetadata'],
});
expect(result.satisfied).toBe(true);
expect(result.missing).toEqual([]);Two stamps expected, both present, satisfied. The same helper runs in the entity runtime's tests (verifying that the runtime stamps the right fields), in the entity analyzer's tests (verifying that the analyzer recognises the stamps), and in the entity codegen's tests (verifying that the codegen reads the stamps). One contract, three consumers, one definition of truth.
Cross-Links
- Imported by every corpus package's
*.test.ts— the helper is universal. - Composes with the requirements suite's
@FeatureTest/@Verifiesdecorations — tests that useassertDecoratorContracttypically carry those decorations to bind their assertions to specific ACs. - Will be extended over time as the corpus discovers new shared test patterns; the current scope is deliberately tight.
Back to the series index.