193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# 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.
|