@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.
- package/.github/workflows/publish.yml +92 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +45 -0
- package/README.md +165 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +142 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +23 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/schema.d.ts +551 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/cache-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/cache-manager.test.js +202 -0
- package/dist/lib/__tests__/cache-manager.test.js.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/connection-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/connection-manager.test.js +188 -0
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts +2 -0
- package/dist/lib/__tests__/proxy-server.test.d.ts.map +1 -0
- package/dist/lib/__tests__/proxy-server.test.js +205 -0
- package/dist/lib/__tests__/proxy-server.test.js.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts +2 -0
- package/dist/lib/__tests__/qwickbrain-client.test.d.ts.map +1 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js +233 -0
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +25 -0
- package/dist/lib/cache-manager.d.ts.map +1 -0
- package/dist/lib/cache-manager.js +149 -0
- package/dist/lib/cache-manager.js.map +1 -0
- package/dist/lib/connection-manager.d.ts +26 -0
- package/dist/lib/connection-manager.d.ts.map +1 -0
- package/dist/lib/connection-manager.js +130 -0
- package/dist/lib/connection-manager.js.map +1 -0
- package/dist/lib/proxy-server.d.ts +19 -0
- package/dist/lib/proxy-server.d.ts.map +1 -0
- package/dist/lib/proxy-server.js +258 -0
- package/dist/lib/proxy-server.js.map +1 -0
- package/dist/lib/qwickbrain-client.d.ts +24 -0
- package/dist/lib/qwickbrain-client.d.ts.map +1 -0
- package/dist/lib/qwickbrain-client.js +197 -0
- package/dist/lib/qwickbrain-client.js.map +1 -0
- package/dist/types/config.d.ts +186 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +42 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/mcp.d.ts +223 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +78 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/drizzle/0000_fat_rafael_vega.sql +41 -0
- package/drizzle/0001_goofy_invisible_woman.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +276 -0
- package/drizzle/meta/0001_snapshot.json +295 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +12 -0
- package/package.json +65 -0
- package/src/bin/cli.ts +158 -0
- package/src/db/client.ts +34 -0
- package/src/db/schema.ts +68 -0
- package/src/index.ts +6 -0
- package/src/lib/__tests__/cache-manager.test.ts +264 -0
- package/src/lib/__tests__/connection-manager.test.ts +255 -0
- package/src/lib/__tests__/proxy-server.test.ts +261 -0
- package/src/lib/__tests__/qwickbrain-client.test.ts +310 -0
- package/src/lib/cache-manager.ts +201 -0
- package/src/lib/connection-manager.ts +156 -0
- package/src/lib/proxy-server.ts +320 -0
- package/src/lib/qwickbrain-client.ts +260 -0
- package/src/types/config.ts +47 -0
- package/src/types/mcp.ts +97 -0
- package/src/version.ts +11 -0
- package/test/fixtures/test-mcp.json +5 -0
- package/test-mcp-client.js +67 -0
- package/test-proxy.sh +25 -0
- 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();
|
package/src/db/client.ts
ADDED
|
@@ -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'];
|
package/src/db/schema.ts
ADDED
|
@@ -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
|
+
});
|