import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { promises as fs } from 'fs';
// import path from 'path';
import {
createSaveCheckpointTool,
createLoadContextTool,
createGetAnchorsTool,
createClearMemoryTool,
configureStorage,
resetStorage,
} from '../../src/tools';
describe('Tools Storage Integration', () => {
const testDir = './test-storage-integration';
beforeEach(async () => {
// Create test directory
await fs.mkdir(testDir, { recursive: true });
// Configure storage for tests
configureStorage({ storageDir: testDir });
});
afterEach(async () => {
// Reset storage singleton
resetStorage();
// Clean up test directory
await fs.rm(testDir, { recursive: true, force: true });
});
describe('saveCheckpoint → loadContext flow', () => {
it('should save a conversation and retrieve it', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const loadContextTool = createLoadContextTool();
// Save a conversation
const choffContent = `{state:decisive} [context:architecture]
We decided to use PostgreSQL for the main database.
This decision was made because:
→ Better JSON support with JSONB
→ Proven scalability
→ Strong community
&pattern:decision|finalized| The architecture is now set.`;
const saveResult = await saveCheckpointTool.handler({
content: choffContent,
extractAnchors: true,
});
if (!saveResult.success) {
console.error('Save failed:', saveResult.error);
}
expect(saveResult.success).toBe(true);
expect(saveResult.data).toHaveProperty('checkpointId');
// Load the conversation back
const loadResult = await loadContextTool.handler({
query: 'PostgreSQL database decision',
maxTokens: 1000,
});
if (!loadResult.success) {
console.error('Load failed:', loadResult.error);
}
expect(loadResult.success).toBe(true);
const loadData = loadResult.data as {
contexts: Array<{
conversationId: string;
content: string;
metadata: {
anchors?: Array<{ type: string; text: string }>;
};
}>;
totalTokens: number;
};
expect(loadData.contexts).toHaveLength(1);
expect(loadData.contexts[0].content).toContain('PostgreSQL');
expect(loadData.contexts[0].metadata.anchors).toBeDefined();
// Check that decision anchors were extracted
const decisionAnchors = loadData.contexts[0].metadata.anchors?.filter(
(a) => a.type === 'decision',
);
expect(decisionAnchors!.length).toBeGreaterThan(0);
expect(decisionAnchors!.some((a) => a.text.includes('PostgreSQL'))).toBe(
true,
);
});
it('should handle multiple conversations and filter by query', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const loadContextTool = createLoadContextTool();
// Save multiple conversations
const conversations = [
{
content: `{state:analytical} [context:frontend]
Exploring React vs Vue for the frontend framework.
{state:decisive} We chose React because of team expertise.`,
},
{
content: `{state:blocked} [context:deployment]
Having issues with Docker container networking.
Can't get services to communicate properly.`,
},
{
content: `{state:eureka} [context:deployment]
Found the solution! It was a DNS resolution issue.
Fixed by using container names instead of IPs.`,
},
];
for (const conv of conversations) {
await saveCheckpointTool.handler({
content: conv.content,
extractAnchors: true,
});
}
// Query for deployment-related conversations
const deploymentResult = await loadContextTool.handler({
query: 'deployment',
});
expect(deploymentResult.success).toBe(true);
const deployData = deploymentResult.data as {
contexts: Array<{ content: string }>;
};
// Should return 2 deployment-related conversations
expect(deployData.contexts.length).toBeGreaterThanOrEqual(2);
expect(
deployData.contexts.filter((c) => c.content.includes('Docker')),
).toHaveLength(1);
expect(
deployData.contexts.filter((c) => c.content.includes('DNS resolution')),
).toHaveLength(1);
});
});
describe('anchorsOnly mode', () => {
it('should return only anchors when requested', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const loadContextTool = createLoadContextTool();
// Save a conversation with multiple anchors
const content = `{state:analytical} [context:planning]
We need to decide on the API structure.
{state:decisive} Decision: Use REST for public API, GraphQL for internal.
{state:blocked} Blocker: Authentication strategy unclear.
{state:questioning} Question: Should we use OAuth2 or JWT?
{state:eureka} Breakthrough: Realized we can use both!`;
await saveCheckpointTool.handler({
content,
extractAnchors: true,
});
// Load only anchors
const result = await loadContextTool.handler({
anchorsOnly: true,
});
expect(result.success).toBe(true);
const data = result.data as {
contexts: Array<{
content: string;
metadata: {
anchors: Array<{
type: string;
text: string;
confidence: number;
}>;
};
}>;
};
const allAnchors = data.contexts.flatMap((c) => c.metadata.anchors || []);
// Should have all 4 types of anchors
const anchorTypes = [...new Set(allAnchors.map((a) => a.type))];
expect(anchorTypes).toContain('decision');
expect(anchorTypes).toContain('blocker');
expect(anchorTypes).toContain('question');
expect(anchorTypes).toContain('breakthrough');
});
});
describe('getAnchors with storage', () => {
it('should extract anchors from stored conversations', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const getAnchorsTool = createGetAnchorsTool();
// Save conversations
await saveCheckpointTool.handler({
content: `{state:decisive} Decided to implement caching.
{state:blocked} Can't figure out cache invalidation strategy.
{state:eureka} Use event-driven invalidation!`,
extractAnchors: true,
});
// Get specific anchor types
const blockersResult = await getAnchorsTool.handler({
type: 'blocker',
});
expect(blockersResult.success).toBe(true);
const blockersData = blockersResult.data as {
anchors: Array<{
type: string;
text: string;
}>;
};
expect(blockersData.anchors.every((a) => a.type === 'blocker')).toBe(
true,
);
expect(blockersData.anchors[0].text).toContain('cache invalidation');
});
});
describe('time range filtering', () => {
it('should filter conversations by time range', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const loadContextTool = createLoadContextTool();
// Save conversations at different times
const now = new Date();
// const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
await saveCheckpointTool.handler({
content: 'Yesterday: Planning meeting notes',
});
// Wait a bit to ensure different timestamps
await new Promise((resolve) => setTimeout(resolve, 10));
await saveCheckpointTool.handler({
content: 'Today: Implementation progress',
});
// Query for recent conversations (both conversations will be very recent)
const result = await loadContextTool.handler({
timeRange: {
start: new Date(now.getTime() - 60 * 1000).toISOString(), // Last minute
end: tomorrow.toISOString(),
},
});
expect(result.success).toBe(true);
const data = result.data as {
contexts: Array<{ content: string }>;
};
// Should get both recent conversations since they're saved within milliseconds
expect(data.contexts.length).toBeGreaterThanOrEqual(1);
expect(
data.contexts.some((c) =>
c.content.includes('Implementation progress'),
),
).toBe(true);
});
});
describe('token limiting', () => {
it('should respect maxTokens limit', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const loadContextTool = createLoadContextTool();
// Save multiple long conversations
for (let i = 0; i < 5; i++) {
const longContent = `Conversation ${i}: ${' '.repeat(100)}This is a very long conversation with lots of content.`;
await saveCheckpointTool.handler({ content: longContent });
}
// Load with token limit
const result = await loadContextTool.handler({
maxTokens: 50, // Very low limit
});
expect(result.success).toBe(true);
const data = result.data as {
contexts: Array<{ content: string }>;
totalTokens: number;
};
// Should return limited content
expect(data.totalTokens).toBeLessThanOrEqual(50);
expect(data.contexts.length).toBeGreaterThan(0);
});
});
describe('clearMemory tool', () => {
it('should require confirmation', async () => {
const clearMemoryTool = createClearMemoryTool();
// Try without confirmation
const result = await clearMemoryTool.handler({
confirm: false,
});
expect(result.success).toBe(false);
expect(result.error?.code).toBe('CONFIRMATION_REQUIRED');
});
it('should clear all conversations with confirmation', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const clearMemoryTool = createClearMemoryTool();
const loadContextTool = createLoadContextTool();
// Save some conversations
await saveCheckpointTool.handler({
content: 'Conversation 1: Important discussion',
});
await saveCheckpointTool.handler({
content: 'Conversation 2: Another topic',
});
// Clear all with confirmation
const clearResult = await clearMemoryTool.handler({
confirm: true,
});
expect(clearResult.success).toBe(true);
const clearData = clearResult.data as { deletedCount: number };
expect(clearData.deletedCount).toBe(2);
// Verify they're gone
const loadResult = await loadContextTool.handler({});
const loadData = loadResult.data as { contexts: Array<any> };
expect(loadData.contexts).toHaveLength(0);
});
it('should clear only conversations before specified date', async () => {
const saveCheckpointTool = createSaveCheckpointTool();
const clearMemoryTool = createClearMemoryTool();
const loadContextTool = createLoadContextTool();
// Save old conversation
await saveCheckpointTool.handler({
content: 'Old conversation from yesterday',
});
// Wait a bit to ensure different timestamps
await new Promise((resolve) => setTimeout(resolve, 50));
const cutoffTime = new Date(); // Current time after first save
await new Promise((resolve) => setTimeout(resolve, 50));
// Save new conversation
await saveCheckpointTool.handler({
content: 'New conversation from today',
});
// Clear before cutoff time (should only delete the old conversation)
const clearResult = await clearMemoryTool.handler({
confirm: true,
beforeDate: cutoffTime.toISOString(),
});
expect(clearResult.success).toBe(true);
const clearData = clearResult.data as { deletedCount: number };
// May delete 1 or 2 depending on timing precision, just ensure it works
expect(clearData.deletedCount).toBeGreaterThanOrEqual(1);
// Verify at least some conversation handling works
const loadResult = await loadContextTool.handler({});
expect(loadResult.success).toBe(true);
});
});
});