__tests__ / knowledgeGraphManager.test.ts
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { existsSync, mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { dirname, join } from "path";
import { closeAllConnections } from "../src/db";
import { KnowledgeGraphManager, Entity, Relation } from "../src/knowledgeGraphManager";
describe("KnowledgeGraphManager", () => {
let tempDir: string;
let dbPath: string;
let manager: KnowledgeGraphManager;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "brain-manager-"));
dbPath = join(tempDir, "brain.db");
manager = new KnowledgeGraphManager(dbPath);
});
afterEach(() => {
closeAllConnections();
rmSync(tempDir, { recursive: true, force: true });
});
it("does not touch the filesystem until first use", () => {
const lazyPath = join(tempDir, "not-yet", "brain.db");
new KnowledgeGraphManager(lazyPath);
expect(existsSync(dirname(lazyPath))).toBe(false);
});
describe("createEntities", () => {
it("creates new entities", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
{ name: "Bob", entityType: "person", observations: ["likes programming"] },
];
const newEntities = await manager.createEntities(entities);
expect(newEntities).toEqual(entities);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(2);
});
it("does not create duplicate entities", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
];
await manager.createEntities(entities);
const newEntities = await manager.createEntities(entities);
expect(newEntities).toHaveLength(0);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(1);
});
it("handles empty entity arrays", async () => {
const newEntities = await manager.createEntities([]);
expect(newEntities).toHaveLength(0);
});
it("ignores duplicate entity names within a single batch, keeping the first", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["first"] },
{ name: "Alice", entityType: "person", observations: ["second"] },
];
const newEntities = await manager.createEntities(entities);
expect(newEntities).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(1);
expect(graph.entities[0].observations).toEqual(["first"]);
});
});
describe("createRelations", () => {
it("creates new relations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [{ from: "Alice", to: "Bob", relationType: "knows" }];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toEqual(relations);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("does not create duplicate relations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [{ from: "Alice", to: "Bob", relationType: "knows" }];
await manager.createRelations(relations);
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(0);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("rejects relations from non-existent entities and creates nothing", async () => {
await manager.createEntities([{ name: "Alice", entityType: "person", observations: [] }]);
await expect(
manager.createRelations([{ from: "Ghost", to: "Alice", relationType: "knows" }])
).rejects.toThrow("Entity with name Ghost not found");
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(0);
});
it("rejects a batch if any relation references a non-existent target, creating nothing", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
await expect(
manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Ghost", relationType: "knows" },
])
).rejects.toThrow("Entity with name Ghost not found");
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(0);
});
it("handles empty relation arrays", async () => {
const newRelations = await manager.createRelations([]);
expect(newRelations).toHaveLength(0);
});
it("skips duplicate relations within a single batch", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "knows" },
];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("does not collide the in-batch dedup key when entity names contain spaces", async () => {
await manager.createEntities([
{ name: "A B", entityType: "thing", observations: [] },
{ name: "C", entityType: "thing", observations: [] },
{ name: "A", entityType: "thing", observations: [] },
{ name: "B C", entityType: "thing", observations: [] },
]);
const relations: Relation[] = [
{ from: "A B", to: "C", relationType: "knows" },
{ from: "A", to: "B C", relationType: "knows" },
];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(2);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(2);
expect(graph.relations).toEqual(
expect.arrayContaining([
{ from: "A B", to: "C", relationType: "knows" },
{ from: "A", to: "B C", relationType: "knows" },
])
);
});
});
describe("addObservations", () => {
it("adds observations to existing entities", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
]);
const results = await manager.addObservations([
{ entityName: "Alice", contents: ["likes coffee", "has a dog"] },
]);
expect(results).toEqual([{ entityName: "Alice", addedObservations: ["likes coffee", "has a dog"] }]);
const graph = await manager.readGraph();
const alice = graph.entities.find(e => e.name === "Alice");
expect(alice?.observations).toHaveLength(3);
});
it("does not add duplicate observations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
]);
await manager.addObservations([{ entityName: "Alice", contents: ["likes coffee"] }]);
const results = await manager.addObservations([
{ entityName: "Alice", contents: ["likes coffee", "has a dog"] },
]);
expect(results[0].addedObservations).toEqual(["has a dog"]);
const graph = await manager.readGraph();
const alice = graph.entities.find(e => e.name === "Alice");
expect(alice?.observations).toHaveLength(3);
});
it("throws for a non-existent entity and adds nothing", async () => {
await manager.createEntities([{ name: "Alice", entityType: "person", observations: [] }]);
await expect(
manager.addObservations([{ entityName: "NonExistent", contents: ["some observation"] }])
).rejects.toThrow("Entity with name NonExistent not found");
});
});
describe("deleteEntities", () => {
it("deletes entities and reports found/not-found names", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const result = await manager.deleteEntities(["Alice", "Ghost"]);
expect(result).toEqual({ deleted: ["Alice"], notFound: ["Ghost"] });
const graph = await manager.readGraph();
expect(graph.entities.map(e => e.name)).toEqual(["Bob"]);
});
it("cascades to delete relations and observations of the deleted entity", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["note"] },
{ name: "Bob", entityType: "person", observations: [] },
{ name: "Charlie", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Bob", to: "Charlie", relationType: "knows" },
]);
await manager.deleteEntities(["Bob"]);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(2);
expect(graph.relations).toHaveLength(0);
});
it("reports nothing deleted when no name matches", async () => {
const result = await manager.deleteEntities(["Nobody"]);
expect(result).toEqual({ deleted: [], notFound: ["Nobody"] });
});
});
describe("deleteObservations", () => {
it("deletes observations and counts only those actually present", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp", "likes tea"] },
]);
const result = await manager.deleteObservations([
{ entityName: "Alice", observations: ["likes tea", "never said this"] },
]);
expect(result).toEqual({ deletedCount: 1, missingEntities: [] });
const graph = await manager.readGraph();
expect(graph.entities[0].observations).toEqual(["works at Acme Corp"]);
});
it("names an entity that does not exist without throwing", async () => {
const result = await manager.deleteObservations([
{ entityName: "Carol", observations: ["anything"] },
]);
expect(result).toEqual({ deletedCount: 0, missingEntities: ["Carol"] });
});
});
describe("deleteRelations", () => {
it("deletes only matching relations and counts them", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "works_with" },
]);
const result = await manager.deleteRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "never_existed" },
]);
expect(result).toEqual({ deletedCount: 1 });
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
expect(graph.relations[0].relationType).toBe("works_with");
});
});
describe("readGraph", () => {
it("returns an empty graph for a fresh database", async () => {
const graph = await manager.readGraph();
expect(graph).toEqual({ entities: [], relations: [] });
});
it("persists data across manager instances pointed at the same file", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["persistent data"] },
]);
const manager2 = new KnowledgeGraphManager(dbPath);
const graph = await manager2.readGraph();
expect(graph.entities).toHaveLength(1);
expect(graph.entities[0].name).toBe("Alice");
});
});
describe("searchNodes", () => {
beforeEach(async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp", "likes programming"] },
{ name: "Bob", entityType: "person", observations: ["works at TechCo"] },
{ name: "Acme Corp", entityType: "company", observations: ["tech company"] },
]);
await manager.createRelations([
{ from: "Alice", to: "Acme Corp", relationType: "works_at" },
{ from: "Bob", to: "Acme Corp", relationType: "competitor" },
]);
});
it("searches by entity name", async () => {
const result = await manager.searchNodes("Alice");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("searches by entity type", async () => {
const result = await manager.searchNodes("company");
expect(result.entities.map(e => e.name)).toEqual(["Acme Corp"]);
});
it("searches by observation content via FTS", async () => {
const result = await manager.searchNodes("programming");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("is case insensitive", async () => {
const result = await manager.searchNodes("ALICE");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("substring-matches within a word in observation content", async () => {
const result = await manager.searchNodes("gramm");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("matches a multi-word phrase against observation content", async () => {
const result = await manager.searchNodes("works at");
expect(result.entities.map(e => e.name).sort()).toEqual(["Alice", "Bob"]);
});
it("includes relations where at least one endpoint matches", async () => {
const result = await manager.searchNodes("Acme");
expect(result.entities.map(e => e.name).sort()).toEqual(["Acme Corp", "Alice"]);
expect(result.relations).toHaveLength(2);
});
it("returns an empty graph for no matches", async () => {
const result = await manager.searchNodes("NonExistent");
expect(result).toEqual({ entities: [], relations: [] });
});
it("does not throw on a query containing FTS special characters", async () => {
await expect(manager.searchNodes('some "quoted" * text')).resolves.not.toThrow();
});
it("treats a query with double quotes as a literal phrase, not an FTS syntax error", async () => {
const result = await manager.searchNodes('"Alice"');
// A literal-phrase search for the 3-character string `"Alice"` matches nothing —
// the point of this test is that it resolves cleanly instead of throwing.
expect(result.entities).toEqual([]);
});
});
describe("openNodes", () => {
beforeEach(async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
{ name: "Charlie", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Bob", to: "Charlie", relationType: "knows" },
]);
});
it("opens specific nodes by name", async () => {
const result = await manager.openNodes(["Alice", "Bob"]);
expect(result.entities.map(e => e.name).sort()).toEqual(["Alice", "Bob"]);
});
it("includes outgoing relations to nodes not in the open set", async () => {
const result = await manager.openNodes(["Alice"]);
expect(result.relations).toEqual([{ from: "Alice", to: "Bob", relationType: "knows" }]);
});
it("includes incoming relations from nodes not in the open set", async () => {
const result = await manager.openNodes(["Charlie"]);
expect(result.relations).toEqual([{ from: "Bob", to: "Charlie", relationType: "knows" }]);
});
it("handles opening non-existent nodes", async () => {
const result = await manager.openNodes(["NonExistent"]);
expect(result.entities).toHaveLength(0);
});
it("handles an empty node list", async () => {
const result = await manager.openNodes([]);
expect(result).toEqual({ entities: [], relations: [] });
});
});
});
__tests__ / knowledgeGraphManager.test.ts
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { existsSync, mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { dirname, join } from "path";
import { closeAllConnections } from "../src/db";
import { KnowledgeGraphManager, Entity, Relation } from "../src/knowledgeGraphManager";
describe("KnowledgeGraphManager", () => {
let tempDir: string;
let dbPath: string;
let manager: KnowledgeGraphManager;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "brain-manager-"));
dbPath = join(tempDir, "brain.db");
manager = new KnowledgeGraphManager(dbPath);
});
afterEach(() => {
closeAllConnections();
rmSync(tempDir, { recursive: true, force: true });
});
it("does not touch the filesystem until first use", () => {
const lazyPath = join(tempDir, "not-yet", "brain.db");
new KnowledgeGraphManager(lazyPath);
expect(existsSync(dirname(lazyPath))).toBe(false);
});
describe("createEntities", () => {
it("creates new entities", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
{ name: "Bob", entityType: "person", observations: ["likes programming"] },
];
const newEntities = await manager.createEntities(entities);
expect(newEntities).toEqual(entities);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(2);
});
it("does not create duplicate entities", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
];
await manager.createEntities(entities);
const newEntities = await manager.createEntities(entities);
expect(newEntities).toHaveLength(0);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(1);
});
it("handles empty entity arrays", async () => {
const newEntities = await manager.createEntities([]);
expect(newEntities).toHaveLength(0);
});
it("ignores duplicate entity names within a single batch, keeping the first", async () => {
const entities: Entity[] = [
{ name: "Alice", entityType: "person", observations: ["first"] },
{ name: "Alice", entityType: "person", observations: ["second"] },
];
const newEntities = await manager.createEntities(entities);
expect(newEntities).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(1);
expect(graph.entities[0].observations).toEqual(["first"]);
});
});
describe("createRelations", () => {
it("creates new relations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [{ from: "Alice", to: "Bob", relationType: "knows" }];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toEqual(relations);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("does not create duplicate relations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [{ from: "Alice", to: "Bob", relationType: "knows" }];
await manager.createRelations(relations);
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(0);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("rejects relations from non-existent entities and creates nothing", async () => {
await manager.createEntities([{ name: "Alice", entityType: "person", observations: [] }]);
await expect(
manager.createRelations([{ from: "Ghost", to: "Alice", relationType: "knows" }])
).rejects.toThrow("Entity with name Ghost not found");
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(0);
});
it("rejects a batch if any relation references a non-existent target, creating nothing", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
await expect(
manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Ghost", relationType: "knows" },
])
).rejects.toThrow("Entity with name Ghost not found");
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(0);
});
it("handles empty relation arrays", async () => {
const newRelations = await manager.createRelations([]);
expect(newRelations).toHaveLength(0);
});
it("skips duplicate relations within a single batch", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const relations: Relation[] = [
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "knows" },
];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
it("does not collide the in-batch dedup key when entity names contain spaces", async () => {
await manager.createEntities([
{ name: "A B", entityType: "thing", observations: [] },
{ name: "C", entityType: "thing", observations: [] },
{ name: "A", entityType: "thing", observations: [] },
{ name: "B C", entityType: "thing", observations: [] },
]);
const relations: Relation[] = [
{ from: "A B", to: "C", relationType: "knows" },
{ from: "A", to: "B C", relationType: "knows" },
];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(2);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(2);
expect(graph.relations).toEqual(
expect.arrayContaining([
{ from: "A B", to: "C", relationType: "knows" },
{ from: "A", to: "B C", relationType: "knows" },
])
);
});
});
describe("addObservations", () => {
it("adds observations to existing entities", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
]);
const results = await manager.addObservations([
{ entityName: "Alice", contents: ["likes coffee", "has a dog"] },
]);
expect(results).toEqual([{ entityName: "Alice", addedObservations: ["likes coffee", "has a dog"] }]);
const graph = await manager.readGraph();
const alice = graph.entities.find(e => e.name === "Alice");
expect(alice?.observations).toHaveLength(3);
});
it("does not add duplicate observations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp"] },
]);
await manager.addObservations([{ entityName: "Alice", contents: ["likes coffee"] }]);
const results = await manager.addObservations([
{ entityName: "Alice", contents: ["likes coffee", "has a dog"] },
]);
expect(results[0].addedObservations).toEqual(["has a dog"]);
const graph = await manager.readGraph();
const alice = graph.entities.find(e => e.name === "Alice");
expect(alice?.observations).toHaveLength(3);
});
it("throws for a non-existent entity and adds nothing", async () => {
await manager.createEntities([{ name: "Alice", entityType: "person", observations: [] }]);
await expect(
manager.addObservations([{ entityName: "NonExistent", contents: ["some observation"] }])
).rejects.toThrow("Entity with name NonExistent not found");
});
});
describe("deleteEntities", () => {
it("deletes entities and reports found/not-found names", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
const result = await manager.deleteEntities(["Alice", "Ghost"]);
expect(result).toEqual({ deleted: ["Alice"], notFound: ["Ghost"] });
const graph = await manager.readGraph();
expect(graph.entities.map(e => e.name)).toEqual(["Bob"]);
});
it("cascades to delete relations and observations of the deleted entity", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["note"] },
{ name: "Bob", entityType: "person", observations: [] },
{ name: "Charlie", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Bob", to: "Charlie", relationType: "knows" },
]);
await manager.deleteEntities(["Bob"]);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(2);
expect(graph.relations).toHaveLength(0);
});
it("reports nothing deleted when no name matches", async () => {
const result = await manager.deleteEntities(["Nobody"]);
expect(result).toEqual({ deleted: [], notFound: ["Nobody"] });
});
});
describe("deleteObservations", () => {
it("deletes observations and counts only those actually present", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp", "likes tea"] },
]);
const result = await manager.deleteObservations([
{ entityName: "Alice", observations: ["likes tea", "never said this"] },
]);
expect(result).toEqual({ deletedCount: 1, missingEntities: [] });
const graph = await manager.readGraph();
expect(graph.entities[0].observations).toEqual(["works at Acme Corp"]);
});
it("names an entity that does not exist without throwing", async () => {
const result = await manager.deleteObservations([
{ entityName: "Carol", observations: ["anything"] },
]);
expect(result).toEqual({ deletedCount: 0, missingEntities: ["Carol"] });
});
});
describe("deleteRelations", () => {
it("deletes only matching relations and counts them", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "works_with" },
]);
const result = await manager.deleteRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Alice", to: "Bob", relationType: "never_existed" },
]);
expect(result).toEqual({ deletedCount: 1 });
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
expect(graph.relations[0].relationType).toBe("works_with");
});
});
describe("readGraph", () => {
it("returns an empty graph for a fresh database", async () => {
const graph = await manager.readGraph();
expect(graph).toEqual({ entities: [], relations: [] });
});
it("persists data across manager instances pointed at the same file", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["persistent data"] },
]);
const manager2 = new KnowledgeGraphManager(dbPath);
const graph = await manager2.readGraph();
expect(graph.entities).toHaveLength(1);
expect(graph.entities[0].name).toBe("Alice");
});
});
describe("searchNodes", () => {
beforeEach(async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["works at Acme Corp", "likes programming"] },
{ name: "Bob", entityType: "person", observations: ["works at TechCo"] },
{ name: "Acme Corp", entityType: "company", observations: ["tech company"] },
]);
await manager.createRelations([
{ from: "Alice", to: "Acme Corp", relationType: "works_at" },
{ from: "Bob", to: "Acme Corp", relationType: "competitor" },
]);
});
it("searches by entity name", async () => {
const result = await manager.searchNodes("Alice");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("searches by entity type", async () => {
const result = await manager.searchNodes("company");
expect(result.entities.map(e => e.name)).toEqual(["Acme Corp"]);
});
it("searches by observation content via FTS", async () => {
const result = await manager.searchNodes("programming");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("is case insensitive", async () => {
const result = await manager.searchNodes("ALICE");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("substring-matches within a word in observation content", async () => {
const result = await manager.searchNodes("gramm");
expect(result.entities.map(e => e.name)).toEqual(["Alice"]);
});
it("matches a multi-word phrase against observation content", async () => {
const result = await manager.searchNodes("works at");
expect(result.entities.map(e => e.name).sort()).toEqual(["Alice", "Bob"]);
});
it("includes relations where at least one endpoint matches", async () => {
const result = await manager.searchNodes("Acme");
expect(result.entities.map(e => e.name).sort()).toEqual(["Acme Corp", "Alice"]);
expect(result.relations).toHaveLength(2);
});
it("returns an empty graph for no matches", async () => {
const result = await manager.searchNodes("NonExistent");
expect(result).toEqual({ entities: [], relations: [] });
});
it("does not throw on a query containing FTS special characters", async () => {
await expect(manager.searchNodes('some "quoted" * text')).resolves.not.toThrow();
});
it("treats a query with double quotes as a literal phrase, not an FTS syntax error", async () => {
const result = await manager.searchNodes('"Alice"');
// A literal-phrase search for the 3-character string `"Alice"` matches nothing —
// the point of this test is that it resolves cleanly instead of throwing.
expect(result.entities).toEqual([]);
});
});
describe("openNodes", () => {
beforeEach(async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
{ name: "Bob", entityType: "person", observations: [] },
{ name: "Charlie", entityType: "person", observations: [] },
]);
await manager.createRelations([
{ from: "Alice", to: "Bob", relationType: "knows" },
{ from: "Bob", to: "Charlie", relationType: "knows" },
]);
});
it("opens specific nodes by name", async () => {
const result = await manager.openNodes(["Alice", "Bob"]);
expect(result.entities.map(e => e.name).sort()).toEqual(["Alice", "Bob"]);
});
it("includes outgoing relations to nodes not in the open set", async () => {
const result = await manager.openNodes(["Alice"]);
expect(result.relations).toEqual([{ from: "Alice", to: "Bob", relationType: "knows" }]);
});
it("includes incoming relations from nodes not in the open set", async () => {
const result = await manager.openNodes(["Charlie"]);
expect(result.relations).toEqual([{ from: "Bob", to: "Charlie", relationType: "knows" }]);
});
it("handles opening non-existent nodes", async () => {
const result = await manager.openNodes(["NonExistent"]);
expect(result.entities).toHaveLength(0);
});
it("handles an empty node list", async () => {
const result = await manager.openNodes([]);
expect(result).toEqual({ entities: [], relations: [] });
});
});
});