@nahisaho/shikigami-mcp-server 1.7.0 → 1.10.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/README.md +29 -0
- package/dist/cache/__tests__/global.test.d.ts +6 -0
- package/dist/cache/__tests__/global.test.d.ts.map +1 -0
- package/dist/cache/__tests__/global.test.js +269 -0
- package/dist/cache/__tests__/global.test.js.map +1 -0
- package/dist/cache/__tests__/manager.test.d.ts +6 -0
- package/dist/cache/__tests__/manager.test.d.ts.map +1 -0
- package/dist/cache/__tests__/manager.test.js +286 -0
- package/dist/cache/__tests__/manager.test.js.map +1 -0
- package/dist/cache/__tests__/semantic.test.d.ts +6 -0
- package/dist/cache/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/cache/__tests__/semantic.test.js +271 -0
- package/dist/cache/__tests__/semantic.test.js.map +1 -0
- package/dist/cache/__tests__/store.test.d.ts +6 -0
- package/dist/cache/__tests__/store.test.d.ts.map +1 -0
- package/dist/cache/__tests__/store.test.js +289 -0
- package/dist/cache/__tests__/store.test.js.map +1 -0
- package/dist/cache/global.d.ts +140 -0
- package/dist/cache/global.d.ts.map +1 -0
- package/dist/cache/global.js +260 -0
- package/dist/cache/global.js.map +1 -0
- package/dist/cache/index.d.ts +10 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +10 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/manager.d.ts +146 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +229 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cache/semantic.d.ts +164 -0
- package/dist/cache/semantic.d.ts.map +1 -0
- package/dist/cache/semantic.js +241 -0
- package/dist/cache/semantic.js.map +1 -0
- package/dist/cache/store.d.ts +98 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +469 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cache/types.d.ts +171 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +8 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/config/types.d.ts +67 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +30 -0
- package/dist/config/types.js.map +1 -1
- package/dist/tools/__tests__/multilingual-search.test.d.ts +7 -0
- package/dist/tools/__tests__/multilingual-search.test.d.ts.map +1 -0
- package/dist/tools/__tests__/multilingual-search.test.js +71 -0
- package/dist/tools/__tests__/multilingual-search.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js +249 -0
- package/dist/tools/search/recovery/__tests__/logger.test.js.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts +8 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.d.ts.map +1 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js +158 -0
- package/dist/tools/search/recovery/__tests__/manager-logger.test.js.map +1 -0
- package/dist/tools/search/recovery/index.d.ts +31 -2
- package/dist/tools/search/recovery/index.d.ts.map +1 -1
- package/dist/tools/search/recovery/index.js +51 -7
- package/dist/tools/search/recovery/index.js.map +1 -1
- package/dist/tools/search/recovery/logger.d.ts +149 -0
- package/dist/tools/search/recovery/logger.d.ts.map +1 -0
- package/dist/tools/search/recovery/logger.js +218 -0
- package/dist/tools/search/recovery/logger.js.map +1 -0
- package/dist/tools/search.d.ts +48 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +152 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts +10 -0
- package/dist/tools/visit/recovery/__tests__/index.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js +239 -0
- package/dist/tools/visit/recovery/__tests__/index.test.js.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts +8 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.d.ts.map +1 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js +271 -0
- package/dist/tools/visit/recovery/__tests__/wayback.test.js.map +1 -0
- package/dist/tools/visit/recovery/index.d.ts +126 -0
- package/dist/tools/visit/recovery/index.d.ts.map +1 -0
- package/dist/tools/visit/recovery/index.js +203 -0
- package/dist/tools/visit/recovery/index.js.map +1 -0
- package/dist/tools/visit/recovery/wayback.d.ts +101 -0
- package/dist/tools/visit/recovery/wayback.d.ts.map +1 -0
- package/dist/tools/visit/recovery/wayback.js +140 -0
- package/dist/tools/visit/recovery/wayback.js.map +1 -0
- package/dist/tools/visit.d.ts +33 -0
- package/dist/tools/visit.d.ts.map +1 -1
- package/dist/tools/visit.js +127 -1
- package/dist/tools/visit.js.map +1 -1
- package/package.json +7 -3
- package/shikigami.config.example.yaml +9 -0
- package/src/cache/__tests__/global.test.ts +340 -0
- package/src/cache/__tests__/manager.test.ts +353 -0
- package/src/cache/__tests__/semantic.test.ts +331 -0
- package/src/cache/__tests__/store.test.ts +369 -0
- package/src/cache/global.ts +351 -0
- package/src/cache/index.ts +10 -0
- package/src/cache/manager.ts +325 -0
- package/src/cache/semantic.ts +368 -0
- package/src/cache/store.ts +555 -0
- package/src/cache/types.ts +189 -0
- package/src/config/types.ts +108 -0
- package/src/tools/__tests__/multilingual-search.test.ts +88 -0
- package/src/tools/search/recovery/__tests__/logger.test.ts +334 -0
- package/src/tools/search/recovery/__tests__/manager-logger.test.ts +199 -0
- package/src/tools/search/recovery/index.ts +67 -9
- package/src/tools/search/recovery/logger.ts +351 -0
- package/src/tools/search.ts +212 -0
- package/src/tools/visit/recovery/__tests__/index.test.ts +297 -0
- package/src/tools/visit/recovery/__tests__/wayback.test.ts +344 -0
- package/src/tools/visit/recovery/index.ts +312 -0
- package/src/tools/visit/recovery/wayback.ts +210 -0
- package/src/tools/visit.ts +159 -2
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobalCacheStore Unit Tests
|
|
3
|
+
* v1.0.0 - REQ-CACHE-001-03
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import {
|
|
10
|
+
GlobalCacheStore,
|
|
11
|
+
DEFAULT_GLOBAL_CACHE_CONFIG,
|
|
12
|
+
getGlobalCacheStore,
|
|
13
|
+
resetGlobalCacheStore,
|
|
14
|
+
createGlobalCacheStore,
|
|
15
|
+
} from '../global.js';
|
|
16
|
+
|
|
17
|
+
// テスト用一時ディレクトリ
|
|
18
|
+
let TEST_BASE_DIR: string;
|
|
19
|
+
let TEST_PROJECT_DIR: string;
|
|
20
|
+
|
|
21
|
+
describe('GlobalCacheStore', () => {
|
|
22
|
+
let globalStore: GlobalCacheStore;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
const suffix = Date.now() + '-' + Math.random().toString(36).substring(7);
|
|
26
|
+
TEST_BASE_DIR = `/tmp/shikigami-global-test-${suffix}`;
|
|
27
|
+
TEST_PROJECT_DIR = `/tmp/shikigami-project-test-${suffix}`;
|
|
28
|
+
|
|
29
|
+
globalStore = new GlobalCacheStore({
|
|
30
|
+
baseDir: TEST_BASE_DIR,
|
|
31
|
+
enabled: true,
|
|
32
|
+
preferProjectCache: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// シングルトンをリセット
|
|
36
|
+
resetGlobalCacheStore();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await fs.rm(TEST_BASE_DIR, { recursive: true, force: true });
|
|
42
|
+
await fs.rm(TEST_PROJECT_DIR, { recursive: true, force: true });
|
|
43
|
+
} catch {
|
|
44
|
+
// 無視
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('constructor', () => {
|
|
49
|
+
it('should create with default config', () => {
|
|
50
|
+
const store = new GlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
51
|
+
expect(store).toBeDefined();
|
|
52
|
+
expect(store.getConfig().enabled).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should create with custom config', () => {
|
|
56
|
+
const store = new GlobalCacheStore({
|
|
57
|
+
baseDir: TEST_BASE_DIR,
|
|
58
|
+
maxEntries: 1000,
|
|
59
|
+
preferProjectCache: false,
|
|
60
|
+
});
|
|
61
|
+
expect(store.getConfig().maxEntries).toBe(1000);
|
|
62
|
+
expect(store.getConfig().preferProjectCache).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should set global cache dir correctly', () => {
|
|
66
|
+
const store = new GlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
67
|
+
expect(store.getGlobalCacheDir()).toBe(path.join(TEST_BASE_DIR, 'global-cache'));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('project store', () => {
|
|
72
|
+
it('should set project store', () => {
|
|
73
|
+
expect(globalStore.hasProjectStore()).toBe(false);
|
|
74
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
75
|
+
expect(globalStore.hasProjectStore()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should clear project store', () => {
|
|
79
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
80
|
+
expect(globalStore.hasProjectStore()).toBe(true);
|
|
81
|
+
globalStore.clearProjectStore();
|
|
82
|
+
expect(globalStore.hasProjectStore()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should get project store', () => {
|
|
86
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
87
|
+
const projectStore = globalStore.getProjectStore();
|
|
88
|
+
expect(projectStore).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('get/set operations', () => {
|
|
93
|
+
it('should store and retrieve from global scope', async () => {
|
|
94
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
95
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
96
|
+
|
|
97
|
+
expect(result.hit).toBe(true);
|
|
98
|
+
expect(result.value?.data).toBe('global');
|
|
99
|
+
expect(result.scope).toBe('global');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should store and retrieve from project scope', async () => {
|
|
103
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
104
|
+
|
|
105
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
106
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
107
|
+
|
|
108
|
+
expect(result.hit).toBe(true);
|
|
109
|
+
expect(result.value?.data).toBe('project');
|
|
110
|
+
expect(result.scope).toBe('project');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should prefer project cache when configured', async () => {
|
|
114
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
115
|
+
|
|
116
|
+
// 両方のスコープに保存
|
|
117
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
118
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
119
|
+
|
|
120
|
+
// プロジェクトキャッシュが優先される
|
|
121
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
122
|
+
expect(result.value?.data).toBe('project');
|
|
123
|
+
expect(result.scope).toBe('project');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should prefer global cache when configured', async () => {
|
|
127
|
+
globalStore.setPreferProjectCache(false);
|
|
128
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
129
|
+
|
|
130
|
+
// 両方のスコープに保存
|
|
131
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
132
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
133
|
+
|
|
134
|
+
// グローバルキャッシュが優先される
|
|
135
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
136
|
+
expect(result.value?.data).toBe('global');
|
|
137
|
+
expect(result.scope).toBe('global');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should fallback to global when project cache miss', async () => {
|
|
141
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
142
|
+
|
|
143
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
144
|
+
|
|
145
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
146
|
+
expect(result.hit).toBe(true);
|
|
147
|
+
expect(result.value?.data).toBe('global');
|
|
148
|
+
expect(result.scope).toBe('global');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return miss when not found', async () => {
|
|
152
|
+
const result = await globalStore.get('non-existent');
|
|
153
|
+
expect(result.hit).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should not save to global when disabled', async () => {
|
|
157
|
+
globalStore.setEnabled(false);
|
|
158
|
+
await globalStore.set('test-key', { data: 'test' }, { scope: 'global' });
|
|
159
|
+
|
|
160
|
+
const result = await globalStore.get('test-key');
|
|
161
|
+
expect(result.hit).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('delete', () => {
|
|
166
|
+
it('should delete from specific scope', async () => {
|
|
167
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
168
|
+
|
|
169
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
170
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
171
|
+
|
|
172
|
+
// プロジェクトからのみ削除
|
|
173
|
+
const deleted = await globalStore.delete('test-key', 'project');
|
|
174
|
+
expect(deleted).toBe(true);
|
|
175
|
+
|
|
176
|
+
// グローバルにはまだ存在
|
|
177
|
+
globalStore.setPreferProjectCache(false);
|
|
178
|
+
const result = await globalStore.get<{ data: string }>('test-key');
|
|
179
|
+
expect(result.hit).toBe(true);
|
|
180
|
+
expect(result.value?.data).toBe('global');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should delete from both scopes when no scope specified', async () => {
|
|
184
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
185
|
+
|
|
186
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
187
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
188
|
+
|
|
189
|
+
const deleted = await globalStore.delete('test-key');
|
|
190
|
+
expect(deleted).toBe(true);
|
|
191
|
+
|
|
192
|
+
const result = await globalStore.get('test-key');
|
|
193
|
+
expect(result.hit).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('has', () => {
|
|
198
|
+
it('should check existence in project scope', async () => {
|
|
199
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
200
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
201
|
+
|
|
202
|
+
const result = await globalStore.has('test-key');
|
|
203
|
+
expect(result.exists).toBe(true);
|
|
204
|
+
expect(result.scope).toBe('project');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should check existence in global scope', async () => {
|
|
208
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
209
|
+
|
|
210
|
+
const result = await globalStore.has('test-key');
|
|
211
|
+
expect(result.exists).toBe(true);
|
|
212
|
+
expect(result.scope).toBe('global');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should return false for non-existent key', async () => {
|
|
216
|
+
const result = await globalStore.has('non-existent');
|
|
217
|
+
expect(result.exists).toBe(false);
|
|
218
|
+
expect(result.scope).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('clear', () => {
|
|
223
|
+
it('should clear specific scope', async () => {
|
|
224
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
225
|
+
|
|
226
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
227
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
228
|
+
|
|
229
|
+
await globalStore.clear('project');
|
|
230
|
+
|
|
231
|
+
// プロジェクトはクリアされた
|
|
232
|
+
const projectStore = globalStore.getProjectStore()!;
|
|
233
|
+
const projectStats = await projectStore.getStats();
|
|
234
|
+
expect(projectStats.totalEntries).toBe(0);
|
|
235
|
+
|
|
236
|
+
// グローバルは残っている
|
|
237
|
+
const globalStats = await globalStore.getGlobalStore().getStats();
|
|
238
|
+
expect(globalStats.totalEntries).toBe(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should clear all scopes when no scope specified', async () => {
|
|
242
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
243
|
+
|
|
244
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
245
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
246
|
+
|
|
247
|
+
await globalStore.clear();
|
|
248
|
+
|
|
249
|
+
const result = await globalStore.get('test-key');
|
|
250
|
+
expect(result.hit).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('getStats', () => {
|
|
255
|
+
it('should return global stats', async () => {
|
|
256
|
+
await globalStore.set('test-key', { data: 'global' }, { scope: 'global' });
|
|
257
|
+
|
|
258
|
+
const stats = await globalStore.getStats('global');
|
|
259
|
+
expect(stats.global).toBeDefined();
|
|
260
|
+
expect(stats.global!.totalEntries).toBe(1);
|
|
261
|
+
expect(stats.project).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should return project stats', async () => {
|
|
265
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
266
|
+
await globalStore.set('test-key', { data: 'project' }, { scope: 'project' });
|
|
267
|
+
|
|
268
|
+
const stats = await globalStore.getStats('project');
|
|
269
|
+
expect(stats.project).toBeDefined();
|
|
270
|
+
expect(stats.project!.totalEntries).toBe(1);
|
|
271
|
+
expect(stats.global).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should return combined stats', async () => {
|
|
275
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
276
|
+
await globalStore.set('key1', { data: 'global' }, { scope: 'global' });
|
|
277
|
+
await globalStore.set('key2', { data: 'project' }, { scope: 'project' });
|
|
278
|
+
|
|
279
|
+
const stats = await globalStore.getStats();
|
|
280
|
+
expect(stats.global).toBeDefined();
|
|
281
|
+
expect(stats.project).toBeDefined();
|
|
282
|
+
expect(stats.combined).toBeDefined();
|
|
283
|
+
expect(stats.combined!.totalEntries).toBe(2);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('evictExpired', () => {
|
|
288
|
+
it('should evict expired entries from both scopes', async () => {
|
|
289
|
+
globalStore.setProjectStore(TEST_PROJECT_DIR);
|
|
290
|
+
|
|
291
|
+
// 短いTTLで保存
|
|
292
|
+
await globalStore.set('global-key', { data: 'global' }, { scope: 'global', ttlSeconds: 1 });
|
|
293
|
+
await globalStore.set('project-key', { data: 'project' }, { scope: 'project', ttlSeconds: 1 });
|
|
294
|
+
|
|
295
|
+
// TTL期限後
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
297
|
+
|
|
298
|
+
const result = await globalStore.evictExpired();
|
|
299
|
+
expect(result.global).toBeGreaterThanOrEqual(0);
|
|
300
|
+
expect(result.project).toBeGreaterThanOrEqual(0);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('configuration', () => {
|
|
305
|
+
it('should toggle enabled state', () => {
|
|
306
|
+
expect(globalStore.getConfig().enabled).toBe(true);
|
|
307
|
+
globalStore.setEnabled(false);
|
|
308
|
+
expect(globalStore.getConfig().enabled).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should toggle preferProjectCache', () => {
|
|
312
|
+
expect(globalStore.getConfig().preferProjectCache).toBe(true);
|
|
313
|
+
globalStore.setPreferProjectCache(false);
|
|
314
|
+
expect(globalStore.getConfig().preferProjectCache).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('singleton', () => {
|
|
319
|
+
it('should return singleton instance', () => {
|
|
320
|
+
const store1 = getGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
321
|
+
const store2 = getGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
322
|
+
expect(store1).toBe(store2);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should reset singleton', () => {
|
|
326
|
+
const store1 = getGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
327
|
+
resetGlobalCacheStore();
|
|
328
|
+
const store2 = getGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
329
|
+
expect(store1).not.toBe(store2);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('createGlobalCacheStore', () => {
|
|
334
|
+
it('should create new instance', () => {
|
|
335
|
+
const store1 = createGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
336
|
+
const store2 = createGlobalCacheStore({ baseDir: TEST_BASE_DIR });
|
|
337
|
+
expect(store1).not.toBe(store2);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryCacheManager Unit Tests
|
|
3
|
+
* v1.0.0 - REQ-CACHE-001-01
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import {
|
|
9
|
+
QueryCacheManager,
|
|
10
|
+
createCacheManager,
|
|
11
|
+
getDefaultCacheManager,
|
|
12
|
+
SearchCacheKeyParams,
|
|
13
|
+
VisitCacheKeyParams,
|
|
14
|
+
EmbeddingCacheKeyParams,
|
|
15
|
+
} from '../manager.js';
|
|
16
|
+
|
|
17
|
+
// テスト用一時ディレクトリ
|
|
18
|
+
const TEST_CACHE_DIR = '/tmp/shikigami-manager-test-' + Date.now();
|
|
19
|
+
|
|
20
|
+
describe('QueryCacheManager', () => {
|
|
21
|
+
let manager: QueryCacheManager;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
manager = new QueryCacheManager({
|
|
25
|
+
cacheDir: TEST_CACHE_DIR,
|
|
26
|
+
maxEntries: 100,
|
|
27
|
+
defaultTtlSeconds: 3600,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
manager.stopMaintenanceTasks();
|
|
33
|
+
try {
|
|
34
|
+
await fs.rm(TEST_CACHE_DIR, { recursive: true, force: true });
|
|
35
|
+
} catch {
|
|
36
|
+
// 無視
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('generateSearchKey', () => {
|
|
41
|
+
it('should generate key with query only', () => {
|
|
42
|
+
const key = QueryCacheManager.generateSearchKey({ query: 'test query' });
|
|
43
|
+
expect(key).toBe('search:default:test query');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should generate key with engine', () => {
|
|
47
|
+
const key = QueryCacheManager.generateSearchKey({
|
|
48
|
+
query: 'test query',
|
|
49
|
+
engine: 'google',
|
|
50
|
+
});
|
|
51
|
+
expect(key).toBe('search:google:test query');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should generate key with options', () => {
|
|
55
|
+
const key = QueryCacheManager.generateSearchKey({
|
|
56
|
+
query: 'test query',
|
|
57
|
+
engine: 'google',
|
|
58
|
+
options: { lang: 'ja', safe: true },
|
|
59
|
+
});
|
|
60
|
+
expect(key).toContain('search:google:test query');
|
|
61
|
+
expect(key).toContain('lang');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('generateVisitKey', () => {
|
|
66
|
+
it('should generate key with URL only', () => {
|
|
67
|
+
const key = QueryCacheManager.generateVisitKey({
|
|
68
|
+
url: 'https://example.com/page',
|
|
69
|
+
});
|
|
70
|
+
expect(key).toBe('visit:https://example.com/page');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should generate key with options', () => {
|
|
74
|
+
const key = QueryCacheManager.generateVisitKey({
|
|
75
|
+
url: 'https://example.com/page',
|
|
76
|
+
options: { extractImages: true },
|
|
77
|
+
});
|
|
78
|
+
expect(key).toContain('visit:https://example.com/page');
|
|
79
|
+
expect(key).toContain('extractImages');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('generateEmbeddingKey', () => {
|
|
84
|
+
it('should generate key with short text', () => {
|
|
85
|
+
const key = QueryCacheManager.generateEmbeddingKey({
|
|
86
|
+
text: 'short text',
|
|
87
|
+
});
|
|
88
|
+
expect(key).toBe('embedding:default:short text');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should generate key with model', () => {
|
|
92
|
+
const key = QueryCacheManager.generateEmbeddingKey({
|
|
93
|
+
text: 'test',
|
|
94
|
+
model: 'ada-002',
|
|
95
|
+
});
|
|
96
|
+
expect(key).toBe('embedding:ada-002:test');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should hash long text', () => {
|
|
100
|
+
const longText = 'a'.repeat(200);
|
|
101
|
+
const key = QueryCacheManager.generateEmbeddingKey({ text: longText });
|
|
102
|
+
expect(key).toContain('embedding:default:');
|
|
103
|
+
expect(key.length).toBeLessThan(50); // ハッシュ化されている
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('search cache', () => {
|
|
108
|
+
it('should cache and retrieve search results', async () => {
|
|
109
|
+
const params: SearchCacheKeyParams = {
|
|
110
|
+
query: 'AI trends 2026',
|
|
111
|
+
engine: 'google',
|
|
112
|
+
};
|
|
113
|
+
const value = {
|
|
114
|
+
results: [
|
|
115
|
+
{ title: 'AI in 2026', url: 'https://example.com/ai' },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await manager.setSearchResult(params, value);
|
|
120
|
+
const result = await manager.getSearchResult(params);
|
|
121
|
+
|
|
122
|
+
expect(result.hit).toBe(true);
|
|
123
|
+
expect(result.value).toEqual(value);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return miss for uncached search', async () => {
|
|
127
|
+
const result = await manager.getSearchResult({
|
|
128
|
+
query: 'uncached query',
|
|
129
|
+
});
|
|
130
|
+
expect(result.hit).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should set source to search', async () => {
|
|
134
|
+
const params: SearchCacheKeyParams = { query: 'test' };
|
|
135
|
+
await manager.setSearchResult(params, { data: 'test' });
|
|
136
|
+
const result = await manager.getSearchResult(params);
|
|
137
|
+
|
|
138
|
+
expect(result.meta?.source).toBe('search');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('visit cache', () => {
|
|
143
|
+
it('should cache and retrieve visit results', async () => {
|
|
144
|
+
const params: VisitCacheKeyParams = {
|
|
145
|
+
url: 'https://example.com/article',
|
|
146
|
+
};
|
|
147
|
+
const value = {
|
|
148
|
+
title: 'Article Title',
|
|
149
|
+
content: 'Article content...',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
await manager.setVisitResult(params, value);
|
|
153
|
+
const result = await manager.getVisitResult(params);
|
|
154
|
+
|
|
155
|
+
expect(result.hit).toBe(true);
|
|
156
|
+
expect(result.value).toEqual(value);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return miss for uncached visit', async () => {
|
|
160
|
+
const result = await manager.getVisitResult({
|
|
161
|
+
url: 'https://uncached.com/page',
|
|
162
|
+
});
|
|
163
|
+
expect(result.hit).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should set source to visit', async () => {
|
|
167
|
+
const params: VisitCacheKeyParams = { url: 'https://example.com' };
|
|
168
|
+
await manager.setVisitResult(params, { data: 'test' });
|
|
169
|
+
const result = await manager.getVisitResult(params);
|
|
170
|
+
|
|
171
|
+
expect(result.meta?.source).toBe('visit');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('embedding cache', () => {
|
|
176
|
+
it('should cache and retrieve embeddings', async () => {
|
|
177
|
+
const params: EmbeddingCacheKeyParams = {
|
|
178
|
+
text: 'Hello world',
|
|
179
|
+
model: 'ada-002',
|
|
180
|
+
};
|
|
181
|
+
const value = {
|
|
182
|
+
embedding: [0.1, 0.2, 0.3, 0.4],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
await manager.setEmbedding(params, value);
|
|
186
|
+
const result = await manager.getEmbedding(params);
|
|
187
|
+
|
|
188
|
+
expect(result.hit).toBe(true);
|
|
189
|
+
expect(result.value).toEqual(value);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return miss for uncached embedding', async () => {
|
|
193
|
+
const result = await manager.getEmbedding({
|
|
194
|
+
text: 'uncached text',
|
|
195
|
+
});
|
|
196
|
+
expect(result.hit).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should set source to embedding', async () => {
|
|
200
|
+
const params: EmbeddingCacheKeyParams = { text: 'test' };
|
|
201
|
+
await manager.setEmbedding(params, { vector: [0.1] });
|
|
202
|
+
const result = await manager.getEmbedding(params);
|
|
203
|
+
|
|
204
|
+
expect(result.meta?.source).toBe('embedding');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('generic cache', () => {
|
|
209
|
+
it('should cache and retrieve generic data', async () => {
|
|
210
|
+
const key = 'custom:analysis:report-001';
|
|
211
|
+
const value = { score: 85, details: 'Good' };
|
|
212
|
+
|
|
213
|
+
await manager.set(key, value, { source: 'analysis' });
|
|
214
|
+
const result = await manager.get(key);
|
|
215
|
+
|
|
216
|
+
expect(result.hit).toBe(true);
|
|
217
|
+
expect(result.value).toEqual(value);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('delete', () => {
|
|
222
|
+
it('should delete a cached entry', async () => {
|
|
223
|
+
const params: SearchCacheKeyParams = { query: 'to delete' };
|
|
224
|
+
await manager.setSearchResult(params, { data: 'test' });
|
|
225
|
+
|
|
226
|
+
const key = QueryCacheManager.generateSearchKey(params);
|
|
227
|
+
const deleted = await manager.delete(key);
|
|
228
|
+
|
|
229
|
+
expect(deleted).toBe(true);
|
|
230
|
+
|
|
231
|
+
const result = await manager.getSearchResult(params);
|
|
232
|
+
expect(result.hit).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('invalidateUrl', () => {
|
|
237
|
+
it('should invalidate URL-related caches', async () => {
|
|
238
|
+
const url = 'https://example.com/page';
|
|
239
|
+
await manager.setVisitResult({ url }, { content: 'test' });
|
|
240
|
+
|
|
241
|
+
const deletedCount = await manager.invalidateUrl(url);
|
|
242
|
+
|
|
243
|
+
expect(deletedCount).toBeGreaterThanOrEqual(1);
|
|
244
|
+
|
|
245
|
+
const result = await manager.getVisitResult({ url });
|
|
246
|
+
expect(result.hit).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('invalidateBySource', () => {
|
|
251
|
+
it('should invalidate all entries of a source', async () => {
|
|
252
|
+
await manager.setSearchResult({ query: 'q1' }, { data: '1' });
|
|
253
|
+
await manager.setSearchResult({ query: 'q2' }, { data: '2' });
|
|
254
|
+
await manager.setVisitResult({ url: 'https://example.com' }, { data: '3' });
|
|
255
|
+
|
|
256
|
+
const deletedCount = await manager.invalidateBySource('search');
|
|
257
|
+
|
|
258
|
+
expect(deletedCount).toBe(2);
|
|
259
|
+
|
|
260
|
+
const r1 = await manager.getSearchResult({ query: 'q1' });
|
|
261
|
+
const r2 = await manager.getSearchResult({ query: 'q2' });
|
|
262
|
+
const r3 = await manager.getVisitResult({ url: 'https://example.com' });
|
|
263
|
+
|
|
264
|
+
expect(r1.hit).toBe(false);
|
|
265
|
+
expect(r2.hit).toBe(false);
|
|
266
|
+
expect(r3.hit).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('clear', () => {
|
|
271
|
+
it('should clear all cached entries', async () => {
|
|
272
|
+
await manager.setSearchResult({ query: 'q1' }, { data: '1' });
|
|
273
|
+
await manager.setVisitResult({ url: 'https://example.com' }, { data: '2' });
|
|
274
|
+
await manager.setEmbedding({ text: 'test' }, { vector: [0.1] });
|
|
275
|
+
|
|
276
|
+
await manager.clear();
|
|
277
|
+
|
|
278
|
+
const r1 = await manager.getSearchResult({ query: 'q1' });
|
|
279
|
+
const r2 = await manager.getVisitResult({ url: 'https://example.com' });
|
|
280
|
+
const r3 = await manager.getEmbedding({ text: 'test' });
|
|
281
|
+
|
|
282
|
+
expect(r1.hit).toBe(false);
|
|
283
|
+
expect(r2.hit).toBe(false);
|
|
284
|
+
expect(r3.hit).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('getStats', () => {
|
|
289
|
+
it('should return cache statistics', async () => {
|
|
290
|
+
await manager.setSearchResult({ query: 'q1' }, { data: '1' });
|
|
291
|
+
await manager.getSearchResult({ query: 'q1' }); // hit
|
|
292
|
+
await manager.getSearchResult({ query: 'q2' }); // miss
|
|
293
|
+
|
|
294
|
+
const stats = await manager.getStats();
|
|
295
|
+
|
|
296
|
+
expect(stats.totalEntries).toBe(1);
|
|
297
|
+
expect(stats.hits).toBeGreaterThanOrEqual(1);
|
|
298
|
+
expect(stats.misses).toBeGreaterThanOrEqual(1);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('getSummary', () => {
|
|
303
|
+
it('should return formatted summary string', async () => {
|
|
304
|
+
await manager.setSearchResult({ query: 'test' }, { data: 'value' });
|
|
305
|
+
await manager.getSearchResult({ query: 'test' });
|
|
306
|
+
|
|
307
|
+
const summary = await manager.getSummary();
|
|
308
|
+
|
|
309
|
+
expect(summary).toContain('Cache Summary:');
|
|
310
|
+
expect(summary).toContain('Entries:');
|
|
311
|
+
expect(summary).toContain('Size:');
|
|
312
|
+
expect(summary).toContain('Hit Rate:');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('maintenance tasks', () => {
|
|
317
|
+
it('should start and stop maintenance tasks', () => {
|
|
318
|
+
manager.startMaintenanceTasks();
|
|
319
|
+
// タイマーが設定されていることを確認
|
|
320
|
+
expect(() => manager.stopMaintenanceTasks()).not.toThrow();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('getStore', () => {
|
|
325
|
+
it('should return the underlying store', () => {
|
|
326
|
+
const store = manager.getStore();
|
|
327
|
+
expect(store).toBeDefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('createCacheManager', () => {
|
|
333
|
+
it('should create a new QueryCacheManager instance', () => {
|
|
334
|
+
const manager = createCacheManager();
|
|
335
|
+
expect(manager).toBeInstanceOf(QueryCacheManager);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should accept custom config', () => {
|
|
339
|
+
const manager = createCacheManager({
|
|
340
|
+
maxEntries: 500,
|
|
341
|
+
defaultTtlSeconds: 7200,
|
|
342
|
+
});
|
|
343
|
+
expect(manager).toBeInstanceOf(QueryCacheManager);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('getDefaultCacheManager', () => {
|
|
348
|
+
it('should return a singleton instance', () => {
|
|
349
|
+
const manager1 = getDefaultCacheManager();
|
|
350
|
+
const manager2 = getDefaultCacheManager();
|
|
351
|
+
expect(manager1).toBe(manager2);
|
|
352
|
+
});
|
|
353
|
+
});
|