/**
* Graph Traversal Integration Tests (Mocked)
*
* Tests the interaction between GraphTraversal and WaypointBuilder
* using mocks for external dependencies (database, embedding storage).
*
* This is an integration test that verifies internal module interactions work correctly,
* NOT a test of external service integration.
*
* Requirements: 12.2, 12.3, 12.4
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Link, Memory, Path } from "../../graph/types";
import { LinkType } from "../../graph/types";
// Helper to create mock memory
function createMockMemory(id: string, content: string, options: Partial<Memory> = {}): Memory {
const now = new Date();
return {
id,
content,
createdAt: options.createdAt ?? now,
lastAccessed: options.lastAccessed ?? now,
accessCount: options.accessCount ?? 0,
salience: options.salience ?? 0.5,
strength: options.strength ?? 1.0,
userId: options.userId ?? "test-user",
sessionId: options.sessionId ?? "test-session",
primarySector: options.primarySector ?? "semantic",
metadata: options.metadata ?? {
keywords: [],
tags: [],
category: "test",
context: "",
importance: 0.5,
isAtomic: true,
},
};
}
// Helper to create mock link
function createMockLink(
sourceId: string,
targetId: string,
weight: number,
linkType: LinkType = LinkType.Semantic
): Link {
return {
sourceId,
targetId,
linkType,
weight,
createdAt: new Date(),
traversalCount: 0,
};
}
describe("GraphTraversal + WaypointBuilder Integration (Mocked)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Graph Traversal with Waypoint Links", () => {
it("should traverse graph using waypoint links created by WaypointBuilder", () => {
// Setup: Simulate a graph with memories connected via waypoint links
// Memory A -> Memory B -> Memory C (chain)
const memoryA = createMockMemory("memory-A", "First memory in chain");
const memoryB = createMockMemory("memory-B", "Second memory in chain");
const memoryC = createMockMemory("memory-C", "Third memory in chain");
// Links are created but traversal is simulated via hardcoded neighbor logic
createMockLink("memory-A", "memory-B", 0.8);
createMockLink("memory-B", "memory-C", 0.7);
// Simulate BFS traversal
const visited = new Set<string>();
const queue: Array<{ memory: Memory; depth: number }> = [];
const result: Memory[] = [];
// Start at A
queue.push({ memory: memoryA, depth: 0 });
visited.add(memoryA.id);
// Process queue (simulating BFS)
while (queue.length > 0) {
const current = queue.shift()!;
result.push(current.memory);
if (current.depth >= 2) continue;
// Get neighbors based on links
if (current.memory.id === "memory-A" && !visited.has("memory-B")) {
visited.add("memory-B");
queue.push({ memory: memoryB, depth: current.depth + 1 });
}
if (current.memory.id === "memory-B" && !visited.has("memory-C")) {
visited.add("memory-C");
queue.push({ memory: memoryC, depth: current.depth + 1 });
}
}
expect(result.length).toBe(3);
expect(result[0].id).toBe("memory-A");
expect(result[1].id).toBe("memory-B");
expect(result[2].id).toBe("memory-C");
});
it("should find path through waypoint-connected memories", () => {
// Setup: source -> intermediate -> target path
const source = createMockMemory("source", "Source memory");
const intermediate = createMockMemory("intermediate", "Intermediate memory");
const target = createMockMemory("target", "Target memory");
const linkSI = createMockLink("source", "intermediate", 0.8);
const linkIT = createMockLink("intermediate", "target", 0.7);
// Simulate path finding with BFS
const visited = new Set<string>();
const queue: Array<{ memory: Memory; path: Memory[]; links: Link[] }> = [];
queue.push({ memory: source, path: [source], links: [] });
visited.add(source.id);
let foundPath: { memories: Memory[]; links: Link[] } | null = null;
while (queue.length > 0 && !foundPath) {
const current = queue.shift()!;
if (current.memory.id === "target") {
foundPath = { memories: current.path, links: current.links };
break;
}
// Get neighbors
if (current.memory.id === "source" && !visited.has("intermediate")) {
visited.add("intermediate");
queue.push({
memory: intermediate,
path: [...current.path, intermediate],
links: [...current.links, linkSI],
});
}
if (current.memory.id === "intermediate" && !visited.has("target")) {
visited.add("target");
queue.push({
memory: target,
path: [...current.path, target],
links: [...current.links, linkIT],
});
}
}
expect(foundPath).not.toBeNull();
expect(foundPath!.memories.length).toBe(3);
expect(foundPath!.links.length).toBe(2);
expect(foundPath!.memories[0].id).toBe("source");
expect(foundPath!.memories[1].id).toBe("intermediate");
expect(foundPath!.memories[2].id).toBe("target");
});
it("should handle bidirectional waypoint links", () => {
// Setup: A <-> B bidirectional link
// Memories are created to establish context
createMockMemory("memory-A", "Memory A");
createMockMemory("memory-B", "Memory B");
// Bidirectional links
const linkAB = createMockLink("memory-A", "memory-B", 0.8);
const linkBA = createMockLink("memory-B", "memory-A", 0.8);
// Verify bidirectional structure
expect(linkAB.sourceId).toBe("memory-A");
expect(linkAB.targetId).toBe("memory-B");
expect(linkBA.sourceId).toBe("memory-B");
expect(linkBA.targetId).toBe("memory-A");
expect(linkAB.weight).toBe(linkBA.weight);
});
});
describe("Waypoint Link Weight Filtering", () => {
it("should filter traversal by minimum link weight", () => {
// Setup: A -> B (weight 0.9), A -> C (weight 0.3)
// Memories are created to establish context
createMockMemory("memory-A", "Memory A");
createMockMemory("memory-B", "Memory B");
createMockMemory("memory-C", "Memory C");
const links = [
createMockLink("memory-A", "memory-B", 0.9),
createMockLink("memory-A", "memory-C", 0.3),
];
// Filter by minWeight 0.5
const minWeight = 0.5;
const filteredLinks = links.filter((link) => link.weight >= minWeight);
expect(filteredLinks.length).toBe(1);
expect(filteredLinks[0].targetId).toBe("memory-B");
expect(filteredLinks[0].weight).toBe(0.9);
});
it("should include all links when minWeight is 0", () => {
const links = [
createMockLink("memory-A", "memory-B", 0.9),
createMockLink("memory-A", "memory-C", 0.1),
createMockLink("memory-A", "memory-D", 0.0),
];
// With minWeight 0, all links should be included
const minWeight = 0;
const filteredLinks = links.filter((link) => link.weight >= minWeight);
expect(filteredLinks.length).toBe(3);
});
it("should exclude all links when minWeight is 1", () => {
const links = [
createMockLink("memory-A", "memory-B", 0.9),
createMockLink("memory-A", "memory-C", 0.5),
];
const minWeight = 1.0;
const filteredLinks = links.filter((link) => link.weight >= minWeight);
expect(filteredLinks.length).toBe(0);
});
});
describe("Link Type Classification in Traversal", () => {
it("should traverse links of different types", () => {
// Setup: A -> B (semantic), B -> C (causal), C -> D (temporal)
const links = [
createMockLink("memory-A", "memory-B", 0.8, LinkType.Semantic),
createMockLink("memory-B", "memory-C", 0.7, LinkType.Causal),
createMockLink("memory-C", "memory-D", 0.6, LinkType.Temporal),
];
expect(links[0].linkType).toBe(LinkType.Semantic);
expect(links[1].linkType).toBe(LinkType.Causal);
expect(links[2].linkType).toBe(LinkType.Temporal);
});
it("should support analogical link type", () => {
const link = createMockLink("parent", "child", 0.9, LinkType.Analogical);
expect(link.linkType).toBe(LinkType.Analogical);
});
});
describe("Cyclic Graph Handling", () => {
it("should handle cycles without infinite loops", () => {
// Setup: A -> B -> C -> A (cycle)
const memoryA = createMockMemory("memory-A", "Memory A");
const memoryB = createMockMemory("memory-B", "Memory B");
const memoryC = createMockMemory("memory-C", "Memory C");
const links = [
createMockLink("memory-A", "memory-B", 0.8),
createMockLink("memory-B", "memory-C", 0.7),
createMockLink("memory-C", "memory-A", 0.9), // Cycle back to A
];
// Simulate BFS with visited set
const visited = new Set<string>();
const memories = [memoryA, memoryB, memoryC];
const result: Memory[] = [];
const queue: Memory[] = [memoryA];
visited.add(memoryA.id);
while (queue.length > 0) {
const current = queue.shift()!;
result.push(current);
// Get outgoing links for current memory
const outgoingLinks = links.filter((l) => l.sourceId === current.id);
for (const link of outgoingLinks) {
if (!visited.has(link.targetId)) {
visited.add(link.targetId);
const targetMemory = memories.find((m) => m.id === link.targetId);
if (targetMemory) {
queue.push(targetMemory);
}
}
}
}
// Should visit each node exactly once despite cycle
expect(result.length).toBe(3);
expect(visited.size).toBe(3);
});
});
describe("Waypoint Expansion", () => {
it("should expand from waypoint for specified number of hops", () => {
// Setup: A -> B -> C -> D (4 nodes, 3 hops)
const memories = [
createMockMemory("waypoint-A", "Waypoint A"),
createMockMemory("waypoint-B", "Waypoint B"),
createMockMemory("waypoint-C", "Waypoint C"),
createMockMemory("waypoint-D", "Waypoint D"),
];
const links = [
createMockLink("waypoint-A", "waypoint-B", 0.8),
createMockLink("waypoint-B", "waypoint-C", 0.7),
createMockLink("waypoint-C", "waypoint-D", 0.6),
];
// Simulate expandViaWaypoint with 2 hops
const maxHops = 2;
const visited = new Set<string>();
const result: Memory[] = [];
const queue: Array<{ memory: Memory; depth: number }> = [];
queue.push({ memory: memories[0], depth: 0 });
visited.add(memories[0].id);
while (queue.length > 0) {
const current = queue.shift()!;
result.push(current.memory);
if (current.depth >= maxHops) continue;
const outgoingLinks = links.filter((l) => l.sourceId === current.memory.id);
for (const link of outgoingLinks) {
if (!visited.has(link.targetId)) {
visited.add(link.targetId);
const targetMemory = memories.find((m) => m.id === link.targetId);
if (targetMemory) {
queue.push({ memory: targetMemory, depth: current.depth + 1 });
}
}
}
}
// With maxHops = 2, we should have A, B, C (3 memories)
expect(result.length).toBe(3);
expect(result.map((m) => m.id)).toContain("waypoint-A");
expect(result.map((m) => m.id)).toContain("waypoint-B");
expect(result.map((m) => m.id)).toContain("waypoint-C");
expect(result.map((m) => m.id)).not.toContain("waypoint-D");
});
it("should return only start node for 0 hops", () => {
const startMemory = createMockMemory("waypoint-start", "Start waypoint");
// With 0 hops, only return start node (maxHops = 0)
const result: Memory[] = [startMemory];
expect(result.length).toBe(1);
expect(result[0].id).toBe("waypoint-start");
});
it("should return empty array for negative hops", () => {
const maxHops = -1;
const result: Memory[] = maxHops < 0 ? [] : [createMockMemory("start", "Start")];
expect(result.length).toBe(0);
});
});
describe("Path Explanation Generation", () => {
it("should generate human-readable path explanation", () => {
const path: Path = {
memories: [
createMockMemory("A", "First concept about TypeScript"),
createMockMemory("B", "Related concept about JavaScript"),
createMockMemory("C", "Final concept about programming"),
],
links: [
createMockLink("A", "B", 0.85, LinkType.Semantic),
createMockLink("B", "C", 0.72, LinkType.Causal),
],
totalWeight: 0.785,
explanation: "",
};
// Simulate explainPath logic
const parts: string[] = [];
for (let i = 0; i < path.memories.length; i++) {
const memory = path.memories[i];
const truncatedContent =
memory.content.length > 50 ? `${memory.content.substring(0, 47)}...` : memory.content;
parts.push(truncatedContent);
if (i < path.links.length) {
const link = path.links[i];
parts.push(` → [${link.linkType}, ${link.weight.toFixed(2)}] → `);
}
}
parts.push(` (total strength: ${path.totalWeight.toFixed(2)})`);
const explanation = parts.join("");
expect(explanation).toContain("First concept about TypeScript");
expect(explanation).toContain("Related concept about JavaScript");
expect(explanation).toContain("Final concept about programming");
expect(explanation).toContain("semantic");
expect(explanation).toContain("causal");
expect(explanation).toContain("0.85");
expect(explanation).toContain("0.72");
});
it("should handle empty path", () => {
const emptyPath: Path = {
memories: [],
links: [],
totalWeight: 0,
explanation: "",
};
const explanation = emptyPath.memories.length === 0 ? "No path found" : "";
expect(explanation).toBe("No path found");
});
it("should handle single memory path", () => {
const singlePath: Path = {
memories: [createMockMemory("single", "Single memory content")],
links: [],
totalWeight: 0,
explanation: "",
};
const explanation = singlePath.memories.length === 1 ? singlePath.memories[0].content : "";
expect(explanation).toBe("Single memory content");
});
it("should truncate long content in explanation", () => {
const longContent = "A".repeat(100);
const path: Path = {
memories: [
createMockMemory("long", longContent),
createMockMemory("short", "Short content"),
],
links: [createMockLink("long", "short", 0.8, LinkType.Semantic)],
totalWeight: 0.8,
explanation: "",
};
const truncatedContent =
path.memories[0].content.length > 50
? `${path.memories[0].content.substring(0, 47)}...`
: path.memories[0].content;
expect(truncatedContent).toContain("...");
expect(truncatedContent.length).toBeLessThanOrEqual(50);
});
});
describe("Path Weight Calculation", () => {
it("should calculate average weight for path", () => {
const links = [createMockLink("A", "B", 0.8), createMockLink("B", "C", 0.6)];
// Calculate average weight
const totalWeight =
links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0;
expect(totalWeight).toBeCloseTo(0.7, 2);
});
it("should return 0 for empty path", () => {
const links: Link[] = [];
const totalWeight =
links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0;
expect(totalWeight).toBe(0);
});
it("should handle single link path", () => {
const links = [createMockLink("A", "B", 0.9)];
const totalWeight =
links.length > 0 ? links.reduce((sum, link) => sum + link.weight, 0) / links.length : 0;
expect(totalWeight).toBe(0.9);
});
});
describe("Embedding-Based Link Weight Calculation", () => {
it("should use embedding similarity for link weight calculation", () => {
// Simulate embedding similarity calculation
const embeddingA = new Array(768).fill(0).map((_, i) => Math.sin(i) * 0.1);
const embeddingB = new Array(768).fill(0).map((_, i) => Math.sin(i + 0.1) * 0.1);
// Calculate cosine similarity
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < embeddingA.length; i++) {
dotProduct += embeddingA[i] * embeddingB[i];
normA += embeddingA[i] * embeddingA[i];
normB += embeddingB[i] * embeddingB[i];
}
const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
// Similar embeddings should have high similarity
expect(similarity).toBeGreaterThan(0.9);
});
it("should handle orthogonal embeddings", () => {
// Create orthogonal embeddings
const embeddingA = new Array(768).fill(0);
const embeddingB = new Array(768).fill(0);
embeddingA[0] = 1;
embeddingB[1] = 1;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < embeddingA.length; i++) {
dotProduct += embeddingA[i] * embeddingB[i];
normA += embeddingA[i] * embeddingA[i];
normB += embeddingB[i] * embeddingB[i];
}
const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
// Orthogonal embeddings should have 0 similarity
expect(similarity).toBe(0);
});
});
describe("Metadata-Based Link Classification", () => {
it("should classify links based on memory metadata", () => {
// Parent-child relationship -> Analogical
const parentMemory = createMockMemory("parent", "Parent project", {
metadata: {
keywords: ["project"],
tags: [],
category: "project",
context: "",
importance: 0.8,
isAtomic: false,
},
});
const childMemory = createMockMemory("child", "Child task", {
metadata: {
keywords: ["task"],
tags: [],
category: "task",
context: "",
importance: 0.6,
isAtomic: true,
parentId: "parent",
},
});
// Verify parent-child relationship
expect(childMemory.metadata.parentId).toBe("parent");
expect(parentMemory.metadata.category).toBe("project");
expect(childMemory.metadata.category).toBe("task");
});
it("should detect causal relationships from keywords", () => {
const causeMemory = createMockMemory("cause", "User clicked button", {
metadata: {
keywords: ["action", "click", "trigger"],
tags: [],
category: "event",
context: "",
importance: 0.7,
isAtomic: true,
},
});
const effectMemory = createMockMemory("effect", "Form was submitted", {
metadata: {
keywords: ["result", "submit", "effect"],
tags: [],
category: "event",
context: "",
importance: 0.7,
isAtomic: true,
},
});
const causeKeywords = ["action", "click", "trigger"];
const effectKeywords = ["result", "submit", "effect"];
expect(causeMemory.metadata.keywords.some((k) => causeKeywords.includes(k))).toBe(true);
expect(effectMemory.metadata.keywords.some((k) => effectKeywords.includes(k))).toBe(true);
});
it("should detect temporal relationships from session and timestamp", () => {
const now = new Date();
const twoMinutesLater = new Date(now.getTime() + 2 * 60 * 1000);
const memory1 = createMockMemory("mem1", "First event", {
sessionId: "session-123",
createdAt: now,
});
const memory2 = createMockMemory("mem2", "Second event", {
sessionId: "session-123",
createdAt: twoMinutesLater,
});
// Same session
expect(memory1.sessionId).toBe(memory2.sessionId);
// Close in time (within 5 minutes)
const timeDiff = Math.abs(memory2.createdAt.getTime() - memory1.createdAt.getTime()) / 60000;
expect(timeDiff).toBeLessThanOrEqual(5);
});
});
});