@vainplex/openclaw-knowledge-engine 0.1.2 → 0.1.4
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/README.md +9 -9
- package/dist/index.d.ts +8 -4
- package/dist/index.js +31 -27
- package/dist/src/config-loader.d.ts +22 -0
- package/dist/src/config-loader.js +104 -0
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +3 -2
- package/openclaw.plugin.json +117 -113
- package/package.json +15 -5
- package/ARCHITECTURE.md +0 -374
- package/index.ts +0 -38
- package/src/config.ts +0 -180
- package/src/embeddings.ts +0 -82
- package/src/entity-extractor.ts +0 -137
- package/src/fact-store.ts +0 -260
- package/src/hooks.ts +0 -125
- package/src/http-client.ts +0 -74
- package/src/llm-enhancer.ts +0 -187
- package/src/maintenance.ts +0 -102
- package/src/patterns.ts +0 -90
- package/src/storage.ts +0 -122
- package/src/types.ts +0 -144
- package/test/config.test.ts +0 -152
- package/test/embeddings.test.ts +0 -118
- package/test/entity-extractor.test.ts +0 -121
- package/test/fact-store.test.ts +0 -266
- package/test/hooks.test.ts +0 -120
- package/test/http-client.test.ts +0 -68
- package/test/llm-enhancer.test.ts +0 -132
- package/test/maintenance.test.ts +0 -117
- package/test/patterns.test.ts +0 -123
- package/test/storage.test.ts +0 -86
- package/tsconfig.json +0 -26
package/test/hooks.test.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// test/hooks.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it, beforeEach, mock, afterEach } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import { HookManager } from '../src/hooks.js';
|
|
6
|
-
import type { OpenClawPluginApi, KnowledgeConfig, HookEvent } from '../src/types.js';
|
|
7
|
-
import { FactStore } from '../src/fact-store.js';
|
|
8
|
-
import { Maintenance } from '../src/maintenance.js';
|
|
9
|
-
|
|
10
|
-
type TriggerFn = (event: string, eventData: HookEvent) => Promise<void>;
|
|
11
|
-
|
|
12
|
-
describe('HookManager', () => {
|
|
13
|
-
let api: OpenClawPluginApi & { _trigger: TriggerFn; handlers: Map<string, (e: HookEvent, ctx: Record<string, unknown>) => void> };
|
|
14
|
-
let config: KnowledgeConfig;
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
config = {
|
|
18
|
-
enabled: true,
|
|
19
|
-
workspace: '/tmp',
|
|
20
|
-
extraction: {
|
|
21
|
-
regex: { enabled: true },
|
|
22
|
-
llm: { enabled: true, model: 'm', endpoint: 'http://e.com', batchSize: 1, cooldownMs: 1 },
|
|
23
|
-
},
|
|
24
|
-
decay: { enabled: true, intervalHours: 1, rate: 0.1 },
|
|
25
|
-
embeddings: { enabled: true, syncIntervalMinutes: 1, endpoint: 'http://e.com', collectionName: 'c' },
|
|
26
|
-
storage: { maxEntities: 1, maxFacts: 1, writeDebounceMs: 0 },
|
|
27
|
-
};
|
|
28
|
-
const handlers = new Map<string, (e: HookEvent, ctx: Record<string, unknown>) => void>();
|
|
29
|
-
api = {
|
|
30
|
-
pluginConfig: {},
|
|
31
|
-
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
|
|
32
|
-
on: (event: string, handler: (e: HookEvent, ctx: Record<string, unknown>) => void) => { handlers.set(event, handler); },
|
|
33
|
-
handlers,
|
|
34
|
-
_trigger: async (event: string, eventData: HookEvent) => {
|
|
35
|
-
const handler = handlers.get(event);
|
|
36
|
-
if (handler) await handler(eventData, {});
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
mock.reset();
|
|
43
|
-
mock.restoreAll();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should handle onSessionStart correctly', async () => {
|
|
47
|
-
const loadMock = mock.method(FactStore.prototype, 'load', async () => {});
|
|
48
|
-
const startMock = mock.method(Maintenance.prototype, 'start', () => {});
|
|
49
|
-
|
|
50
|
-
const hookManager = new HookManager(api, config);
|
|
51
|
-
hookManager.registerHooks();
|
|
52
|
-
|
|
53
|
-
await api._trigger('session_start', {});
|
|
54
|
-
|
|
55
|
-
assert.strictEqual(loadMock.mock.calls.length, 1);
|
|
56
|
-
assert.strictEqual(startMock.mock.calls.length, 1);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should process incoming messages', async () => {
|
|
60
|
-
mock.method(FactStore.prototype, 'load', async () => {});
|
|
61
|
-
const addFactMock = mock.method(FactStore.prototype, 'addFact', () => ({}));
|
|
62
|
-
|
|
63
|
-
const hookManager = new HookManager(api, config);
|
|
64
|
-
hookManager.registerHooks();
|
|
65
|
-
|
|
66
|
-
mock.method(hookManager as Record<string, unknown>, 'processLlmBatchWhenReady', async () => {
|
|
67
|
-
const llmEnhancer = (hookManager as Record<string, unknown>).llmEnhancer as { sendBatch: () => Promise<{ entities: unknown[]; facts: unknown[] } | null> };
|
|
68
|
-
const result = await llmEnhancer.sendBatch();
|
|
69
|
-
if (result && result.facts.length > 0) {
|
|
70
|
-
const factStore = (hookManager as Record<string, unknown>).factStore as FactStore;
|
|
71
|
-
factStore.addFact(result.facts[0] as Parameters<FactStore['addFact']>[0]);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
mock.method(
|
|
76
|
-
(hookManager as Record<string, unknown>).llmEnhancer as Record<string, unknown>,
|
|
77
|
-
'sendBatch',
|
|
78
|
-
async () => ({
|
|
79
|
-
entities: [],
|
|
80
|
-
facts: [{ subject: 'test', predicate: 'is-a', object: 'fact' }],
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const event: HookEvent = { content: 'This is a message.' };
|
|
85
|
-
await api._trigger('message_received', event);
|
|
86
|
-
|
|
87
|
-
assert.strictEqual(addFactMock.mock.calls.length, 1);
|
|
88
|
-
assert.strictEqual(
|
|
89
|
-
(addFactMock.mock.calls[0].arguments[0] as Record<string, unknown>).subject,
|
|
90
|
-
'test'
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should register gateway_stop hook', () => {
|
|
95
|
-
const hookManager = new HookManager(api, config);
|
|
96
|
-
hookManager.registerHooks();
|
|
97
|
-
assert.ok(api.handlers.has('gateway_stop'), 'gateway_stop hook should be registered');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should call maintenance.stop() on shutdown', async () => {
|
|
101
|
-
mock.method(FactStore.prototype, 'load', async () => {});
|
|
102
|
-
const stopMock = mock.method(Maintenance.prototype, 'stop', () => {});
|
|
103
|
-
mock.method(Maintenance.prototype, 'start', () => {});
|
|
104
|
-
|
|
105
|
-
const hookManager = new HookManager(api, config);
|
|
106
|
-
hookManager.registerHooks();
|
|
107
|
-
|
|
108
|
-
await api._trigger('session_start', {});
|
|
109
|
-
await api._trigger('gateway_stop', {});
|
|
110
|
-
|
|
111
|
-
assert.strictEqual(stopMock.mock.calls.length >= 1, true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should not register hooks when disabled', () => {
|
|
115
|
-
config.enabled = false;
|
|
116
|
-
const hookManager = new HookManager(api, config);
|
|
117
|
-
hookManager.registerHooks();
|
|
118
|
-
assert.strictEqual(api.handlers.size, 0);
|
|
119
|
-
});
|
|
120
|
-
});
|
package/test/http-client.test.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// test/http-client.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it, afterEach } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import * as http from 'node:http';
|
|
6
|
-
import { httpPost } from '../src/http-client.js';
|
|
7
|
-
|
|
8
|
-
describe('httpPost', () => {
|
|
9
|
-
let server: http.Server | null = null;
|
|
10
|
-
|
|
11
|
-
afterEach((_, done) => {
|
|
12
|
-
if (server) {
|
|
13
|
-
server.close(() => done());
|
|
14
|
-
server = null;
|
|
15
|
-
} else {
|
|
16
|
-
done();
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should make a successful POST request', async () => {
|
|
21
|
-
let receivedBody = '';
|
|
22
|
-
server = http.createServer((req, res) => {
|
|
23
|
-
let body = '';
|
|
24
|
-
req.on('data', chunk => { body += chunk; });
|
|
25
|
-
req.on('end', () => {
|
|
26
|
-
receivedBody = body;
|
|
27
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
28
|
-
res.end('{"ok":true}');
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
await new Promise<void>(resolve => server!.listen(0, resolve));
|
|
33
|
-
const port = (server.address() as { port: number }).port;
|
|
34
|
-
|
|
35
|
-
const result = await httpPost(`http://localhost:${port}/test`, { key: 'value' });
|
|
36
|
-
assert.strictEqual(result, '{"ok":true}');
|
|
37
|
-
assert.strictEqual(JSON.parse(receivedBody).key, 'value');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should reject on non-2xx status codes', async () => {
|
|
41
|
-
server = http.createServer((_req, res) => {
|
|
42
|
-
res.writeHead(500);
|
|
43
|
-
res.end('Internal Server Error');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
await new Promise<void>(resolve => server!.listen(0, resolve));
|
|
47
|
-
const port = (server.address() as { port: number }).port;
|
|
48
|
-
|
|
49
|
-
await assert.rejects(
|
|
50
|
-
() => httpPost(`http://localhost:${port}/test`, {}),
|
|
51
|
-
(err: Error) => {
|
|
52
|
-
assert.ok(err.message.includes('500'));
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should reject on connection error', async () => {
|
|
59
|
-
// Port that nothing is listening on
|
|
60
|
-
await assert.rejects(
|
|
61
|
-
() => httpPost('http://localhost:19999/test', {}),
|
|
62
|
-
(err: Error) => {
|
|
63
|
-
assert.ok(err.message.includes('request error'));
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// test/llm-enhancer.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import { LlmEnhancer } from '../src/llm-enhancer.js';
|
|
6
|
-
import type { KnowledgeConfig, Logger } from '../src/types.js';
|
|
7
|
-
|
|
8
|
-
const createMockLogger = (): Logger & { logs: { level: string; msg: string }[] } => {
|
|
9
|
-
return {
|
|
10
|
-
logs: [],
|
|
11
|
-
info: function(msg) { this.logs.push({ level: 'info', msg }); },
|
|
12
|
-
warn: function(msg) { this.logs.push({ level: 'warn', msg }); },
|
|
13
|
-
error: function(msg) { this.logs.push({ level: 'error', msg }); },
|
|
14
|
-
debug: function(msg) { this.logs.push({ level: 'debug', msg }); },
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const mockConfig: KnowledgeConfig['extraction']['llm'] = {
|
|
19
|
-
enabled: true,
|
|
20
|
-
model: 'test-model',
|
|
21
|
-
endpoint: 'http://localhost:12345/api/test',
|
|
22
|
-
batchSize: 3,
|
|
23
|
-
cooldownMs: 100,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
describe('LlmEnhancer', () => {
|
|
27
|
-
let logger: ReturnType<typeof createMockLogger>;
|
|
28
|
-
let enhancer: LlmEnhancer;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
logger = createMockLogger();
|
|
32
|
-
enhancer = new LlmEnhancer(mockConfig, logger);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
enhancer.clearTimers();
|
|
37
|
-
mock.reset();
|
|
38
|
-
mock.restoreAll();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const mockHttpRequest = (response: string): void => {
|
|
42
|
-
// Mock the private makeHttpRequest method on the instance prototype
|
|
43
|
-
mock.method(
|
|
44
|
-
enhancer as unknown as Record<string, unknown>,
|
|
45
|
-
'makeHttpRequest',
|
|
46
|
-
async () => response
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
it('should add items to the batch and respect batchSize', () => {
|
|
51
|
-
const llmPayload = { entities: [], facts: [] };
|
|
52
|
-
const llmResponse = { response: JSON.stringify(llmPayload) };
|
|
53
|
-
mockHttpRequest(JSON.stringify(llmResponse));
|
|
54
|
-
|
|
55
|
-
enhancer.addToBatch({ id: 'msg1', text: 'Hello' });
|
|
56
|
-
enhancer.addToBatch({ id: 'msg2', text: 'World' });
|
|
57
|
-
assert.strictEqual(logger.logs.filter(l => l.msg.includes('Sending immediately')).length, 0);
|
|
58
|
-
|
|
59
|
-
enhancer.addToBatch({ id: 'msg3', text: 'Test' });
|
|
60
|
-
assert.strictEqual(logger.logs.filter(l => l.msg.includes('Sending immediately')).length, 1);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should correctly parse a valid LLM response', async () => {
|
|
64
|
-
const llmPayload = {
|
|
65
|
-
entities: [{ type: 'person', value: 'Claude', importance: 0.9 }],
|
|
66
|
-
facts: [{ subject: 'Claude', predicate: 'is-a', object: 'Person' }],
|
|
67
|
-
};
|
|
68
|
-
const llmResponse = { response: JSON.stringify(llmPayload) };
|
|
69
|
-
mockHttpRequest(JSON.stringify(llmResponse));
|
|
70
|
-
|
|
71
|
-
enhancer.addToBatch({ id: 'm1', text: 'The person is Claude.' });
|
|
72
|
-
const result = await enhancer.sendBatch();
|
|
73
|
-
|
|
74
|
-
assert.ok(result);
|
|
75
|
-
assert.strictEqual(result.entities.length, 1);
|
|
76
|
-
assert.strictEqual(result.entities[0].value, 'Claude');
|
|
77
|
-
assert.strictEqual(result.facts.length, 1);
|
|
78
|
-
assert.strictEqual(result.facts[0].subject, 'Claude');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should return null when the batch is empty', async () => {
|
|
82
|
-
const result = await enhancer.sendBatch();
|
|
83
|
-
assert.strictEqual(result, null);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should handle HTTP errors gracefully', async () => {
|
|
87
|
-
mock.method(
|
|
88
|
-
enhancer as unknown as Record<string, unknown>,
|
|
89
|
-
'makeHttpRequest',
|
|
90
|
-
async () => { throw new Error('HTTP request failed with status 500'); }
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
enhancer.addToBatch({ id: 'm1', text: 'Test' });
|
|
94
|
-
const result = await enhancer.sendBatch();
|
|
95
|
-
|
|
96
|
-
assert.strictEqual(result, null);
|
|
97
|
-
assert.ok(logger.logs.some(l => l.level === 'error'));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should handle invalid JSON from LLM', async () => {
|
|
101
|
-
mockHttpRequest('not json');
|
|
102
|
-
|
|
103
|
-
enhancer.addToBatch({ id: 'm1', text: 'Test' });
|
|
104
|
-
const result = await enhancer.sendBatch();
|
|
105
|
-
|
|
106
|
-
assert.strictEqual(result, null);
|
|
107
|
-
assert.ok(logger.logs.some(l => l.level === 'error'));
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should clear the batch after sending', async () => {
|
|
111
|
-
const llmPayload = { entities: [], facts: [] };
|
|
112
|
-
mockHttpRequest(JSON.stringify({ response: JSON.stringify(llmPayload) }));
|
|
113
|
-
|
|
114
|
-
enhancer.addToBatch({ id: 'm1', text: 'Test' });
|
|
115
|
-
await enhancer.sendBatch();
|
|
116
|
-
|
|
117
|
-
// Second send should return null (empty batch)
|
|
118
|
-
const result = await enhancer.sendBatch();
|
|
119
|
-
assert.strictEqual(result, null);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should handle LLM response with missing entities/facts gracefully', async () => {
|
|
123
|
-
mockHttpRequest(JSON.stringify({ response: '{}' }));
|
|
124
|
-
|
|
125
|
-
enhancer.addToBatch({ id: 'm1', text: 'Test' });
|
|
126
|
-
const result = await enhancer.sendBatch();
|
|
127
|
-
|
|
128
|
-
assert.ok(result);
|
|
129
|
-
assert.strictEqual(result.entities.length, 0);
|
|
130
|
-
assert.strictEqual(result.facts.length, 0);
|
|
131
|
-
});
|
|
132
|
-
});
|
package/test/maintenance.test.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// test/maintenance.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import { Maintenance } from '../src/maintenance.js';
|
|
6
|
-
import { Embeddings } from '../src/embeddings.js';
|
|
7
|
-
import type { KnowledgeConfig, Logger, Fact } from '../src/types.js';
|
|
8
|
-
import * as timers from 'node:timers/promises';
|
|
9
|
-
|
|
10
|
-
class MockFactStore {
|
|
11
|
-
decayRate = 0;
|
|
12
|
-
decayedCount = 0;
|
|
13
|
-
unembeddedFacts: Fact[] = [];
|
|
14
|
-
markedAsEmbeddedIds: string[] = [];
|
|
15
|
-
|
|
16
|
-
decayFacts(rate: number) {
|
|
17
|
-
this.decayRate = rate;
|
|
18
|
-
this.decayedCount++;
|
|
19
|
-
return { decayedCount: 1 };
|
|
20
|
-
}
|
|
21
|
-
getUnembeddedFacts() { return this.unembeddedFacts; }
|
|
22
|
-
markFactsAsEmbedded(ids: string[]) {
|
|
23
|
-
this.markedAsEmbeddedIds.push(...ids);
|
|
24
|
-
// Clear unembedded facts after marking (mimics real behavior)
|
|
25
|
-
this.unembeddedFacts = this.unembeddedFacts.filter(f => !ids.includes(f.id));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class MockEmbeddings {
|
|
30
|
-
isEnabledState = true;
|
|
31
|
-
syncedFacts: Fact[] = [];
|
|
32
|
-
isEnabled() { return this.isEnabledState; }
|
|
33
|
-
sync(facts: Fact[]) {
|
|
34
|
-
this.syncedFacts.push(...facts);
|
|
35
|
-
return Promise.resolve(facts.length);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const createMockLogger = (): Logger => ({
|
|
40
|
-
info: () => {}, warn: () => {}, error: () => {}, debug: () => {},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const mockConfig: KnowledgeConfig = {
|
|
44
|
-
enabled: true,
|
|
45
|
-
workspace: '/tmp',
|
|
46
|
-
decay: { enabled: true, intervalHours: 0.0001, rate: 0.1 },
|
|
47
|
-
embeddings: {
|
|
48
|
-
enabled: true,
|
|
49
|
-
syncIntervalMinutes: 0.0001,
|
|
50
|
-
endpoint: 'http://test.com',
|
|
51
|
-
collectionName: 'test',
|
|
52
|
-
},
|
|
53
|
-
storage: { maxEntities: 100, maxFacts: 100, writeDebounceMs: 0 },
|
|
54
|
-
extraction: {
|
|
55
|
-
regex: { enabled: true },
|
|
56
|
-
llm: { enabled: false, model: '', endpoint: '', batchSize: 1, cooldownMs: 0 },
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
describe('Maintenance', () => {
|
|
61
|
-
let logger: Logger;
|
|
62
|
-
let mockFactStore: MockFactStore;
|
|
63
|
-
let mockEmbeddings: MockEmbeddings;
|
|
64
|
-
let maintenance: Maintenance;
|
|
65
|
-
|
|
66
|
-
beforeEach(() => {
|
|
67
|
-
logger = createMockLogger();
|
|
68
|
-
mockFactStore = new MockFactStore();
|
|
69
|
-
mockEmbeddings = new MockEmbeddings();
|
|
70
|
-
// @ts-ignore - Using mock class
|
|
71
|
-
maintenance = new Maintenance(mockConfig, logger, mockFactStore, mockEmbeddings);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
maintenance.stop();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should schedule and run decay task', async () => {
|
|
79
|
-
maintenance.start();
|
|
80
|
-
await timers.setTimeout(mockConfig.decay.intervalHours * 60 * 60 * 1000 + 10);
|
|
81
|
-
assert.strictEqual(mockFactStore.decayedCount > 0, true);
|
|
82
|
-
assert.strictEqual(mockFactStore.decayRate, mockConfig.decay.rate);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should schedule and run embeddings sync task', async () => {
|
|
86
|
-
const testFact: Fact = {
|
|
87
|
-
id: 'fact1', subject: 's', predicate: 'p', object: 'o',
|
|
88
|
-
relevance: 1, createdAt: 't', lastAccessed: 't', source: 'ingested',
|
|
89
|
-
};
|
|
90
|
-
mockFactStore.unembeddedFacts = [testFact];
|
|
91
|
-
maintenance.start();
|
|
92
|
-
await timers.setTimeout(mockConfig.embeddings.syncIntervalMinutes * 60 * 1000 + 10);
|
|
93
|
-
assert.strictEqual(mockEmbeddings.syncedFacts.length > 0, true);
|
|
94
|
-
assert.deepStrictEqual(mockEmbeddings.syncedFacts[0], testFact);
|
|
95
|
-
assert.deepStrictEqual(mockFactStore.markedAsEmbeddedIds, [testFact.id]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should not schedule embeddings if disabled', async () => {
|
|
99
|
-
mockEmbeddings.isEnabledState = false;
|
|
100
|
-
maintenance.start();
|
|
101
|
-
await timers.setTimeout(5);
|
|
102
|
-
assert.strictEqual(mockEmbeddings.syncedFacts.length, 0);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should stop all timers cleanly', () => {
|
|
106
|
-
maintenance.start();
|
|
107
|
-
maintenance.stop();
|
|
108
|
-
// No error means timers were cleared successfully
|
|
109
|
-
assert.ok(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should run decay manually', () => {
|
|
113
|
-
maintenance.runDecay();
|
|
114
|
-
assert.strictEqual(mockFactStore.decayedCount, 1);
|
|
115
|
-
assert.strictEqual(mockFactStore.decayRate, mockConfig.decay.rate);
|
|
116
|
-
});
|
|
117
|
-
});
|
package/test/patterns.test.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// test/patterns.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import { REGEX_PATTERNS } from '../src/patterns.js';
|
|
6
|
-
|
|
7
|
-
type TestCase = [string, string | null | string[]];
|
|
8
|
-
|
|
9
|
-
const runTestCases = (regex: RegExp, testCases: TestCase[]) => {
|
|
10
|
-
for (const [input, expected] of testCases) {
|
|
11
|
-
// Reset regex state for each test case
|
|
12
|
-
regex.lastIndex = 0;
|
|
13
|
-
const matches = input.match(regex);
|
|
14
|
-
if (expected === null) {
|
|
15
|
-
assert.strictEqual(matches, null, `Expected no match for: "${input}"`);
|
|
16
|
-
} else if (Array.isArray(expected)) {
|
|
17
|
-
assert.deepStrictEqual(matches, expected, `Mismatch for: "${input}"`);
|
|
18
|
-
} else {
|
|
19
|
-
assert.deepStrictEqual(matches, [expected], `Mismatch for: "${input}"`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
describe('REGEX_PATTERNS', () => {
|
|
25
|
-
|
|
26
|
-
it('should match valid email addresses', () => {
|
|
27
|
-
const testCases: TestCase[] = [
|
|
28
|
-
['contact support at support@example.com', 'support@example.com'],
|
|
29
|
-
['my email is john.doe123@sub.domain.co.uk.', 'john.doe123@sub.domain.co.uk'],
|
|
30
|
-
['invalid-email@', null],
|
|
31
|
-
['user@localhost', null],
|
|
32
|
-
['test@.com', null],
|
|
33
|
-
['multiple emails: a@b.com and c@d.org', ['a@b.com', 'c@d.org']],
|
|
34
|
-
];
|
|
35
|
-
runTestCases(REGEX_PATTERNS.email, testCases);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should match valid URLs', () => {
|
|
39
|
-
const testCases: TestCase[] = [
|
|
40
|
-
['visit https://www.example.com for more info', 'https://www.example.com'],
|
|
41
|
-
['check http://sub.domain.org/path?query=1', 'http://sub.domain.org/path?query=1'],
|
|
42
|
-
['ftp://invalid.com', null],
|
|
43
|
-
['www.example.com', null],
|
|
44
|
-
['a link: https://a.co and another http://b.com/end.', ['https://a.co', 'http://b.com/end']],
|
|
45
|
-
];
|
|
46
|
-
runTestCases(REGEX_PATTERNS.url, testCases);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should match ISO 8601 dates', () => {
|
|
50
|
-
const testCases: TestCase[] = [
|
|
51
|
-
['The date is 2026-02-17.', '2026-02-17'],
|
|
52
|
-
['Timestamp: 2026-02-17T15:30:00Z', '2026-02-17T15:30:00Z'],
|
|
53
|
-
['With milliseconds: 2026-02-17T15:30:00.123Z', '2026-02-17T15:30:00.123Z'],
|
|
54
|
-
['Not a date: 2026-02-17T', null],
|
|
55
|
-
['Invalid format 2026/02/17', null],
|
|
56
|
-
];
|
|
57
|
-
runTestCases(REGEX_PATTERNS.iso_date, testCases);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should match common date formats (US & EU)', () => {
|
|
61
|
-
const testCases: TestCase[] = [
|
|
62
|
-
['US date: 02/17/2026.', '02/17/2026'],
|
|
63
|
-
['EU date: 17.02.2026,', '17.02.2026'],
|
|
64
|
-
['Short year: 1.1.99', '1.1.99'],
|
|
65
|
-
['Two dates: 12/25/2024 and 24.12.2024', ['12/25/2024', '24.12.2024']],
|
|
66
|
-
];
|
|
67
|
-
runTestCases(REGEX_PATTERNS.common_date, testCases);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should match German date formats', () => {
|
|
71
|
-
const testCases: TestCase[] = [
|
|
72
|
-
['Datum: 17. Februar 2026', '17. Februar 2026'],
|
|
73
|
-
['Am 1. Januar 2025 war es kalt.', '1. Januar 2025'],
|
|
74
|
-
['No match: 17 Februar 2026', null],
|
|
75
|
-
];
|
|
76
|
-
runTestCases(REGEX_PATTERNS.german_date, testCases);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should match English date formats', () => {
|
|
80
|
-
const testCases: TestCase[] = [
|
|
81
|
-
['Date: February 17, 2026', 'February 17, 2026'],
|
|
82
|
-
['On March 1st, 2025, we launched.', 'March 1st, 2025'],
|
|
83
|
-
['Also August 2nd, 2024 and May 3rd, 2023.', ['August 2nd, 2024', 'May 3rd, 2023']],
|
|
84
|
-
['No match: February 17 2026', null],
|
|
85
|
-
];
|
|
86
|
-
runTestCases(REGEX_PATTERNS.english_date, testCases);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should match proper nouns (names, places)', () => {
|
|
90
|
-
const testCases: TestCase[] = [
|
|
91
|
-
['Hello, my name is Claude Keller.', ['Claude Keller']],
|
|
92
|
-
['This is Jean-Luc Picard of the USS Enterprise.', ['Jean-Luc Picard', 'USS Enterprise']],
|
|
93
|
-
['Talk to O\'Malley about it.', ['O\'Malley']],
|
|
94
|
-
['OpenClaw is a project.', ['OpenClaw']],
|
|
95
|
-
['Not a name: lower case', null],
|
|
96
|
-
['Multiple: Forge and Atlas are agents.', ['Forge', 'Atlas']],
|
|
97
|
-
];
|
|
98
|
-
runTestCases(REGEX_PATTERNS.proper_noun, testCases);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should match product-like names', () => {
|
|
102
|
-
const testCases: TestCase[] = [
|
|
103
|
-
['I have an iPhone 15.', 'iPhone 15'],
|
|
104
|
-
['We are using Windows 11.', 'Windows 11'],
|
|
105
|
-
['The latest model is GPT-4.', 'GPT-4'],
|
|
106
|
-
['Also look at ProductX.', 'ProductX'],
|
|
107
|
-
['The Roman Empire used IV.', 'Roman Empire used IV'], // Imperfect but acceptable
|
|
108
|
-
];
|
|
109
|
-
runTestCases(REGEX_PATTERNS.product_name, testCases);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should match organization names with suffixes', () => {
|
|
113
|
-
const testCases: TestCase[] = [
|
|
114
|
-
['He works at Acme GmbH.', 'Acme GmbH'],
|
|
115
|
-
['The owner of Stark Industries, LLC is Tony Stark.', 'Stark Industries, LLC'],
|
|
116
|
-
['Globex Corp. is another example.', 'Globex Corp.'],
|
|
117
|
-
['This also catches Acme Inc. and Cyberdyne Systems Ltd.', ['Acme Inc.', 'Cyberdyne Systems Ltd.']],
|
|
118
|
-
['No match for Acme alone', null],
|
|
119
|
-
];
|
|
120
|
-
runTestCases(REGEX_PATTERNS.organization_suffix, testCases);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
});
|
package/test/storage.test.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// test/storage.test.ts
|
|
2
|
-
|
|
3
|
-
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
4
|
-
import * as assert from 'node:assert';
|
|
5
|
-
import * as fs from 'node:fs/promises';
|
|
6
|
-
import * as path from 'node:path';
|
|
7
|
-
import { AtomicStorage } from '../src/storage.js';
|
|
8
|
-
import type { Logger } from '../src/types.js';
|
|
9
|
-
|
|
10
|
-
const createMockLogger = (): Logger & { logs: { level: string; msg: string }[] } => {
|
|
11
|
-
const logs: { level: string; msg: string }[] = [];
|
|
12
|
-
return { logs, info: (msg) => logs.push({ level: 'info', msg }), warn: (msg) => logs.push({ level: 'warn', msg }), error: (msg) => logs.push({ level: 'error', msg }), debug: (msg) => logs.push({ level: 'debug', msg }) };
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
describe('AtomicStorage', () => {
|
|
16
|
-
const testDir = path.join('/tmp', `atomic-storage-test-${Date.now()}`);
|
|
17
|
-
let logger: ReturnType<typeof createMockLogger>;
|
|
18
|
-
let storage: AtomicStorage;
|
|
19
|
-
|
|
20
|
-
before(async () => await fs.mkdir(testDir, { recursive: true }));
|
|
21
|
-
after(async () => await fs.rm(testDir, { recursive: true, force: true }));
|
|
22
|
-
beforeEach(() => { logger = createMockLogger(); storage = new AtomicStorage(testDir, logger); });
|
|
23
|
-
|
|
24
|
-
it('should initialize and create the storage directory', async () => {
|
|
25
|
-
const newDir = path.join(testDir, 'new-dir');
|
|
26
|
-
const newStorage = new AtomicStorage(newDir, logger);
|
|
27
|
-
await newStorage.init();
|
|
28
|
-
const stats = await fs.stat(newDir);
|
|
29
|
-
assert.ok(stats.isDirectory(), 'Directory should be created');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('writeJson', () => {
|
|
33
|
-
it('should write a JSON object to a file', async () => {
|
|
34
|
-
const fileName = 'test.json';
|
|
35
|
-
const data = { key: 'value', number: 123 };
|
|
36
|
-
await storage.writeJson(fileName, data);
|
|
37
|
-
const content = await fs.readFile(path.join(testDir, fileName), 'utf-8');
|
|
38
|
-
assert.deepStrictEqual(JSON.parse(content), data);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('readJson', () => {
|
|
43
|
-
it('should read and parse a valid JSON file', async () => {
|
|
44
|
-
const fileName = 'read.json';
|
|
45
|
-
const data = { a: 1, b: [2, 3] };
|
|
46
|
-
await fs.writeFile(path.join(testDir, fileName), JSON.stringify(data));
|
|
47
|
-
const result = await storage.readJson<typeof data>(fileName);
|
|
48
|
-
assert.deepStrictEqual(result, data);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should return null if the file does not exist', async () => {
|
|
52
|
-
const result = await storage.readJson('nonexistent.json');
|
|
53
|
-
assert.strictEqual(result, null);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('debounce', () => {
|
|
58
|
-
it('should only call the async function once after the delay', async () => {
|
|
59
|
-
let callCount = 0;
|
|
60
|
-
const asyncFn = async () => { callCount++; return callCount; };
|
|
61
|
-
const debouncedFn = AtomicStorage.debounce(asyncFn, 50);
|
|
62
|
-
|
|
63
|
-
const p1 = debouncedFn();
|
|
64
|
-
const p2 = debouncedFn();
|
|
65
|
-
const p3 = debouncedFn();
|
|
66
|
-
|
|
67
|
-
const results = await Promise.all([p1, p2, p3]);
|
|
68
|
-
assert.strictEqual(callCount, 1);
|
|
69
|
-
assert.deepStrictEqual(results, [1, 1, 1]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should pass the arguments of the last call to the async function', async () => {
|
|
73
|
-
let finalArgs: any[] = [];
|
|
74
|
-
const asyncFn = async (...args: any[]) => { finalArgs = args; return finalArgs; };
|
|
75
|
-
const debouncedFn = AtomicStorage.debounce(asyncFn, 50);
|
|
76
|
-
|
|
77
|
-
debouncedFn(1);
|
|
78
|
-
debouncedFn(2, 3);
|
|
79
|
-
const finalPromise = debouncedFn(4, 5, 6);
|
|
80
|
-
|
|
81
|
-
const result = await finalPromise;
|
|
82
|
-
assert.deepStrictEqual(finalArgs, [4, 5, 6]);
|
|
83
|
-
assert.deepStrictEqual(result, [4, 5, 6]);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"moduleResolution": "node",
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noImplicitAny": true,
|
|
10
|
-
"strictNullChecks": true,
|
|
11
|
-
"strictFunctionTypes": true,
|
|
12
|
-
"strictBindCallApply": true,
|
|
13
|
-
"strictPropertyInitialization": true,
|
|
14
|
-
"noImplicitThis": true,
|
|
15
|
-
"alwaysStrict": true,
|
|
16
|
-
"noUnusedLocals": true,
|
|
17
|
-
"noUnusedParameters": true,
|
|
18
|
-
"noImplicitReturns": true,
|
|
19
|
-
"noFallthroughCasesInSwitch": true,
|
|
20
|
-
"skipLibCheck": true,
|
|
21
|
-
"outDir": "./dist",
|
|
22
|
-
"declaration": true
|
|
23
|
-
},
|
|
24
|
-
"include": ["src/**/*.ts", "index.ts"],
|
|
25
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
26
|
-
}
|