@auxiora/memory 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 (56) hide show
  1. package/LICENSE +191 -0
  2. package/dist/extractor.d.ts +40 -0
  3. package/dist/extractor.d.ts.map +1 -0
  4. package/dist/extractor.js +130 -0
  5. package/dist/extractor.js.map +1 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +8 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/partition.d.ts +25 -0
  11. package/dist/partition.d.ts.map +1 -0
  12. package/dist/partition.js +104 -0
  13. package/dist/partition.js.map +1 -0
  14. package/dist/pattern-detector.d.ts +27 -0
  15. package/dist/pattern-detector.d.ts.map +1 -0
  16. package/dist/pattern-detector.js +260 -0
  17. package/dist/pattern-detector.js.map +1 -0
  18. package/dist/personality-adapter.d.ts +10 -0
  19. package/dist/personality-adapter.d.ts.map +1 -0
  20. package/dist/personality-adapter.js +72 -0
  21. package/dist/personality-adapter.js.map +1 -0
  22. package/dist/retriever.d.ts +15 -0
  23. package/dist/retriever.d.ts.map +1 -0
  24. package/dist/retriever.js +179 -0
  25. package/dist/retriever.js.map +1 -0
  26. package/dist/sentiment.d.ts +7 -0
  27. package/dist/sentiment.d.ts.map +1 -0
  28. package/dist/sentiment.js +118 -0
  29. package/dist/sentiment.js.map +1 -0
  30. package/dist/store.d.ts +41 -0
  31. package/dist/store.d.ts.map +1 -0
  32. package/dist/store.js +352 -0
  33. package/dist/store.js.map +1 -0
  34. package/dist/types.d.ts +80 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +26 -0
  39. package/src/extractor.ts +212 -0
  40. package/src/index.ts +22 -0
  41. package/src/partition.ts +123 -0
  42. package/src/pattern-detector.ts +304 -0
  43. package/src/personality-adapter.ts +82 -0
  44. package/src/retriever.ts +213 -0
  45. package/src/sentiment.ts +129 -0
  46. package/src/store.ts +384 -0
  47. package/src/types.ts +92 -0
  48. package/tests/extractor.test.ts +247 -0
  49. package/tests/partition.test.ts +168 -0
  50. package/tests/pattern-detector.test.ts +150 -0
  51. package/tests/personality-adapter.test.ts +155 -0
  52. package/tests/retriever.test.ts +240 -0
  53. package/tests/sentiment.test.ts +207 -0
  54. package/tests/store.test.ts +390 -0
  55. package/tsconfig.json +13 -0
  56. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { MemoryStore } from '../src/store.js';
6
+ import { MemoryExtractor } from '../src/extractor.js';
7
+ import type { AIProvider } from '../src/extractor.js';
8
+
9
+ let tmpDir: string;
10
+
11
+ function createMockProvider(response: string): AIProvider {
12
+ return {
13
+ complete: async () => ({ content: response }),
14
+ };
15
+ }
16
+
17
+ describe('MemoryExtractor', () => {
18
+ beforeEach(async () => {
19
+ tmpDir = path.join(os.tmpdir(), `auxiora-extractor-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
20
+ await fs.mkdir(tmpDir, { recursive: true });
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ it('should extract facts from AI response', async () => {
28
+ const store = new MemoryStore({ dir: tmpDir });
29
+ const provider = createMockProvider(JSON.stringify({
30
+ facts: [
31
+ { content: 'User works at Acme Corp', category: 'fact', importance: 0.8 },
32
+ { content: 'User prefers dark mode', category: 'preference', importance: 0.6 },
33
+ ],
34
+ relationships: [],
35
+ patterns: [],
36
+ contradictions: [],
37
+ personalitySignals: [],
38
+ }));
39
+
40
+ const extractor = new MemoryExtractor(store, provider);
41
+ const result = await extractor.extract(
42
+ 'I work at Acme Corp and I love dark mode',
43
+ 'That sounds great!',
44
+ );
45
+
46
+ expect(result.factsExtracted).toHaveLength(2);
47
+ expect(result.factsExtracted[0].content).toBe('User works at Acme Corp');
48
+ expect(result.factsExtracted[0].importance).toBe(0.8);
49
+
50
+ const all = await store.getAll();
51
+ expect(all).toHaveLength(2);
52
+ });
53
+
54
+ it('should extract relationships', async () => {
55
+ const store = new MemoryStore({ dir: tmpDir });
56
+ const provider = createMockProvider(JSON.stringify({
57
+ facts: [],
58
+ relationships: [
59
+ { content: 'Shared a joke about recursion', type: 'inside_joke' },
60
+ ],
61
+ patterns: [],
62
+ contradictions: [],
63
+ personalitySignals: [],
64
+ }));
65
+
66
+ const extractor = new MemoryExtractor(store, provider);
67
+ const result = await extractor.extract(
68
+ 'That recursion joke was hilarious',
69
+ 'Ha, the classic "to understand recursion..."!',
70
+ );
71
+
72
+ expect(result.relationshipsFound).toHaveLength(1);
73
+ expect(result.relationshipsFound[0].category).toBe('relationship');
74
+ });
75
+
76
+ it('should extract patterns', async () => {
77
+ const store = new MemoryStore({ dir: tmpDir });
78
+ const provider = createMockProvider(JSON.stringify({
79
+ facts: [],
80
+ relationships: [],
81
+ patterns: [
82
+ { pattern: 'User prefers concise responses', type: 'communication' },
83
+ ],
84
+ contradictions: [],
85
+ personalitySignals: [],
86
+ }));
87
+
88
+ const extractor = new MemoryExtractor(store, provider);
89
+ const result = await extractor.extract('Just the answer please', 'Sure: 42.');
90
+
91
+ expect(result.patternsDetected).toHaveLength(1);
92
+ expect(result.patternsDetected[0].category).toBe('pattern');
93
+ });
94
+
95
+ it('should extract personality signals', async () => {
96
+ const store = new MemoryStore({ dir: tmpDir });
97
+ const provider = createMockProvider(JSON.stringify({
98
+ facts: [],
99
+ relationships: [],
100
+ patterns: [],
101
+ contradictions: [],
102
+ personalitySignals: [
103
+ { trait: 'humor', direction: 'increase', reason: 'User made a joke' },
104
+ { trait: 'formality', direction: 'decrease', reason: 'Casual tone' },
105
+ ],
106
+ }));
107
+
108
+ const extractor = new MemoryExtractor(store, provider);
109
+ const result = await extractor.extract('lol that was funny', 'Glad you liked it!');
110
+
111
+ expect(result.personalitySignals).toHaveLength(2);
112
+ expect(result.personalitySignals[0].trait).toBe('humor');
113
+ expect(result.personalitySignals[0].adjustment).toBe(0.1);
114
+ expect(result.personalitySignals[1].trait).toBe('formality');
115
+ expect(result.personalitySignals[1].adjustment).toBe(-0.1);
116
+ });
117
+
118
+ it('should handle contradictions with update resolution', async () => {
119
+ const store = new MemoryStore({ dir: tmpDir });
120
+ // Seed existing fact
121
+ await store.add('User works at Acme Corp', 'fact', 'extracted');
122
+
123
+ const provider = createMockProvider(JSON.stringify({
124
+ facts: [],
125
+ relationships: [],
126
+ patterns: [],
127
+ contradictions: [
128
+ { existingFact: 'works at Acme Corp', newFact: 'User works at Globex now', resolution: 'update' },
129
+ ],
130
+ personalitySignals: [],
131
+ }));
132
+
133
+ const extractor = new MemoryExtractor(store, provider);
134
+ const result = await extractor.extract(
135
+ 'I switched jobs, now at Globex',
136
+ 'Congrats on the new position!',
137
+ );
138
+
139
+ expect(result.contradictionsFound).toHaveLength(1);
140
+ expect(result.contradictionsFound[0].resolution).toBe('update');
141
+
142
+ // The existing fact should have been updated
143
+ const all = await store.getAll();
144
+ expect(all.some(m => m.content === 'User works at Globex now')).toBe(true);
145
+ });
146
+
147
+ it('should handle empty AI response gracefully', async () => {
148
+ const store = new MemoryStore({ dir: tmpDir });
149
+ const provider = createMockProvider(JSON.stringify({
150
+ facts: [],
151
+ relationships: [],
152
+ patterns: [],
153
+ contradictions: [],
154
+ personalitySignals: [],
155
+ }));
156
+
157
+ const extractor = new MemoryExtractor(store, provider);
158
+ const result = await extractor.extract('hello', 'Hi there!');
159
+
160
+ expect(result.factsExtracted).toHaveLength(0);
161
+ expect(result.patternsDetected).toHaveLength(0);
162
+ expect(result.relationshipsFound).toHaveLength(0);
163
+ expect(result.personalitySignals).toHaveLength(0);
164
+ });
165
+
166
+ it('should handle malformed AI response', async () => {
167
+ const store = new MemoryStore({ dir: tmpDir });
168
+ const provider = createMockProvider('not valid json at all');
169
+
170
+ const extractor = new MemoryExtractor(store, provider);
171
+ const result = await extractor.extract('hello', 'Hi!');
172
+
173
+ expect(result.factsExtracted).toHaveLength(0);
174
+ expect(result.patternsDetected).toHaveLength(0);
175
+ });
176
+
177
+ it('should handle AI provider error gracefully', async () => {
178
+ const store = new MemoryStore({ dir: tmpDir });
179
+ const provider: AIProvider = {
180
+ complete: async () => { throw new Error('API down'); },
181
+ };
182
+
183
+ const extractor = new MemoryExtractor(store, provider);
184
+ const result = await extractor.extract('hello', 'Hi!');
185
+
186
+ expect(result.factsExtracted).toHaveLength(0);
187
+ });
188
+
189
+ it('should strip markdown code fences from response', async () => {
190
+ const store = new MemoryStore({ dir: tmpDir });
191
+ const json = JSON.stringify({
192
+ facts: [{ content: 'User likes TypeScript', category: 'preference', importance: 0.7 }],
193
+ relationships: [],
194
+ patterns: [],
195
+ contradictions: [],
196
+ personalitySignals: [],
197
+ });
198
+ const provider = createMockProvider('```json\n' + json + '\n```');
199
+
200
+ const extractor = new MemoryExtractor(store, provider);
201
+ const result = await extractor.extract('I love TypeScript', 'Me too!');
202
+
203
+ expect(result.factsExtracted).toHaveLength(1);
204
+ });
205
+
206
+ it('should clamp importance to 0-1', async () => {
207
+ const store = new MemoryStore({ dir: tmpDir });
208
+ const provider = createMockProvider(JSON.stringify({
209
+ facts: [
210
+ { content: 'High importance fact', category: 'fact', importance: 5.0 },
211
+ { content: 'Negative importance fact', category: 'fact', importance: -2.0 },
212
+ ],
213
+ relationships: [],
214
+ patterns: [],
215
+ contradictions: [],
216
+ personalitySignals: [],
217
+ }));
218
+
219
+ const extractor = new MemoryExtractor(store, provider);
220
+ const result = await extractor.extract('test', 'test');
221
+
222
+ expect(result.factsExtracted[0].importance).toBe(1.0);
223
+ expect(result.factsExtracted[1].importance).toBe(0);
224
+ });
225
+
226
+ it('should skip entries with missing content', async () => {
227
+ const store = new MemoryStore({ dir: tmpDir });
228
+ const provider = createMockProvider(JSON.stringify({
229
+ facts: [
230
+ { content: '', category: 'fact', importance: 0.5 },
231
+ { content: 'Valid fact', category: 'fact', importance: 0.5 },
232
+ ],
233
+ relationships: [{ content: '', type: 'inside_joke' }],
234
+ patterns: [{ pattern: '', type: 'communication' }],
235
+ contradictions: [],
236
+ personalitySignals: [{ trait: '', direction: 'increase', reason: 'test' }],
237
+ }));
238
+
239
+ const extractor = new MemoryExtractor(store, provider);
240
+ const result = await extractor.extract('test', 'test');
241
+
242
+ expect(result.factsExtracted).toHaveLength(1);
243
+ expect(result.relationshipsFound).toHaveLength(0);
244
+ expect(result.patternsDetected).toHaveLength(0);
245
+ expect(result.personalitySignals).toHaveLength(0);
246
+ });
247
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { MemoryPartitionManager } from '../src/partition.js';
6
+ import { MemoryStore } from '../src/store.js';
7
+ import { MemoryRetriever } from '../src/retriever.js';
8
+
9
+ describe('MemoryPartitionManager', () => {
10
+ let tmpDir: string;
11
+ let manager: MemoryPartitionManager;
12
+
13
+ beforeEach(async () => {
14
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-partition-'));
15
+ manager = new MemoryPartitionManager({ dir: tmpDir });
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await fs.rm(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ it('should create a private partition', async () => {
23
+ const partition = await manager.createPartition('Alice Private', 'private', {
24
+ ownerId: 'user-alice',
25
+ });
26
+ expect(partition.id).toMatch(/^part-/);
27
+ expect(partition.type).toBe('private');
28
+ expect(partition.ownerId).toBe('user-alice');
29
+ });
30
+
31
+ it('should create a shared partition', async () => {
32
+ const partition = await manager.createPartition('Team Shared', 'shared', {
33
+ ownerId: 'user-alice',
34
+ memberIds: ['user-bob', 'user-charlie'],
35
+ });
36
+ expect(partition.type).toBe('shared');
37
+ expect(partition.memberIds).toContain('user-bob');
38
+ });
39
+
40
+ it('should always have implicit global partition', async () => {
41
+ const global = await manager.getPartition('global');
42
+ expect(global).toBeDefined();
43
+ expect(global!.type).toBe('global');
44
+ });
45
+
46
+ it('should list partitions for a user', async () => {
47
+ await manager.createPartition('Alice Private', 'private', { ownerId: 'user-alice' });
48
+ await manager.createPartition('Bob Private', 'private', { ownerId: 'user-bob' });
49
+ await manager.createPartition('Team', 'shared', {
50
+ ownerId: 'user-alice',
51
+ memberIds: ['user-bob'],
52
+ });
53
+
54
+ const alicePartitions = await manager.listPartitions('user-alice');
55
+ // global + alice private + team (as owner)
56
+ expect(alicePartitions.length).toBe(3);
57
+
58
+ const bobPartitions = await manager.listPartitions('user-bob');
59
+ // global + bob private + team (as member)
60
+ expect(bobPartitions.length).toBe(3);
61
+ });
62
+
63
+ it('should check access correctly', async () => {
64
+ const priv = await manager.createPartition('Alice Only', 'private', {
65
+ ownerId: 'user-alice',
66
+ });
67
+
68
+ expect(await manager.hasAccess(priv.id, 'user-alice')).toBe(true);
69
+ expect(await manager.hasAccess(priv.id, 'user-bob')).toBe(false);
70
+ expect(await manager.hasAccess('global', 'user-bob')).toBe(true);
71
+ });
72
+
73
+ it('should delete partition', async () => {
74
+ const partition = await manager.createPartition('Temp', 'private', {
75
+ ownerId: 'user-alice',
76
+ });
77
+ const deleted = await manager.deletePartition(partition.id);
78
+ expect(deleted).toBe(true);
79
+
80
+ const found = await manager.getPartition(partition.id);
81
+ expect(found).toBeUndefined();
82
+ });
83
+
84
+ it('should not delete global partition', async () => {
85
+ const deleted = await manager.deletePartition('global');
86
+ expect(deleted).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('Memory Partition Isolation', () => {
91
+ let tmpDir: string;
92
+ let store: MemoryStore;
93
+ let retriever: MemoryRetriever;
94
+
95
+ beforeEach(async () => {
96
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-isolation-'));
97
+ store = new MemoryStore({ dir: tmpDir });
98
+ retriever = new MemoryRetriever();
99
+ });
100
+
101
+ afterEach(async () => {
102
+ await fs.rm(tmpDir, { recursive: true, force: true });
103
+ });
104
+
105
+ it('should tag memories with partitionId', async () => {
106
+ const entry = await store.add('Alice likes cats', 'preference', 'extracted', {
107
+ partitionId: 'alice-private',
108
+ sourceUserId: 'user-alice',
109
+ });
110
+
111
+ expect(entry.partitionId).toBe('alice-private');
112
+ expect(entry.sourceUserId).toBe('user-alice');
113
+ });
114
+
115
+ it('should default partition to global', async () => {
116
+ const entry = await store.add('Shared fact', 'fact', 'explicit');
117
+ expect(entry.partitionId).toBe('global');
118
+ });
119
+
120
+ it('should filter memories by partition', async () => {
121
+ await store.add('Alice secret', 'fact', 'explicit', { partitionId: 'alice-private' });
122
+ await store.add('Bob secret', 'fact', 'explicit', { partitionId: 'bob-private' });
123
+ await store.add('Shared info', 'fact', 'explicit', { partitionId: 'global' });
124
+
125
+ const aliceMemories = await store.getByPartition('alice-private');
126
+ expect(aliceMemories).toHaveLength(1);
127
+ expect(aliceMemories[0].content).toBe('Alice secret');
128
+
129
+ const globalMemories = await store.getByPartition('global');
130
+ expect(globalMemories).toHaveLength(1);
131
+ expect(globalMemories[0].content).toBe('Shared info');
132
+ });
133
+
134
+ it('should get memories by multiple partitions', async () => {
135
+ await store.add('Alice pref', 'preference', 'explicit', { partitionId: 'alice-private' });
136
+ await store.add('Bob pref', 'preference', 'explicit', { partitionId: 'bob-private' });
137
+ await store.add('Global pref', 'preference', 'explicit', { partitionId: 'global' });
138
+
139
+ const aliceAccessible = await store.getByPartitions(['alice-private', 'global']);
140
+ expect(aliceAccessible).toHaveLength(2);
141
+ });
142
+
143
+ it('should not leak private memories in retriever', async () => {
144
+ await store.add('Alice coffee preference', 'preference', 'explicit', {
145
+ partitionId: 'alice-private',
146
+ importance: 0.9,
147
+ });
148
+ await store.add('Bob tea preference', 'preference', 'explicit', {
149
+ partitionId: 'bob-private',
150
+ importance: 0.9,
151
+ });
152
+ await store.add('Global coffee facts', 'fact', 'explicit', {
153
+ partitionId: 'global',
154
+ importance: 0.9,
155
+ });
156
+
157
+ const allMemories = await store.getAll();
158
+
159
+ // Bob should not see Alice's private memories
160
+ const bobResult = retriever.retrieve(allMemories, 'coffee', ['bob-private', 'global']);
161
+ expect(bobResult).not.toContain('Alice coffee');
162
+
163
+ // Alice should see her own + global
164
+ const aliceResult = retriever.retrieve(allMemories, 'coffee', ['alice-private', 'global']);
165
+ expect(aliceResult).toContain('Alice coffee');
166
+ expect(aliceResult).toContain('Global coffee');
167
+ });
168
+ });
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PatternDetector } from '../src/pattern-detector.js';
3
+
4
+ describe('PatternDetector', () => {
5
+ const detector = new PatternDetector();
6
+
7
+ it('should return empty for too few messages', () => {
8
+ const signals = detector.detect([
9
+ { content: 'hi', role: 'user', timestamp: Date.now() },
10
+ ]);
11
+ expect(signals).toHaveLength(0);
12
+ });
13
+
14
+ it('should detect brief message pattern', () => {
15
+ const messages = Array.from({ length: 10 }, (_, i) => ({
16
+ content: 'short msg',
17
+ role: 'user',
18
+ timestamp: Date.now() + i * 1000,
19
+ }));
20
+
21
+ const signals = detector.detect(messages);
22
+ const commSignals = signals.filter(s => s.type === 'communication');
23
+ expect(commSignals.some(s => s.pattern.includes('brief'))).toBe(true);
24
+ });
25
+
26
+ it('should detect detailed message pattern', () => {
27
+ const longContent = 'This is a very detailed message about various topics that goes on for quite a while and contains much information and many words to push the character count above two hundred characters easily and without much difficulty at all.';
28
+ const messages = Array.from({ length: 10 }, (_, i) => ({
29
+ content: longContent,
30
+ role: 'user',
31
+ timestamp: Date.now() + i * 1000,
32
+ }));
33
+
34
+ const signals = detector.detect(messages);
35
+ const commSignals = signals.filter(s => s.type === 'communication');
36
+ expect(commSignals.some(s => s.pattern.includes('detailed'))).toBe(true);
37
+ });
38
+
39
+ it('should detect question-heavy pattern', () => {
40
+ const messages = Array.from({ length: 10 }, (_, i) => ({
41
+ content: `What is the answer to question ${i}?`,
42
+ role: 'user',
43
+ timestamp: Date.now() + i * 1000,
44
+ }));
45
+
46
+ const signals = detector.detect(messages);
47
+ const commSignals = signals.filter(s => s.type === 'communication');
48
+ expect(commSignals.some(s => s.pattern.includes('questions'))).toBe(true);
49
+ });
50
+
51
+ it('should detect code-heavy pattern', () => {
52
+ const messages = Array.from({ length: 10 }, (_, i) => ({
53
+ content: `const value${i} = ${i}; function doSomething() {}`,
54
+ role: 'user',
55
+ timestamp: Date.now() + i * 1000,
56
+ }));
57
+
58
+ const signals = detector.detect(messages);
59
+ const commSignals = signals.filter(s => s.type === 'communication');
60
+ expect(commSignals.some(s => s.pattern.includes('code'))).toBe(true);
61
+ });
62
+
63
+ it('should detect schedule patterns', () => {
64
+ // All messages at 10 PM
65
+ const messages = Array.from({ length: 10 }, (_, i) => {
66
+ const date = new Date();
67
+ date.setHours(22, 0, 0, 0);
68
+ date.setDate(date.getDate() - i);
69
+ return {
70
+ content: `Working late on day ${i}`,
71
+ role: 'user',
72
+ timestamp: date.getTime(),
73
+ };
74
+ });
75
+
76
+ const signals = detector.detect(messages);
77
+ const scheduleSignals = signals.filter(s => s.type === 'schedule');
78
+ expect(scheduleSignals.length).toBeGreaterThanOrEqual(1);
79
+ expect(scheduleSignals[0].pattern).toContain('active');
80
+ });
81
+
82
+ it('should detect topic patterns', () => {
83
+ const messages = [
84
+ { content: 'TypeScript generics are tricky', role: 'user', timestamp: Date.now() },
85
+ { content: 'TypeScript types need work', role: 'user', timestamp: Date.now() + 1000 },
86
+ { content: 'TypeScript compiler is slow', role: 'user', timestamp: Date.now() + 2000 },
87
+ { content: 'React components need TypeScript', role: 'user', timestamp: Date.now() + 3000 },
88
+ { content: 'TypeScript interfaces rock', role: 'user', timestamp: Date.now() + 4000 },
89
+ { content: 'Need help with TypeScript React', role: 'user', timestamp: Date.now() + 5000 },
90
+ ];
91
+
92
+ const signals = detector.detect(messages);
93
+ const topicSignals = signals.filter(s => s.type === 'topic');
94
+ expect(topicSignals.length).toBeGreaterThanOrEqual(1);
95
+ expect(topicSignals[0].pattern).toContain('typescript');
96
+ });
97
+
98
+ it('should detect enthusiastic mood', () => {
99
+ const messages = Array.from({ length: 10 }, (_, i) => ({
100
+ content: `This is amazing! Great work! I love it!`,
101
+ role: 'user',
102
+ timestamp: Date.now() + i * 1000,
103
+ }));
104
+
105
+ const signals = detector.detect(messages);
106
+ const moodSignals = signals.filter(s => s.type === 'mood');
107
+ expect(moodSignals.some(s => s.pattern.includes('enthusiastic'))).toBe(true);
108
+ });
109
+
110
+ it('should detect casual mood', () => {
111
+ const messages = Array.from({ length: 10 }, (_, i) => ({
112
+ content: `lol tbh gonna try this haha`,
113
+ role: 'user',
114
+ timestamp: Date.now() + i * 1000,
115
+ }));
116
+
117
+ const signals = detector.detect(messages);
118
+ const moodSignals = signals.filter(s => s.type === 'mood');
119
+ expect(moodSignals.some(s => s.pattern.includes('casual'))).toBe(true);
120
+ });
121
+
122
+ it('should only analyze user messages', () => {
123
+ const messages = [
124
+ { content: 'hi', role: 'user', timestamp: Date.now() },
125
+ { content: 'Hello! How can I help?', role: 'assistant', timestamp: Date.now() + 1000 },
126
+ { content: 'thanks', role: 'user', timestamp: Date.now() + 2000 },
127
+ { content: 'You are welcome!', role: 'assistant', timestamp: Date.now() + 3000 },
128
+ ];
129
+
130
+ // Should return empty because only 2 user messages (need >= 5 for most patterns)
131
+ const signals = detector.detect(messages);
132
+ // Communication patterns require >= 5 user messages
133
+ const commSignals = signals.filter(s => s.type === 'communication');
134
+ expect(commSignals).toHaveLength(0);
135
+ });
136
+
137
+ it('should have confidence between 0 and 1', () => {
138
+ const messages = Array.from({ length: 20 }, (_, i) => ({
139
+ content: 'short',
140
+ role: 'user',
141
+ timestamp: Date.now() + i * 1000,
142
+ }));
143
+
144
+ const signals = detector.detect(messages);
145
+ for (const s of signals) {
146
+ expect(s.confidence).toBeGreaterThanOrEqual(0);
147
+ expect(s.confidence).toBeLessThanOrEqual(1);
148
+ }
149
+ });
150
+ });