/** * Singleton lifecycle integration tests — exercise the real surrealdb singleton * helpers against a live SurrealDB v3.0.4 server. * * Spec: openspec/changes/surrealdb-integration-tests/specs/database-surrealdb-integration/spec.md * "Singleton lifecycle verified against the real engine". * * The spec is written against a generic `getSingleton(name, factory, _, _, registry)` * shape; the real `@suaveplan/database/surrealdb` API is * getSurrealSingleton(options, name, clientFactory) — using a module-level Map. * These tests adapt the spec scenarios to that real shape: same-instance reuse, * close-observable-via-NOT_CONNECTED, closeAll closes every registered name, * and named-singleton isolation (closing "a" leaves "b" connected). * * @module */ import { describe, expect, it } from "@suaveplan/testing/runner"; import { SURREAL_CODES } from "../../src/errors/index.js"; import { closeAllSurrealSingletons, closeSurrealSingleton, getSurrealSingleton, hasSurrealSingleton, } from "../../src/singleton/index.js"; import { inject, surrealUnavailable } from "./integration-setup.js"; function adapterOptions(): { url: string; namespace: string; database: string; username: string; password: string; } { const config = inject("surrealdb"); return { url: config.url, namespace: config.namespace, database: config.database, username: config.username, password: config.password, }; } describe.skipIf(surrealUnavailable)( "SurrealDB singleton lifecycle (real SurrealDB v3.0.4)", () => { it("getSurrealSingleton returns the same connected instance on second call", async () => { const opts = adapterOptions(); const name = `singleton-same-${Date.now()}`; try { const a = await getSurrealSingleton(opts, name); const b = await getSurrealSingleton(opts, name); expect(b).toBe(a); expect(a.isConnected).toBe(true); const rows = await a.query<{ n: number }>("RETURN { n: 1 }"); expect(rows).toEqual([{ n: 1 }]); } finally { await closeSurrealSingleton(name); } }, 30000); it("closeSurrealSingleton closes the underlying WebSocket — query throws NOT_CONNECTED on the previously-returned reference", async () => { const opts = adapterOptions(); const name = `singleton-close-${Date.now()}`; const adapter = await getSurrealSingleton(opts, name); expect(adapter.isConnected).toBe(true); await closeSurrealSingleton(name); expect(hasSurrealSingleton(name)).toBe(false); expect(adapter.isConnected).toBe(false); await expect(adapter.query("SELECT 1")).rejects.toMatchObject({ code: SURREAL_CODES.NOT_CONNECTED, }); }, 30000); it("closeAllSurrealSingletons closes every registered singleton", async () => { const opts = adapterOptions(); const stamp = Date.now(); const names = [ `closeall-a-${stamp}`, `closeall-b-${stamp}`, `closeall-c-${stamp}`, ]; const adapters = await Promise.all( names.map((n) => getSurrealSingleton(opts, n)) ); for (const a of adapters) expect(a.isConnected).toBe(true); await closeAllSurrealSingletons(); for (const n of names) expect(hasSurrealSingleton(n)).toBe(false); for (const a of adapters) expect(a.isConnected).toBe(false); for (const a of adapters) { await expect(a.query("SELECT 1")).rejects.toMatchObject({ code: SURREAL_CODES.NOT_CONNECTED, }); } }, 60000); it("named singletons are isolated — closing one leaves others connected", async () => { const opts = adapterOptions(); const stamp = Date.now(); const nameA = `isolated-a-${stamp}`; const nameB = `isolated-b-${stamp}`; const a = await getSurrealSingleton(opts, nameA); const b = await getSurrealSingleton(opts, nameB); expect(b).not.toBe(a); try { await closeSurrealSingleton(nameA); expect(hasSurrealSingleton(nameA)).toBe(false); expect(hasSurrealSingleton(nameB)).toBe(true); expect(a.isConnected).toBe(false); expect(b.isConnected).toBe(true); const rows = await b.query<{ n: number }>("RETURN { n: 1 }"); expect(rows).toEqual([{ n: 1 }]); } finally { await closeSurrealSingleton(nameB); } }, 30000); } );