@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,155 @@
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 { PersonalityAdapter } from '../src/personality-adapter.js';
7
+
8
+ let tmpDir: string;
9
+
10
+ describe('PersonalityAdapter', () => {
11
+ beforeEach(async () => {
12
+ tmpDir = path.join(os.tmpdir(), `auxiora-personality-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
13
+ await fs.mkdir(tmpDir, { recursive: true });
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await fs.rm(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it('should record a personality signal', async () => {
21
+ const store = new MemoryStore({ dir: tmpDir });
22
+ const adapter = new PersonalityAdapter(store);
23
+
24
+ await adapter.recordSignal({
25
+ trait: 'humor',
26
+ adjustment: 0.3,
27
+ reason: 'User responds positively to jokes',
28
+ signalCount: 1,
29
+ });
30
+
31
+ const adjustments = await adapter.getAdjustments();
32
+ expect(adjustments).toHaveLength(1);
33
+ expect(adjustments[0].trait).toBe('humor');
34
+ });
35
+
36
+ it('should accumulate signals for the same trait', async () => {
37
+ const store = new MemoryStore({ dir: tmpDir });
38
+ const adapter = new PersonalityAdapter(store);
39
+
40
+ await adapter.recordSignal({
41
+ trait: 'humor',
42
+ adjustment: 0.3,
43
+ reason: 'User laughed at a joke',
44
+ signalCount: 1,
45
+ });
46
+
47
+ await adapter.recordSignal({
48
+ trait: 'humor',
49
+ adjustment: 0.3,
50
+ reason: 'User made a joke back',
51
+ signalCount: 1,
52
+ });
53
+
54
+ const adjustments = await adapter.getAdjustments();
55
+ expect(adjustments).toHaveLength(1);
56
+ expect(adjustments[0].signalCount).toBe(2);
57
+ // Adjustment should grow but not exceed 1
58
+ expect(adjustments[0].adjustment).toBeGreaterThan(0);
59
+ expect(adjustments[0].adjustment).toBeLessThanOrEqual(1);
60
+ });
61
+
62
+ it('should track different traits independently', async () => {
63
+ const store = new MemoryStore({ dir: tmpDir });
64
+ const adapter = new PersonalityAdapter(store);
65
+
66
+ await adapter.recordSignal({
67
+ trait: 'humor',
68
+ adjustment: 0.3,
69
+ reason: 'User likes humor',
70
+ signalCount: 1,
71
+ });
72
+
73
+ await adapter.recordSignal({
74
+ trait: 'formality',
75
+ adjustment: -0.2,
76
+ reason: 'User uses casual language',
77
+ signalCount: 1,
78
+ });
79
+
80
+ const adjustments = await adapter.getAdjustments();
81
+ expect(adjustments).toHaveLength(2);
82
+ const humor = adjustments.find(a => a.trait === 'humor');
83
+ const formality = adjustments.find(a => a.trait === 'formality');
84
+ expect(humor).toBeDefined();
85
+ expect(formality).toBeDefined();
86
+ });
87
+
88
+ it('should generate prompt modifier', async () => {
89
+ const store = new MemoryStore({ dir: tmpDir });
90
+ const adapter = new PersonalityAdapter(store);
91
+
92
+ await adapter.recordSignal({
93
+ trait: 'humor',
94
+ adjustment: 0.5,
95
+ reason: 'User responds positively to jokes',
96
+ signalCount: 3,
97
+ });
98
+
99
+ await adapter.recordSignal({
100
+ trait: 'formality',
101
+ adjustment: -0.5,
102
+ reason: 'User uses casual language',
103
+ signalCount: 3,
104
+ });
105
+
106
+ const modifier = await adapter.getPromptModifier();
107
+ expect(modifier).toContain('Personality Adaptations');
108
+ expect(modifier).toContain('humor');
109
+ expect(modifier).toContain('formality');
110
+ });
111
+
112
+ it('should return empty string when no adjustments', async () => {
113
+ const store = new MemoryStore({ dir: tmpDir });
114
+ const adapter = new PersonalityAdapter(store);
115
+
116
+ const modifier = await adapter.getPromptModifier();
117
+ expect(modifier).toBe('');
118
+ });
119
+
120
+ it('should skip very small adjustments in prompt modifier', async () => {
121
+ const store = new MemoryStore({ dir: tmpDir });
122
+ const adapter = new PersonalityAdapter(store);
123
+
124
+ await adapter.recordSignal({
125
+ trait: 'verbosity',
126
+ adjustment: 0.05,
127
+ reason: 'Weak signal',
128
+ signalCount: 1,
129
+ });
130
+
131
+ const modifier = await adapter.getPromptModifier();
132
+ // Adjustment of 0.05 < 0.1 threshold, so should not appear
133
+ // However the accumulation with 0.2 factor makes it 0.01, definitely below threshold
134
+ expect(modifier).toBe('');
135
+ });
136
+
137
+ it('should clamp adjustments to [-1, 1]', async () => {
138
+ const store = new MemoryStore({ dir: tmpDir });
139
+ const adapter = new PersonalityAdapter(store);
140
+
141
+ // Send many strong positive signals
142
+ for (let i = 0; i < 50; i++) {
143
+ await adapter.recordSignal({
144
+ trait: 'humor',
145
+ adjustment: 1.0,
146
+ reason: 'Very funny',
147
+ signalCount: 1,
148
+ });
149
+ }
150
+
151
+ const adjustments = await adapter.getAdjustments();
152
+ expect(adjustments[0].adjustment).toBeLessThanOrEqual(1);
153
+ expect(adjustments[0].adjustment).toBeGreaterThanOrEqual(-1);
154
+ });
155
+ });
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { MemoryRetriever } from '../src/retriever.js';
3
+ import type { MemoryEntry } from '../src/types.js';
4
+
5
+ function makeMemory(overrides: Partial<MemoryEntry> = {}): MemoryEntry {
6
+ return {
7
+ id: 'mem-test',
8
+ content: 'Test memory',
9
+ category: 'fact',
10
+ source: 'explicit',
11
+ createdAt: Date.now(),
12
+ updatedAt: Date.now(),
13
+ accessCount: 0,
14
+ tags: ['test', 'memory'],
15
+ importance: 0.5,
16
+ confidence: 0.8,
17
+ sentiment: 'neutral',
18
+ encrypted: false,
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ describe('MemoryRetriever', () => {
24
+ const retriever = new MemoryRetriever();
25
+
26
+ it('should return matching memories formatted for prompt', () => {
27
+ const memories = [
28
+ makeMemory({ content: 'Likes TypeScript', tags: ['typescript', 'programming'], category: 'preference' }),
29
+ makeMemory({ id: 'mem-2', content: 'Works at Acme', tags: ['acme', 'work', 'company'], category: 'fact' }),
30
+ ];
31
+
32
+ const result = retriever.retrieve(memories, 'Tell me about TypeScript');
33
+ expect(result).toContain('Likes TypeScript');
34
+ expect(result).toContain('What you know about the user');
35
+ });
36
+
37
+ it('should rank by tag overlap', () => {
38
+ const memories = [
39
+ makeMemory({ id: 'a', content: 'Likes Python', tags: ['python', 'programming'], category: 'preference' }),
40
+ makeMemory({ id: 'b', content: 'Loves TypeScript deeply', tags: ['typescript', 'programming', 'loves'], category: 'preference' }),
41
+ ];
42
+
43
+ const result = retriever.retrieve(memories, 'typescript programming');
44
+ // TypeScript memory should appear first (more tag overlap)
45
+ const tsIndex = result.indexOf('TypeScript');
46
+ const pyIndex = result.indexOf('Python');
47
+ expect(tsIndex).toBeLessThan(pyIndex);
48
+ });
49
+
50
+ it('should respect token budget', () => {
51
+ const memories = Array.from({ length: 100 }, (_, i) =>
52
+ makeMemory({
53
+ id: `mem-${i}`,
54
+ content: `Memory item number ${i} with some extra text to fill space`,
55
+ tags: ['matching', 'keyword'],
56
+ }),
57
+ );
58
+
59
+ const result = retriever.retrieve(memories, 'matching keyword');
60
+ // Should not include all 100
61
+ expect(result.length).toBeLessThan(100 * 60);
62
+ expect(result).toContain('What you know about the user');
63
+ });
64
+
65
+ it('should return empty string when no memories match', () => {
66
+ const thirtyOneDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000;
67
+ const memories = [
68
+ makeMemory({
69
+ content: 'Likes cats',
70
+ tags: ['cats', 'animals'],
71
+ updatedAt: thirtyOneDaysAgo,
72
+ accessCount: 0,
73
+ importance: 0,
74
+ confidence: 0,
75
+ }),
76
+ ];
77
+
78
+ const result = retriever.retrieve(memories, 'quantum physics');
79
+ expect(result).toBe('');
80
+ });
81
+
82
+ it('should return empty string for empty memory list', () => {
83
+ const result = retriever.retrieve([], 'anything');
84
+ expect(result).toBe('');
85
+ });
86
+
87
+ // === New tests for importance weighting ===
88
+
89
+ it('should favor high-importance memories', () => {
90
+ const memories = [
91
+ makeMemory({ id: 'low', content: 'Low importance fact', tags: ['fact'], category: 'fact', importance: 0.1 }),
92
+ makeMemory({ id: 'high', content: 'High importance fact', tags: ['fact'], category: 'fact', importance: 0.9 }),
93
+ ];
94
+
95
+ const result = retriever.retrieve(memories, 'fact');
96
+ const highIndex = result.indexOf('High importance');
97
+ const lowIndex = result.indexOf('Low importance');
98
+ // High importance should appear before low importance
99
+ if (lowIndex >= 0) {
100
+ expect(highIndex).toBeLessThan(lowIndex);
101
+ } else {
102
+ // At minimum, high importance should appear
103
+ expect(highIndex).toBeGreaterThan(-1);
104
+ }
105
+ });
106
+
107
+ // === Category budget allocation ===
108
+
109
+ it('should include section headers by category', () => {
110
+ const memories = [
111
+ makeMemory({ id: 'f1', content: 'Works at Acme Corp', tags: ['acme', 'work'], category: 'fact' }),
112
+ makeMemory({ id: 'p1', content: 'Prefers dark mode', tags: ['dark', 'mode'], category: 'preference' }),
113
+ makeMemory({ id: 'r1', content: 'Shared debugging session', tags: ['debug', 'session'], category: 'relationship' }),
114
+ makeMemory({ id: 'pt1', content: 'Prefers brief responses', tags: ['brief', 'responses'], category: 'pattern' }),
115
+ ];
116
+
117
+ const result = retriever.retrieve(memories, 'dark mode debug session work acme brief responses');
118
+ expect(result).toContain('### Key Facts');
119
+ expect(result).toContain('### Preferences');
120
+ });
121
+
122
+ // === Expired memory filtering ===
123
+
124
+ it('should skip expired memories', () => {
125
+ const past = Date.now() - 1000;
126
+ const memories = [
127
+ makeMemory({
128
+ id: 'expired',
129
+ content: 'Expired memory',
130
+ tags: ['test', 'expired'],
131
+ expiresAt: past,
132
+ }),
133
+ makeMemory({
134
+ id: 'valid',
135
+ content: 'Valid memory still here',
136
+ tags: ['test', 'valid'],
137
+ }),
138
+ ];
139
+
140
+ const result = retriever.retrieve(memories, 'test');
141
+ expect(result).not.toContain('Expired memory');
142
+ expect(result).toContain('Valid memory');
143
+ });
144
+
145
+ it('should return empty when all memories are expired', () => {
146
+ const past = Date.now() - 1000;
147
+ const memories = [
148
+ makeMemory({ id: 'e1', content: 'Expired one', tags: ['test'], expiresAt: past }),
149
+ makeMemory({ id: 'e2', content: 'Expired two', tags: ['test'], expiresAt: past }),
150
+ ];
151
+
152
+ const result = retriever.retrieve(memories, 'test');
153
+ expect(result).toBe('');
154
+ });
155
+
156
+ // === Related memory boosting ===
157
+
158
+ it('should boost related memories', () => {
159
+ const memories = [
160
+ makeMemory({
161
+ id: 'primary',
162
+ content: 'Loves TypeScript',
163
+ tags: ['typescript'],
164
+ category: 'preference',
165
+ importance: 0.9,
166
+ relatedMemories: ['related'],
167
+ }),
168
+ makeMemory({
169
+ id: 'related',
170
+ content: 'Uses TypeScript at work',
171
+ tags: ['work', 'coding'],
172
+ category: 'fact',
173
+ importance: 0.3,
174
+ relatedMemories: ['primary'],
175
+ }),
176
+ makeMemory({
177
+ id: 'unrelated',
178
+ content: 'Likes hiking on weekends',
179
+ tags: ['hiking', 'weekends'],
180
+ category: 'preference',
181
+ importance: 0.3,
182
+ }),
183
+ ];
184
+
185
+ const result = retriever.retrieve(memories, 'typescript');
186
+ // The related memory should appear because it gets boosted by the primary match
187
+ expect(result).toContain('TypeScript at work');
188
+ });
189
+
190
+ // === Confidence scoring ===
191
+
192
+ it('should show high confidence annotation for facts', () => {
193
+ const memories = [
194
+ makeMemory({
195
+ id: 'hc',
196
+ content: 'Works at Acme Corp',
197
+ tags: ['acme', 'work'],
198
+ category: 'fact',
199
+ confidence: 0.95,
200
+ }),
201
+ ];
202
+
203
+ const result = retriever.retrieve(memories, 'acme work');
204
+ expect(result).toContain('high confidence');
205
+ });
206
+
207
+ // === Relationship bonus ===
208
+
209
+ it('should give bonus score to relationship memories', () => {
210
+ const thirtyDaysAgo = Date.now() - 29 * 24 * 60 * 60 * 1000;
211
+ const memories = [
212
+ makeMemory({
213
+ id: 'rel',
214
+ content: 'Shared a joke about recursion',
215
+ tags: ['joke'],
216
+ category: 'relationship',
217
+ importance: 0.5,
218
+ confidence: 0.5,
219
+ updatedAt: thirtyDaysAgo,
220
+ }),
221
+ makeMemory({
222
+ id: 'ctx',
223
+ content: 'Was in a meeting',
224
+ tags: ['meeting'],
225
+ category: 'context',
226
+ importance: 0.5,
227
+ confidence: 0.5,
228
+ updatedAt: thirtyDaysAgo,
229
+ }),
230
+ ];
231
+
232
+ // Both have same importance, confidence, recency but relationship gets a 0.05 bonus
233
+ const result = retriever.retrieve(memories, 'something unrelated');
234
+ // Relationship memory should still appear due to the bonus
235
+ if (result.length > 0) {
236
+ // If anything shows, relationship should be present
237
+ expect(result).toContain('recursion');
238
+ }
239
+ });
240
+ });
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SentimentAnalyzer } from '../src/sentiment.js';
3
+ import { PatternDetector } from '../src/pattern-detector.js';
4
+ import type { SentimentSnapshot } from '../src/types.js';
5
+
6
+ describe('SentimentAnalyzer', () => {
7
+ const analyzer = new SentimentAnalyzer();
8
+
9
+ describe('positive sentiment', () => {
10
+ it('should detect positive sentiment from positive words', () => {
11
+ const result = analyzer.analyzeSentiment('This is great and amazing!');
12
+ expect(result.sentiment).toBe('positive');
13
+ expect(result.confidence).toBeGreaterThan(0.4);
14
+ expect(result.keywords).toContain('great');
15
+ expect(result.keywords).toContain('amazing');
16
+ });
17
+
18
+ it('should detect positive sentiment from gratitude', () => {
19
+ const result = analyzer.analyzeSentiment('Thanks so much, this is really helpful!');
20
+ expect(result.sentiment).toBe('positive');
21
+ expect(result.keywords).toContain('thanks');
22
+ expect(result.keywords).toContain('helpful');
23
+ });
24
+
25
+ it('should detect positive sentiment from excitement', () => {
26
+ const result = analyzer.analyzeSentiment('I love it, this is perfect and beautiful!');
27
+ expect(result.sentiment).toBe('positive');
28
+ expect(result.confidence).toBeGreaterThan(0.5);
29
+ });
30
+
31
+ it('should boost positive sentiment with emojis', () => {
32
+ const withoutEmoji = analyzer.analyzeSentiment('This is good');
33
+ const withEmoji = analyzer.analyzeSentiment('This is good 😊👍');
34
+ expect(withEmoji.confidence).toBeGreaterThanOrEqual(withoutEmoji.confidence);
35
+ });
36
+ });
37
+
38
+ describe('negative sentiment', () => {
39
+ it('should detect negative sentiment from negative words', () => {
40
+ const result = analyzer.analyzeSentiment('This is terrible and broken');
41
+ expect(result.sentiment).toBe('negative');
42
+ expect(result.confidence).toBeGreaterThan(0.4);
43
+ expect(result.keywords).toContain('terrible');
44
+ expect(result.keywords).toContain('broken');
45
+ });
46
+
47
+ it('should detect negative sentiment from frustration', () => {
48
+ const result = analyzer.analyzeSentiment('Ugh, this is so annoying and frustrating');
49
+ expect(result.sentiment).toBe('negative');
50
+ });
51
+
52
+ it('should detect negative sentiment from error-related words', () => {
53
+ const result = analyzer.analyzeSentiment('The error keeps happening, the bug is still there, it failed again');
54
+ expect(result.sentiment).toBe('negative');
55
+ expect(result.keywords.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ it('should boost negative sentiment with negative emojis', () => {
59
+ const result = analyzer.analyzeSentiment('This is bad 😢😡');
60
+ expect(result.sentiment).toBe('negative');
61
+ });
62
+ });
63
+
64
+ describe('neutral sentiment', () => {
65
+ it('should return neutral for factual statements', () => {
66
+ const result = analyzer.analyzeSentiment('The function takes two parameters and returns a string');
67
+ expect(result.sentiment).toBe('neutral');
68
+ });
69
+
70
+ it('should return neutral for questions without sentiment', () => {
71
+ const result = analyzer.analyzeSentiment('How do I configure the database connection?');
72
+ expect(result.sentiment).toBe('neutral');
73
+ });
74
+
75
+ it('should return neutral for balanced sentiment', () => {
76
+ const result = analyzer.analyzeSentiment('It has a good side but also a bad side');
77
+ expect(result.sentiment).toBe('neutral');
78
+ });
79
+ });
80
+
81
+ describe('negation handling', () => {
82
+ it('should handle "not good" as less positive', () => {
83
+ const good = analyzer.analyzeSentiment('This is good');
84
+ const notGood = analyzer.analyzeSentiment('This is not good');
85
+ // "not good" should be less positive or neutral/negative
86
+ expect(notGood.sentiment !== 'positive' || notGood.confidence < good.confidence).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe('confidence levels', () => {
91
+ it('should have higher confidence with more signal words', () => {
92
+ const weak = analyzer.analyzeSentiment('This is good');
93
+ const strong = analyzer.analyzeSentiment('This is good, great, amazing, excellent, and perfect!');
94
+ expect(strong.confidence).toBeGreaterThanOrEqual(weak.confidence);
95
+ });
96
+
97
+ it('should return reasonable confidence for neutral text', () => {
98
+ const result = analyzer.analyzeSentiment('Set the variable to 42');
99
+ expect(result.confidence).toBeGreaterThan(0);
100
+ expect(result.confidence).toBeLessThanOrEqual(1);
101
+ });
102
+ });
103
+
104
+ describe('keyword extraction', () => {
105
+ it('should return unique keywords', () => {
106
+ const result = analyzer.analyzeSentiment('good good good');
107
+ const uniqueCount = new Set(result.keywords).size;
108
+ expect(result.keywords.length).toBe(uniqueCount);
109
+ });
110
+
111
+ it('should return empty keywords for neutral text', () => {
112
+ const result = analyzer.analyzeSentiment('Define a variable');
113
+ expect(result.keywords).toEqual([]);
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('PatternDetector mood tracking', () => {
119
+ it('should record and retrieve sentiment history', () => {
120
+ const detector = new PatternDetector();
121
+
122
+ const snapshot: SentimentSnapshot = {
123
+ sentiment: 'positive',
124
+ confidence: 0.8,
125
+ timestamp: Date.now(),
126
+ hour: 10,
127
+ dayOfWeek: 1,
128
+ };
129
+
130
+ detector.recordSentiment(snapshot);
131
+ const history = detector.getSentimentHistory();
132
+ expect(history).toHaveLength(1);
133
+ expect(history[0].sentiment).toBe('positive');
134
+ });
135
+
136
+ it('should detect mood patterns by time of day', () => {
137
+ const detector = new PatternDetector();
138
+
139
+ // Add 5 positive morning snapshots
140
+ for (let i = 0; i < 5; i++) {
141
+ detector.recordSentiment({
142
+ sentiment: 'positive',
143
+ confidence: 0.7,
144
+ timestamp: Date.now() - i * 86400000,
145
+ hour: 9,
146
+ dayOfWeek: (i + 1) % 7,
147
+ });
148
+ }
149
+
150
+ const signals = detector.detectMoodByTime();
151
+ const morningSignal = signals.find(s => s.pattern.includes('morning'));
152
+ expect(morningSignal).toBeDefined();
153
+ expect(morningSignal!.pattern).toContain('positive');
154
+ });
155
+
156
+ it('should detect mood patterns by day of week', () => {
157
+ const detector = new PatternDetector();
158
+
159
+ // Add 4 negative Monday snapshots
160
+ for (let i = 0; i < 4; i++) {
161
+ detector.recordSentiment({
162
+ sentiment: 'negative',
163
+ confidence: 0.8,
164
+ timestamp: Date.now() - i * 604800000,
165
+ hour: 14,
166
+ dayOfWeek: 1, // Monday
167
+ });
168
+ }
169
+
170
+ const signals = detector.detectMoodByTime();
171
+ const mondaySignal = signals.find(s => s.pattern.includes('Monday'));
172
+ expect(mondaySignal).toBeDefined();
173
+ expect(mondaySignal!.pattern).toContain('negative');
174
+ });
175
+
176
+ it('should not detect patterns with insufficient data', () => {
177
+ const detector = new PatternDetector();
178
+
179
+ detector.recordSentiment({
180
+ sentiment: 'positive',
181
+ confidence: 0.8,
182
+ timestamp: Date.now(),
183
+ hour: 10,
184
+ dayOfWeek: 1,
185
+ });
186
+
187
+ const signals = detector.detectMoodByTime();
188
+ expect(signals).toHaveLength(0);
189
+ });
190
+
191
+ it('should limit history to 200 snapshots', () => {
192
+ const detector = new PatternDetector();
193
+
194
+ for (let i = 0; i < 250; i++) {
195
+ detector.recordSentiment({
196
+ sentiment: 'neutral',
197
+ confidence: 0.5,
198
+ timestamp: Date.now() - i * 1000,
199
+ hour: i % 24,
200
+ dayOfWeek: i % 7,
201
+ });
202
+ }
203
+
204
+ const history = detector.getSentimentHistory();
205
+ expect(history.length).toBeLessThanOrEqual(200);
206
+ });
207
+ });