@qwickapps/qwickbrain-proxy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.github/workflows/publish.yml +92 -0
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +45 -0
  4. package/README.md +165 -0
  5. package/dist/bin/cli.d.ts +3 -0
  6. package/dist/bin/cli.d.ts.map +1 -0
  7. package/dist/bin/cli.js +142 -0
  8. package/dist/bin/cli.js.map +1 -0
  9. package/dist/db/client.d.ts +10 -0
  10. package/dist/db/client.d.ts.map +1 -0
  11. package/dist/db/client.js +23 -0
  12. package/dist/db/client.js.map +1 -0
  13. package/dist/db/schema.d.ts +551 -0
  14. package/dist/db/schema.d.ts.map +1 -0
  15. package/dist/db/schema.js +65 -0
  16. package/dist/db/schema.js.map +1 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lib/__tests__/cache-manager.test.d.ts +2 -0
  22. package/dist/lib/__tests__/cache-manager.test.d.ts.map +1 -0
  23. package/dist/lib/__tests__/cache-manager.test.js +202 -0
  24. package/dist/lib/__tests__/cache-manager.test.js.map +1 -0
  25. package/dist/lib/__tests__/connection-manager.test.d.ts +2 -0
  26. package/dist/lib/__tests__/connection-manager.test.d.ts.map +1 -0
  27. package/dist/lib/__tests__/connection-manager.test.js +188 -0
  28. package/dist/lib/__tests__/connection-manager.test.js.map +1 -0
  29. package/dist/lib/__tests__/proxy-server.test.d.ts +2 -0
  30. package/dist/lib/__tests__/proxy-server.test.d.ts.map +1 -0
  31. package/dist/lib/__tests__/proxy-server.test.js +205 -0
  32. package/dist/lib/__tests__/proxy-server.test.js.map +1 -0
  33. package/dist/lib/__tests__/qwickbrain-client.test.d.ts +2 -0
  34. package/dist/lib/__tests__/qwickbrain-client.test.d.ts.map +1 -0
  35. package/dist/lib/__tests__/qwickbrain-client.test.js +233 -0
  36. package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -0
  37. package/dist/lib/cache-manager.d.ts +25 -0
  38. package/dist/lib/cache-manager.d.ts.map +1 -0
  39. package/dist/lib/cache-manager.js +149 -0
  40. package/dist/lib/cache-manager.js.map +1 -0
  41. package/dist/lib/connection-manager.d.ts +26 -0
  42. package/dist/lib/connection-manager.d.ts.map +1 -0
  43. package/dist/lib/connection-manager.js +130 -0
  44. package/dist/lib/connection-manager.js.map +1 -0
  45. package/dist/lib/proxy-server.d.ts +19 -0
  46. package/dist/lib/proxy-server.d.ts.map +1 -0
  47. package/dist/lib/proxy-server.js +258 -0
  48. package/dist/lib/proxy-server.js.map +1 -0
  49. package/dist/lib/qwickbrain-client.d.ts +24 -0
  50. package/dist/lib/qwickbrain-client.d.ts.map +1 -0
  51. package/dist/lib/qwickbrain-client.js +197 -0
  52. package/dist/lib/qwickbrain-client.js.map +1 -0
  53. package/dist/types/config.d.ts +186 -0
  54. package/dist/types/config.d.ts.map +1 -0
  55. package/dist/types/config.js +42 -0
  56. package/dist/types/config.js.map +1 -0
  57. package/dist/types/mcp.d.ts +223 -0
  58. package/dist/types/mcp.d.ts.map +1 -0
  59. package/dist/types/mcp.js +78 -0
  60. package/dist/types/mcp.js.map +1 -0
  61. package/dist/version.d.ts +2 -0
  62. package/dist/version.d.ts.map +1 -0
  63. package/dist/version.js +9 -0
  64. package/dist/version.js.map +1 -0
  65. package/drizzle/0000_fat_rafael_vega.sql +41 -0
  66. package/drizzle/0001_goofy_invisible_woman.sql +2 -0
  67. package/drizzle/meta/0000_snapshot.json +276 -0
  68. package/drizzle/meta/0001_snapshot.json +295 -0
  69. package/drizzle/meta/_journal.json +20 -0
  70. package/drizzle.config.ts +12 -0
  71. package/package.json +65 -0
  72. package/src/bin/cli.ts +158 -0
  73. package/src/db/client.ts +34 -0
  74. package/src/db/schema.ts +68 -0
  75. package/src/index.ts +6 -0
  76. package/src/lib/__tests__/cache-manager.test.ts +264 -0
  77. package/src/lib/__tests__/connection-manager.test.ts +255 -0
  78. package/src/lib/__tests__/proxy-server.test.ts +261 -0
  79. package/src/lib/__tests__/qwickbrain-client.test.ts +310 -0
  80. package/src/lib/cache-manager.ts +201 -0
  81. package/src/lib/connection-manager.ts +156 -0
  82. package/src/lib/proxy-server.ts +320 -0
  83. package/src/lib/qwickbrain-client.ts +260 -0
  84. package/src/types/config.ts +47 -0
  85. package/src/types/mcp.ts +97 -0
  86. package/src/version.ts +11 -0
  87. package/test/fixtures/test-mcp.json +5 -0
  88. package/test-mcp-client.js +67 -0
  89. package/test-proxy.sh +25 -0
  90. package/tsconfig.json +22 -0
package/src/bin/cli.ts ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createDatabase, runMigrations } from '../db/client.js';
5
+ import { ProxyServer } from '../lib/proxy-server.js';
6
+ import { ConfigSchema, type Config } from '../types/config.js';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { VERSION } from '../version.js';
11
+
12
+ const DEFAULT_CONFIG_DIR = join(homedir(), '.qwickbrain');
13
+ const DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, 'config.json');
14
+
15
+ function loadConfig(): Config {
16
+ if (!existsSync(DEFAULT_CONFIG_PATH)) {
17
+ return ConfigSchema.parse({});
18
+ }
19
+
20
+ try {
21
+ const raw = readFileSync(DEFAULT_CONFIG_PATH, 'utf-8');
22
+ const parsed = JSON.parse(raw);
23
+ return ConfigSchema.parse(parsed);
24
+ } catch (error) {
25
+ console.error('Failed to load config:', error);
26
+ return ConfigSchema.parse({});
27
+ }
28
+ }
29
+
30
+ function saveConfig(config: Config): void {
31
+ mkdirSync(DEFAULT_CONFIG_DIR, { recursive: true });
32
+ writeFileSync(DEFAULT_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
33
+ }
34
+
35
+ const program = new Command();
36
+
37
+ program
38
+ .name('qwickbrain-proxy')
39
+ .description('Local MCP proxy for QwickBrain with caching and resilience')
40
+ .version(VERSION);
41
+
42
+ program
43
+ .command('serve')
44
+ .description('Start the MCP proxy server (stdio mode)')
45
+ .action(async () => {
46
+ try {
47
+ const config = loadConfig();
48
+ const { db } = createDatabase(config.cache.dir);
49
+
50
+ // Run migrations to ensure database schema is up to date
51
+ try {
52
+ runMigrations(db);
53
+ } catch (migrationError) {
54
+ console.error('Failed to run database migrations:', migrationError);
55
+ console.error('Please ensure the database directory is writable and try again.');
56
+ process.exit(1);
57
+ }
58
+
59
+ const server = new ProxyServer(db, config);
60
+
61
+ process.on('SIGINT', async () => {
62
+ console.error('\nShutting down...');
63
+ await server.stop();
64
+ process.exit(0);
65
+ });
66
+
67
+ await server.start();
68
+ } catch (error) {
69
+ console.error('Failed to start server:', error);
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ program
75
+ .command('init')
76
+ .description('Initialize configuration')
77
+ .action(async () => {
78
+ console.log('QwickBrain Proxy Configuration');
79
+ console.log('==============================\n');
80
+
81
+ // Simple init - create default config
82
+ const config = ConfigSchema.parse({
83
+ qwickbrain: {
84
+ url: 'http://macmini-devserver.local:3000',
85
+ },
86
+ });
87
+
88
+ saveConfig(config);
89
+ console.log(`Configuration saved to: ${DEFAULT_CONFIG_PATH}`);
90
+ console.log('\nDefault settings:');
91
+ console.log(` QwickBrain URL: ${config.qwickbrain.url}`);
92
+ console.log(` Cache directory: ${join(homedir(), '.qwickbrain', 'cache')}`);
93
+ console.log('\nTo customize, edit the config file or use "qwickbrain-proxy config" commands.');
94
+ });
95
+
96
+ const configCmd = program.command('config').description('Manage configuration');
97
+
98
+ configCmd
99
+ .command('get <key>')
100
+ .description('Get a configuration value')
101
+ .action((key) => {
102
+ const config = loadConfig();
103
+ const value = key.split('.').reduce((obj: any, k: string) => obj?.[k], config);
104
+ console.log(value !== undefined ? value : 'Not set');
105
+ });
106
+
107
+ configCmd
108
+ .command('set <key> <value>')
109
+ .description('Set a configuration value')
110
+ .action((key, value) => {
111
+ const config = loadConfig();
112
+ const keys = key.split('.');
113
+ const lastKey = keys.pop()!;
114
+ const target = keys.reduce((obj: any, k: string) => {
115
+ if (!obj[k]) obj[k] = {};
116
+ return obj[k];
117
+ }, config);
118
+
119
+ // Try to parse as JSON, otherwise use as string
120
+ try {
121
+ target[lastKey] = JSON.parse(value);
122
+ } catch {
123
+ target[lastKey] = value;
124
+ }
125
+
126
+ // Validate against schema to prevent bypassing validation
127
+ try {
128
+ const validatedConfig = ConfigSchema.parse(config);
129
+ saveConfig(validatedConfig);
130
+ console.log(`Updated ${key} = ${value}`);
131
+ } catch (error) {
132
+ console.error('Configuration validation failed:', error instanceof Error ? error.message : String(error));
133
+ process.exit(1);
134
+ }
135
+ });
136
+
137
+ configCmd
138
+ .command('show')
139
+ .description('Show current configuration')
140
+ .action(() => {
141
+ const config = loadConfig();
142
+ console.log(JSON.stringify(config, null, 2));
143
+ });
144
+
145
+ program
146
+ .command('status')
147
+ .description('Show proxy status')
148
+ .action(() => {
149
+ const config = loadConfig();
150
+ console.log('QwickBrain Proxy Status');
151
+ console.log('======================\n');
152
+ console.log(`Config file: ${DEFAULT_CONFIG_PATH}`);
153
+ console.log(`Config exists: ${existsSync(DEFAULT_CONFIG_PATH) ? 'Yes' : 'No'}`);
154
+ console.log(`QwickBrain URL: ${config.qwickbrain.url}`);
155
+ console.log(`Cache directory: ${config.cache.dir || join(homedir(), '.qwickbrain', 'cache')}`);
156
+ });
157
+
158
+ program.parse();
@@ -0,0 +1,34 @@
1
+ import Database from 'better-sqlite3';
2
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
3
+ import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
4
+ import * as schema from './schema.js';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ import { mkdirSync } from 'fs';
8
+
9
+ const DEFAULT_CACHE_DIR = join(homedir(), '.qwickbrain', 'cache');
10
+
11
+ export function createDatabase(cacheDir: string = DEFAULT_CACHE_DIR): {
12
+ db: ReturnType<typeof drizzle<typeof schema>>;
13
+ sqlite: Database.Database
14
+ } {
15
+ // Ensure cache directory exists
16
+ mkdirSync(cacheDir, { recursive: true });
17
+
18
+ const dbPath = join(cacheDir, 'qwickbrain.db');
19
+ const sqlite = new Database(dbPath);
20
+
21
+ // Enable WAL mode for better concurrency
22
+ sqlite.pragma('journal_mode = WAL');
23
+
24
+ const db = drizzle(sqlite, { schema });
25
+
26
+ return { db, sqlite };
27
+ }
28
+
29
+ export function runMigrations(db: ReturnType<typeof drizzle>) {
30
+ // Drizzle will look for migrations in drizzle/ directory
31
+ migrate(db, { migrationsFolder: './drizzle' });
32
+ }
33
+
34
+ export type DB = ReturnType<typeof createDatabase>['db'];
@@ -0,0 +1,68 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { integer, sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
3
+
4
+ /**
5
+ * Cached documents (workflows, rules, FRDs, designs, etc.)
6
+ */
7
+ export const documents = sqliteTable('documents', {
8
+ id: integer('id').primaryKey({ autoIncrement: true }),
9
+ docType: text('doc_type').notNull(), // workflow, rule, frd, design, spike, etc.
10
+ name: text('name').notNull(),
11
+ project: text('project'), // null for global documents
12
+ content: text('content').notNull(),
13
+ metadata: text('metadata'), // JSON string
14
+ cachedAt: integer('cached_at', { mode: 'timestamp' })
15
+ .notNull()
16
+ .default(sql`(unixepoch())`),
17
+ expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
18
+ synced: integer('synced', { mode: 'boolean' }).notNull().default(true),
19
+ }, (table) => ({
20
+ uniqueDocument: unique().on(table.docType, table.name, table.project),
21
+ }));
22
+
23
+ /**
24
+ * Cached memories (project context, patterns, decisions)
25
+ */
26
+ export const memories = sqliteTable('memories', {
27
+ id: integer('id').primaryKey({ autoIncrement: true }),
28
+ name: text('name').notNull(),
29
+ project: text('project'), // null for global memories
30
+ content: text('content').notNull(),
31
+ metadata: text('metadata'), // JSON string
32
+ cachedAt: integer('cached_at', { mode: 'timestamp' })
33
+ .notNull()
34
+ .default(sql`(unixepoch())`),
35
+ expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
36
+ synced: integer('synced', { mode: 'boolean' }).notNull().default(true),
37
+ }, (table) => ({
38
+ uniqueMemory: unique().on(table.name, table.project),
39
+ }));
40
+
41
+ /**
42
+ * Write queue for offline operations
43
+ */
44
+ export const syncQueue = sqliteTable('sync_queue', {
45
+ id: integer('id').primaryKey({ autoIncrement: true }),
46
+ operation: text('operation').notNull(), // create_document, update_document, set_memory, etc.
47
+ payload: text('payload').notNull(), // JSON string
48
+ createdAt: integer('created_at', { mode: 'timestamp' })
49
+ .notNull()
50
+ .default(sql`(unixepoch())`),
51
+ status: text('status').notNull().default('pending'), // pending, completed, failed
52
+ error: text('error'),
53
+ attempts: integer('attempts').notNull().default(0),
54
+ lastAttemptAt: integer('last_attempt_at', { mode: 'timestamp' }),
55
+ });
56
+
57
+ /**
58
+ * Connection health log
59
+ */
60
+ export const connectionLog = sqliteTable('connection_log', {
61
+ id: integer('id').primaryKey({ autoIncrement: true }),
62
+ timestamp: integer('timestamp', { mode: 'timestamp' })
63
+ .notNull()
64
+ .default(sql`(unixepoch())`),
65
+ state: text('state').notNull(), // connected, disconnected, reconnecting, failed
66
+ latencyMs: integer('latency_ms'),
67
+ error: text('error'),
68
+ });
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { ProxyServer } from './lib/proxy-server.js';
2
+ export { ConnectionManager } from './lib/connection-manager.js';
3
+ export { CacheManager } from './lib/cache-manager.js';
4
+ export { createDatabase, runMigrations } from './db/client.js';
5
+ export type { Config } from './types/config.js';
6
+ export type { MCPRequest, MCPResponse, ConnectionState } from './types/mcp.js';
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { createDatabase, runMigrations } from '../../db/client.js';
6
+ import { CacheManager } from '../cache-manager.js';
7
+ import type { Config } from '../../types/config.js';
8
+
9
+ describe('CacheManager', () => {
10
+ let tmpDir: string;
11
+ let cacheManager: CacheManager;
12
+ let db: ReturnType<typeof createDatabase>['db'];
13
+
14
+ beforeEach(() => {
15
+ // Create temporary directory for test database
16
+ tmpDir = mkdtempSync(join(tmpdir(), 'cache-test-'));
17
+ const dbResult = createDatabase(tmpDir);
18
+ db = dbResult.db;
19
+
20
+ // Run migrations to create tables
21
+ runMigrations(db);
22
+
23
+ const config: Config['cache'] = {
24
+ dir: tmpDir,
25
+ ttl: {
26
+ workflows: 3600,
27
+ rules: 3600,
28
+ documents: 1800,
29
+ memories: 900,
30
+ },
31
+ preload: [],
32
+ };
33
+
34
+ cacheManager = new CacheManager(db, config);
35
+ });
36
+
37
+ afterEach(() => {
38
+ // Clean up temporary directory
39
+ rmSync(tmpDir, { recursive: true, force: true });
40
+ });
41
+
42
+ describe('setDocument and getDocument', () => {
43
+ it('should store and retrieve a document', async () => {
44
+ await cacheManager.setDocument('workflow', 'test-workflow', 'content here');
45
+
46
+ const cached = await cacheManager.getDocument('workflow', 'test-workflow');
47
+
48
+ expect(cached).not.toBeNull();
49
+ expect(cached?.data.content).toBe('content here');
50
+ expect(cached?.data.doc_type).toBe('workflow');
51
+ expect(cached?.data.name).toBe('test-workflow');
52
+ expect(cached?.isExpired).toBe(false);
53
+ });
54
+
55
+ it('should store document with metadata', async () => {
56
+ const metadata = { author: 'test', version: 1 };
57
+ await cacheManager.setDocument('frd', 'test-frd', 'frd content', undefined, metadata);
58
+
59
+ const cached = await cacheManager.getDocument('frd', 'test-frd');
60
+
61
+ expect(cached?.data.metadata).toEqual(metadata);
62
+ });
63
+
64
+ it('should store document with project scope', async () => {
65
+ await cacheManager.setDocument('design', 'test-design', 'design content', 'my-project');
66
+
67
+ const cached = await cacheManager.getDocument('design', 'test-design', 'my-project');
68
+
69
+ expect(cached).not.toBeNull();
70
+ expect(cached?.data.project).toBe('my-project');
71
+ });
72
+
73
+ it('should distinguish between global and project-scoped documents', async () => {
74
+ await cacheManager.setDocument('rule', 'test-rule', 'global rule');
75
+ await cacheManager.setDocument('rule', 'test-rule', 'project rule', 'my-project');
76
+
77
+ const globalDoc = await cacheManager.getDocument('rule', 'test-rule');
78
+ const projectDoc = await cacheManager.getDocument('rule', 'test-rule', 'my-project');
79
+
80
+ expect(globalDoc?.data.content).toBe('global rule');
81
+ expect(globalDoc?.data.project).toBe('');
82
+ expect(projectDoc?.data.content).toBe('project rule');
83
+ expect(projectDoc?.data.project).toBe('my-project');
84
+ });
85
+
86
+ it('should return null for non-existent document', async () => {
87
+ const cached = await cacheManager.getDocument('workflow', 'non-existent');
88
+ expect(cached).toBeNull();
89
+ });
90
+
91
+ it('should update existing document on conflict', async () => {
92
+ await cacheManager.setDocument('workflow', 'test', 'version 1');
93
+ await cacheManager.setDocument('workflow', 'test', 'version 2');
94
+
95
+ const cached = await cacheManager.getDocument('workflow', 'test');
96
+
97
+ expect(cached?.data.content).toBe('version 2');
98
+ });
99
+
100
+ it('should mark document as expired after TTL', async () => {
101
+ // Override config to have very short TTL for testing
102
+ const shortTTLConfig: Config['cache'] = {
103
+ dir: tmpDir,
104
+ ttl: {
105
+ workflows: 0, // Expire immediately
106
+ rules: 0,
107
+ documents: 0,
108
+ memories: 0,
109
+ },
110
+ preload: [],
111
+ };
112
+
113
+ const shortCacheManager = new CacheManager(db, shortTTLConfig);
114
+ await shortCacheManager.setDocument('workflow', 'test', 'content');
115
+
116
+ // Wait to ensure timestamp difference
117
+ await new Promise(resolve => setTimeout(resolve, 100));
118
+
119
+ const cached = await shortCacheManager.getDocument('workflow', 'test');
120
+
121
+ expect(cached).not.toBeNull();
122
+ expect(cached?.isExpired).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe('setMemory and getMemory', () => {
127
+ it('should store and retrieve a memory', async () => {
128
+ await cacheManager.setMemory('test-memory', 'memory content');
129
+
130
+ const cached = await cacheManager.getMemory('test-memory');
131
+
132
+ expect(cached).not.toBeNull();
133
+ expect(cached?.data.content).toBe('memory content');
134
+ expect(cached?.data.name).toBe('test-memory');
135
+ expect(cached?.isExpired).toBe(false);
136
+ });
137
+
138
+ it('should store memory with metadata', async () => {
139
+ const metadata = { lastUpdated: Date.now() };
140
+ await cacheManager.setMemory('test-memory', 'content', undefined, metadata);
141
+
142
+ const cached = await cacheManager.getMemory('test-memory');
143
+
144
+ expect(cached?.data.metadata).toEqual(metadata);
145
+ });
146
+
147
+ it('should store memory with project scope', async () => {
148
+ await cacheManager.setMemory('patterns', 'project patterns', 'my-project');
149
+
150
+ const cached = await cacheManager.getMemory('patterns', 'my-project');
151
+
152
+ expect(cached).not.toBeNull();
153
+ expect(cached?.data.project).toBe('my-project');
154
+ });
155
+
156
+ it('should distinguish between global and project-scoped memories', async () => {
157
+ await cacheManager.setMemory('context', 'global context');
158
+ await cacheManager.setMemory('context', 'project context', 'my-project');
159
+
160
+ const globalMem = await cacheManager.getMemory('context');
161
+ const projectMem = await cacheManager.getMemory('context', 'my-project');
162
+
163
+ expect(globalMem?.data.content).toBe('global context');
164
+ expect(projectMem?.data.content).toBe('project context');
165
+ });
166
+
167
+ it('should return null for non-existent memory', async () => {
168
+ const cached = await cacheManager.getMemory('non-existent');
169
+ expect(cached).toBeNull();
170
+ });
171
+
172
+ it('should mark memory as expired after TTL', async () => {
173
+ const shortTTLConfig: Config['cache'] = {
174
+ dir: tmpDir,
175
+ ttl: {
176
+ workflows: 0,
177
+ rules: 0,
178
+ documents: 0,
179
+ memories: 0,
180
+ },
181
+ preload: [],
182
+ };
183
+
184
+ const shortCacheManager = new CacheManager(db, shortTTLConfig);
185
+ await shortCacheManager.setMemory('test', 'content');
186
+
187
+ await new Promise(resolve => setTimeout(resolve, 100));
188
+
189
+ const cached = await shortCacheManager.getMemory('test');
190
+
191
+ expect(cached).not.toBeNull();
192
+ expect(cached?.isExpired).toBe(true);
193
+ });
194
+ });
195
+
196
+ describe('cleanupExpiredEntries', () => {
197
+ it('should delete expired documents and memories', async () => {
198
+ const shortTTLConfig: Config['cache'] = {
199
+ dir: tmpDir,
200
+ ttl: {
201
+ workflows: 0,
202
+ rules: 0,
203
+ documents: 0,
204
+ memories: 0,
205
+ },
206
+ preload: [],
207
+ };
208
+
209
+ const shortCacheManager = new CacheManager(db, shortTTLConfig);
210
+
211
+ // Add some items that will immediately expire
212
+ await shortCacheManager.setDocument('workflow', 'test1', 'content1');
213
+ await shortCacheManager.setDocument('rule', 'test2', 'content2');
214
+ await shortCacheManager.setMemory('memory1', 'content3');
215
+
216
+ // Wait to ensure they're expired (need sufficient time for clock to advance)
217
+ await new Promise(resolve => setTimeout(resolve, 100));
218
+
219
+ // Clean up
220
+ const result = await shortCacheManager.cleanupExpiredEntries();
221
+
222
+ expect(result.documentsDeleted).toBe(2);
223
+ expect(result.memoriesDeleted).toBe(1);
224
+
225
+ // Verify they're gone
226
+ const doc1 = await shortCacheManager.getDocument('workflow', 'test1');
227
+ const mem1 = await shortCacheManager.getMemory('memory1');
228
+
229
+ expect(doc1).toBeNull();
230
+ expect(mem1).toBeNull();
231
+ });
232
+
233
+ it('should not delete non-expired items', async () => {
234
+ await cacheManager.setDocument('workflow', 'test', 'content');
235
+ await cacheManager.setMemory('memory', 'content');
236
+
237
+ const result = await cacheManager.cleanupExpiredEntries();
238
+
239
+ expect(result.documentsDeleted).toBe(0);
240
+ expect(result.memoriesDeleted).toBe(0);
241
+
242
+ // Verify they're still there
243
+ const doc = await cacheManager.getDocument('workflow', 'test');
244
+ const mem = await cacheManager.getMemory('memory');
245
+
246
+ expect(doc).not.toBeNull();
247
+ expect(mem).not.toBeNull();
248
+ });
249
+ });
250
+
251
+ describe('cache age calculation', () => {
252
+ it('should calculate age correctly', async () => {
253
+ await cacheManager.setDocument('workflow', 'test', 'content');
254
+
255
+ // Wait 100ms
256
+ await new Promise(resolve => setTimeout(resolve, 100));
257
+
258
+ const cached = await cacheManager.getDocument('workflow', 'test');
259
+
260
+ expect(cached?.age).toBeGreaterThanOrEqual(0);
261
+ expect(cached?.age).toBeLessThan(1); // Less than 1 second
262
+ });
263
+ });
264
+ });