@guestcraft/testing
Questi contenuti non sono ancora disponibili nella tua lingua.
Generic test helpers (ADR 0005). The package depends on no GuestCraft package, so any package can use it in its tests without creating a dependency cycle, and it is imported only by test and setup files.
Temporal assertions
Section titled “Temporal assertions”Every Vitest project loads the shared Temporal equality tester from
tooling/scripts/src/vitest/temporal-equality.setup.ts. Temporal values compare by value in
all Vitest equality matchers, including nested toMatchObject and objectContaining
expectations. Temporal.Duration compares its ten component fields exactly; this avoids
choosing a calendar-relative interpretation for durations, which do not provide equals.
@guestcraft/testing/postgres
Section titled “@guestcraft/testing/postgres”A real PostgreSQL 17 for integration tests, started once per test run with Testcontainers, and isolated databases for each test file.
// global setup: the returned function is teardown; failed preparation stops the serverreturn setupPostgres({ pooler: { users: { gc_test_app: appPassword } } }, async (server) => { await preparePostgresTemplate(server.adminUrl, "template", owner, async (client) => { // … run the migrations and grants of the package under test … }); project.provide("postgres", server.adminUrl);});// each test file: a copy of the template, dropped at the endconst adminUrl = inject("postgres");await cloneDatabase(adminUrl, "template", "file_1");const url = databaseUrl(adminUrl, "file_1", { user: "gc_test_app", password: appPassword });startPostgres()runsPOSTGRES_IMAGE, pinned by digest, with durability turned off for speed. The superuser URL is for the test harness only, never for the code under test.setupPostgres(options, prepare)owns the server during preparation and test-context publication, then returns its teardown. Whenoptionsrequest apooler,preparereceives itspoolerUrlas a string: startup fails rather than continue without it.preparePostgresTemplate(adminUrl, template, owner, prepare)creates the owner and its template database, then calls the package’s migrations/grants with a connected owner client.ownersuppliesuserandpassword;createPostgresOwner(adminUrl, owner)can also create that role independently, leaving an existing role unchanged.- Each acquired network, server, pooler and proxy is released if a later step fails. Teardown attempts every release in reverse order. A setup failure is rethrown unchanged, or, if cleanup also fails, as an
AggregateErrorwhose first entry andcauseis the original failure. Externally supplied servers remain caller-owned. - With
pooler, PgBouncer (PGBOUNCER_IMAGE) runs in front of it in transaction mode, the way the pooled URLs of Neon and Supabase work. By default every client of a user shares one server connection, so a test can prove that nothing a transaction leaves on a connection reaches the next client. - With
poolers, more PgBouncers run by name, each with itsmode(transactionorsession, as Supabase’s session pooler),poolSizeandroundRobin(lend the server connection idle the longest, so a client’s transactions move between connections);poolerUrlsgives their addresses. startCancelKeyProxy()starts a 1:1 TCP proxy that announces its own cancellation key, as the proxies of Neon and Supavisor do: each client still has its own server session, but the process ID it receives is notpg_backend_pid().cloneDatabase()copies a prepared template, which is much faster than migrating a database for every file.withClient()runs a function with a connected client and always closes it.
To use an existing server instead of Docker, set GUESTCRAFT_TEST_POSTGRES_URL to the URL of a superuser of PostgreSQL 17 or later, and GUESTCRAFT_TEST_POOLER_URL to PgBouncer in transaction mode in front of the same server, with the users the tests need. Without the second, tests that need a pooler stop with an explanation rather than running without one; named poolers always need Docker.
@guestcraft/testing/cleanup
Section titled “@guestcraft/testing/cleanup”withCleanup(work) gives work a cleanup to register each resource’s release right after acquiring it. If work fails, every release runs in reverse order, each attempted even when another rejects, and the failure is rethrown unchanged, or, if a release also fails, as an AggregateError whose first entry and cause is that failure. On success work keeps its resources; cleanup.stop() releases them the same way and reports release failures as one AggregateError.
return withCleanup(async (cleanup) => { const database = await createWorkspaceDatabase(); cleanup.add(() => database.drop()); return { database, close: cleanup.stop };});@guestcraft/testing/cases
Section titled “@guestcraft/testing/cases”CONVERSION_CASES: unit conversions that every implementation must agree on. The core and
i18n (MIT, so it cannot import the core) both convert measures, and both run these cases.
@guestcraft/testing/determinism
Section titled “@guestcraft/testing/determinism”manualClock(start): a clock that moves only when the test says so (advance,set).seededRandom(seed): a deterministic source of random bytes, so that generated identifiers are the same on every run.
@guestcraft/testing/memory
Section titled “@guestcraft/testing/memory”memoryPms<DomainTypes>() supplies transaction-scoped property, reservation/inventory,
rate, policy, folio and booking-import repositories for fast use-case tests. It copies its
maps before each unit of work, commits them together on success, discards them on a thrown
error, and serializes concurrent writes. Tenant scope and property gates apply to reads
and writes. The package uses structural types so it remains MIT and does not import the
AGPL core. The shared reservation contract in
packages/db/test/integration/memory-contract.integration.test.ts compares its results
with the migrated PostgreSQL adapter. The fake’s channexConnections.lockOwnership and
ensureOwnershipFence are deliberately no-ops: its serialized transactions cannot model
concurrent row locks. Its linked and cleanup targets are upstream Channex property IDs, as in
PostgreSQL; the shared contract checks them with the key-change mark of a pending removal.
Acknowledgement admission and ownership races are verified against real PostgreSQL/PgBouncer.
packages/db/test/integration/memory-eligibility-contract.integration.test.ts runs the
same narrow rate-eligibility and recovery-scope cases against both adapters. Booking-import
recovery supports fixture-only cursor traversal: startRecovery creates one history cursor
with a fixed synthetic start and no checkpoint. It ignores deployment and failure-age
admission and does not model database-time recovery barriers, even when flags are supplied.
Test those policies against PostgreSQL.
packages/db/test/integration/memory-financial-contract.integration.test.ts checks rate
plans, sparse cells, version increments, queued changes, folio postings, batch balances,
credits and extras against both adapters, including rollback, read-only work and tenant
scope. Initial rate versions start at 1 in both adapters. Out-of-scope rate-version
calls differ: memory rejects them, while PostgreSQL returns 0 without changing a row.
Use cases check property visibility first; the contract does not model this unreachable call.
The fake models reservation lists, stay projections, overdue arrivals and due-prepayment
candidates for fast use-case tests. The shared PostgreSQL contract checks these projections.
Room blocks and ARI have no memory ports. The booking-import methods pendingProperties,
beginDrain and overbookings throw MemoryUnsupportedError so a test cannot silently
accept an empty result. Tests of those paths must use the real PostgreSQL adapter or a more
specific fixture.
Shifted-clock fast suites
Section titled “Shifted-clock fast suites”Set GUESTCRAFT_TEST_INSTANT to an ISO UTC instant and TZ to an IANA time zone for the fast suites:
TZ=Europe/Rome GUESTCRAFT_TEST_INSTANT=2026-03-29T01:30:00Z \ pnpm exec turbo run test --concurrency=1 -- --maxWorkers=1The shifted-clock Vitest setup shifts Date and the polyfilled Temporal.Now in unit and property suites while leaving elapsed time, timers and performance.now() real. It applies before test modules load, including date-based fixtures. Tests with a manual clock continue to use their explicitly injected instants. Integration suites use their own injected clocks and real PostgreSQL time; the shifted-clock command does not run them. The scheduled clock-guard workflow runs the fast suites at month, leap-day and DST boundaries without affecting pull-request checks. Every Vitest project also loads the Temporal equality tester so toMatchObject compares Temporal values by value.
The memory ports also model durable night-audit identity, immutable completion, business-date
advancement and financial/quarantine warnings. Supply a structural nightAudit type in
MemoryPmsTypes; state participates in the same transaction copies and rollback.
The guest ports model duplicate-pair pagination and remembered dismissals, separate
consent histories and guest-reference changes during merge. Supply guestConsent in
DomainTypes to preserve the consumer’s identifier and instant types. Consent evidence
follows guest visibility and is copied transactionally, so a failed write leaves the
committed histories intact. PostgreSQL integration tests remain authoritative for foreign
keys, RLS, merge lock ordering and the complete guest-reference inventory.
The guest memory adapter mirrors strong contact matching, shared-contact grouping, server-side dismissal filtering and undo, live-only lists, merge provenance and independent current consent reads. Key examples are checked against the pure domain normalizer and real PostgreSQL. Both adapters order consent decisions by the recorded guest version, including decisions recorded at the same instant. Merge results return their moved reservation ids explicitly, bump reservation versions and retain independent histories. Derived guest reads include visible reservation counts and owner names. Reservation OTA contact snapshots remain separate from the guest and yield notices only for differing fields newer than their independent review timestamps. Dismissals remember all exact historical values, including null, across future snapshots. Reviews copy transactionally, do not move on merge, and never include operator notes. The domain checks the current guest version and exact visible snapshot before recording a dismissal; repeated values do not add another version, audit or event. Concurrency and shared/exclusive advisory gates require the real PostgreSQL suites; the memory adapter does not simulate advisory-lock scheduling.
Planning inverse receipts use the structural planningChange type supplied to
memoryPms. They are immutable, scoped to workspace and property, and consumed in
the same transaction as a room change; a failed transaction retains the receipt.
Scoped expired receipts are removed when another receipt is inserted. The core
checks actor, version and expiry; the PostgreSQL contract tests verify the persisted
adapter separately.