@kya-os/create-mcpi-app 1.7.38-canary.1 → 1.7.38
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +755 -0
- package/.turbo/turbo-test.log +200 -0
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js +43 -912
- package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
- package/dist/utils/fetch-remote-config.d.ts.map +1 -1
- package/dist/utils/fetch-remote-config.js +2 -2
- package/dist/utils/fetch-remote-config.js.map +1 -1
- package/package/package.json +77 -0
- package/package.json +1 -1
- package/ARCHITECTURE_ANALYSIS.md +0 -392
- package/CHANGELOG.md +0 -372
- package/DEPRECATION_WARNINGS_ANALYSIS.md +0 -192
- package/IMPLEMENTATION_SUMMARY.md +0 -108
- package/REMEDIATION_PLAN.md +0 -99
- package/dist/.tsbuildinfo +0 -1
- package/scripts/prepare-pack.js +0 -47
- package/scripts/validate-no-workspace.js +0 -79
- package/src/__tests__/cloudflare-template.test.ts +0 -490
- package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +0 -337
- package/src/__tests__/helpers/generate-config.test.ts +0 -312
- package/src/__tests__/helpers/generate-identity.test.ts +0 -271
- package/src/__tests__/helpers/install.test.ts +0 -370
- package/src/__tests__/helpers/validate-project-structure.test.ts +0 -467
- package/src/__tests__.bak/regression.test.ts +0 -434
- package/src/effects/index.ts +0 -80
- package/src/helpers/__tests__/config-builder.spec.ts +0 -231
- package/src/helpers/apply-identity-preset.ts +0 -209
- package/src/helpers/config-builder.ts +0 -165
- package/src/helpers/copy-template.ts +0 -11
- package/src/helpers/create.ts +0 -239
- package/src/helpers/fetch-cloudflare-mcpi-template.ts +0 -2393
- package/src/helpers/fetch-cloudflare-template.ts +0 -361
- package/src/helpers/fetch-mcpi-template.ts +0 -236
- package/src/helpers/fetch-xmcp-template.ts +0 -153
- package/src/helpers/generate-config.ts +0 -118
- package/src/helpers/generate-identity.ts +0 -163
- package/src/helpers/identity-manager.ts +0 -186
- package/src/helpers/install.ts +0 -79
- package/src/helpers/rename.ts +0 -17
- package/src/helpers/validate-project-structure.ts +0 -127
- package/src/index.ts +0 -520
- package/src/utils/__tests__/fetch-remote-config.test.ts +0 -271
- package/src/utils/check-node.ts +0 -17
- package/src/utils/fetch-remote-config.ts +0 -179
- package/src/utils/is-folder-empty.ts +0 -60
- package/src/utils/validate-project-name.ts +0 -132
- package/test-cloudflare/README.md +0 -164
- package/test-cloudflare/package.json +0 -28
- package/test-cloudflare/src/index.ts +0 -341
- package/test-cloudflare/src/tools/greet.ts +0 -19
- package/test-cloudflare/tests/cache-invalidation.test.ts +0 -410
- package/test-cloudflare/tests/cors-security.test.ts +0 -349
- package/test-cloudflare/tests/delegation.test.ts +0 -335
- package/test-cloudflare/tests/do-routing.test.ts +0 -314
- package/test-cloudflare/tests/integration.test.ts +0 -205
- package/test-cloudflare/tests/session-management.test.ts +0 -359
- package/test-cloudflare/tsconfig.json +0 -16
- package/test-cloudflare/vitest.config.ts +0 -9
- package/test-cloudflare/wrangler.toml +0 -37
- package/test-node/README.md +0 -44
- package/test-node/package.json +0 -23
- package/test-node/src/tools/greet.ts +0 -25
- package/test-node/xmcp.config.ts +0 -20
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -14
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cache Invalidation Tests
|
|
5
|
-
* Tests various cache invalidation strategies for security and performance
|
|
6
|
-
*/
|
|
7
|
-
describe('Cache Invalidation Strategies', () => {
|
|
8
|
-
|
|
9
|
-
// Mock KV namespaces
|
|
10
|
-
const mockKV = {
|
|
11
|
-
get: vi.fn(),
|
|
12
|
-
put: vi.fn(),
|
|
13
|
-
delete: vi.fn(),
|
|
14
|
-
list: vi.fn()
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
global.fetch = vi.fn();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('Short TTL Strategy (Recommended)', () => {
|
|
23
|
-
test('should use 5-minute TTL for delegation cache', async () => {
|
|
24
|
-
const token = 'test-token';
|
|
25
|
-
const CACHE_TTL = 5 * 60; // 5 minutes
|
|
26
|
-
|
|
27
|
-
// Store with short TTL
|
|
28
|
-
await mockKV.put(
|
|
29
|
-
`delegation:${token}`,
|
|
30
|
-
'valid',
|
|
31
|
-
{ expirationTtl: CACHE_TTL }
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
expect(mockKV.put).toHaveBeenCalledWith(
|
|
35
|
-
`delegation:${token}`,
|
|
36
|
-
'valid',
|
|
37
|
-
{ expirationTtl: 300 } // 5 minutes in seconds
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('should use 1-minute TTL for verification cache', async () => {
|
|
42
|
-
const token = 'verified-token';
|
|
43
|
-
const VERIFICATION_TTL = 60; // 1 minute
|
|
44
|
-
|
|
45
|
-
// Cache verification result
|
|
46
|
-
await mockKV.put(
|
|
47
|
-
`verified:${token.substring(0, 16)}`,
|
|
48
|
-
'1',
|
|
49
|
-
{ expirationTtl: VERIFICATION_TTL }
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
expect(mockKV.put).toHaveBeenCalledWith(
|
|
53
|
-
expect.stringContaining('verified:'),
|
|
54
|
-
'1',
|
|
55
|
-
{ expirationTtl: 60 }
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('should re-verify after TTL expiration', async () => {
|
|
60
|
-
const token = 'expired-cache-token';
|
|
61
|
-
const cacheKey = `verified:${token.substring(0, 16)}`;
|
|
62
|
-
|
|
63
|
-
// First check - cache miss
|
|
64
|
-
mockKV.get.mockResolvedValueOnce(null);
|
|
65
|
-
|
|
66
|
-
// Verify with API
|
|
67
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
68
|
-
ok: true
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Actually call the API
|
|
72
|
-
await global.fetch('https://test-api/verify');
|
|
73
|
-
|
|
74
|
-
// Cache the result
|
|
75
|
-
await mockKV.put(cacheKey, '1', { expirationTtl: 60 });
|
|
76
|
-
|
|
77
|
-
// After TTL expires, should re-verify
|
|
78
|
-
mockKV.get.mockResolvedValueOnce(null); // Cache expired
|
|
79
|
-
|
|
80
|
-
// Call API again
|
|
81
|
-
await global.fetch('https://test-api/verify');
|
|
82
|
-
|
|
83
|
-
// Should have been called twice
|
|
84
|
-
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('should handle cache miss gracefully', async () => {
|
|
88
|
-
const token = 'no-cache-token';
|
|
89
|
-
|
|
90
|
-
// Cache miss
|
|
91
|
-
mockKV.get.mockResolvedValueOnce(null);
|
|
92
|
-
|
|
93
|
-
// Should proceed with verification
|
|
94
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
95
|
-
ok: true
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Should not throw
|
|
99
|
-
expect(() => mockKV.get(`verified:${token.substring(0, 16)}`)).not.toThrow();
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('Immediate Invalidation on Revocation', () => {
|
|
104
|
-
test('should invalidate all caches when delegation revoked', async () => {
|
|
105
|
-
const sessionId = 'revoked-session';
|
|
106
|
-
const agentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
107
|
-
const token = 'revoked-token';
|
|
108
|
-
|
|
109
|
-
const keysToDelete = [
|
|
110
|
-
`session:${sessionId}`,
|
|
111
|
-
`agent:${agentDid}:delegation`,
|
|
112
|
-
`verified:${token.substring(0, 16)}`
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
// Perform invalidation
|
|
116
|
-
for (const key of keysToDelete) {
|
|
117
|
-
await mockKV.delete(key);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Verify all caches cleared
|
|
121
|
-
expect(mockKV.delete).toHaveBeenCalledTimes(3);
|
|
122
|
-
expect(mockKV.delete).toHaveBeenCalledWith(`session:${sessionId}`);
|
|
123
|
-
expect(mockKV.delete).toHaveBeenCalledWith(`agent:${agentDid}:delegation`);
|
|
124
|
-
expect(mockKV.delete).toHaveBeenCalledWith(`verified:${token.substring(0, 16)}`);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('should handle partial invalidation failures', async () => {
|
|
128
|
-
const sessionId = 'partial-fail-session';
|
|
129
|
-
const token = 'partial-fail-token';
|
|
130
|
-
|
|
131
|
-
// Some deletions fail
|
|
132
|
-
mockKV.delete
|
|
133
|
-
.mockResolvedValueOnce(undefined) // Success
|
|
134
|
-
.mockRejectedValueOnce(new Error('KV error')) // Fail
|
|
135
|
-
.mockResolvedValueOnce(undefined); // Success
|
|
136
|
-
|
|
137
|
-
const deletions = [
|
|
138
|
-
mockKV.delete(`session:${sessionId}`),
|
|
139
|
-
mockKV.delete(`agent:test:delegation`).catch(() => null),
|
|
140
|
-
mockKV.delete(`verified:${token.substring(0, 16)}`)
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
const results = await Promise.allSettled(deletions);
|
|
144
|
-
|
|
145
|
-
// Should not throw, handle failures gracefully
|
|
146
|
-
expect(results[0].status).toBe('fulfilled');
|
|
147
|
-
expect(results[1].status).toBe('fulfilled'); // Caught error
|
|
148
|
-
expect(results[2].status).toBe('fulfilled');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('should invalidate when API returns 401/403', async () => {
|
|
152
|
-
const token = 'unauthorized-token';
|
|
153
|
-
|
|
154
|
-
// API returns unauthorized
|
|
155
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
156
|
-
ok: false,
|
|
157
|
-
status: 401
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Should trigger invalidation
|
|
161
|
-
await mockKV.delete(`verified:${token.substring(0, 16)}`);
|
|
162
|
-
await mockKV.delete(`delegation:${token}`);
|
|
163
|
-
|
|
164
|
-
expect(mockKV.delete).toHaveBeenCalledTimes(2);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('Batch Invalidation', () => {
|
|
169
|
-
test('should invalidate all tokens for a user', async () => {
|
|
170
|
-
const userId = 'user-123';
|
|
171
|
-
|
|
172
|
-
// List all user's tokens
|
|
173
|
-
mockKV.list.mockResolvedValueOnce({
|
|
174
|
-
keys: [
|
|
175
|
-
{ name: `user:${userId}:delegation:1` },
|
|
176
|
-
{ name: `user:${userId}:delegation:2` },
|
|
177
|
-
{ name: `user:${userId}:delegation:3` }
|
|
178
|
-
]
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Get list of keys
|
|
182
|
-
const result = await mockKV.list({ prefix: `user:${userId}:` });
|
|
183
|
-
|
|
184
|
-
// Delete all
|
|
185
|
-
const deletions = result.keys.map(key =>
|
|
186
|
-
mockKV.delete(key.name)
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
await Promise.all(deletions);
|
|
190
|
-
|
|
191
|
-
expect(mockKV.delete).toHaveBeenCalledTimes(3);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('should invalidate all tokens for an agent', async () => {
|
|
195
|
-
const agentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
196
|
-
|
|
197
|
-
// List all agent's tokens
|
|
198
|
-
mockKV.list.mockResolvedValueOnce({
|
|
199
|
-
keys: [
|
|
200
|
-
{ name: `agent:${agentDid}:delegation` },
|
|
201
|
-
{ name: `agent:${agentDid}:session:1` },
|
|
202
|
-
{ name: `agent:${agentDid}:session:2` }
|
|
203
|
-
]
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
const result = await mockKV.list({ prefix: `agent:${agentDid}:` });
|
|
207
|
-
|
|
208
|
-
// Delete all
|
|
209
|
-
const deletions = result.keys.map(key =>
|
|
210
|
-
mockKV.delete(key.name)
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
await Promise.all(deletions);
|
|
214
|
-
|
|
215
|
-
expect(mockKV.delete).toHaveBeenCalledTimes(3);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe('Performance Optimization', () => {
|
|
220
|
-
test('should batch verification requests', async () => {
|
|
221
|
-
const tokens = ['token1', 'token2', 'token3'];
|
|
222
|
-
|
|
223
|
-
// Check cache for all tokens
|
|
224
|
-
mockKV.get
|
|
225
|
-
.mockResolvedValueOnce(null) // token1 miss
|
|
226
|
-
.mockResolvedValueOnce('1') // token2 hit
|
|
227
|
-
.mockResolvedValueOnce(null); // token3 miss
|
|
228
|
-
|
|
229
|
-
// Only verify uncached tokens
|
|
230
|
-
const toVerify = ['token1', 'token3'];
|
|
231
|
-
|
|
232
|
-
// Batch API call
|
|
233
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
234
|
-
ok: true,
|
|
235
|
-
json: async () => ({
|
|
236
|
-
results: {
|
|
237
|
-
token1: { valid: true },
|
|
238
|
-
token3: { valid: false }
|
|
239
|
-
}
|
|
240
|
-
})
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Make the batch API call
|
|
244
|
-
await global.fetch('https://test-api/batch-verify', {
|
|
245
|
-
method: 'POST',
|
|
246
|
-
body: JSON.stringify({ tokens: toVerify })
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// Should have made single API call for multiple tokens
|
|
250
|
-
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test('should use stale-while-revalidate pattern', async () => {
|
|
254
|
-
const token = 'stale-token';
|
|
255
|
-
const cacheKey = `verified:${token.substring(0, 16)}`;
|
|
256
|
-
|
|
257
|
-
// Stale-while-revalidate pattern test
|
|
258
|
-
// This test verifies that:
|
|
259
|
-
// 1. Cached value is returned immediately (fast response)
|
|
260
|
-
// 2. Background revalidation happens asynchronously
|
|
261
|
-
|
|
262
|
-
// Simulate getting a cached value
|
|
263
|
-
const staleCachedValue = '1';
|
|
264
|
-
|
|
265
|
-
// In a real implementation:
|
|
266
|
-
// - Check cache first, return immediately if found (stale but fast)
|
|
267
|
-
// - Trigger background revalidation without blocking
|
|
268
|
-
// - User experiences fast response with eventual consistency
|
|
269
|
-
|
|
270
|
-
expect(staleCachedValue).toBe('1'); // Immediate return
|
|
271
|
-
|
|
272
|
-
// Background revalidation would happen asynchronously
|
|
273
|
-
const backgroundRevalidation = async () => {
|
|
274
|
-
await mockKV.put(cacheKey, '1', { expirationTtl: 60 });
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// Verify pattern is conceptually sound
|
|
278
|
-
expect(typeof backgroundRevalidation).toBe('function');
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
describe('Cache Consistency', () => {
|
|
283
|
-
test('should maintain consistency across DO instances', async () => {
|
|
284
|
-
const token = 'consistent-token';
|
|
285
|
-
|
|
286
|
-
// Instance 1 invalidates
|
|
287
|
-
await mockKV.delete(`verified:${token.substring(0, 16)}`);
|
|
288
|
-
|
|
289
|
-
// Instance 2 should see invalidation
|
|
290
|
-
mockKV.get.mockResolvedValueOnce(null);
|
|
291
|
-
|
|
292
|
-
const result = await mockKV.get(`verified:${token.substring(0, 16)}`);
|
|
293
|
-
expect(result).toBeNull();
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('should handle race conditions during invalidation', async () => {
|
|
297
|
-
const token = 'race-condition-token';
|
|
298
|
-
|
|
299
|
-
// Simulate concurrent operations
|
|
300
|
-
const operations = [
|
|
301
|
-
mockKV.put(`verified:${token.substring(0, 16)}`, '1', { expirationTtl: 60 }),
|
|
302
|
-
mockKV.delete(`verified:${token.substring(0, 16)}`),
|
|
303
|
-
mockKV.get(`verified:${token.substring(0, 16)}`)
|
|
304
|
-
];
|
|
305
|
-
|
|
306
|
-
// All operations should complete without error
|
|
307
|
-
const results = await Promise.allSettled(operations);
|
|
308
|
-
|
|
309
|
-
expect(results.every(r => r.status === 'fulfilled')).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('Monitoring and Metrics', () => {
|
|
314
|
-
test('should track cache hit rate', () => {
|
|
315
|
-
const metrics = {
|
|
316
|
-
cacheHits: 0,
|
|
317
|
-
cacheMisses: 0,
|
|
318
|
-
get hitRate() {
|
|
319
|
-
const total = this.cacheHits + this.cacheMisses;
|
|
320
|
-
return total > 0 ? this.cacheHits / total : 0;
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// Simulate cache operations
|
|
325
|
-
metrics.cacheHits += 8;
|
|
326
|
-
metrics.cacheMisses += 2;
|
|
327
|
-
|
|
328
|
-
expect(metrics.hitRate).toBe(0.8); // 80% hit rate
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test('should track invalidation frequency', () => {
|
|
332
|
-
const metrics = {
|
|
333
|
-
invalidations: [],
|
|
334
|
-
addInvalidation(reason: string) {
|
|
335
|
-
this.invalidations.push({
|
|
336
|
-
timestamp: Date.now(),
|
|
337
|
-
reason
|
|
338
|
-
});
|
|
339
|
-
},
|
|
340
|
-
getRate(windowMs: number) {
|
|
341
|
-
const now = Date.now();
|
|
342
|
-
const recent = this.invalidations.filter(
|
|
343
|
-
i => i.timestamp > now - windowMs
|
|
344
|
-
);
|
|
345
|
-
return recent.length;
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// Track invalidations
|
|
350
|
-
metrics.addInvalidation('revocation');
|
|
351
|
-
metrics.addInvalidation('expiration');
|
|
352
|
-
metrics.addInvalidation('manual');
|
|
353
|
-
|
|
354
|
-
// Check rate in last minute
|
|
355
|
-
const ratePerMinute = metrics.getRate(60000);
|
|
356
|
-
expect(ratePerMinute).toBe(3);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe('Edge Cases', () => {
|
|
361
|
-
test('should handle KV storage limits', async () => {
|
|
362
|
-
// KV has 25 MiB value limit
|
|
363
|
-
// Use a smaller token for testing (1 MiB instead of 26 MiB to avoid test output issues)
|
|
364
|
-
const largeToken = 'x'.repeat(1024 * 1024); // 1 MiB
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
await mockKV.put('large-key', largeToken);
|
|
368
|
-
} catch (error: any) {
|
|
369
|
-
expect(error.message).toContain('too large');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Should use token hash instead
|
|
373
|
-
const tokenHash = 'hash-of-large-token';
|
|
374
|
-
await mockKV.put(`verified:${tokenHash.substring(0, 16)}`, '1');
|
|
375
|
-
|
|
376
|
-
expect(mockKV.put).toHaveBeenCalledWith(
|
|
377
|
-
expect.stringContaining('verified:'),
|
|
378
|
-
'1'
|
|
379
|
-
// No third argument expected
|
|
380
|
-
);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('should handle KV operation failures', async () => {
|
|
384
|
-
// KV operations can fail
|
|
385
|
-
mockKV.get.mockRejectedValueOnce(new Error('KV unavailable'));
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
await mockKV.get('test-key');
|
|
389
|
-
} catch (error: any) {
|
|
390
|
-
expect(error.message).toBe('KV unavailable');
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Should fail open (allow request) in read failures
|
|
394
|
-
// Should fail closed (deny request) in write failures
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test('should handle clock skew', () => {
|
|
398
|
-
const serverTime = Date.now();
|
|
399
|
-
const clientTime = serverTime + (5 * 60 * 1000); // 5 minutes ahead
|
|
400
|
-
|
|
401
|
-
const skew = Math.abs(clientTime - serverTime);
|
|
402
|
-
const maxSkew = 2 * 60 * 1000; // 2 minutes tolerance
|
|
403
|
-
|
|
404
|
-
// Should detect excessive skew
|
|
405
|
-
if (skew > maxSkew) {
|
|
406
|
-
expect(skew).toBeGreaterThan(maxSkew);
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
});
|