Files
2026-05-27 23:29:53 +01:00

11 KiB

Capability Spec Delta: database-surrealdb-integration (database-surrealdb-integration)

Package: @suaveplan/database/surrealdb Status: PROPOSED

ADDED Requirements

Requirement: Real-engine integration suite exists at tests/integration

The package SHALL ship a tests/integration/ directory whose tests run against a real SurrealDB v3.0.4 server. The suite SHALL be invoked via the test:integration script and SHALL NOT run as part of the default test or test:coverage scripts. The default unit suite (makeFakeClient() + src/**/*.test.ts) MUST remain present and unmodified.

Scenario: default unit suite ignores integration tests

  • WHEN bunx turbo test --filter=@suaveplan/database/surrealdb runs
  • THEN files under tests/integration/** are not executed and no docker container is started

Scenario: integration suite runs only via test:integration

  • WHEN bun run test:integration is invoked inside packages/database/surrealdb
  • THEN vitest loads vitest.integration.config.ts, the global-setup starts SurrealDBFixture, and only tests/integration/**/*.integration.test.ts files are executed

Verified by: vitest.integration.config.ts::test.include, package.json::scripts.test:integration


Requirement: SurrealDBFixture is the only source of connection config

Integration tests MUST obtain SurrealDB connection details exclusively via the injected SurrealDBConfig provided by @suaveplan/testing-integration-surrealdb. Tests MUST NOT hard-code URLs, namespaces, databases, usernames, or passwords. Reading raw process.env for these values inside test files is prohibited.

Scenario: tests read config via inject helper

  • WHEN an integration test sets up its adapter
  • THEN it calls connectAdapter(inject) (or equivalent) which resolves inject("surrealdb") and returns SurrealDBConfig

Verified by: tests/integration/helpers/connect.ts, every tests/integration/*.integration.test.ts


Requirement: Skip-when-unavailable is mandatory

Every integration test SHALL invoke skipIfUnavailable("surrealdb", inject) as its first action. When the fixture failed to start (docker not running, network unavailable), the test MUST report SKIPPED and MUST NOT throw, hang, or fail the suite.

Scenario: docker-down skip path

  • WHEN docker is not running and no env-bridge variables are set
  • THEN every integration test reports SKIPPED, the runner exits 0, and no testcontainers operation is attempted past the failed health probe

Scenario: hard failure is not allowed

  • WHEN inject("surrealdb") is undefined because startup failed
  • THEN the helper short-circuits via skipIfUnavailable instead of throwing a TypeError on a property access

Verified by: tests/integration/helpers/connect.ts, every tests/integration/*.integration.test.ts


Requirement: Env-bridge passthrough is honoured

When SURREALDB_URL, SURREALDB_USERNAME, and SURREALDB_PASSWORD are all set and non-empty, the integration suite SHALL run against the existing SurrealDB instance reachable at that URL (typically the docker-compose.test.yml stack on port 7004) and SHALL NOT spin up a testcontainers instance. When any of those three variables is missing or empty, the fixture spins up its own container.

Scenario: passthrough used when all three vars present

  • WHEN SURREALDB_URL=ws://localhost:7004 SURREALDB_USERNAME=root SURREALDB_PASSWORD=root bun run test:integration
  • THEN no testcontainers instance is started and tests connect directly to the host URL

Scenario: container mode used when any var missing

  • WHEN only SURREALDB_URL is set
  • THEN the fixture starts an ephemeral container with a generated random password and ignores the partial env-bridge

Verified by: the upstream SurrealDBFixture contract in openspec/specs/testing-integration-surrealdb/spec.md; integration consumer relies on it.


Requirement: Adapter behaviours verified against the real engine

The integration suite SHALL verify the following adapter behaviours against a live SurrealDB v3.0.4 server:

  1. connect + use(namespace, database) + signin(creds) + query("SELECT 1") returns [{ n: 1 }].
  2. Parameterised queries round-trip parameters through the live engine without value loss.
  3. execute() of DDL persists schema changes observable via subsequent query("INFO FOR DB").
  4. transaction(fn) commits on resolved callbacks and rolls back on thrown callbacks; rollback verified by reading the row back outside the transaction and asserting absence.
  5. Calling any data method before connect() throws an error whose code is DB.NOT_CONNECTED.
  6. Connecting to an unbound WebSocket port surfaces a connection error whose code is "ConnectionRefused" OR whose message contains "Unable to connect", wrapped in a DatabaseError with code DB.CONNECTION_FAILED.

Scenario: parameterised query passthrough (regression for create-content-drop class)

  • WHEN adapter.query("SELECT $val AS v", { val: "abc" }) runs against a real container
  • THEN the result is [{ v: "abc" }] — proving the client does not silently drop the param the way db.create() was observed to drop content in surrealdb.js v2 + SurrealDB v3

Scenario: connection-refused branch matches documented predicate

  • WHEN the adapter is pointed at ws://127.0.0.1:1 and connect() is awaited
  • THEN the rejection's code equals "ConnectionRefused" OR its message includes "Unable to connect", AND the wrapped error code is DB.CONNECTION_FAILED

Verified by: tests/integration/surreal-adapter.integration.test.ts, tests/integration/connection.integration.test.ts


Requirement: Singleton lifecycle verified against the real engine

The integration suite SHALL verify that:

  1. getSingleton(name, factory, config, _, registry) returns the same connected DatabaseAdapter instance on a second call within the same registry Map.
  2. closeSingleton(name, _, registry) actually closes the underlying WebSocket — observable by query() on the previously-returned reference throwing DB.NOT_CONNECTED.
  3. closeAllSingletons(_, registry) closes registered adapters in reverse insertion order.
  4. Two distinct registry Maps remain isolated.

Scenario: closeAllSingletons reverses insertion order

  • WHEN singletons "a", "b", "c" are registered in that order and closeAllSingletons(_, registry) is awaited
  • THEN the close sequence observed via spies is c, b, a

Verified by: tests/integration/singleton.integration.test.ts


Requirement: Insert-via-INSERT-INTO regression coverage

The integration suite SHALL contain at least one test that inserts a row using the documented INSERT INTO workaround (NOT db.create()) and reads it back unchanged. This pins the workaround for the surrealdb.js v2 + SurrealDB v3 content-drop bug captured in feedback memory surrealdb-create-content-drop.

Scenario: INSERT INTO round-trips full content

  • WHEN a row { id: "person:alice", name: "Alice", age: 30 } is inserted via INSERT INTO person { … } and then selected
  • THEN the read row equals the inserted row field-for-field

Verified by: tests/integration/init.integration.test.ts


Requirement: Version detector verified against the real engine

The integration suite SHALL verify that the version detector returns a non-empty version string from the live server and that the parsed major version equals the major of the pinned image (3 for surrealdb:v3.0.4). Hard-coding the full version string is prohibited (the patch may change); only the major is asserted.

Scenario: version major matches pinned image

  • WHEN the version detector runs against the started fixture
  • THEN the parsed major is 3

Verified by: tests/integration/version-detector.integration.test.ts


Requirement: Schema discovery verified against the real engine

The integration suite SHALL verify schema discovery against a fresh randomised namespace + database: defining two tables with explicit fields, running discovery, and asserting that the returned schema lists both tables and every defined field with the expected type.

Scenario: discovery lists every defined table and field

  • WHEN DEFINE TABLE a SCHEMAFULL FIELD x TYPE string and DEFINE TABLE b SCHEMAFULL FIELD y TYPE int have been executed
  • THEN discovery returns both a and b, with a.x: string and b.y: int

Verified by: tests/integration/discovery.integration.test.ts


Requirement: Per-test namespace isolation

To avoid cross-test bleed when running against a shared SurrealDB instance (env-bridge mode), each integration test SHALL use a randomised namespace (e.g. test_<uuid-without-dashes>) for any DDL it performs. Tests MUST NOT depend on artefacts created by sibling tests.

Scenario: every test runs in a private namespace

  • WHEN two integration tests run in parallel against the same container
  • THEN each chooses a distinct random namespace and neither observes the other's tables

Verified by: tests/integration/helpers/random-ns.ts, every test that performs DDL


Requirement: CI runs the suite against docker-compose.test.yml

CI SHALL bring up docker/docker-compose.test.yml with --wait (relying on the existing healthchecks) before invoking bunx turbo test:integration --filter=@suaveplan/database/surrealdb, export the env-bridge variables so testcontainers passthrough is taken, and tear the stack down in an if: always() step. The integration job MUST be a separate gate from the unit/coverage gate so a failed container does not poison Istanbul coverage reporting.

Scenario: CI passthrough avoids per-test container churn

  • WHEN the CI integration job runs
  • THEN exactly one SurrealDB container exists for the duration of the job and every test connects to it via env-bridge

Verified by: the CI workflow file added in this change.


Requirement: Integration tests do not count toward unit coverage

bunx turbo test:coverage --filter=@suaveplan/database/surrealdb MUST continue to invoke ONLY the unit suite. Integration tests under tests/integration/** MUST be excluded from Istanbul coverage instrumentation. The 100% coverage gate is preserved on src/** source modules; integration tests verify behaviour orthogonal to source coverage.

Scenario: integration files excluded from coverage

  • WHEN the coverage report is generated
  • THEN no entries for tests/integration/** appear in the report and the threshold check is unaffected by the integration suite's existence

Verified by: vitest.config.ts::test.coverage.exclude (unchanged) and vitest.integration.config.ts operating as a separate config.