gitmem-mcp 0.2.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.
- package/CHANGELOG.md +47 -0
- package/CLAUDE.md.template +65 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/bin/gitmem.js +383 -0
- package/dist/commands/check.d.ts +33 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +492 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/constants/closing-questions.d.ts +40 -0
- package/dist/constants/closing-questions.d.ts.map +1 -0
- package/dist/constants/closing-questions.js +107 -0
- package/dist/constants/closing-questions.js.map +1 -0
- package/dist/diagnostics/anonymizer.d.ts +55 -0
- package/dist/diagnostics/anonymizer.d.ts.map +1 -0
- package/dist/diagnostics/anonymizer.js +191 -0
- package/dist/diagnostics/anonymizer.js.map +1 -0
- package/dist/diagnostics/channels.d.ts +132 -0
- package/dist/diagnostics/channels.d.ts.map +1 -0
- package/dist/diagnostics/channels.js +150 -0
- package/dist/diagnostics/channels.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +183 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +227 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/index.d.ts +28 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +31 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/absorb-observations.d.ts +63 -0
- package/dist/schemas/absorb-observations.d.ts.map +1 -0
- package/dist/schemas/absorb-observations.js +25 -0
- package/dist/schemas/absorb-observations.js.map +1 -0
- package/dist/schemas/active-sessions.d.ts +71 -0
- package/dist/schemas/active-sessions.d.ts.map +1 -0
- package/dist/schemas/active-sessions.js +19 -0
- package/dist/schemas/active-sessions.js.map +1 -0
- package/dist/schemas/analyze.d.ts +38 -0
- package/dist/schemas/analyze.d.ts.map +1 -0
- package/dist/schemas/analyze.js +30 -0
- package/dist/schemas/analyze.js.map +1 -0
- package/dist/schemas/common.d.ts +55 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +65 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/create-decision.d.ts +48 -0
- package/dist/schemas/create-decision.d.ts.map +1 -0
- package/dist/schemas/create-decision.js +31 -0
- package/dist/schemas/create-decision.js.map +1 -0
- package/dist/schemas/create-learning.d.ts +107 -0
- package/dist/schemas/create-learning.d.ts.map +1 -0
- package/dist/schemas/create-learning.js +64 -0
- package/dist/schemas/create-learning.js.map +1 -0
- package/dist/schemas/get-transcript.d.ts +24 -0
- package/dist/schemas/get-transcript.d.ts.map +1 -0
- package/dist/schemas/get-transcript.js +22 -0
- package/dist/schemas/get-transcript.js.map +1 -0
- package/dist/schemas/index.d.ts +23 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +23 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/log.d.ts +36 -0
- package/dist/schemas/log.d.ts.map +1 -0
- package/dist/schemas/log.js +27 -0
- package/dist/schemas/log.js.map +1 -0
- package/dist/schemas/prepare-context.d.ts +41 -0
- package/dist/schemas/prepare-context.d.ts.map +1 -0
- package/dist/schemas/prepare-context.js +31 -0
- package/dist/schemas/prepare-context.js.map +1 -0
- package/dist/schemas/recall.d.ts +41 -0
- package/dist/schemas/recall.d.ts.map +1 -0
- package/dist/schemas/recall.js +47 -0
- package/dist/schemas/recall.js.map +1 -0
- package/dist/schemas/record-scar-usage-batch.d.ts +82 -0
- package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -0
- package/dist/schemas/record-scar-usage-batch.js +25 -0
- package/dist/schemas/record-scar-usage-batch.js.map +1 -0
- package/dist/schemas/record-scar-usage.d.ts +51 -0
- package/dist/schemas/record-scar-usage.d.ts.map +1 -0
- package/dist/schemas/record-scar-usage.js +32 -0
- package/dist/schemas/record-scar-usage.js.map +1 -0
- package/dist/schemas/save-transcript.d.ts +38 -0
- package/dist/schemas/save-transcript.d.ts.map +1 -0
- package/dist/schemas/save-transcript.js +30 -0
- package/dist/schemas/save-transcript.js.map +1 -0
- package/dist/schemas/search.d.ts +36 -0
- package/dist/schemas/search.d.ts.map +1 -0
- package/dist/schemas/search.js +27 -0
- package/dist/schemas/search.js.map +1 -0
- package/dist/schemas/session-close.d.ts +371 -0
- package/dist/schemas/session-close.d.ts.map +1 -0
- package/dist/schemas/session-close.js +95 -0
- package/dist/schemas/session-close.js.map +1 -0
- package/dist/schemas/session-start.d.ts +46 -0
- package/dist/schemas/session-start.d.ts.map +1 -0
- package/dist/schemas/session-start.js +33 -0
- package/dist/schemas/session-start.js.map +1 -0
- package/dist/schemas/thread.d.ts +72 -0
- package/dist/schemas/thread.d.ts.map +1 -0
- package/dist/schemas/thread.js +39 -0
- package/dist/schemas/thread.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +313 -0
- package/dist/server.js.map +1 -0
- package/dist/services/active-sessions.d.ts +66 -0
- package/dist/services/active-sessions.d.ts.map +1 -0
- package/dist/services/active-sessions.js +311 -0
- package/dist/services/active-sessions.js.map +1 -0
- package/dist/services/agent-detection.d.ts +25 -0
- package/dist/services/agent-detection.d.ts.map +1 -0
- package/dist/services/agent-detection.js +93 -0
- package/dist/services/agent-detection.js.map +1 -0
- package/dist/services/analytics.d.ts +201 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/analytics.js +483 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/cache.d.ts +148 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +384 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/cache.test.d.ts +8 -0
- package/dist/services/cache.test.d.ts.map +1 -0
- package/dist/services/cache.test.js +267 -0
- package/dist/services/cache.test.js.map +1 -0
- package/dist/services/compliance-validator.d.ts +30 -0
- package/dist/services/compliance-validator.d.ts.map +1 -0
- package/dist/services/compliance-validator.js +257 -0
- package/dist/services/compliance-validator.js.map +1 -0
- package/dist/services/config.d.ts +48 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +128 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/embedding.d.ts +58 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +243 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/gitmem-dir.d.ts +38 -0
- package/dist/services/gitmem-dir.d.ts.map +1 -0
- package/dist/services/gitmem-dir.js +84 -0
- package/dist/services/gitmem-dir.js.map +1 -0
- package/dist/services/local-file-storage.d.ts +56 -0
- package/dist/services/local-file-storage.d.ts.map +1 -0
- package/dist/services/local-file-storage.js +213 -0
- package/dist/services/local-file-storage.js.map +1 -0
- package/dist/services/local-vector-search.d.ts +137 -0
- package/dist/services/local-vector-search.d.ts.map +1 -0
- package/dist/services/local-vector-search.js +311 -0
- package/dist/services/local-vector-search.js.map +1 -0
- package/dist/services/metrics.d.ts +104 -0
- package/dist/services/metrics.d.ts.map +1 -0
- package/dist/services/metrics.js +264 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/session-state.d.ts +113 -0
- package/dist/services/session-state.d.ts.map +1 -0
- package/dist/services/session-state.js +203 -0
- package/dist/services/session-state.js.map +1 -0
- package/dist/services/startup.d.ts +112 -0
- package/dist/services/startup.d.ts.map +1 -0
- package/dist/services/startup.js +436 -0
- package/dist/services/startup.js.map +1 -0
- package/dist/services/storage.d.ts +43 -0
- package/dist/services/storage.d.ts.map +1 -0
- package/dist/services/storage.js +92 -0
- package/dist/services/storage.js.map +1 -0
- package/dist/services/supabase-client.d.ts +163 -0
- package/dist/services/supabase-client.d.ts.map +1 -0
- package/dist/services/supabase-client.js +510 -0
- package/dist/services/supabase-client.js.map +1 -0
- package/dist/services/thread-dedup.d.ts +44 -0
- package/dist/services/thread-dedup.d.ts.map +1 -0
- package/dist/services/thread-dedup.js +113 -0
- package/dist/services/thread-dedup.js.map +1 -0
- package/dist/services/thread-manager.d.ts +77 -0
- package/dist/services/thread-manager.d.ts.map +1 -0
- package/dist/services/thread-manager.js +250 -0
- package/dist/services/thread-manager.js.map +1 -0
- package/dist/services/thread-suggestions.d.ts +66 -0
- package/dist/services/thread-suggestions.d.ts.map +1 -0
- package/dist/services/thread-suggestions.js +243 -0
- package/dist/services/thread-suggestions.js.map +1 -0
- package/dist/services/thread-supabase.d.ts +111 -0
- package/dist/services/thread-supabase.d.ts.map +1 -0
- package/dist/services/thread-supabase.js +459 -0
- package/dist/services/thread-supabase.js.map +1 -0
- package/dist/services/thread-vitality.d.ts +65 -0
- package/dist/services/thread-vitality.d.ts.map +1 -0
- package/dist/services/thread-vitality.js +143 -0
- package/dist/services/thread-vitality.js.map +1 -0
- package/dist/services/tier.d.ts +52 -0
- package/dist/services/tier.d.ts.map +1 -0
- package/dist/services/tier.js +109 -0
- package/dist/services/tier.js.map +1 -0
- package/dist/services/timezone.d.ts +37 -0
- package/dist/services/timezone.d.ts.map +1 -0
- package/dist/services/timezone.js +147 -0
- package/dist/services/timezone.js.map +1 -0
- package/dist/services/transcript-chunker.d.ts +18 -0
- package/dist/services/transcript-chunker.d.ts.map +1 -0
- package/dist/services/transcript-chunker.js +237 -0
- package/dist/services/transcript-chunker.js.map +1 -0
- package/dist/services/triple-writer.d.ts +128 -0
- package/dist/services/triple-writer.d.ts.map +1 -0
- package/dist/services/triple-writer.js +338 -0
- package/dist/services/triple-writer.js.map +1 -0
- package/dist/services/variant-assignment.d.ts +92 -0
- package/dist/services/variant-assignment.d.ts.map +1 -0
- package/dist/services/variant-assignment.js +196 -0
- package/dist/services/variant-assignment.js.map +1 -0
- package/dist/tools/absorb-observations.d.ts +16 -0
- package/dist/tools/absorb-observations.d.ts.map +1 -0
- package/dist/tools/absorb-observations.js +82 -0
- package/dist/tools/absorb-observations.js.map +1 -0
- package/dist/tools/analyze.d.ts +55 -0
- package/dist/tools/analyze.d.ts.map +1 -0
- package/dist/tools/analyze.js +139 -0
- package/dist/tools/analyze.js.map +1 -0
- package/dist/tools/cleanup-threads.d.ts +47 -0
- package/dist/tools/cleanup-threads.d.ts.map +1 -0
- package/dist/tools/cleanup-threads.js +127 -0
- package/dist/tools/cleanup-threads.js.map +1 -0
- package/dist/tools/confirm-scars.d.ts +23 -0
- package/dist/tools/confirm-scars.d.ts.map +1 -0
- package/dist/tools/confirm-scars.js +209 -0
- package/dist/tools/confirm-scars.js.map +1 -0
- package/dist/tools/create-decision.d.ts +15 -0
- package/dist/tools/create-decision.d.ts.map +1 -0
- package/dist/tools/create-decision.js +138 -0
- package/dist/tools/create-decision.js.map +1 -0
- package/dist/tools/create-learning.d.ts +15 -0
- package/dist/tools/create-learning.d.ts.map +1 -0
- package/dist/tools/create-learning.js +226 -0
- package/dist/tools/create-learning.js.map +1 -0
- package/dist/tools/create-thread.d.ts +42 -0
- package/dist/tools/create-thread.d.ts.map +1 -0
- package/dist/tools/create-thread.js +180 -0
- package/dist/tools/create-thread.js.map +1 -0
- package/dist/tools/definitions.d.ts +5013 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +2017 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/dismiss-suggestion.d.ts +20 -0
- package/dist/tools/dismiss-suggestion.d.ts.map +1 -0
- package/dist/tools/dismiss-suggestion.js +40 -0
- package/dist/tools/dismiss-suggestion.js.map +1 -0
- package/dist/tools/get-transcript.d.ts +24 -0
- package/dist/tools/get-transcript.d.ts.map +1 -0
- package/dist/tools/get-transcript.js +52 -0
- package/dist/tools/get-transcript.js.map +1 -0
- package/dist/tools/graph-traverse.d.ts +83 -0
- package/dist/tools/graph-traverse.d.ts.map +1 -0
- package/dist/tools/graph-traverse.js +394 -0
- package/dist/tools/graph-traverse.js.map +1 -0
- package/dist/tools/list-threads.d.ts +15 -0
- package/dist/tools/list-threads.d.ts.map +1 -0
- package/dist/tools/list-threads.js +114 -0
- package/dist/tools/list-threads.js.map +1 -0
- package/dist/tools/log.d.ts +43 -0
- package/dist/tools/log.d.ts.map +1 -0
- package/dist/tools/log.js +157 -0
- package/dist/tools/log.js.map +1 -0
- package/dist/tools/prepare-context.d.ts +36 -0
- package/dist/tools/prepare-context.d.ts.map +1 -0
- package/dist/tools/prepare-context.js +353 -0
- package/dist/tools/prepare-context.js.map +1 -0
- package/dist/tools/promote-suggestion.d.ts +25 -0
- package/dist/tools/promote-suggestion.d.ts.map +1 -0
- package/dist/tools/promote-suggestion.js +60 -0
- package/dist/tools/promote-suggestion.js.map +1 -0
- package/dist/tools/recall.d.ts +77 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +423 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/recall.test.d.ts +5 -0
- package/dist/tools/recall.test.d.ts.map +1 -0
- package/dist/tools/recall.test.js +155 -0
- package/dist/tools/recall.test.js.map +1 -0
- package/dist/tools/record-scar-usage-batch.d.ts +10 -0
- package/dist/tools/record-scar-usage-batch.d.ts.map +1 -0
- package/dist/tools/record-scar-usage-batch.js +153 -0
- package/dist/tools/record-scar-usage-batch.js.map +1 -0
- package/dist/tools/record-scar-usage.d.ts +14 -0
- package/dist/tools/record-scar-usage.d.ts.map +1 -0
- package/dist/tools/record-scar-usage.js +94 -0
- package/dist/tools/record-scar-usage.js.map +1 -0
- package/dist/tools/resolve-thread.d.ts +16 -0
- package/dist/tools/resolve-thread.d.ts.map +1 -0
- package/dist/tools/resolve-thread.js +102 -0
- package/dist/tools/resolve-thread.js.map +1 -0
- package/dist/tools/save-transcript.d.ts +29 -0
- package/dist/tools/save-transcript.d.ts.map +1 -0
- package/dist/tools/save-transcript.js +97 -0
- package/dist/tools/save-transcript.js.map +1 -0
- package/dist/tools/search.d.ts +46 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +186 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/session-close.d.ts +14 -0
- package/dist/tools/session-close.d.ts.map +1 -0
- package/dist/tools/session-close.js +881 -0
- package/dist/tools/session-close.js.map +1 -0
- package/dist/tools/session-start.d.ts +38 -0
- package/dist/tools/session-start.d.ts.map +1 -0
- package/dist/tools/session-start.js +1104 -0
- package/dist/tools/session-start.js.map +1 -0
- package/dist/types/index.d.ts +456 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +76 -0
- package/schema/setup.sql +193 -0
- package/schema/starter-scars.json +206 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitMem Cache Service
|
|
3
|
+
*
|
|
4
|
+
* File-based cache for GitMem MCP operations.
|
|
5
|
+
* Caches search results to avoid repeated ww-mcp calls.
|
|
6
|
+
*
|
|
7
|
+
* Design: docs/systems/gitmem-caching.md
|
|
8
|
+
* Issue: OD-473
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync, statSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
// Default cache directory
|
|
14
|
+
// Uses env var if set, otherwise falls back to user's home directory or /tmp
|
|
15
|
+
const DEFAULT_CACHE_DIR = process.env.GITMEM_CACHE_DIR ||
|
|
16
|
+
(process.env.HOME ? `${process.env.HOME}/.cache/gitmem` : "/tmp/gitmem-cache");
|
|
17
|
+
// TTL values in milliseconds
|
|
18
|
+
const TTL = {
|
|
19
|
+
SCAR_SEARCH: 15 * 60 * 1000, // 15 minutes
|
|
20
|
+
DECISIONS: 5 * 60 * 1000, // 5 minutes
|
|
21
|
+
WINS: 5 * 60 * 1000, // 5 minutes
|
|
22
|
+
SESSIONS: 10 * 60 * 1000, // 10 minutes (sessions change slowly)
|
|
23
|
+
SCAR_USAGE: 10 * 60 * 1000, // 10 minutes
|
|
24
|
+
};
|
|
25
|
+
// Max cache sizes
|
|
26
|
+
const MAX_RESULT_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
27
|
+
/**
|
|
28
|
+
* Generate a short hash for cache keys
|
|
29
|
+
*/
|
|
30
|
+
function hashText(text) {
|
|
31
|
+
return createHash("sha256")
|
|
32
|
+
.update(text.toLowerCase().trim())
|
|
33
|
+
.digest("hex")
|
|
34
|
+
.slice(0, 16);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* CacheService class
|
|
38
|
+
*
|
|
39
|
+
* Provides file-based caching for GitMem operations.
|
|
40
|
+
* Designed for graceful degradation - cache failures never block operations.
|
|
41
|
+
*/
|
|
42
|
+
export class CacheService {
|
|
43
|
+
cacheDir;
|
|
44
|
+
resultsDir;
|
|
45
|
+
enabled = true;
|
|
46
|
+
// Stats tracking
|
|
47
|
+
hits = 0;
|
|
48
|
+
misses = 0;
|
|
49
|
+
constructor(cacheDir = DEFAULT_CACHE_DIR) {
|
|
50
|
+
this.cacheDir = cacheDir;
|
|
51
|
+
this.resultsDir = join(cacheDir, "results");
|
|
52
|
+
this.init();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Initialize cache directories
|
|
56
|
+
*/
|
|
57
|
+
init() {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(this.cacheDir)) {
|
|
60
|
+
mkdirSync(this.cacheDir, { recursive: true, mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
if (!existsSync(this.resultsDir)) {
|
|
63
|
+
mkdirSync(this.resultsDir, { recursive: true, mode: 0o700 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn("[cache] Failed to initialize cache directory:", error);
|
|
68
|
+
this.enabled = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if cache is enabled and working
|
|
73
|
+
*/
|
|
74
|
+
isEnabled() {
|
|
75
|
+
return this.enabled;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Disable cache (for testing or on errors)
|
|
79
|
+
*/
|
|
80
|
+
disable() {
|
|
81
|
+
this.enabled = false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Generate cache key for scar search
|
|
85
|
+
*/
|
|
86
|
+
scarSearchKey(query, project, matchCount) {
|
|
87
|
+
return `scar_search:${hashText(query)}:${project}:${matchCount}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Generate cache key for decisions
|
|
91
|
+
*/
|
|
92
|
+
decisionsKey(project, limit) {
|
|
93
|
+
return `decisions:${project}:${limit}`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Generate cache key for wins
|
|
97
|
+
*/
|
|
98
|
+
winsKey(project, limit) {
|
|
99
|
+
return `wins:${project}:${limit}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get cached result
|
|
103
|
+
*/
|
|
104
|
+
async getResult(key) {
|
|
105
|
+
if (!this.enabled)
|
|
106
|
+
return null;
|
|
107
|
+
try {
|
|
108
|
+
const filename = this.keyToFilename(key);
|
|
109
|
+
const filepath = join(this.resultsDir, filename);
|
|
110
|
+
if (!existsSync(filepath)) {
|
|
111
|
+
this.misses++;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const content = readFileSync(filepath, "utf-8");
|
|
115
|
+
const entry = JSON.parse(content);
|
|
116
|
+
// Check expiration
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (now > entry.expires_at) {
|
|
119
|
+
// Expired - delete and return null
|
|
120
|
+
try {
|
|
121
|
+
unlinkSync(filepath);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore delete errors
|
|
125
|
+
}
|
|
126
|
+
this.misses++;
|
|
127
|
+
console.error(`[cache] EXPIRED: ${key}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
this.hits++;
|
|
131
|
+
const age_ms = now - entry.created_at;
|
|
132
|
+
console.error(`[cache] HIT: ${key} (age: ${age_ms}ms)`);
|
|
133
|
+
return { data: entry.data, age_ms };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.warn(`[cache] Error reading ${key}:`, error);
|
|
137
|
+
this.misses++;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Set cached result
|
|
143
|
+
*/
|
|
144
|
+
async setResult(key, data, ttlMs) {
|
|
145
|
+
if (!this.enabled)
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const entry = {
|
|
150
|
+
key,
|
|
151
|
+
created_at: now,
|
|
152
|
+
expires_at: now + ttlMs,
|
|
153
|
+
data,
|
|
154
|
+
};
|
|
155
|
+
const filename = this.keyToFilename(key);
|
|
156
|
+
const filepath = join(this.resultsDir, filename);
|
|
157
|
+
const content = JSON.stringify(entry, null, 2);
|
|
158
|
+
writeFileSync(filepath, content, { mode: 0o600 });
|
|
159
|
+
console.error(`[cache] SET: ${key} (TTL: ${ttlMs}ms)`);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.warn(`[cache] Error writing ${key}:`, error);
|
|
163
|
+
// Don't disable cache on write errors - might be transient
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Convenience method: get or fetch scar search results
|
|
168
|
+
*/
|
|
169
|
+
async getOrFetchScarSearch(query, project, matchCount, fetcher) {
|
|
170
|
+
const key = this.scarSearchKey(query, project, matchCount);
|
|
171
|
+
const cached = await this.getResult(key);
|
|
172
|
+
if (cached) {
|
|
173
|
+
return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
|
|
174
|
+
}
|
|
175
|
+
// Cache miss - fetch from source
|
|
176
|
+
const data = await fetcher();
|
|
177
|
+
// Cache the result (async, don't await)
|
|
178
|
+
this.setResult(key, data, TTL.SCAR_SEARCH).catch(() => { });
|
|
179
|
+
return { data, cache_hit: false };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Convenience method: get or fetch decisions
|
|
183
|
+
*/
|
|
184
|
+
async getOrFetchDecisions(project, limit, fetcher) {
|
|
185
|
+
const key = this.decisionsKey(project, limit);
|
|
186
|
+
const cached = await this.getResult(key);
|
|
187
|
+
if (cached) {
|
|
188
|
+
return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
|
|
189
|
+
}
|
|
190
|
+
// Cache miss - fetch from source
|
|
191
|
+
const data = await fetcher();
|
|
192
|
+
// Cache the result (async, don't await)
|
|
193
|
+
this.setResult(key, data, TTL.DECISIONS).catch(() => { });
|
|
194
|
+
return { data, cache_hit: false };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Convenience method: get or fetch wins
|
|
198
|
+
*/
|
|
199
|
+
async getOrFetchWins(project, limit, fetcher) {
|
|
200
|
+
const key = this.winsKey(project, limit);
|
|
201
|
+
const cached = await this.getResult(key);
|
|
202
|
+
if (cached) {
|
|
203
|
+
return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
|
|
204
|
+
}
|
|
205
|
+
// Cache miss - fetch from source
|
|
206
|
+
const data = await fetcher();
|
|
207
|
+
// Cache the result (async, don't await)
|
|
208
|
+
this.setResult(key, data, TTL.WINS).catch(() => { });
|
|
209
|
+
return { data, cache_hit: false };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Generate cache key for sessions (analytics)
|
|
213
|
+
*/
|
|
214
|
+
sessionsKey(project, days, agent) {
|
|
215
|
+
return `sessions:${project}:${days}d${agent ? `:${agent}` : ""}`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Generate cache key for scar usage (analytics)
|
|
219
|
+
*/
|
|
220
|
+
scarUsageKey(project, days, agent) {
|
|
221
|
+
return `scar_usage:${project}:${days}d${agent ? `:${agent}` : ""}`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Convenience method: get or fetch sessions
|
|
225
|
+
*/
|
|
226
|
+
async getOrFetchSessions(project, days, agent, fetcher) {
|
|
227
|
+
const key = this.sessionsKey(project, days, agent);
|
|
228
|
+
const cached = await this.getResult(key);
|
|
229
|
+
if (cached) {
|
|
230
|
+
return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
|
|
231
|
+
}
|
|
232
|
+
const data = await fetcher();
|
|
233
|
+
this.setResult(key, data, TTL.SESSIONS).catch(() => { });
|
|
234
|
+
return { data, cache_hit: false };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Convenience method: get or fetch scar usage
|
|
238
|
+
*/
|
|
239
|
+
async getOrFetchScarUsage(project, days, agent, fetcher) {
|
|
240
|
+
const key = this.scarUsageKey(project, days, agent);
|
|
241
|
+
const cached = await this.getResult(key);
|
|
242
|
+
if (cached) {
|
|
243
|
+
return { data: cached.data, cache_hit: true, cache_age_ms: cached.age_ms };
|
|
244
|
+
}
|
|
245
|
+
const data = await fetcher();
|
|
246
|
+
this.setResult(key, data, TTL.SCAR_USAGE).catch(() => { });
|
|
247
|
+
return { data, cache_hit: false };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Clean up expired entries
|
|
251
|
+
*/
|
|
252
|
+
async cleanup() {
|
|
253
|
+
if (!this.enabled)
|
|
254
|
+
return 0;
|
|
255
|
+
let cleaned = 0;
|
|
256
|
+
try {
|
|
257
|
+
const files = readdirSync(this.resultsDir);
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
if (!file.endsWith(".json"))
|
|
261
|
+
continue;
|
|
262
|
+
const filepath = join(this.resultsDir, file);
|
|
263
|
+
try {
|
|
264
|
+
const content = readFileSync(filepath, "utf-8");
|
|
265
|
+
const entry = JSON.parse(content);
|
|
266
|
+
if (now > entry.expires_at) {
|
|
267
|
+
unlinkSync(filepath);
|
|
268
|
+
cleaned++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Delete corrupted files
|
|
273
|
+
try {
|
|
274
|
+
unlinkSync(filepath);
|
|
275
|
+
cleaned++;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Ignore
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (cleaned > 0) {
|
|
283
|
+
console.error(`[cache] Cleaned up ${cleaned} expired entries`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
console.warn("[cache] Error during cleanup:", error);
|
|
288
|
+
}
|
|
289
|
+
return cleaned;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Clear all cached data
|
|
293
|
+
*/
|
|
294
|
+
async clear() {
|
|
295
|
+
try {
|
|
296
|
+
const files = readdirSync(this.resultsDir);
|
|
297
|
+
for (const file of files) {
|
|
298
|
+
try {
|
|
299
|
+
unlinkSync(join(this.resultsDir, file));
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Ignore individual file errors
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
console.error("[cache] Cache cleared");
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.warn("[cache] Error clearing cache:", error);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get cache statistics
|
|
313
|
+
*/
|
|
314
|
+
async getStats() {
|
|
315
|
+
const stats = {
|
|
316
|
+
resultCount: 0,
|
|
317
|
+
resultBytes: 0,
|
|
318
|
+
oldestEntry: null,
|
|
319
|
+
};
|
|
320
|
+
if (!this.enabled)
|
|
321
|
+
return stats;
|
|
322
|
+
try {
|
|
323
|
+
const files = readdirSync(this.resultsDir);
|
|
324
|
+
let oldestTime = Infinity;
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
if (!file.endsWith(".json"))
|
|
327
|
+
continue;
|
|
328
|
+
const filepath = join(this.resultsDir, file);
|
|
329
|
+
try {
|
|
330
|
+
const fileStat = statSync(filepath);
|
|
331
|
+
stats.resultCount++;
|
|
332
|
+
stats.resultBytes += fileStat.size;
|
|
333
|
+
const content = readFileSync(filepath, "utf-8");
|
|
334
|
+
const entry = JSON.parse(content);
|
|
335
|
+
if (entry.created_at < oldestTime) {
|
|
336
|
+
oldestTime = entry.created_at;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Skip problematic files
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (oldestTime !== Infinity) {
|
|
344
|
+
stats.oldestEntry = new Date(oldestTime);
|
|
345
|
+
}
|
|
346
|
+
// Calculate hit rate
|
|
347
|
+
const total = this.hits + this.misses;
|
|
348
|
+
if (total > 0) {
|
|
349
|
+
stats.hitRate = this.hits / total;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
console.warn("[cache] Error getting stats:", error);
|
|
354
|
+
}
|
|
355
|
+
return stats;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Convert cache key to safe filename
|
|
359
|
+
*/
|
|
360
|
+
keyToFilename(key) {
|
|
361
|
+
// Replace unsafe chars with underscores, keep it readable
|
|
362
|
+
return key.replace(/[^a-zA-Z0-9_:-]/g, "_") + ".json";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Singleton instance
|
|
366
|
+
let cacheInstance = null;
|
|
367
|
+
/**
|
|
368
|
+
* Get the cache service singleton
|
|
369
|
+
*/
|
|
370
|
+
export function getCache() {
|
|
371
|
+
if (!cacheInstance) {
|
|
372
|
+
cacheInstance = new CacheService();
|
|
373
|
+
}
|
|
374
|
+
return cacheInstance;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Reset cache instance (for testing)
|
|
378
|
+
*/
|
|
379
|
+
export function resetCache() {
|
|
380
|
+
cacheInstance = null;
|
|
381
|
+
}
|
|
382
|
+
// Export TTL values for external use
|
|
383
|
+
export { TTL };
|
|
384
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/services/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/services/cache.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheService Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the GitMem file-based caching layer.
|
|
5
|
+
* Issue: OD-473
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import { existsSync, rmSync, writeFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { CacheService, TTL, getCache, resetCache } from "./cache.js";
|
|
11
|
+
// Test cache directory (isolated from production)
|
|
12
|
+
const TEST_CACHE_DIR = "/tmp/gitmem-cache-test";
|
|
13
|
+
describe("CacheService", () => {
|
|
14
|
+
let cache;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Clean up any existing test cache
|
|
17
|
+
if (existsSync(TEST_CACHE_DIR)) {
|
|
18
|
+
rmSync(TEST_CACHE_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
cache = new CacheService(TEST_CACHE_DIR);
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Clean up test cache
|
|
24
|
+
if (existsSync(TEST_CACHE_DIR)) {
|
|
25
|
+
rmSync(TEST_CACHE_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
resetCache();
|
|
28
|
+
});
|
|
29
|
+
describe("initialization", () => {
|
|
30
|
+
it("creates cache directory on initialization", () => {
|
|
31
|
+
expect(existsSync(TEST_CACHE_DIR)).toBe(true);
|
|
32
|
+
expect(existsSync(join(TEST_CACHE_DIR, "results"))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it("is enabled after successful initialization", () => {
|
|
35
|
+
expect(cache.isEnabled()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("can be disabled", () => {
|
|
38
|
+
cache.disable();
|
|
39
|
+
expect(cache.isEnabled()).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("cache key generation", () => {
|
|
43
|
+
it("generates consistent scar search keys", () => {
|
|
44
|
+
const key1 = cache.scarSearchKey("test query", "orchestra_dev", 5);
|
|
45
|
+
const key2 = cache.scarSearchKey("test query", "orchestra_dev", 5);
|
|
46
|
+
expect(key1).toBe(key2);
|
|
47
|
+
});
|
|
48
|
+
it("generates different keys for different queries", () => {
|
|
49
|
+
const key1 = cache.scarSearchKey("query one", "orchestra_dev", 5);
|
|
50
|
+
const key2 = cache.scarSearchKey("query two", "orchestra_dev", 5);
|
|
51
|
+
expect(key1).not.toBe(key2);
|
|
52
|
+
});
|
|
53
|
+
it("generates different keys for different projects", () => {
|
|
54
|
+
const key1 = cache.scarSearchKey("test", "orchestra_dev", 5);
|
|
55
|
+
const key2 = cache.scarSearchKey("test", "weekend_warrior", 5);
|
|
56
|
+
expect(key1).not.toBe(key2);
|
|
57
|
+
});
|
|
58
|
+
it("generates different keys for different match counts", () => {
|
|
59
|
+
const key1 = cache.scarSearchKey("test", "orchestra_dev", 3);
|
|
60
|
+
const key2 = cache.scarSearchKey("test", "orchestra_dev", 5);
|
|
61
|
+
expect(key1).not.toBe(key2);
|
|
62
|
+
});
|
|
63
|
+
it("normalizes query text (lowercase, trim)", () => {
|
|
64
|
+
const key1 = cache.scarSearchKey(" Test Query ", "orchestra_dev", 5);
|
|
65
|
+
const key2 = cache.scarSearchKey("test query", "orchestra_dev", 5);
|
|
66
|
+
expect(key1).toBe(key2);
|
|
67
|
+
});
|
|
68
|
+
it("generates decisions keys", () => {
|
|
69
|
+
const key = cache.decisionsKey("orchestra_dev", 5);
|
|
70
|
+
expect(key).toBe("decisions:orchestra_dev:5");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("setResult and getResult", () => {
|
|
74
|
+
it("stores and retrieves data", async () => {
|
|
75
|
+
const testData = { items: [1, 2, 3], message: "test" };
|
|
76
|
+
await cache.setResult("test-key", testData, 60000);
|
|
77
|
+
const result = await cache.getResult("test-key");
|
|
78
|
+
expect(result).not.toBeNull();
|
|
79
|
+
expect(result.data).toEqual(testData);
|
|
80
|
+
});
|
|
81
|
+
it("returns null for non-existent key", async () => {
|
|
82
|
+
const result = await cache.getResult("non-existent");
|
|
83
|
+
expect(result).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
it("returns cache age in milliseconds", async () => {
|
|
86
|
+
const testData = { test: true };
|
|
87
|
+
await cache.setResult("test-key", testData, 60000);
|
|
88
|
+
// Wait a small amount
|
|
89
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
90
|
+
const result = await cache.getResult("test-key");
|
|
91
|
+
expect(result).not.toBeNull();
|
|
92
|
+
expect(result.age_ms).toBeGreaterThan(0);
|
|
93
|
+
expect(result.age_ms).toBeLessThan(1000); // Should be very fast
|
|
94
|
+
});
|
|
95
|
+
it("returns null for expired entries", async () => {
|
|
96
|
+
const testData = { test: true };
|
|
97
|
+
// Set with very short TTL
|
|
98
|
+
await cache.setResult("expiring-key", testData, 1);
|
|
99
|
+
// Wait for expiration
|
|
100
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
101
|
+
const result = await cache.getResult("expiring-key");
|
|
102
|
+
expect(result).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
it("returns null when cache is disabled", async () => {
|
|
105
|
+
cache.disable();
|
|
106
|
+
await cache.setResult("test-key", { test: true }, 60000);
|
|
107
|
+
const result = await cache.getResult("test-key");
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe("getOrFetchScarSearch", () => {
|
|
112
|
+
it("returns cached data on hit", async () => {
|
|
113
|
+
const mockData = [{ id: "1", title: "Scar 1" }];
|
|
114
|
+
let fetchCount = 0;
|
|
115
|
+
const fetcher = async () => {
|
|
116
|
+
fetchCount++;
|
|
117
|
+
return mockData;
|
|
118
|
+
};
|
|
119
|
+
// First call - should fetch
|
|
120
|
+
const result1 = await cache.getOrFetchScarSearch("test query", "orchestra_dev", 5, fetcher);
|
|
121
|
+
expect(result1.cache_hit).toBe(false);
|
|
122
|
+
expect(result1.data).toEqual(mockData);
|
|
123
|
+
expect(fetchCount).toBe(1);
|
|
124
|
+
// Second call - should use cache
|
|
125
|
+
const result2 = await cache.getOrFetchScarSearch("test query", "orchestra_dev", 5, fetcher);
|
|
126
|
+
expect(result2.cache_hit).toBe(true);
|
|
127
|
+
expect(result2.data).toEqual(mockData);
|
|
128
|
+
expect(result2.cache_age_ms).toBeDefined();
|
|
129
|
+
expect(fetchCount).toBe(1); // Fetcher not called again
|
|
130
|
+
});
|
|
131
|
+
it("fetches on cache miss", async () => {
|
|
132
|
+
const mockData = [{ id: "1", title: "Scar 1" }];
|
|
133
|
+
const fetcher = async () => mockData;
|
|
134
|
+
const result = await cache.getOrFetchScarSearch("new query", "orchestra_dev", 5, fetcher);
|
|
135
|
+
expect(result.cache_hit).toBe(false);
|
|
136
|
+
expect(result.data).toEqual(mockData);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("getOrFetchDecisions", () => {
|
|
140
|
+
it("returns cached data on hit", async () => {
|
|
141
|
+
const mockData = [{ id: "1", title: "Decision 1" }];
|
|
142
|
+
let fetchCount = 0;
|
|
143
|
+
const fetcher = async () => {
|
|
144
|
+
fetchCount++;
|
|
145
|
+
return mockData;
|
|
146
|
+
};
|
|
147
|
+
// First call - should fetch
|
|
148
|
+
const result1 = await cache.getOrFetchDecisions("orchestra_dev", 5, fetcher);
|
|
149
|
+
expect(result1.cache_hit).toBe(false);
|
|
150
|
+
expect(fetchCount).toBe(1);
|
|
151
|
+
// Second call - should use cache
|
|
152
|
+
const result2 = await cache.getOrFetchDecisions("orchestra_dev", 5, fetcher);
|
|
153
|
+
expect(result2.cache_hit).toBe(true);
|
|
154
|
+
expect(fetchCount).toBe(1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe("cleanup", () => {
|
|
158
|
+
it("removes expired entries", async () => {
|
|
159
|
+
// Create an expired entry manually
|
|
160
|
+
const expiredEntry = {
|
|
161
|
+
key: "expired-key",
|
|
162
|
+
created_at: Date.now() - 100000,
|
|
163
|
+
expires_at: Date.now() - 50000, // Expired
|
|
164
|
+
data: { test: true },
|
|
165
|
+
};
|
|
166
|
+
const filepath = join(TEST_CACHE_DIR, "results", "expired-key.json");
|
|
167
|
+
writeFileSync(filepath, JSON.stringify(expiredEntry));
|
|
168
|
+
// Create a valid entry
|
|
169
|
+
await cache.setResult("valid-key", { test: true }, 60000);
|
|
170
|
+
// Run cleanup
|
|
171
|
+
const cleaned = await cache.cleanup();
|
|
172
|
+
expect(cleaned).toBe(1);
|
|
173
|
+
// Verify expired entry is gone
|
|
174
|
+
expect(existsSync(filepath)).toBe(false);
|
|
175
|
+
// Verify valid entry remains
|
|
176
|
+
const validResult = await cache.getResult("valid-key");
|
|
177
|
+
expect(validResult).not.toBeNull();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe("clear", () => {
|
|
181
|
+
it("removes all cached entries", async () => {
|
|
182
|
+
await cache.setResult("key1", { test: 1 }, 60000);
|
|
183
|
+
await cache.setResult("key2", { test: 2 }, 60000);
|
|
184
|
+
await cache.clear();
|
|
185
|
+
const result1 = await cache.getResult("key1");
|
|
186
|
+
const result2 = await cache.getResult("key2");
|
|
187
|
+
expect(result1).toBeNull();
|
|
188
|
+
expect(result2).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe("getStats", () => {
|
|
192
|
+
it("returns cache statistics", async () => {
|
|
193
|
+
await cache.setResult("key1", { test: 1 }, 60000);
|
|
194
|
+
await cache.setResult("key2", { test: 2 }, 60000);
|
|
195
|
+
const stats = await cache.getStats();
|
|
196
|
+
expect(stats.resultCount).toBe(2);
|
|
197
|
+
expect(stats.resultBytes).toBeGreaterThan(0);
|
|
198
|
+
expect(stats.oldestEntry).toBeInstanceOf(Date);
|
|
199
|
+
});
|
|
200
|
+
it("tracks hit rate", async () => {
|
|
201
|
+
await cache.setResult("key1", { test: 1 }, 60000);
|
|
202
|
+
// Generate some hits and misses
|
|
203
|
+
await cache.getResult("key1"); // Hit
|
|
204
|
+
await cache.getResult("key1"); // Hit
|
|
205
|
+
await cache.getResult("nonexistent"); // Miss
|
|
206
|
+
const stats = await cache.getStats();
|
|
207
|
+
expect(stats.hitRate).toBeCloseTo(2 / 3, 1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe("TTL values", () => {
|
|
211
|
+
it("exports correct TTL values", () => {
|
|
212
|
+
expect(TTL.SCAR_SEARCH).toBe(15 * 60 * 1000); // 15 minutes
|
|
213
|
+
expect(TTL.DECISIONS).toBe(5 * 60 * 1000); // 5 minutes
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe("singleton pattern", () => {
|
|
217
|
+
it("getCache returns same instance", () => {
|
|
218
|
+
// Note: getCache uses default dir which may not be writable in test env
|
|
219
|
+
// This test verifies the singleton behavior, not the cache functionality
|
|
220
|
+
resetCache();
|
|
221
|
+
// Set env var to use test directory
|
|
222
|
+
const originalDir = process.env.GITMEM_CACHE_DIR;
|
|
223
|
+
process.env.GITMEM_CACHE_DIR = TEST_CACHE_DIR;
|
|
224
|
+
try {
|
|
225
|
+
const cache1 = getCache();
|
|
226
|
+
const cache2 = getCache();
|
|
227
|
+
expect(cache1).toBe(cache2);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
process.env.GITMEM_CACHE_DIR = originalDir;
|
|
231
|
+
resetCache();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
it("resetCache creates new instance", () => {
|
|
235
|
+
const originalDir = process.env.GITMEM_CACHE_DIR;
|
|
236
|
+
process.env.GITMEM_CACHE_DIR = TEST_CACHE_DIR;
|
|
237
|
+
try {
|
|
238
|
+
const cache1 = getCache();
|
|
239
|
+
resetCache();
|
|
240
|
+
const cache2 = getCache();
|
|
241
|
+
expect(cache1).not.toBe(cache2);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
process.env.GITMEM_CACHE_DIR = originalDir;
|
|
245
|
+
resetCache();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe("error handling", () => {
|
|
250
|
+
it("gracefully handles corrupted cache files", async () => {
|
|
251
|
+
// Write corrupted JSON
|
|
252
|
+
const filepath = join(TEST_CACHE_DIR, "results", "corrupted.json");
|
|
253
|
+
writeFileSync(filepath, "not valid json {{{");
|
|
254
|
+
// Should return null, not throw
|
|
255
|
+
const result = await cache.getResult("corrupted");
|
|
256
|
+
expect(result).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
it("gracefully handles file system errors", async () => {
|
|
259
|
+
// Disable cache to simulate error condition
|
|
260
|
+
cache.disable();
|
|
261
|
+
// Operations should not throw
|
|
262
|
+
await expect(cache.setResult("key", { test: true }, 60000)).resolves.not.toThrow();
|
|
263
|
+
await expect(cache.getResult("key")).resolves.toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
//# sourceMappingURL=cache.test.js.map
|