Salta ai contenuti

@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.

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.

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 server
return 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 end
const adminUrl = inject("postgres");
await cloneDatabase(adminUrl, "template", "file_1");
const url = databaseUrl(adminUrl, "file_1", { user: "gc_test_app", password: appPassword });
  • startPostgres() runs POSTGRES_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. When options request a pooler, prepare receives its poolerUrl as 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. owner supplies user and password; 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 AggregateError whose first entry and cause is 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 its mode (transaction or session, as Supabase’s session pooler), poolSize and roundRobin (lend the server connection idle the longest, so a client’s transactions move between connections); poolerUrls gives 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 not pg_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.

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 };
});

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.

  • 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.

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.

Set GUESTCRAFT_TEST_INSTANT to an ISO UTC instant and TZ to an IANA time zone for the fast suites:

Terminal window
TZ=Europe/Rome GUESTCRAFT_TEST_INSTANT=2026-03-29T01:30:00Z \
pnpm exec turbo run test --concurrency=1 -- --maxWorkers=1

The 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.