@vheins/local-memory-mcp 0.1.4 → 0.1.5
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/dist/dashboard/dashboard.test.js +362 -0
- package/dist/mcp/client.test.js +130 -0
- package/dist/resources/index.test.js +96 -0
- package/dist/router.test.js +113 -0
- package/dist/storage/sqlite.test.js +358 -0
- package/dist/tools/memory.search.test.js +181 -0
- package/dist/utils/logger.test.js +84 -0
- package/dist/utils/normalize.test.js +159 -0
- package/dist/utils/query-expander.test.js +35 -0
- package/package.json +8 -2
- package/.kiro/specs/memory-mcp-optimization/.config.kiro +0 -1
- package/.vscode/tasks.json +0 -27
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Feature: memory-mcp-optimization
|
|
2
|
+
// Property 20: StructuredLogger output adalah JSON valid dengan field wajib
|
|
3
|
+
// Validates: Requirements 21.1, 21.2
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import * as fc from "fast-check";
|
|
6
|
+
describe("Property 20: StructuredLogger output adalah JSON valid dengan field wajib", () => {
|
|
7
|
+
let stderrOutput = [];
|
|
8
|
+
let stderrSpy;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
stderrOutput = [];
|
|
11
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
12
|
+
stderrOutput.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
// Reset LOG_LEVEL to ensure all levels are logged
|
|
16
|
+
process.env.LOG_LEVEL = "debug";
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
stderrSpy.mockRestore();
|
|
20
|
+
delete process.env.LOG_LEVEL;
|
|
21
|
+
});
|
|
22
|
+
it("each log call produces valid JSON output with required fields", async () => {
|
|
23
|
+
// Re-import logger after setting LOG_LEVEL
|
|
24
|
+
const { logger } = await import("./logger.js?t=" + Date.now());
|
|
25
|
+
fc.assert(fc.property(fc.oneof(fc.constant("debug"), fc.constant("info"), fc.constant("warn"), fc.constant("error")), fc.string({ minLength: 1, maxLength: 100 }), (level, message) => {
|
|
26
|
+
stderrOutput = [];
|
|
27
|
+
logger[level](message);
|
|
28
|
+
expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
|
|
29
|
+
const lastOutput = stderrOutput[stderrOutput.length - 1];
|
|
30
|
+
// Must be parseable JSON
|
|
31
|
+
let parsed;
|
|
32
|
+
expect(() => {
|
|
33
|
+
parsed = JSON.parse(lastOutput.trim());
|
|
34
|
+
}).not.toThrow();
|
|
35
|
+
parsed = JSON.parse(lastOutput.trim());
|
|
36
|
+
// Must have required fields
|
|
37
|
+
expect(parsed).toHaveProperty("level");
|
|
38
|
+
expect(parsed).toHaveProperty("timestamp");
|
|
39
|
+
expect(parsed).toHaveProperty("message");
|
|
40
|
+
// level must match
|
|
41
|
+
expect(parsed.level).toBe(level);
|
|
42
|
+
// message must match
|
|
43
|
+
expect(parsed.message).toBe(message);
|
|
44
|
+
// timestamp must be ISO 8601
|
|
45
|
+
expect(typeof parsed.timestamp).toBe("string");
|
|
46
|
+
const ts = new Date(parsed.timestamp);
|
|
47
|
+
expect(isNaN(ts.getTime())).toBe(false);
|
|
48
|
+
}), { numRuns: 100 });
|
|
49
|
+
});
|
|
50
|
+
it("log output with context includes context field", async () => {
|
|
51
|
+
const { logger } = await import("./logger.js?t=" + Date.now() + "ctx");
|
|
52
|
+
stderrOutput = [];
|
|
53
|
+
logger.info("test message", { key: "value", count: 42 });
|
|
54
|
+
expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
|
|
55
|
+
const lastOutput = stderrOutput[stderrOutput.length - 1];
|
|
56
|
+
const parsed = JSON.parse(lastOutput.trim());
|
|
57
|
+
expect(parsed).toHaveProperty("context");
|
|
58
|
+
expect(parsed.context).toEqual({ key: "value", count: 42 });
|
|
59
|
+
});
|
|
60
|
+
it("log output without context does not include context field", async () => {
|
|
61
|
+
const { logger } = await import("./logger.js?t=" + Date.now() + "noctx");
|
|
62
|
+
stderrOutput = [];
|
|
63
|
+
logger.info("no context message");
|
|
64
|
+
expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
|
|
65
|
+
const lastOutput = stderrOutput[stderrOutput.length - 1];
|
|
66
|
+
const parsed = JSON.parse(lastOutput.trim());
|
|
67
|
+
expect(parsed).not.toHaveProperty("context");
|
|
68
|
+
});
|
|
69
|
+
it("all four log levels produce valid JSON with correct level field", async () => {
|
|
70
|
+
const { logger } = await import("./logger.js?t=" + Date.now() + "levels");
|
|
71
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
72
|
+
for (const level of levels) {
|
|
73
|
+
stderrOutput = [];
|
|
74
|
+
logger[level](`test ${level} message`);
|
|
75
|
+
expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
|
|
76
|
+
const lastOutput = stderrOutput[stderrOutput.length - 1];
|
|
77
|
+
const parsed = JSON.parse(lastOutput.trim());
|
|
78
|
+
expect(parsed.level).toBe(level);
|
|
79
|
+
expect(parsed.message).toBe(`test ${level} message`);
|
|
80
|
+
expect(typeof parsed.timestamp).toBe("string");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=logger.test.js.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Feature: memory-mcp-optimization
|
|
2
|
+
// Unit + Property tests for normalize(), tokenize(), and STOPWORDS
|
|
3
|
+
// Requirements: 5.4, 5.5, 17.3, 17.4, 17.7
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import * as fc from "fast-check";
|
|
6
|
+
import { normalize, tokenize, STOPWORDS } from "./normalize.js";
|
|
7
|
+
// ─── Unit Tests: normalize() ─────────────────────────────────────────────────
|
|
8
|
+
describe("normalize() — unit tests", () => {
|
|
9
|
+
it("converts text to lowercase", () => {
|
|
10
|
+
expect(normalize("Hello World")).toBe("hello world");
|
|
11
|
+
expect(normalize("TYPESCRIPT")).toBe("typescript");
|
|
12
|
+
expect(normalize("CamelCase")).toBe("camelcase");
|
|
13
|
+
});
|
|
14
|
+
it("trims leading and trailing whitespace", () => {
|
|
15
|
+
expect(normalize(" hello ")).toBe("hello");
|
|
16
|
+
expect(normalize("\t text \n")).toBe("text");
|
|
17
|
+
});
|
|
18
|
+
it("replaces special characters with spaces", () => {
|
|
19
|
+
const result = normalize("hello!world@test#value");
|
|
20
|
+
// Special chars replaced by spaces, then collapsed
|
|
21
|
+
expect(result).not.toContain("!");
|
|
22
|
+
expect(result).not.toContain("@");
|
|
23
|
+
expect(result).not.toContain("#");
|
|
24
|
+
});
|
|
25
|
+
it("collapses multiple spaces into one", () => {
|
|
26
|
+
expect(normalize("hello world")).toBe("hello world");
|
|
27
|
+
expect(normalize("a b c")).toBe("a b c");
|
|
28
|
+
});
|
|
29
|
+
it("handles empty string", () => {
|
|
30
|
+
expect(normalize("")).toBe("");
|
|
31
|
+
});
|
|
32
|
+
it("handles string with only special characters", () => {
|
|
33
|
+
expect(normalize("!!!@@@###")).toBe("");
|
|
34
|
+
});
|
|
35
|
+
it("preserves alphanumeric characters", () => {
|
|
36
|
+
expect(normalize("abc123")).toBe("abc123");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ─── Unit Tests: tokenize() ──────────────────────────────────────────────────
|
|
40
|
+
describe("tokenize() — unit tests", () => {
|
|
41
|
+
it("removes stopwords from output", () => {
|
|
42
|
+
const tokens = tokenize("the quick brown fox");
|
|
43
|
+
expect(tokens).not.toContain("the");
|
|
44
|
+
expect(tokens).toContain("quick");
|
|
45
|
+
expect(tokens).toContain("brown");
|
|
46
|
+
expect(tokens).toContain("fox");
|
|
47
|
+
});
|
|
48
|
+
it("removes short words (length <= 0 after normalize)", () => {
|
|
49
|
+
// tokenize filters tokens with length > 0 and not in STOPWORDS
|
|
50
|
+
const tokens = tokenize("a b c hello");
|
|
51
|
+
// "a", "b", "c" are stopwords or single chars
|
|
52
|
+
expect(tokens).toContain("hello");
|
|
53
|
+
});
|
|
54
|
+
it("returns empty array for all-stopword input", () => {
|
|
55
|
+
const tokens = tokenize("the and or but");
|
|
56
|
+
expect(tokens).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it("handles empty string", () => {
|
|
59
|
+
expect(tokenize("")).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it("normalizes before tokenizing (lowercase, trim)", () => {
|
|
62
|
+
const tokens1 = tokenize("TypeScript");
|
|
63
|
+
const tokens2 = tokenize("typescript");
|
|
64
|
+
expect(tokens1).toEqual(tokens2);
|
|
65
|
+
});
|
|
66
|
+
it("removes Indonesian stopwords", () => {
|
|
67
|
+
const tokens = tokenize("yang dan di ke dari ini itu coding");
|
|
68
|
+
expect(tokens).not.toContain("yang");
|
|
69
|
+
expect(tokens).not.toContain("dan");
|
|
70
|
+
expect(tokens).toContain("coding");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// ─── Property 4: Tokenisasi konsisten antara SQLiteStore dan StubVectorStore ──
|
|
74
|
+
// Feature: memory-mcp-optimization, Property 4: Tokenisasi konsisten
|
|
75
|
+
// Validates: Requirements 4.4, 5.2, 5.3
|
|
76
|
+
describe("Property 4: Tokenisasi konsisten antara SQLiteStore dan StubVectorStore", () => {
|
|
77
|
+
it("tokenize() is deterministic — same input always produces same output", () => {
|
|
78
|
+
fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 100 }), (text) => {
|
|
79
|
+
// Both SQLiteStore and StubVectorStore use tokenize() from normalize.ts
|
|
80
|
+
// So testing tokenize() determinism validates consistency between them
|
|
81
|
+
const result1 = tokenize(text);
|
|
82
|
+
const result2 = tokenize(text);
|
|
83
|
+
expect(result1).toEqual(result2);
|
|
84
|
+
}), { numRuns: 200 });
|
|
85
|
+
});
|
|
86
|
+
it("tokenize() output is a subset of normalize() tokens", () => {
|
|
87
|
+
fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 100 }), (text) => {
|
|
88
|
+
const normalized = normalize(text);
|
|
89
|
+
const allTokens = normalized.split(" ").filter((t) => t.length > 0);
|
|
90
|
+
const filtered = tokenize(text);
|
|
91
|
+
// Every token in filtered must appear in allTokens
|
|
92
|
+
for (const token of filtered) {
|
|
93
|
+
expect(allTokens).toContain(token);
|
|
94
|
+
}
|
|
95
|
+
}), { numRuns: 200 });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ─── Property 5: normalize() idempoten ganda ─────────────────────────────────
|
|
99
|
+
// Feature: memory-mcp-optimization, Property 5: normalize() idempoten ganda
|
|
100
|
+
// Validates: Requirements 5.4, 5.5
|
|
101
|
+
describe("Property 5: normalize() idempoten ganda", () => {
|
|
102
|
+
it("normalize(normalize(text)) === normalize(text) for 100+ random inputs", () => {
|
|
103
|
+
fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 200 }), (text) => {
|
|
104
|
+
const once = normalize(text);
|
|
105
|
+
const twice = normalize(once);
|
|
106
|
+
expect(twice).toBe(once);
|
|
107
|
+
}), { numRuns: 200 });
|
|
108
|
+
});
|
|
109
|
+
it("normalize is idempotent for specific edge cases", () => {
|
|
110
|
+
const cases = [
|
|
111
|
+
"",
|
|
112
|
+
" ",
|
|
113
|
+
"Hello World!",
|
|
114
|
+
"UPPERCASE",
|
|
115
|
+
"already normalized",
|
|
116
|
+
"multiple spaces",
|
|
117
|
+
"special!@#chars",
|
|
118
|
+
"123numbers456",
|
|
119
|
+
];
|
|
120
|
+
for (const text of cases) {
|
|
121
|
+
const once = normalize(text);
|
|
122
|
+
const twice = normalize(once);
|
|
123
|
+
expect(twice).toBe(once);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ─── Property 21: Stopword list tidak mengandung duplikat ────────────────────
|
|
128
|
+
// Feature: memory-mcp-optimization, Property 21: Stopword no duplicates
|
|
129
|
+
// Validates: Requirement 17.4
|
|
130
|
+
describe("Property 21: Stopword list tidak mengandung duplikat", () => {
|
|
131
|
+
it("STOPWORDS has no duplicate entries", () => {
|
|
132
|
+
// Since STOPWORDS is a Set, duplicates are automatically removed.
|
|
133
|
+
// We verify the source array (before Set construction) has no duplicates
|
|
134
|
+
// by checking that Set size equals the number of unique entries.
|
|
135
|
+
// The Set itself guarantees uniqueness, so we verify the exported Set is consistent.
|
|
136
|
+
const stopwordsArray = Array.from(STOPWORDS);
|
|
137
|
+
const uniqueSet = new Set(stopwordsArray);
|
|
138
|
+
expect(uniqueSet.size).toBe(stopwordsArray.length);
|
|
139
|
+
});
|
|
140
|
+
it("all STOPWORDS entries are lowercase strings", () => {
|
|
141
|
+
for (const word of STOPWORDS) {
|
|
142
|
+
expect(typeof word).toBe("string");
|
|
143
|
+
expect(word).toBe(word.toLowerCase());
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
it("STOPWORDS is non-empty", () => {
|
|
147
|
+
expect(STOPWORDS.size).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
it("property: no string appears more than once in STOPWORDS", () => {
|
|
150
|
+
// This is a structural property — verified once (not random inputs needed)
|
|
151
|
+
// since STOPWORDS is a constant Set
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (const word of STOPWORDS) {
|
|
154
|
+
expect(seen.has(word)).toBe(false);
|
|
155
|
+
seen.add(word);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
//# sourceMappingURL=normalize.test.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { expandQuery } from "../utils/query-expander.js";
|
|
3
|
+
describe("expandQuery", () => {
|
|
4
|
+
it("returns original query when no prompt provided", () => {
|
|
5
|
+
expect(expandQuery("database")).toBe("database");
|
|
6
|
+
expect(expandQuery("api endpoint")).toBe("api endpoint");
|
|
7
|
+
});
|
|
8
|
+
it("expands query with prompt keywords", () => {
|
|
9
|
+
const result = expandQuery("database", "user authentication implementation");
|
|
10
|
+
expect(result).toContain("database");
|
|
11
|
+
expect(result).toContain("user");
|
|
12
|
+
expect(result).toContain("auth");
|
|
13
|
+
});
|
|
14
|
+
it("expands known keywords", () => {
|
|
15
|
+
const result = expandQuery("auth", "login system");
|
|
16
|
+
expect(result).toContain("auth");
|
|
17
|
+
expect(result).toContain("login");
|
|
18
|
+
expect(result).toContain("password");
|
|
19
|
+
});
|
|
20
|
+
it("limits to 10 keywords", () => {
|
|
21
|
+
const result = expandQuery("api", "building a rest endpoint with controller and route handling");
|
|
22
|
+
const words = result.split(" ");
|
|
23
|
+
expect(words.length).toBeLessThanOrEqual(10);
|
|
24
|
+
});
|
|
25
|
+
it("removes duplicates", () => {
|
|
26
|
+
const result = expandQuery("database", "database query optimization");
|
|
27
|
+
const words = result.split(" ");
|
|
28
|
+
const unique = new Set(words);
|
|
29
|
+
expect(words.length).toBe(unique.size);
|
|
30
|
+
});
|
|
31
|
+
it("handles empty prompt gracefully", () => {
|
|
32
|
+
expect(expandQuery("test", "")).toBe("test");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=query-expander.test.js.map
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vheins/local-memory-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "MCP Local Memory Service for coding copilot agents",
|
|
5
|
+
"mcpName": "io.github.vheins/local-memory-mcp",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/server.js",
|
|
7
8
|
"bin": {
|
|
8
9
|
"local-memory-mcp": "dist/server.js",
|
|
9
10
|
"mcp-memory-dashboard": "dist/dashboard/server.js"
|
|
10
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
11
15
|
"author": "Muhammad Rheza Alfin <m.rheza.alfin@gmail.com>",
|
|
12
16
|
"license": "MIT",
|
|
13
17
|
"scripts": {
|
|
14
|
-
"build": "tsc && mkdir -p dist/dashboard/public && rsync -a --delete src/dashboard/public/ dist/dashboard/public/",
|
|
18
|
+
"build": "tsc && mkdir -p dist/dashboard/public && rsync -a --delete src/dashboard/public/ dist/dashboard/public/ && shx chmod +x dist/server.js dist/dashboard/server.js",
|
|
19
|
+
"prepare": "npm run build",
|
|
15
20
|
"dev": "tsc --watch",
|
|
16
21
|
"start": "node dist/server.js",
|
|
17
22
|
"dashboard": "node dist/dashboard/server.js",
|
|
@@ -31,6 +36,7 @@
|
|
|
31
36
|
"@types/express": "^5.0.6",
|
|
32
37
|
"@types/node": "^22.19.7",
|
|
33
38
|
"fast-check": "^4.6.0",
|
|
39
|
+
"shx": "^0.4.0",
|
|
34
40
|
"typescript": "^5.7.3",
|
|
35
41
|
"vitest": "^4.1.1"
|
|
36
42
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"specId": "a9aa2a01-bac5-484c-8b95-7f5536bca00e", "workflowType": "requirements-first", "specType": "feature"}
|
package/.vscode/tasks.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "2.0.0",
|
|
3
|
-
"tasks": [
|
|
4
|
-
{
|
|
5
|
-
"label": "Start Dashboard",
|
|
6
|
-
"type": "shell",
|
|
7
|
-
"command": "npm",
|
|
8
|
-
"args": ["run", "dashboard"],
|
|
9
|
-
"options": {
|
|
10
|
-
"cwd": "/home/vheins/Projects/local-memory-mcp"
|
|
11
|
-
},
|
|
12
|
-
"group": "build",
|
|
13
|
-
"presentation": {
|
|
14
|
-
"echo": true,
|
|
15
|
-
"reveal": "silent",
|
|
16
|
-
"focus": false,
|
|
17
|
-
"panel": "shared",
|
|
18
|
-
"showReuseMessage": true,
|
|
19
|
-
"clear": false
|
|
20
|
-
},
|
|
21
|
-
"isBackground": true,
|
|
22
|
-
"runOptions": {
|
|
23
|
-
"runOn": "folderOpen"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|