mirror of
https://github.com/hanzoai/bot.git
synced 2026-10-30 17:24:26 +00:00
Refactor following review:
• Dropped Postgres backend entirely. Don't ship complexity solo devs
don't need. SQLite is the one canonical default — single file at
~/.hanzo-bot/brain/brain.db, zero infra, single static binary.
• Kept the BrainStore interface pluggable. New `registerBackend(name,
factory)` API lets 3rd parties ship Postgres / LanceDB / D1 / libSQL
as sibling extensions without forking core memory. Env override
HANZO_BRAIN_BACKEND=<name> selects at runtime.
• Renamed package @hanzo/bot-memory-postgres → @hanzo/bot-memory.
Single source of truth for the BrainStore contract; SQLite is the
one backend that ships with the bot.
• README per extension:
brain/README.md — meta-pack overview + gbrain comparison
memory/README.md — schema spec + pluggability + driver
graph-links/README.md — edge-type table + zero-LLM rationale
recipes-brain/README.md — swipe-train loop + recipe authoring
• Pluggability tests added — registerBackend exposes new backends,
unknown backend throws a helpful error.
Tests: 18/18 brain extensions pass (was 16, +2 for pluggability).
End-to-end smoke confirms: SQLite default, listBackends()=["sqlite"],
edges + facts + FTS hybrid search + recipe load all green.
The Postgres path can come back as @hanzo/bot-memory-postgres in a
separate package later. Schema is fully spec'd in memory/sqlite.ts so
re-implementation is mechanical.
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import { SqliteStore } from "./sqlite.js";
|
|
|
|
describe("memory: SQLite (canonical default)", () => {
|
|
let dir: string;
|
|
let store: SqliteStore;
|
|
|
|
beforeAll(async () => {
|
|
dir = await mkdtemp(join(tmpdir(), "hanzo-brain-test-"));
|
|
store = new SqliteStore({ dbPath: join(dir, "brain.db") });
|
|
try {
|
|
await store.init();
|
|
} catch (e: any) {
|
|
// CI runners without bun:sqlite or better-sqlite3 — skip the suite.
|
|
if (String(e.message).includes("neither bun:sqlite nor better-sqlite3")) {
|
|
console.warn("[memory test] SQLite driver unavailable, skipping suite");
|
|
// Mark as skipped by short-circuiting subsequent tests.
|
|
store = null as any;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await store?.close?.();
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("upserts and reads a page", async () => {
|
|
if (!store) return;
|
|
await store.upsertPage("people/alice", "Alice is the CEO of Acme.", { type: "person" });
|
|
const got = await store.getPage("people/alice");
|
|
expect(got?.slug).toBe("people/alice");
|
|
expect(got?.content).toContain("Alice");
|
|
});
|
|
|
|
it("upserts edges and reads them back", async () => {
|
|
if (!store) return;
|
|
await store.upsertEdges("meetings/m1", [
|
|
{ source: "meetings/m1", target: "people/alice", type: "attended" },
|
|
{ source: "meetings/m1", target: "people/bob", type: "attended" },
|
|
]);
|
|
const out = await store.edgesFor("meetings/m1", "out");
|
|
expect(out).toHaveLength(2);
|
|
const inAlice = await store.edgesFor("people/alice", "in");
|
|
expect(inAlice).toHaveLength(1);
|
|
});
|
|
|
|
it("upserts and recalls facts", async () => {
|
|
if (!store) return;
|
|
await store.upsertFact({
|
|
subject: "people/alice",
|
|
predicate: "preference",
|
|
object: "Always replies in under 2 hours",
|
|
});
|
|
const facts = await store.recall("people/alice");
|
|
expect(facts.length).toBeGreaterThan(0);
|
|
expect(facts[0].predicate).toBe("preference");
|
|
});
|
|
|
|
it("hybrid search returns FTS hits", async () => {
|
|
if (!store) return;
|
|
const hits = await store.hybridSearch("Acme");
|
|
expect(hits.length).toBeGreaterThan(0);
|
|
expect(hits[0].slug).toBe("people/alice");
|
|
});
|
|
});
|
|
|
|
describe("memory: pluggability", () => {
|
|
it("registerBackend exposes new backends to open()", async () => {
|
|
const { open, registerBackend, listBackends } = await import("./index.js");
|
|
// Stub backend that records init() being called.
|
|
let initialized = false;
|
|
registerBackend("test-stub", () => ({
|
|
init: async () => {
|
|
initialized = true;
|
|
},
|
|
upsertPage: async () => {},
|
|
getPage: async () => null,
|
|
upsertEdges: async () => {},
|
|
edgesFor: async () => [],
|
|
upsertFact: async () => {},
|
|
recall: async () => [],
|
|
hybridSearch: async () => [],
|
|
close: async () => {},
|
|
}));
|
|
expect(listBackends()).toContain("test-stub");
|
|
expect(listBackends()).toContain("sqlite");
|
|
await open({ backend: "test-stub" });
|
|
expect(initialized).toBe(true);
|
|
});
|
|
|
|
it("unknown backend throws with a helpful message", async () => {
|
|
const { open } = await import("./index.js");
|
|
await expect(open({ backend: "no-such-thing" })).rejects.toThrow(/unknown backend/);
|
|
});
|
|
});
|