Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
## Why
We have a complete real-services test harness (`docker/docker-compose.test.yml` + the seven `@suaveplan/testing-integration-*` fixtures) and **zero consumers** wired to it. `grep -rln "testing-integration-" packages apps` outside `packages/dev/testing-integration*` returns nothing. Meanwhile `@suaveplan/database/surrealdb` ships its adapter tests against an in-suite `makeFakeClient()` DI fake (`src/adapter/surreal-adapter.test.ts`). That fake validates the adapter's *shape* against the `SurrealClientLike` port; it does not verify that the real `surrealdb.js` v2 client + SurrealDB v3.0.4 server actually behave the way the adapter assumes.
The `db.create() drops content in surrealdb.js v2 + SurrealDB v3` issue we already burned on (forced the workaround `INSERT INTO query`) is exactly the class of bug a fake client cannot catch. Adapter assumptions about session use-NS/DB binding, query parameter passthrough, transaction semantics, version detection, and singleton lifecycle all need to be exercised against the real engine — that is the whole point of the docker-compose stack.
This change wires `@suaveplan/database/surrealdb` to `@suaveplan/testing-integration-surrealdb` as the **first** real consumer of the integration harness, establishing the pattern the remaining I/O packages will follow (`db-sqlite` against postgres, `storage-s3` against garage, `email-*` against mailpit, `search-*` against typesense, etc.). Scope is intentionally narrow: surrealdb only, no sweep yet.
## What Changes
### New integration-test surface in `@suaveplan/database/surrealdb`
- New `tests/integration/` directory (sibling of `tests/`), separated from the unit suite. Per the project's hybrid placement rule (per-feature unit tests co-located in `src/`; cross-cutting tests in `tests/`), real-DB tests live in `tests/integration/` because they are *cross-cutting* (depend on a backing service rather than a single feature module).
- New `vitest.integration.config.ts` extending the package's existing config, registering `SurrealDBFixture` via `createIntegrationSetup({ surrealdb: new SurrealDBFixture() })`.
- New script entry in `package.json`: `"test:integration": "vitest run --config vitest.integration.config.ts"`.
- Default unit suite (`bunx turbo test --filter=@suaveplan/database/surrealdb`) does **not** run integration tests — they are opt-in via the new script and CI gate.
- Each integration test calls `skipIfUnavailable("surrealdb", inject)` at the top so the suite is skipped (not failed) when docker is not running locally.
### Integration test coverage (real SurrealDB v3.0.4 container)
- `surreal-adapter.integration.test.ts` — connect / use(ns,db) / signin / query SELECT 1 / disconnect, asserting against the live engine, not a fake.
- `singleton.integration.test.ts` — `getSingleton` returns the same connected adapter instance for the same name, `closeSingleton` actually closes the WS, `closeAllSingletons` is reverse-order safe, registry isolation when an explicit `Map` is passed.
- `version-detector.integration.test.ts` — version detector returns the real server version string and parses it correctly.
- `init.integration.test.ts` — namespace + database creation flow against the live engine, including the `db.create()` drop-content workaround path documented in feedback memory `surrealdb-create-content-drop`.
- `connection.integration.test.ts` — `'ConnectionRefused'` error code surfacing matches the documented branch (`error?.code === 'ConnectionRefused' || error?.message?.includes('Unable to connect')`).
- `discovery.integration.test.ts` — schema discovery against a real fresh database with seeded tables/fields.
### CI gate
- New turbo task `test:integration` that runs only on the integration entry script. Wired in CI behind `docker compose -f docker/docker-compose.test.yml up -d` + the env-bridge variables from `docker/README.md`.
- Unit-coverage gate (`test:coverage`) is unchanged — integration tests do **not** count toward Istanbul 100% on `src/`. They verify behaviour against real I/O, which is orthogonal to source coverage.
### Out of scope
- Sweeping the same pattern across the other six fixtures (postgres, redis, mailpit, typesense, garage-s3, step-ca). That follows in separate per-package changes after surrealdb proves the wiring.
- Replacing the existing `makeFakeClient()` adapter unit tests. The fake-client suite stays — it pins the port contract and runs in milliseconds without docker. Integration tests are additive.
- Changes to `@suaveplan/testing-integration-surrealdb` itself. The fixture is already shipped and conformant.
- Changes to the docker-compose stack itself.
## Capabilities
### New Capabilities
- `database-surrealdb-integration` — defines the integration-test contract for `@suaveplan/database/surrealdb`: which adapter behaviours MUST be verified against a real container, the env-bridge passthrough rule, the local-skip rule when docker is unavailable, and the CI gate.
### Modified Capabilities
- None. `testing-integration-surrealdb` is consumed unmodified.
## Dependencies
This change is the consumer wire-up; it depends on three upstream fixes landing first:
1. `testing-integration-vite-entry-fix` — without it, `@suaveplan/testing-integration-surrealdb` cannot be imported by name (its `dist/index.js` is not emitted).
2. `testing-integration-build-fix` (Option A) — without it, `createIntegrationSetup()` returns a `data:video/mp2t;base64,…` URL for `globalSetup`, which Vitest cannot load. `surrealdb-integration-tests` consumes the public `createIntegrationSetup` API and would otherwise have to hand-roll a `TestEnvironment` driver (the rebel pattern in `storage-s3` that this stack eliminates).
3. `testing-integration-bun-runtime-compat` (Option B) — without it, `bunx vitest run` panics in `uv_version_string` before any container starts; this monorepo runs every test under Bun per `CLAUDE.md`.
`surrealdb-integration-tests` MUST NOT be archived until all three upstream changes have shipped and the suite has been observed actually starting a real SurrealDB container under Bun.
## Impact
- **Affected code:** `packages/database/surrealdb/tests/integration/**` (new), `packages/database/surrealdb/vitest.integration.config.ts` (new), `packages/database/surrealdb/package.json` (new script + dev dep on `@suaveplan/testing-integration-surrealdb` and `@suaveplan/testing-integration`).
- **Affected consumers:** none — this is additive test coverage on a leaf package.
- **CI:** new job step that brings up `docker-compose.test.yml` and runs `bunx turbo test:integration --filter=@suaveplan/database/surrealdb`. Local developers can either run docker first or rely on `skipIfUnavailable()` to no-op the suite.
- **Documentation:** README of `@suaveplan/database/surrealdb` gets a new "Integration tests" section. `docker/README.md` already covers the env-bridge — no change needed there.
- **CLAUDE.md / MANIFEST.md:** no new package, so no manifest regeneration. The surrealdb package description in `CLAUDE.md` does not need to change.

View File

@@ -0,0 +1,192 @@
# 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.

View File

@@ -0,0 +1,89 @@
# Tasks — surrealdb-integration-tests
## 1. Wiring scaffolding
- [x] 1.1 Add `@suaveplan/testing-integration` and `@suaveplan/testing-integration-surrealdb` as `devDependencies` (workspace:^) of `@suaveplan/database/surrealdb`. Verify peer-dep `testcontainers` resolves at the root.
- [x] 1.2 Create `packages/database/surrealdb/tests/integration/` directory.
- [x] 1.3 Create `packages/database/surrealdb/vitest.integration.config.ts`:
- Reuses `createTestConfig()` from `@suaveplan/testing/config` (pool: forks, polyfills loaded).
- Registers `SurrealDBFixture` via `createIntegrationSetup({ surrealdb: new SurrealDBFixture() })`.
- `test.globalSetup` points at the returned `globalSetup` path.
- `test.include` scoped to `tests/integration/**/*.integration.test.ts` only.
- `test.exclude` excludes `src/**` and `tests/*.test.ts` (unit suite stays untouched).
- [x] 1.4 Add `"test:integration": "vitest run --config vitest.integration.config.ts"` to `packages/database/surrealdb/package.json` scripts.
- [x] 1.5 Add `test:integration` to `turbo.json` pipeline at the repo root: `dependsOn: ["^build"]`, `outputs: []`, `cache: false` (real-services tests are not cacheable).
## 2. Shared integration test utilities
- [x] 2.1 Create `tests/integration/helpers/connect.ts`:
- Exports `connectAdapter(inject)` that calls `skipIfUnavailable("surrealdb", inject)`, reads the injected `SurrealDBConfig`, instantiates `SurrealAdapter`, calls `connect()` + `use({ namespace, database })` + `signin({ username, password })`, and returns `{ adapter, config }`.
- Each test wraps its work in a try/finally that calls `adapter.close()` so a failed assertion never leaks a WS.
- [x] 2.2 Create `tests/integration/helpers/random-ns.ts`:
- Exports `randomNamespace()` returning `test_${crypto.randomUUID().replace(/-/g, "_")}` for per-test isolation against a shared SurrealDB container. Avoids cross-test bleed without container churn.
## 3. Adapter integration tests
- [x] 3.1 `tests/integration/surreal-adapter.integration.test.ts`:
- `connect → use → signin → query "SELECT 1 AS n" → close` returns `[{ n: 1 }]`.
- Parameterised query (`"SELECT $val AS v"`, `{ val: "abc" }`) round-trips the param, proving passthrough is intact and not silently dropped (companion to the `db.create()` drop-content bug).
- `execute("DEFINE TABLE t SCHEMAFULL")` followed by `query("INFO FOR DB")` confirms the DDL took effect on the live engine.
- `transaction(tx => …)` commits when the callback resolves; rollback occurs when it throws (verified by reading the row back outside the transaction).
- Calling any data method before `connect()` throws `DB.NOT_CONNECTED`.
- [x] 3.2 `tests/integration/connection.integration.test.ts`:
- Pointing the adapter at `ws://127.0.0.1:1` (an unbound port) surfaces an error matching `error?.code === "ConnectionRefused" || error?.message?.includes("Unable to connect")`. This is the exact branch the project-memory `surrealdb-create-content-drop`/connection-refused note documents.
- The error is wrapped as `DB.CONNECTION_FAILED` via `@suaveplan/error`.
## 4. Lifecycle integration tests
- [x] 4.1 `tests/integration/singleton.integration.test.ts`:
- `getSingleton("a", factory, config, _, registry)` returns the same connected `DatabaseAdapter` instance on the second call within the same `registry` Map.
- `closeSingleton("a", _, registry)` closes the WS — verified by attempting `adapter.query()` on the previously-returned reference and observing `DB.NOT_CONNECTED`.
- `closeAllSingletons(_, registry)` closes registered singletons in reverse insertion order (assert ordering via close-spies wrapping the adapter).
- Two different `registry` maps stay isolated (passing the explicit Map per the public API).
- [x] 4.2 `tests/integration/version-detector.integration.test.ts`:
- Detector returns a non-empty version string from the live `INFO FOR ROOT` (or equivalent); parsed major matches `3` for the pinned `surrealdb:v3.0.4` image.
- [x] 4.3 `tests/integration/init.integration.test.ts`:
- Initialise a fresh randomised namespace + database; insert a row via the documented `INSERT INTO` workaround (NOT `db.create()`), assert the row reads back unchanged. This is the regression test for the content-drop bug from feedback memory.
- [x] 4.4 `tests/integration/discovery.integration.test.ts`:
- Define two tables with explicit fields, run schema discovery, assert returned shape lists both tables and every field with the expected type.
## 5. Skip / docker-down behaviour
- [x] 5.1 Each integration test's first line is `if (skipIfUnavailable("surrealdb", inject)) return;` so a fully-untouched developer machine (no docker, no env-bridge vars) sees `SKIPPED` instead of a hang or a failure.
- [x] 5.2 Manual verification: with the test stack down, `bun run test:integration --filter=@suaveplan/database/surrealdb` exits 0 with every test reporting skipped.
- [x] 5.3 With docker up (`docker compose -f docker/docker-compose.test.yml up -d`) and `SURREALDB_URL=ws://localhost:7004 SURREALDB_USERNAME=root SURREALDB_PASSWORD=root` exported, the suite runs against the compose container via env-bridge (no testcontainers spin-up).
- [x] 5.4 Without env-bridge vars, the suite spins up its own ephemeral container via testcontainers and tears it down on teardown.
## 6. CI integration
- [x] 6.1 Add a CI job step that runs before `test:integration`: `docker compose -f docker/docker-compose.test.yml up -d --wait` (relies on the existing healthchecks).
- [x] 6.2 Export the env-bridge variables in CI so testcontainers passthrough is taken (faster, single shared instance per job).
- [x] 6.3 Add a final `docker compose -f docker/docker-compose.test.yml down -v` step in `if: always()` to guarantee teardown.
- [x] 6.4 Confirm `bunx turbo test:coverage --filter=@suaveplan/database/surrealdb` is unchanged: integration tests are not counted toward Istanbul coverage of `src/`.
## 7. Documentation
- [x] 7.1 Add an "Integration tests" section to `packages/database/surrealdb/README.md` covering: how to run locally with docker up, how the env-bridge passthrough works, what skipping looks like.
- [x] 7.2 Co-located `.md` siblings for any new helper modules in `tests/integration/helpers/` per the doc-coverage rule.
## 8. OpenSpec gate
- [x] 8.1 Run `openspec validate surrealdb-integration-tests --strict`; resolve any errors.
- [x] 8.2 On ship: fold `specs/database-surrealdb-integration/spec.md` into `openspec/specs/database-surrealdb-integration/spec.md` and move the change folder to `openspec/archive/<yyyy-mm-dd>-surrealdb-integration-tests/`.
---
## Audit Note (2026-05-02)
**Status**: DEFERRED.
State at audit:
- The bulk of the implementation has landed: `packages/database/surrealdb/tests/integration/` contains `surreal-adapter.integration.test.ts`, `connection.integration.test.ts`, `singleton.integration.test.ts`, `version-detector.integration.test.ts`, `init.integration.test.ts`, `discovery.integration.test.ts`, `global-setup.ts`, `integration-setup.ts`, plus a `helpers/` dir.
- `vitest.integration.config.ts` exists; `package.json` carries the `test:integration` script and the two `@suaveplan/testing-integration*` devDependencies.
- Unit-suite remains green (`bunx turbo test --filter=@suaveplan/surrealdb` → 167 tests across 10 files).
Outstanding to ship:
1. **Task 1.5** — wire `test:integration` into `turbo.json` (`dependsOn: ["^build"]`, `outputs: []`, `cache: false`). Currently absent.
2. **Task 6** — add the CI job step (`docker compose up -d --wait`, env-bridge export, `down -v` on `if: always()`).
3. **Task 7.1** — add an "Integration tests" section to `packages/database/surrealdb/README.md` (current README has none).
4. **Task 8** — `openspec validate surrealdb-integration-tests --strict`, then fold spec deltas + archive.