cozo-memory 1.2.6 → 1.2.10

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.
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const logger_1 = require("./logger");
4
+ describe("Logger", () => {
5
+ let consoleErrorSpy;
6
+ let consoleWarnSpy;
7
+ beforeEach(() => {
8
+ consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
9
+ consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { });
10
+ logger_1.logger.setLevel(logger_1.LogLevel.INFO);
11
+ });
12
+ afterEach(() => {
13
+ consoleErrorSpy.mockRestore();
14
+ consoleWarnSpy.mockRestore();
15
+ });
16
+ describe("Log Level Filtering", () => {
17
+ it("should log ERROR messages", () => {
18
+ logger_1.logger.error("TestComponent", "Error message");
19
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[TestComponent] ERROR:"), "Error message");
20
+ });
21
+ it("should log WARN messages", () => {
22
+ logger_1.logger.warn("TestComponent", "Warn message");
23
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("[TestComponent] WARN:"), "Warn message");
24
+ });
25
+ it("should log INFO messages", () => {
26
+ logger_1.logger.info("TestComponent", "Info message");
27
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[TestComponent] INFO:"), "Info message");
28
+ });
29
+ it("should NOT log DEBUG messages at INFO level", () => {
30
+ logger_1.logger.debug("TestComponent", "Debug message");
31
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
32
+ });
33
+ it("should NOT log TRACE messages at INFO level", () => {
34
+ logger_1.logger.trace("TestComponent", "Trace message");
35
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
36
+ });
37
+ });
38
+ describe("Level Changes", () => {
39
+ it("should log DEBUG after setting DEBUG level", () => {
40
+ logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
41
+ logger_1.logger.debug("TestComponent", "Debug message");
42
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[TestComponent] DEBUG:"), "Debug message");
43
+ });
44
+ it("should log TRACE after setting TRACE level", () => {
45
+ logger_1.logger.setLevel(logger_1.LogLevel.TRACE);
46
+ logger_1.logger.trace("TestComponent", "Trace message");
47
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[TestComponent] TRACE:"), "Trace message");
48
+ });
49
+ it("should NOT log INFO after setting ERROR level", () => {
50
+ logger_1.logger.setLevel(logger_1.LogLevel.ERROR);
51
+ logger_1.logger.info("TestComponent", "Info message");
52
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
53
+ });
54
+ it("should NOT log WARN after setting ERROR level", () => {
55
+ logger_1.logger.setLevel(logger_1.LogLevel.ERROR);
56
+ logger_1.logger.warn("TestComponent", "Warn message");
57
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
58
+ });
59
+ });
60
+ describe("Message Formatting", () => {
61
+ it("should include prefix and component in output", () => {
62
+ logger_1.logger.info("MyComp", "Hello World");
63
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[CozoDB]"), "Hello World");
64
+ });
65
+ it("should pass additional arguments", () => {
66
+ const error = new Error("Test error");
67
+ logger_1.logger.error("ErrComp", "Something failed", error);
68
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("[ErrComp] ERROR:"), "Something failed", error);
69
+ });
70
+ it("should pass multiple extra arguments", () => {
71
+ logger_1.logger.warn("WarnComp", "Warning", { code: 42 }, "context");
72
+ expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("[WarnComp] WARN:"), "Warning", { code: 42 }, "context");
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // ESM-Module und Dependencies mocken, die nicht mit ts-jest/commonjs kompatibel sind
4
+ let uuidCounter = 0;
5
+ jest.mock("uuid", () => ({
6
+ v4: jest.fn(() => `mocked-uuid-${++uuidCounter}`),
7
+ validate: jest.fn(() => true),
8
+ }));
9
+ jest.mock("pdfjs-dist/legacy/build/pdf.mjs", () => ({
10
+ getDocument: jest.fn(() => ({
11
+ promise: Promise.resolve({ numPages: 0 }),
12
+ })),
13
+ }));
14
+ jest.mock("onnxruntime-node", () => ({}), { virtual: true });
15
+ jest.mock("./performance-monitor", () => ({
16
+ perfMonitor: {
17
+ start: jest.fn(),
18
+ end: jest.fn(),
19
+ },
20
+ }));
21
+ // Logger mocken (damit loggt er nicht in die Tast-Ausgabe)
22
+ jest.mock("./logger", () => ({
23
+ logger: {
24
+ error: jest.fn(),
25
+ warn: jest.fn(),
26
+ info: jest.fn(),
27
+ debug: jest.fn(),
28
+ trace: jest.fn(),
29
+ },
30
+ }));
31
+ const memory_service_1 = require("./memory-service");
32
+ const db_service_1 = require("./db-service");
33
+ const embedding_service_1 = require("./embedding-service");
34
+ // EmbeddingService mocken – gibt immer gleichbleibende Dummy-Vektoren zurück
35
+ jest.mock("./embedding-service", () => {
36
+ return {
37
+ EmbeddingService: jest.fn().mockImplementation(() => ({
38
+ embed: jest.fn().mockImplementation(async (text) => {
39
+ // Deterministic embedding based on text length
40
+ const dim = 4;
41
+ const base = text.length % 10;
42
+ return [base * 0.1, (base + 1) * 0.1, (base + 2) * 0.1, (base + 3) * 0.1];
43
+ }),
44
+ embedBatch: jest.fn().mockImplementation(async (texts) => {
45
+ return texts.map((t) => {
46
+ const base = t.length % 10;
47
+ return [base * 0.1, (base + 1) * 0.1, (base + 2) * 0.1, (base + 3) * 0.1];
48
+ });
49
+ }),
50
+ getDimensions: jest.fn().mockReturnValue(4),
51
+ getCacheStats: jest.fn().mockReturnValue({ size: 0, maxSize: 1000, model: "test", backend: "mock", dimensions: 4 }),
52
+ clearCache: jest.fn(),
53
+ })),
54
+ };
55
+ });
56
+ describe("MemoryService", () => {
57
+ let db;
58
+ let embeddings;
59
+ let memory;
60
+ beforeEach(async () => {
61
+ uuidCounter = 0;
62
+ db = new db_service_1.DatabaseService(":memory:", "sqlite");
63
+ await db.initialize();
64
+ embeddings = new embedding_service_1.EmbeddingService();
65
+ // MemoryService braucht EmbeddingService für createEntity & search
66
+ memory = new memory_service_1.MemoryService(db, embeddings);
67
+ });
68
+ // ── Entity Operations ────────────────────────────────────────
69
+ describe("Entity Operations", () => {
70
+ it("should create an entity", async () => {
71
+ const entity = await memory.createEntity("Test Person", "Person", { role: "developer" });
72
+ expect(entity.id).toBeDefined();
73
+ expect(entity.name).toBe("Test Person");
74
+ expect(entity.type).toBe("Person");
75
+ expect(entity.metadata).toEqual({ role: "developer" });
76
+ expect(entity.embedding).toHaveLength(4);
77
+ expect(entity.created_at).toBeGreaterThan(0);
78
+ });
79
+ it("should get a created entity by id", async () => {
80
+ const created = await memory.createEntity("Alice", "Person");
81
+ const fetched = await memory.getEntity(created.id);
82
+ expect(fetched).not.toBeNull();
83
+ expect(fetched.name).toBe("Alice");
84
+ });
85
+ it("should return null for non-existent entity", async () => {
86
+ const result = await memory.getEntity("non-existent-id");
87
+ expect(result).toBeNull();
88
+ });
89
+ it("should update an existing entity", async () => {
90
+ const entity = await memory.createEntity("Old Name", "Person");
91
+ await memory.updateEntity(entity.id, { name: "New Name" });
92
+ const updated = await memory.getEntity(entity.id);
93
+ expect(updated.name).toBe("New Name");
94
+ });
95
+ it("should delete an entity", async () => {
96
+ const entity = await memory.createEntity("To Delete", "Temp");
97
+ await memory.deleteEntity(entity.id);
98
+ const result = await memory.getEntity(entity.id);
99
+ expect(result).toBeNull();
100
+ });
101
+ });
102
+ // ── Observation Operations ───────────────────────────────────
103
+ describe("Observation Operations", () => {
104
+ let entityId;
105
+ beforeEach(async () => {
106
+ const entity = await memory.createEntity("Observed Entity", "Test");
107
+ entityId = entity.id;
108
+ });
109
+ it("should add an observation to an entity", async () => {
110
+ const obs = await memory.addObservation(entityId, "This is an important observation");
111
+ expect(obs.id).toBeDefined();
112
+ expect(obs.entity_id).toBe(entityId);
113
+ expect(obs.text).toBe("This is an important observation");
114
+ expect(obs.embedding).toHaveLength(4);
115
+ });
116
+ it("should list observations for an entity", async () => {
117
+ await memory.addObservation(entityId, "Observation 1");
118
+ await memory.addObservation(entityId, "Observation 2");
119
+ await memory.addObservation(entityId, "Observation 3");
120
+ const obs = await memory.getObservations(entityId);
121
+ expect(obs).toHaveLength(3);
122
+ });
123
+ it("should return empty array for entity with no observations", async () => {
124
+ const obs = await memory.getObservations(entityId);
125
+ expect(obs).toHaveLength(0);
126
+ });
127
+ });
128
+ // ── Relation Operations ──────────────────────────────────────
129
+ describe("Relation Operations", () => {
130
+ let idA;
131
+ let idB;
132
+ beforeEach(async () => {
133
+ const a = await memory.createEntity("Entity A", "TypeA");
134
+ const b = await memory.createEntity("Entity B", "TypeB");
135
+ idA = a.id;
136
+ idB = b.id;
137
+ });
138
+ it("should create a relation between entities", async () => {
139
+ const rel = await memory.createRelation(idA, idB, "knows", 0.8);
140
+ expect(rel.from_id).toBe(idA);
141
+ expect(rel.to_id).toBe(idB);
142
+ expect(rel.relation_type).toBe("knows");
143
+ expect(rel.strength).toBe(0.8);
144
+ });
145
+ it("should reject self-references", async () => {
146
+ await expect(memory.createRelation(idA, idA, "self", 1.0)).rejects.toThrow("Self-references are not allowed");
147
+ });
148
+ it("should list relations from an entity", async () => {
149
+ await memory.createRelation(idA, idB, "knows");
150
+ const rels = await memory.getRelations(idA);
151
+ expect(rels).toHaveLength(1);
152
+ expect(rels[0].to_id).toBe(idB);
153
+ });
154
+ });
155
+ // ── Search ───────────────────────────────────────────────────
156
+ describe("Search", () => {
157
+ beforeEach(async () => {
158
+ await memory.createEntity("Python Programming", "Topic", { description: "A programming language" });
159
+ await memory.createEntity("JavaScript Programming", "Topic", { description: "Another programming language" });
160
+ await memory.createEntity("Cooking Recipes", "Topic", { description: "Food and recipes" });
161
+ });
162
+ it("should search and return results", async () => {
163
+ const results = await memory.search("programming", 10);
164
+ expect(results.length).toBeGreaterThan(0);
165
+ });
166
+ it("should filter results by entity type", async () => {
167
+ // Create a non-Topic entity
168
+ await memory.createEntity("Random Note", "Note", {});
169
+ const results = await memory.search("programming", 10, ["Topic"]);
170
+ expect(results.length).toBeGreaterThan(0);
171
+ results.forEach(r => {
172
+ expect(r.entity.type).toBe("Topic");
173
+ });
174
+ });
175
+ it("should return empty array for non-matching query", async () => {
176
+ // Der gemockte EmbeddingService basiert auf Textlänge – daher können
177
+ // auch "nicht passende" Queries matches liefern. Das testen wir hier nicht.
178
+ // Stattdessen: search wirft keinen Fehler
179
+ await expect(memory.search("xyznonexistent", 10)).resolves.toBeDefined();
180
+ });
181
+ it("should respect the limit parameter", async () => {
182
+ const results = await memory.search("programming", 1);
183
+ expect(results.length).toBeLessThanOrEqual(1);
184
+ });
185
+ });
186
+ // ── Context ──────────────────────────────────────────────────
187
+ describe("Context", () => {
188
+ it("should return context for a query", async () => {
189
+ const entity = await memory.createEntity("Test Context", "Test");
190
+ await memory.addObservation(entity.id, "Context observation");
191
+ const context = await memory.getContext("Test", 10);
192
+ expect(context).toHaveProperty("query");
193
+ expect(context).toHaveProperty("entities");
194
+ expect(context).toHaveProperty("total_entities");
195
+ expect(context.total_entities).toBeGreaterThan(0);
196
+ expect(context.entities[0]).toHaveProperty("observations");
197
+ expect(context.entities[0]).toHaveProperty("relations");
198
+ });
199
+ });
200
+ // ── Health ────────────────────────────────────────────────────
201
+ describe("Health", () => {
202
+ it("should return healthy status", async () => {
203
+ const health = await memory.health();
204
+ expect(health.status).toBe("healthy");
205
+ expect(health).toHaveProperty("database");
206
+ expect(health).toHaveProperty("cache");
207
+ expect(health).toHaveProperty("timestamp");
208
+ });
209
+ it("should include correct db stats", async () => {
210
+ await memory.createEntity("Health Check Entity", "Test");
211
+ const health = await memory.health();
212
+ expect(health.database.entities).toBe(1);
213
+ });
214
+ });
215
+ // ── Snapshots ────────────────────────────────────────────────
216
+ describe("Snapshots", () => {
217
+ it("should create a snapshot", async () => {
218
+ const snapshotId = await memory.createSnapshot({ reason: "test" });
219
+ expect(snapshotId).toBeDefined();
220
+ });
221
+ });
222
+ });
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const timestamp_utils_1 = require("./timestamp-utils");
4
+ describe("toDualTimestamp", () => {
5
+ it("should convert microseconds to DualTimestamp", () => {
6
+ const result = (0, timestamp_utils_1.toDualTimestamp)(1_700_000_000_000_000);
7
+ expect(result.timestamp).toBe(1_700_000_000_000_000);
8
+ expect(result.iso).toBe("2023-11-14T22:13:20.000Z");
9
+ });
10
+ it("should round milliseconds floor", () => {
11
+ const result = (0, timestamp_utils_1.toDualTimestamp)(1_700_000_000_000_999);
12
+ expect(result.iso.endsWith(".000Z")).toBe(true);
13
+ });
14
+ it("should handle zero timestamp", () => {
15
+ const result = (0, timestamp_utils_1.toDualTimestamp)(0);
16
+ expect(result.timestamp).toBe(0);
17
+ expect(result.iso).toBe("1970-01-01T00:00:00.000Z");
18
+ });
19
+ it("should handle large future timestamps", () => {
20
+ const future = 2_100_000_000_000_000; // ~2036
21
+ const result = (0, timestamp_utils_1.toDualTimestamp)(future);
22
+ expect(result.timestamp).toBe(future);
23
+ expect(result.iso).toContain("2036");
24
+ });
25
+ });
26
+ describe("nowDual", () => {
27
+ it("should return current time with both formats", () => {
28
+ const before = Date.now() * 1000;
29
+ const result = (0, timestamp_utils_1.nowDual)();
30
+ const after = Date.now() * 1000;
31
+ expect(result.timestamp).toBeGreaterThanOrEqual(before);
32
+ expect(result.timestamp).toBeLessThanOrEqual(after);
33
+ expect(result.iso).toBeDefined();
34
+ expect(result.iso).toContain("T");
35
+ expect(result.iso.endsWith("Z")).toBe(true);
36
+ });
37
+ it("should be consistent between timestamp and iso", () => {
38
+ const result = (0, timestamp_utils_1.nowDual)();
39
+ // Convert ISO back and compare
40
+ const reconstructed = new Date(result.iso).getTime() * 1000;
41
+ // Allow small delta due to execution time
42
+ expect(Math.abs(reconstructed - result.timestamp)).toBeLessThan(100_000); // <100ms
43
+ });
44
+ });
45
+ describe("parseToDual", () => {
46
+ it("should parse a number as microseconds", () => {
47
+ const result = (0, timestamp_utils_1.parseToDual)(1_700_000_000_000_000);
48
+ expect(result.timestamp).toBe(1_700_000_000_000_000);
49
+ expect(result.iso).toBe("2023-11-14T22:13:20.000Z");
50
+ });
51
+ it("should parse an ISO string", () => {
52
+ const input = "2024-01-15T10:30:00.000Z";
53
+ const result = (0, timestamp_utils_1.parseToDual)(input);
54
+ // 2024-01-15T10:30:00.000Z = 1705314600000 ms = 1705314600000000 µs
55
+ expect(result.timestamp).toBe(1_705_314_600_000_000);
56
+ expect(result.iso).toBe(input);
57
+ });
58
+ it("should parse ISO string without Z suffix", () => {
59
+ const input = "2024-06-01T12:00:00.000";
60
+ const result = (0, timestamp_utils_1.parseToDual)(input);
61
+ expect(result.timestamp).toBeDefined();
62
+ expect(result.iso).toContain("2024-06-01");
63
+ });
64
+ it("should throw for invalid date string", () => {
65
+ // parseToDual ruft date.toISOString() auf, das für Invalid Date einen RangeError wirft
66
+ expect(() => (0, timestamp_utils_1.parseToDual)("not-a-date")).toThrow(RangeError);
67
+ });
68
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozo-memory",
3
- "version": "1.2.6",
3
+ "version": "1.2.10",
4
4
  "mcpName": "io.github.tobs-code/cozo-memory",
5
5
  "description": "Local-first persistent memory system for AI agents with hybrid search, graph reasoning, and MCP integration",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,10 @@
35
35
  "dev:tui": "ts-node src/tui-launcher.ts",
36
36
  "clean": "rm -rf dist",
37
37
  "rebuild": "npm run clean && npm run build",
38
- "test": "echo \"Error: no test specified\" && exit 1",
38
+ "test": "jest",
39
+ "test:watch": "jest --watch",
40
+ "test:coverage": "jest --coverage",
41
+ "test:verbose": "jest --verbose",
39
42
  "benchmark": "ts-node src/benchmark.ts",
40
43
  "eval": "ts-node src/eval-suite.ts",
41
44
  "download-model": "ts-node src/download-model.ts",
@@ -97,4 +100,4 @@
97
100
  "tsx": "^4.21.0",
98
101
  "typescript": "^5.9.3"
99
102
  }
100
- }
103
+ }