@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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VisitRecoveryManager テスト
|
|
3
|
+
*
|
|
4
|
+
* TSK-1-004: VisitRecoveryManager実装
|
|
5
|
+
* REQ-SRCH-004-01: visit失敗時フォールバック
|
|
6
|
+
* REQ-SRCH-004-02: 自動リトライ
|
|
7
|
+
* REQ-SRCH-004-03: 結果マージ
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
VisitRecoveryManager,
|
|
13
|
+
createVisitRecoveryManager,
|
|
14
|
+
DEFAULT_VISIT_RECOVERY_CONFIG,
|
|
15
|
+
type PageFetchResult,
|
|
16
|
+
} from '../index.js';
|
|
17
|
+
import { RecoveryLogger } from '../../../search/recovery/logger.js';
|
|
18
|
+
|
|
19
|
+
// globalThis.fetch をモック(WaybackClient用)
|
|
20
|
+
const mockFetch = vi.fn();
|
|
21
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
22
|
+
|
|
23
|
+
describe('VisitRecoveryManager', () => {
|
|
24
|
+
let manager: VisitRecoveryManager;
|
|
25
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
mockFetch.mockReset();
|
|
29
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
30
|
+
manager = new VisitRecoveryManager({
|
|
31
|
+
retryDelayMs: 10, // テスト高速化
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
consoleErrorSpy.mockRestore();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('constructor', () => {
|
|
40
|
+
it('should use default config when no config provided', () => {
|
|
41
|
+
const manager = new VisitRecoveryManager();
|
|
42
|
+
expect(manager).toBeInstanceOf(VisitRecoveryManager);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should merge custom config with defaults', () => {
|
|
46
|
+
const manager = new VisitRecoveryManager({ maxRetries: 5 });
|
|
47
|
+
expect(manager).toBeInstanceOf(VisitRecoveryManager);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should disable wayback when enableWayback is false', () => {
|
|
51
|
+
const manager = new VisitRecoveryManager({ enableWayback: false });
|
|
52
|
+
expect(manager.getWaybackClient()).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should use provided logger instance', () => {
|
|
56
|
+
const customLogger = new RecoveryLogger({ warnThreshold: 100 });
|
|
57
|
+
const manager = new VisitRecoveryManager({ logger: customLogger });
|
|
58
|
+
expect(manager.getLogger()).toBe(customLogger);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('recover', () => {
|
|
63
|
+
it('should return success on first attempt when fetch succeeds', async () => {
|
|
64
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
65
|
+
success: true,
|
|
66
|
+
content: 'Test content',
|
|
67
|
+
title: 'Test Title',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
71
|
+
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
expect(result.content).toBe('Test content');
|
|
74
|
+
expect(result.title).toBe('Test Title');
|
|
75
|
+
expect(result.usedWayback).toBe(false);
|
|
76
|
+
expect(result.attempts).toBe(1);
|
|
77
|
+
expect(mockFetchFn).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should retry on failure', async () => {
|
|
81
|
+
const mockFetchFn = vi
|
|
82
|
+
.fn()
|
|
83
|
+
.mockResolvedValueOnce({ success: false, error: 'Network error' })
|
|
84
|
+
.mockResolvedValueOnce({ success: false, error: 'Network error' })
|
|
85
|
+
.mockResolvedValueOnce({
|
|
86
|
+
success: true,
|
|
87
|
+
content: 'Test content',
|
|
88
|
+
title: 'Test Title',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
92
|
+
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
expect(result.attempts).toBe(3);
|
|
95
|
+
expect(mockFetchFn).toHaveBeenCalledTimes(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should try wayback after all direct retries fail', async () => {
|
|
99
|
+
const mockFetchFn = vi
|
|
100
|
+
.fn()
|
|
101
|
+
.mockResolvedValue({ success: false, error: 'Page not found' });
|
|
102
|
+
|
|
103
|
+
// Wayback API mock
|
|
104
|
+
mockFetch.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => ({
|
|
107
|
+
url: 'https://example.com',
|
|
108
|
+
archived_snapshots: {
|
|
109
|
+
closest: {
|
|
110
|
+
url: 'https://web.archive.org/web/20240101/https://example.com',
|
|
111
|
+
timestamp: '20240101120000',
|
|
112
|
+
available: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Wayback取得も失敗
|
|
119
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
120
|
+
|
|
121
|
+
expect(result.success).toBe(false);
|
|
122
|
+
expect(mockFetch).toHaveBeenCalled(); // Wayback APIが呼ばれた
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return wayback content on wayback success', async () => {
|
|
126
|
+
const mockFetchFn = vi
|
|
127
|
+
.fn()
|
|
128
|
+
.mockImplementation((url: string) => {
|
|
129
|
+
if (url.includes('web.archive.org')) {
|
|
130
|
+
return Promise.resolve({
|
|
131
|
+
success: true,
|
|
132
|
+
content: 'Archived content',
|
|
133
|
+
title: 'Archived Title',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return Promise.resolve({ success: false, error: 'Page not found' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Wayback API mock
|
|
140
|
+
mockFetch.mockResolvedValueOnce({
|
|
141
|
+
ok: true,
|
|
142
|
+
json: async () => ({
|
|
143
|
+
url: 'https://example.com',
|
|
144
|
+
archived_snapshots: {
|
|
145
|
+
closest: {
|
|
146
|
+
url: 'https://web.archive.org/web/20240101/https://example.com',
|
|
147
|
+
timestamp: '20240101120000',
|
|
148
|
+
available: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
155
|
+
|
|
156
|
+
expect(result.success).toBe(true);
|
|
157
|
+
expect(result.usedWayback).toBe(true);
|
|
158
|
+
expect(result.content).toBe('Archived content');
|
|
159
|
+
expect(result.waybackSnapshot).toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return failure when all attempts fail', async () => {
|
|
163
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
164
|
+
success: false,
|
|
165
|
+
error: 'Page not found',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Wayback API mock - no archive available
|
|
169
|
+
mockFetch.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
json: async () => ({
|
|
172
|
+
url: 'https://example.com',
|
|
173
|
+
archived_snapshots: {},
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
178
|
+
|
|
179
|
+
expect(result.success).toBe(false);
|
|
180
|
+
expect(result.error).toBeDefined();
|
|
181
|
+
expect(result.attempts).toBeGreaterThan(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle fetch function throwing errors', async () => {
|
|
185
|
+
const mockFetchFn = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
186
|
+
|
|
187
|
+
// Wayback API mock - no archive
|
|
188
|
+
mockFetch.mockResolvedValueOnce({
|
|
189
|
+
ok: true,
|
|
190
|
+
json: async () => ({
|
|
191
|
+
url: 'https://example.com',
|
|
192
|
+
archived_snapshots: {},
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await manager.recover('https://example.com', mockFetchFn);
|
|
197
|
+
|
|
198
|
+
expect(result.success).toBe(false);
|
|
199
|
+
expect(result.error).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should skip wayback when enableWayback is false', async () => {
|
|
203
|
+
const noWaybackManager = new VisitRecoveryManager({
|
|
204
|
+
enableWayback: false,
|
|
205
|
+
maxRetries: 1,
|
|
206
|
+
retryDelayMs: 10,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
210
|
+
success: false,
|
|
211
|
+
error: 'Page not found',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await noWaybackManager.recover('https://example.com', mockFetchFn);
|
|
215
|
+
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
expect(result.usedWayback).toBe(false);
|
|
218
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('logging', () => {
|
|
223
|
+
it('should log attempts to RecoveryLogger', async () => {
|
|
224
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
225
|
+
success: true,
|
|
226
|
+
content: 'Test content',
|
|
227
|
+
title: 'Test Title',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await manager.recover('https://example.com', mockFetchFn);
|
|
231
|
+
|
|
232
|
+
const entries = manager.getLogger().getEntries();
|
|
233
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
234
|
+
expect(entries[0].type).toBe('visit');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should output to stderr', async () => {
|
|
238
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
239
|
+
success: true,
|
|
240
|
+
content: 'Test content',
|
|
241
|
+
title: 'Test Title',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await manager.recover('https://example.com', mockFetchFn);
|
|
245
|
+
|
|
246
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
247
|
+
expect.stringContaining('[VisitRecovery]')
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('getStats', () => {
|
|
253
|
+
it('should return stats from logger', async () => {
|
|
254
|
+
const mockFetchFn = vi.fn().mockResolvedValue({
|
|
255
|
+
success: true,
|
|
256
|
+
content: 'Test content',
|
|
257
|
+
title: 'Test Title',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await manager.recover('https://example.com', mockFetchFn);
|
|
261
|
+
|
|
262
|
+
const stats = manager.getStats();
|
|
263
|
+
expect(stats.totalAttempts).toBeGreaterThan(0);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('createVisitRecoveryManager', () => {
|
|
268
|
+
it('should create a VisitRecoveryManager instance', () => {
|
|
269
|
+
const manager = createVisitRecoveryManager();
|
|
270
|
+
expect(manager).toBeInstanceOf(VisitRecoveryManager);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should create with custom config', () => {
|
|
274
|
+
const manager = createVisitRecoveryManager({ maxRetries: 5 });
|
|
275
|
+
expect(manager).toBeInstanceOf(VisitRecoveryManager);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('timeout handling', () => {
|
|
280
|
+
it('should timeout long-running fetch', async () => {
|
|
281
|
+
const slowManager = new VisitRecoveryManager({
|
|
282
|
+
timeoutMs: 100,
|
|
283
|
+
maxRetries: 0,
|
|
284
|
+
enableWayback: false,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const mockFetchFn = vi.fn().mockImplementation(
|
|
288
|
+
() => new Promise((resolve) => setTimeout(() => resolve({ success: true }), 500))
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const result = await slowManager.recover('https://example.com', mockFetchFn);
|
|
292
|
+
|
|
293
|
+
expect(result.success).toBe(false);
|
|
294
|
+
expect(result.error).toContain('timed out');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaybackClient テスト
|
|
3
|
+
*
|
|
4
|
+
* TSK-1-003: WaybackClient実装
|
|
5
|
+
* REQ-SRCH-004-01: visit失敗時フォールバック
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
WaybackClient,
|
|
11
|
+
createWaybackClient,
|
|
12
|
+
DEFAULT_WAYBACK_CONFIG,
|
|
13
|
+
type WaybackApiResponse,
|
|
14
|
+
type WaybackSnapshot,
|
|
15
|
+
} from '../wayback.js';
|
|
16
|
+
|
|
17
|
+
// globalThis.fetch をモック
|
|
18
|
+
const mockFetch = vi.fn();
|
|
19
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
20
|
+
|
|
21
|
+
describe('WaybackClient', () => {
|
|
22
|
+
let client: WaybackClient;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
client = new WaybackClient();
|
|
26
|
+
mockFetch.mockReset();
|
|
27
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('constructor', () => {
|
|
35
|
+
it('should use default config when no config provided', () => {
|
|
36
|
+
const client = new WaybackClient();
|
|
37
|
+
expect(client).toBeInstanceOf(WaybackClient);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should merge custom config with defaults', () => {
|
|
41
|
+
const client = new WaybackClient({ timeoutMs: 5000 });
|
|
42
|
+
expect(client).toBeInstanceOf(WaybackClient);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getSnapshot', () => {
|
|
47
|
+
it('should return snapshot when archive is available', async () => {
|
|
48
|
+
const mockResponse: WaybackApiResponse = {
|
|
49
|
+
url: 'https://example.com',
|
|
50
|
+
archived_snapshots: {
|
|
51
|
+
closest: {
|
|
52
|
+
url: 'https://web.archive.org/web/20240101120000/https://example.com',
|
|
53
|
+
timestamp: '20240101120000',
|
|
54
|
+
available: true,
|
|
55
|
+
status: '200',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockFetch.mockResolvedValueOnce({
|
|
61
|
+
ok: true,
|
|
62
|
+
json: async () => mockResponse,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await client.getSnapshot('https://example.com');
|
|
66
|
+
|
|
67
|
+
expect(result).not.toBeNull();
|
|
68
|
+
expect(result?.url).toBe('https://web.archive.org/web/20240101120000/https://example.com');
|
|
69
|
+
expect(result?.originalUrl).toBe('https://example.com');
|
|
70
|
+
expect(result?.available).toBe(true);
|
|
71
|
+
expect(result?.timestamp).toBe('2024-01-01T12:00:00Z');
|
|
72
|
+
expect(result?.status).toBe(200);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return null when no archive is available', async () => {
|
|
76
|
+
const mockResponse: WaybackApiResponse = {
|
|
77
|
+
url: 'https://example.com',
|
|
78
|
+
archived_snapshots: {},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
mockFetch.mockResolvedValueOnce({
|
|
82
|
+
ok: true,
|
|
83
|
+
json: async () => mockResponse,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await client.getSnapshot('https://example.com');
|
|
87
|
+
|
|
88
|
+
expect(result).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should retry on failure', async () => {
|
|
92
|
+
const mockResponse: WaybackApiResponse = {
|
|
93
|
+
url: 'https://example.com',
|
|
94
|
+
archived_snapshots: {
|
|
95
|
+
closest: {
|
|
96
|
+
url: 'https://web.archive.org/web/20240101120000/https://example.com',
|
|
97
|
+
timestamp: '20240101120000',
|
|
98
|
+
available: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 最初の2回は失敗、3回目で成功
|
|
104
|
+
mockFetch
|
|
105
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
106
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
107
|
+
.mockResolvedValueOnce({
|
|
108
|
+
ok: true,
|
|
109
|
+
json: async () => mockResponse,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// リトライ間隔を短くして高速化
|
|
113
|
+
const fastClient = new WaybackClient({ retryDelayMs: 10 });
|
|
114
|
+
const result = await fastClient.getSnapshot('https://example.com');
|
|
115
|
+
|
|
116
|
+
expect(result).not.toBeNull();
|
|
117
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return null after all retries fail', async () => {
|
|
121
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
122
|
+
|
|
123
|
+
const fastClient = new WaybackClient({ retryDelayMs: 10, maxRetries: 2 });
|
|
124
|
+
const result = await fastClient.getSnapshot('https://example.com');
|
|
125
|
+
|
|
126
|
+
expect(result).toBeNull();
|
|
127
|
+
expect(mockFetch).toHaveBeenCalledTimes(3); // 初回 + 2回リトライ
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle HTTP errors', async () => {
|
|
131
|
+
mockFetch.mockResolvedValue({
|
|
132
|
+
ok: false,
|
|
133
|
+
status: 503,
|
|
134
|
+
statusText: 'Service Unavailable',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const fastClient = new WaybackClient({ retryDelayMs: 10, maxRetries: 0 });
|
|
138
|
+
const result = await fastClient.getSnapshot('https://example.com');
|
|
139
|
+
|
|
140
|
+
expect(result).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle timeout', async () => {
|
|
144
|
+
// タイムアウトをシミュレート
|
|
145
|
+
mockFetch.mockImplementation(
|
|
146
|
+
() =>
|
|
147
|
+
new Promise((_, reject) => {
|
|
148
|
+
const error = new Error('Aborted');
|
|
149
|
+
error.name = 'AbortError';
|
|
150
|
+
reject(error);
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const fastClient = new WaybackClient({ timeoutMs: 100, maxRetries: 0 });
|
|
155
|
+
const result = await fastClient.getSnapshot('https://example.com');
|
|
156
|
+
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('isArchived', () => {
|
|
162
|
+
it('should return true when archive is available', async () => {
|
|
163
|
+
const mockResponse: WaybackApiResponse = {
|
|
164
|
+
url: 'https://example.com',
|
|
165
|
+
archived_snapshots: {
|
|
166
|
+
closest: {
|
|
167
|
+
url: 'https://web.archive.org/web/20240101120000/https://example.com',
|
|
168
|
+
timestamp: '20240101120000',
|
|
169
|
+
available: true,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
mockFetch.mockResolvedValueOnce({
|
|
175
|
+
ok: true,
|
|
176
|
+
json: async () => mockResponse,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const result = await client.isArchived('https://example.com');
|
|
180
|
+
|
|
181
|
+
expect(result).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return false when no archive is available', async () => {
|
|
185
|
+
mockFetch.mockResolvedValueOnce({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: async () => ({
|
|
188
|
+
url: 'https://example.com',
|
|
189
|
+
archived_snapshots: {},
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = await client.isArchived('https://example.com');
|
|
194
|
+
|
|
195
|
+
expect(result).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return false on error', async () => {
|
|
199
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
200
|
+
|
|
201
|
+
const fastClient = new WaybackClient({ retryDelayMs: 10, maxRetries: 0 });
|
|
202
|
+
const result = await fastClient.isArchived('https://example.com');
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('getArchiveUrl', () => {
|
|
209
|
+
it('should return archive URL when available', async () => {
|
|
210
|
+
const mockResponse: WaybackApiResponse = {
|
|
211
|
+
url: 'https://example.com',
|
|
212
|
+
archived_snapshots: {
|
|
213
|
+
closest: {
|
|
214
|
+
url: 'https://web.archive.org/web/20240101120000/https://example.com',
|
|
215
|
+
timestamp: '20240101120000',
|
|
216
|
+
available: true,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
mockFetch.mockResolvedValueOnce({
|
|
222
|
+
ok: true,
|
|
223
|
+
json: async () => mockResponse,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await client.getArchiveUrl('https://example.com');
|
|
227
|
+
|
|
228
|
+
expect(result).toBe('https://web.archive.org/web/20240101120000/https://example.com');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return null when not available', async () => {
|
|
232
|
+
mockFetch.mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: async () => ({
|
|
235
|
+
url: 'https://example.com',
|
|
236
|
+
archived_snapshots: {},
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const result = await client.getArchiveUrl('https://example.com');
|
|
241
|
+
|
|
242
|
+
expect(result).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should return null when available is false', async () => {
|
|
246
|
+
const mockResponse: WaybackApiResponse = {
|
|
247
|
+
url: 'https://example.com',
|
|
248
|
+
archived_snapshots: {
|
|
249
|
+
closest: {
|
|
250
|
+
url: 'https://web.archive.org/web/20240101120000/https://example.com',
|
|
251
|
+
timestamp: '20240101120000',
|
|
252
|
+
available: false,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
mockFetch.mockResolvedValueOnce({
|
|
258
|
+
ok: true,
|
|
259
|
+
json: async () => mockResponse,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const result = await client.getArchiveUrl('https://example.com');
|
|
263
|
+
|
|
264
|
+
expect(result).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('createWaybackClient', () => {
|
|
269
|
+
it('should create a WaybackClient instance', () => {
|
|
270
|
+
const client = createWaybackClient();
|
|
271
|
+
expect(client).toBeInstanceOf(WaybackClient);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should create a WaybackClient with custom config', () => {
|
|
275
|
+
const client = createWaybackClient({ timeoutMs: 5000 });
|
|
276
|
+
expect(client).toBeInstanceOf(WaybackClient);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('timestamp formatting', () => {
|
|
281
|
+
it('should format valid timestamp to ISO 8601', async () => {
|
|
282
|
+
const mockResponse: WaybackApiResponse = {
|
|
283
|
+
url: 'https://example.com',
|
|
284
|
+
archived_snapshots: {
|
|
285
|
+
closest: {
|
|
286
|
+
url: 'https://web.archive.org/web/20231215143025/https://example.com',
|
|
287
|
+
timestamp: '20231215143025',
|
|
288
|
+
available: true,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
mockFetch.mockResolvedValueOnce({
|
|
294
|
+
ok: true,
|
|
295
|
+
json: async () => mockResponse,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = await client.getSnapshot('https://example.com');
|
|
299
|
+
|
|
300
|
+
expect(result?.timestamp).toBe('2023-12-15T14:30:25Z');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should return invalid timestamp as-is', async () => {
|
|
304
|
+
const mockResponse: WaybackApiResponse = {
|
|
305
|
+
url: 'https://example.com',
|
|
306
|
+
archived_snapshots: {
|
|
307
|
+
closest: {
|
|
308
|
+
url: 'https://web.archive.org/web/invalid/https://example.com',
|
|
309
|
+
timestamp: 'invalid',
|
|
310
|
+
available: true,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
mockFetch.mockResolvedValueOnce({
|
|
316
|
+
ok: true,
|
|
317
|
+
json: async () => mockResponse,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const result = await client.getSnapshot('https://example.com');
|
|
321
|
+
|
|
322
|
+
expect(result?.timestamp).toBe('invalid');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('API URL encoding', () => {
|
|
327
|
+
it('should properly encode URL with special characters', async () => {
|
|
328
|
+
mockFetch.mockResolvedValueOnce({
|
|
329
|
+
ok: true,
|
|
330
|
+
json: async () => ({
|
|
331
|
+
url: 'https://example.com/path?query=value&foo=bar',
|
|
332
|
+
archived_snapshots: {},
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await client.getSnapshot('https://example.com/path?query=value&foo=bar');
|
|
337
|
+
|
|
338
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
339
|
+
expect.stringContaining(encodeURIComponent('https://example.com/path?query=value&foo=bar')),
|
|
340
|
+
expect.any(Object)
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|