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/surrealdbruns - 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:integrationis invoked insidepackages/database/surrealdb - THEN vitest loads
vitest.integration.config.ts, the global-setup startsSurrealDBFixture, and onlytests/integration/**/*.integration.test.tsfiles 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 resolvesinject("surrealdb")and returnsSurrealDBConfig
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")isundefinedbecause startup failed - THEN the helper short-circuits via
skipIfUnavailableinstead of throwing aTypeErroron 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
testcontainersinstance is started and tests connect directly to the host URL
Scenario: container mode used when any var missing
- WHEN only
SURREALDB_URLis 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:
connect+use(namespace, database)+signin(creds)+query("SELECT 1")returns[{ n: 1 }].- Parameterised queries round-trip parameters through the live engine without value loss.
execute()of DDL persists schema changes observable via subsequentquery("INFO FOR DB").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.- Calling any data method before
connect()throws an error whose code isDB.NOT_CONNECTED. - Connecting to an unbound WebSocket port surfaces a connection error whose
codeis"ConnectionRefused"OR whosemessagecontains"Unable to connect", wrapped in aDatabaseErrorwith codeDB.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 waydb.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:1andconnect()is awaited - THEN the rejection's
codeequals"ConnectionRefused"OR itsmessageincludes"Unable to connect", AND the wrapped error code isDB.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:
getSingleton(name, factory, config, _, registry)returns the same connectedDatabaseAdapterinstance on a second call within the same registry Map.closeSingleton(name, _, registry)actually closes the underlying WebSocket — observable byquery()on the previously-returned reference throwingDB.NOT_CONNECTED.closeAllSingletons(_, registry)closes registered adapters in reverse insertion order.- Two distinct
registryMaps remain isolated.
Scenario: closeAllSingletons reverses insertion order
- WHEN singletons
"a","b","c"are registered in that order andcloseAllSingletons(_, 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 viaINSERT 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 stringandDEFINE TABLE b SCHEMAFULL FIELD y TYPE inthave been executed - THEN discovery returns both
aandb, witha.x: stringandb.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.