Add an optional PostgreSQL backend (QMD_BACKEND=pg) alongside the unchanged default SQLite path. PG store uses pgvector (HNSW) for vectors and pg_jieba + pg_trgm for full-text/Chinese tokenization, with a namespace column isolating multi-agent memory (openclaw/hermes). - src/pg/: config, db-pg, schema bootstrap, memory store - MCP memory_add/memory_search/memory_get tools; qmd pg status + memory CLI - connection via QMD_PG_URL/DATABASE_URL/qmd config, stunnel TLS 5443 - tests: pg-config (unit) + pg-memory integration (gated on QMD_PG_URL) + pg-compose - docs/plan: plan, usage, test report, changelog; track docs/**/*.md SQLite path: zero regression (typecheck clean, 249 passed / 6 skipped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
/**
|
|
* pg-config.test.ts - Pure unit tests for the PG memory bridge configuration.
|
|
*
|
|
* These never touch a database, so they run everywhere. Integration tests that
|
|
* require a live PostgreSQL are in pg-memory.integration.test.ts (gated on
|
|
* QMD_PG_URL).
|
|
*/
|
|
|
|
import { describe, test, expect } from "vitest";
|
|
import {
|
|
resolveBackend,
|
|
isPgBackend,
|
|
resolveNamespace,
|
|
resolvePgConfig,
|
|
resolvePgSsl,
|
|
redactConnectionString,
|
|
DEFAULT_NAMESPACE,
|
|
} from "../src/pg/config.js";
|
|
import { toVectorLiteral } from "../src/pg/db-pg.js";
|
|
|
|
describe("resolveBackend", () => {
|
|
test("defaults to sqlite", () => {
|
|
expect(resolveBackend({})).toBe("sqlite");
|
|
expect(isPgBackend({})).toBe(false);
|
|
});
|
|
|
|
test("accepts pg / postgres / postgresql", () => {
|
|
expect(resolveBackend({ QMD_BACKEND: "pg" })).toBe("pg");
|
|
expect(resolveBackend({ QMD_BACKEND: "postgres" })).toBe("pg");
|
|
expect(resolveBackend({ QMD_BACKEND: "POSTGRESQL" })).toBe("pg");
|
|
expect(isPgBackend({ QMD_BACKEND: "pg" })).toBe(true);
|
|
});
|
|
|
|
test("a bare connection URL does NOT switch the backend", () => {
|
|
// Selection must be explicit so existing SQLite workflows never change.
|
|
expect(resolveBackend({ QMD_PG_URL: "postgres://x/y" })).toBe("sqlite");
|
|
});
|
|
});
|
|
|
|
describe("resolveNamespace", () => {
|
|
test("defaults to the default namespace", () => {
|
|
expect(resolveNamespace({})).toBe(DEFAULT_NAMESPACE);
|
|
expect(resolveNamespace({ QMD_NAMESPACE: " " })).toBe(DEFAULT_NAMESPACE);
|
|
});
|
|
|
|
test("uses QMD_NAMESPACE when set", () => {
|
|
expect(resolveNamespace({ QMD_NAMESPACE: "openclaw" })).toBe("openclaw");
|
|
});
|
|
});
|
|
|
|
describe("resolvePgSsl", () => {
|
|
test("disable turns TLS off", () => {
|
|
expect(resolvePgSsl({ QMD_PG_SSL: "disable" })).toBe(false);
|
|
});
|
|
|
|
test("no-verify keeps TLS but skips verification", () => {
|
|
expect(resolvePgSsl({ QMD_PG_SSL: "no-verify" })).toEqual({ rejectUnauthorized: false });
|
|
});
|
|
|
|
test("defaults to TLS without verification when no CA is supplied", () => {
|
|
expect(resolvePgSsl({})).toEqual({ rejectUnauthorized: false });
|
|
});
|
|
});
|
|
|
|
describe("resolvePgConfig", () => {
|
|
test("throws a helpful error when no URL is set", () => {
|
|
expect(() => resolvePgConfig({ QMD_BACKEND: "pg" })).toThrow(/no connection URL/i);
|
|
});
|
|
|
|
test("builds config from QMD_PG_URL", () => {
|
|
const cfg = resolvePgConfig({
|
|
QMD_BACKEND: "pg",
|
|
QMD_PG_URL: "postgres://postgres:secret@db.example.com:5443/qmd",
|
|
QMD_NAMESPACE: "hermes",
|
|
});
|
|
expect(cfg.connectionString).toContain("db.example.com:5443");
|
|
expect(cfg.namespace).toBe("hermes");
|
|
expect(cfg.max).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("falls back to DATABASE_URL", () => {
|
|
const cfg = resolvePgConfig({ DATABASE_URL: "postgres://u:p@h:5432/d" });
|
|
expect(cfg.connectionString).toContain("h:5432");
|
|
});
|
|
|
|
test("respects pool + timeout overrides", () => {
|
|
const cfg = resolvePgConfig({
|
|
QMD_PG_URL: "postgres://u:p@h:5432/d",
|
|
QMD_PG_POOL_MAX: "12",
|
|
QMD_PG_CONNECT_TIMEOUT_MS: "2500",
|
|
});
|
|
expect(cfg.max).toBe(12);
|
|
expect(cfg.connectionTimeoutMillis).toBe(2500);
|
|
});
|
|
});
|
|
|
|
describe("redactConnectionString", () => {
|
|
test("hides the password", () => {
|
|
const out = redactConnectionString("postgres://postgres:topsecret@db:5443/qmd");
|
|
expect(out).not.toContain("topsecret");
|
|
expect(out).toContain("db:5443");
|
|
});
|
|
});
|
|
|
|
describe("toVectorLiteral", () => {
|
|
test("formats number arrays as a pgvector literal", () => {
|
|
expect(toVectorLiteral([1, 2, 3])).toBe("[1,2,3]");
|
|
});
|
|
|
|
test("handles Float32Array", () => {
|
|
expect(toVectorLiteral(new Float32Array([0.5, -0.25]))).toBe("[0.5,-0.25]");
|
|
});
|
|
});
|