@sasjs/cli 6.30.0 → 6.30.1
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/build/commands/build/internal/mockSite/assets/boot.js +23 -1
- package/build/commands/build/internal/mockSite/assets/persist.js +76 -5
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +413 -50
- package/build/commands/build/internal/mockSite/assets/sw.js +128 -11
- package/build/commands/build/internal/mockSite/mockSite.d.ts +23 -0
- package/build/commands/build/internal/mockSite/mockSite.js +238 -23
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +50 -3
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +131 -3
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +179 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +546 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +308 -4
- package/package.json +1 -1
|
@@ -35,6 +35,31 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
35
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
39
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
40
|
+
if (!m) return o;
|
|
41
|
+
var i = m.call(o), r, ar = [], e;
|
|
42
|
+
try {
|
|
43
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
44
|
+
}
|
|
45
|
+
catch (error) { e = { error: error }; }
|
|
46
|
+
finally {
|
|
47
|
+
try {
|
|
48
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
49
|
+
}
|
|
50
|
+
finally { if (e) throw e.error; }
|
|
51
|
+
}
|
|
52
|
+
return ar;
|
|
53
|
+
};
|
|
54
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
55
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
56
|
+
if (ar || !(i in from)) {
|
|
57
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
58
|
+
ar[i] = from[i];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
62
|
+
};
|
|
38
63
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
64
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
65
|
};
|
|
@@ -118,6 +143,527 @@ describe('browser runtime', function () {
|
|
|
118
143
|
delete global.self;
|
|
119
144
|
delete global.fetch;
|
|
120
145
|
});
|
|
146
|
+
/**
|
|
147
|
+
* A sharded drive is read without fetching what the request does not need.
|
|
148
|
+
*
|
|
149
|
+
* The build splits a captured drive so a first page does not download every
|
|
150
|
+
* app's bundle. What must hold here is that the split is invisible: a listing,
|
|
151
|
+
* an existence check and a stat answer for files whose bytes are still on the
|
|
152
|
+
* site, and only a read goes back for them.
|
|
153
|
+
*/
|
|
154
|
+
it('answers for a sharded drive without fetching the pieces it does not need', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
155
|
+
var runtime, indexShard, bundleShard, bundle, fetched, listing, refused, read;
|
|
156
|
+
return __generator(this, function (_a) {
|
|
157
|
+
switch (_a.label) {
|
|
158
|
+
case 0:
|
|
159
|
+
runtime = loadRuntime();
|
|
160
|
+
indexShard = "".concat(drivePath('index.json'));
|
|
161
|
+
bundleShard = "".concat(drivePath('services/web/index.bundle.js'));
|
|
162
|
+
bundle = 'x'.repeat(400);
|
|
163
|
+
fetched = [];
|
|
164
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
165
|
+
var id, files;
|
|
166
|
+
var _a, _b, _c;
|
|
167
|
+
return __generator(this, function (_d) {
|
|
168
|
+
fetched.push(url);
|
|
169
|
+
if (url.includes('mock-config'))
|
|
170
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
|
|
171
|
+
if (/vfs-snapshot-\d+\.json/.test(url)) {
|
|
172
|
+
id = Number(/vfs-snapshot-(\d+)\.json/.exec(url)[1]);
|
|
173
|
+
files = id === 0 ? (_a = {}, _a[indexShard] = '{"apps":[]}', _a) : (_b = {}, _b[bundleShard] = bundle, _b);
|
|
174
|
+
return [2 /*return*/, jsonResponse({ format: mockSite_1.DRIVE_FORMAT, files: files })];
|
|
175
|
+
}
|
|
176
|
+
if (url.includes('vfs-snapshot'))
|
|
177
|
+
return [2 /*return*/, jsonResponse({
|
|
178
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
179
|
+
sharded: true,
|
|
180
|
+
boot: 0,
|
|
181
|
+
entries: (_c = {},
|
|
182
|
+
_c[indexShard] = { shard: 0, bytes: 12 },
|
|
183
|
+
_c[bundleShard] = { shard: 1, bytes: bundle.length },
|
|
184
|
+
_c),
|
|
185
|
+
shards: [
|
|
186
|
+
{ id: 0, file: 'host/vfs-snapshot-0.json' },
|
|
187
|
+
{ id: 1, file: 'host/vfs-snapshot-1.json' }
|
|
188
|
+
]
|
|
189
|
+
})];
|
|
190
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
191
|
+
return [2 /*return*/, ({})];
|
|
192
|
+
}); }); } }];
|
|
193
|
+
});
|
|
194
|
+
}); });
|
|
195
|
+
return [4 /*yield*/, runtime.seed()
|
|
196
|
+
// the boot shard came with the drive; the bundle did not
|
|
197
|
+
];
|
|
198
|
+
case 1:
|
|
199
|
+
_a.sent();
|
|
200
|
+
// the boot shard came with the drive; the bundle did not
|
|
201
|
+
expect(fetched.some(function (url) { return url.includes('vfs-snapshot-1'); })).toBe(false);
|
|
202
|
+
listing = runtime.run({
|
|
203
|
+
code: [
|
|
204
|
+
"const bundle = '".concat(bundleShard, "'"),
|
|
205
|
+
"const folder = '".concat(drivePath('services/web'), "'"),
|
|
206
|
+
"const seen = fs.readdirSync(folder).join(',')",
|
|
207
|
+
"_webout = [seen, fs.existsSync(bundle), fs.statSync(bundle).size].join('|')"
|
|
208
|
+
].join('\n')
|
|
209
|
+
});
|
|
210
|
+
expect(listing.error).toBeNull();
|
|
211
|
+
expect(listing.webout).toBe("index.bundle.js|true|".concat(bundle.length));
|
|
212
|
+
refused = runtime.run({
|
|
213
|
+
code: [
|
|
214
|
+
"let code = 'none'",
|
|
215
|
+
"try { fs.readFileSync('".concat(bundleShard, "') } catch (e) { code = e.code }"),
|
|
216
|
+
"_webout = code"
|
|
217
|
+
].join('\n')
|
|
218
|
+
});
|
|
219
|
+
expect(refused.webout).toBe(runtime.PENDING_CODE);
|
|
220
|
+
return [4 /*yield*/, runtime.ensurePaths([bundleShard])];
|
|
221
|
+
case 2:
|
|
222
|
+
_a.sent();
|
|
223
|
+
expect(fetched.some(function (url) { return url.includes('vfs-snapshot-1'); })).toBe(true);
|
|
224
|
+
read = runtime.run({
|
|
225
|
+
code: "_webout = String(fs.readFileSync('".concat(bundleShard, "').length)")
|
|
226
|
+
});
|
|
227
|
+
expect(read.webout).toBe(String(bundle.length));
|
|
228
|
+
return [2 /*return*/];
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}); });
|
|
232
|
+
/**
|
|
233
|
+
* A shard that cannot be read is named, not silently absent.
|
|
234
|
+
*
|
|
235
|
+
* The drive answers for a file it holds whether or not its bytes are in
|
|
236
|
+
* memory, so a piece that fails to arrive is the one way a read can end up
|
|
237
|
+
* with nothing: saying which file was missing is the difference between a
|
|
238
|
+
* broken deployment and a mystery.
|
|
239
|
+
*/
|
|
240
|
+
it('names a shard it cannot fetch, and refuses one of another format', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
241
|
+
var runtime, missing, foreign, index;
|
|
242
|
+
return __generator(this, function (_a) {
|
|
243
|
+
switch (_a.label) {
|
|
244
|
+
case 0:
|
|
245
|
+
runtime = loadRuntime();
|
|
246
|
+
missing = "".concat(drivePath('missing.json'));
|
|
247
|
+
foreign = "".concat(drivePath('foreign.bin'));
|
|
248
|
+
index = function (shardFiles) { return ({
|
|
249
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
250
|
+
sharded: true,
|
|
251
|
+
boot: 0,
|
|
252
|
+
entries: Object.fromEntries(shardFiles.map(function (path, i) { return [path, { shard: i + 1, bytes: 4 }]; })),
|
|
253
|
+
shards: __spreadArray([
|
|
254
|
+
{ id: 0, file: 'host/vfs-snapshot-0.json' }
|
|
255
|
+
], __read(shardFiles.map(function (_, i) { return ({
|
|
256
|
+
id: i + 1,
|
|
257
|
+
file: "host/vfs-snapshot-".concat(i + 1, ".json")
|
|
258
|
+
}); })), false)
|
|
259
|
+
}); };
|
|
260
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
261
|
+
return __generator(this, function (_a) {
|
|
262
|
+
if (url.includes('mock-config'))
|
|
263
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
|
|
264
|
+
if (url.includes('vfs-snapshot-0'))
|
|
265
|
+
return [2 /*return*/, jsonResponse({ files: {} })];
|
|
266
|
+
if (url.includes('vfs-snapshot-1'))
|
|
267
|
+
return [2 /*return*/, { ok: false, status: 404 }];
|
|
268
|
+
if (url.includes('vfs-snapshot-2'))
|
|
269
|
+
return [2 /*return*/, jsonResponse({ format: 99, files: {} })];
|
|
270
|
+
if (url.includes('vfs-snapshot'))
|
|
271
|
+
return [2 /*return*/, jsonResponse(index([missing, foreign]))];
|
|
272
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
273
|
+
return [2 /*return*/, ({})];
|
|
274
|
+
}); }); } }];
|
|
275
|
+
});
|
|
276
|
+
}); });
|
|
277
|
+
return [4 /*yield*/, runtime.seed()];
|
|
278
|
+
case 1:
|
|
279
|
+
_a.sent();
|
|
280
|
+
return [4 /*yield*/, expect(runtime.ensurePaths([missing])).rejects.toThrow(/vfs-snapshot-1\.json.*404/)];
|
|
281
|
+
case 2:
|
|
282
|
+
_a.sent();
|
|
283
|
+
return [4 /*yield*/, expect(runtime.ensurePaths([foreign])).rejects.toThrow(/format 99/)];
|
|
284
|
+
case 3:
|
|
285
|
+
_a.sent();
|
|
286
|
+
return [2 /*return*/];
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}); });
|
|
290
|
+
it('boots without its boot shard, and names the piece a read could not fetch', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
291
|
+
var runtime, path;
|
|
292
|
+
return __generator(this, function (_a) {
|
|
293
|
+
switch (_a.label) {
|
|
294
|
+
case 0:
|
|
295
|
+
runtime = loadRuntime();
|
|
296
|
+
path = "".concat(drivePath('config.json'));
|
|
297
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
298
|
+
var _a;
|
|
299
|
+
return __generator(this, function (_b) {
|
|
300
|
+
if (url.includes('mock-config'))
|
|
301
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 1 })];
|
|
302
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
303
|
+
return [2 /*return*/, { ok: false, status: 404 }];
|
|
304
|
+
if (url.includes('vfs-snapshot'))
|
|
305
|
+
return [2 /*return*/, jsonResponse({
|
|
306
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
307
|
+
sharded: true,
|
|
308
|
+
boot: 0,
|
|
309
|
+
entries: (_a = {}, _a[path] = { shard: 0, bytes: 2 }, _a),
|
|
310
|
+
shards: [{ id: 0, file: 'host/vfs-snapshot-0.json' }]
|
|
311
|
+
})];
|
|
312
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
313
|
+
return [2 /*return*/, ({})];
|
|
314
|
+
}); }); } }];
|
|
315
|
+
});
|
|
316
|
+
}); });
|
|
317
|
+
// a half-uploaded deploy must not leave the page with no drive at all
|
|
318
|
+
return [4 /*yield*/, expect(runtime.seed()).resolves.toEqual(expect.objectContaining({ booted: true, driveError: expect.any(String) }))];
|
|
319
|
+
case 1:
|
|
320
|
+
// a half-uploaded deploy must not leave the page with no drive at all
|
|
321
|
+
_a.sent();
|
|
322
|
+
return [4 /*yield*/, expect(runtime.ensurePaths([path])).rejects.toThrow(/vfs-snapshot-0\.json.*404/)];
|
|
323
|
+
case 2:
|
|
324
|
+
_a.sent();
|
|
325
|
+
return [2 /*return*/];
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}); });
|
|
329
|
+
it('counts the pieces still on the site in the drive total', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
330
|
+
var runtime, onSite;
|
|
331
|
+
return __generator(this, function (_a) {
|
|
332
|
+
switch (_a.label) {
|
|
333
|
+
case 0:
|
|
334
|
+
runtime = loadRuntime();
|
|
335
|
+
onSite = "".concat(drivePath('bundle.js'));
|
|
336
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
337
|
+
var _a, _b;
|
|
338
|
+
return __generator(this, function (_c) {
|
|
339
|
+
if (url.includes('mock-config'))
|
|
340
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 2 })];
|
|
341
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
342
|
+
return [2 /*return*/, jsonResponse({ files: (_a = {}, _a[drivePath('small.txt')] = 'ab', _a) })];
|
|
343
|
+
if (url.includes('vfs-snapshot'))
|
|
344
|
+
return [2 /*return*/, jsonResponse({
|
|
345
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
346
|
+
sharded: true,
|
|
347
|
+
boot: 0,
|
|
348
|
+
entries: (_b = {},
|
|
349
|
+
_b[drivePath('small.txt')] = { shard: 0, bytes: 2 },
|
|
350
|
+
_b[onSite] = { shard: 1, bytes: 4096 },
|
|
351
|
+
_b),
|
|
352
|
+
shards: [
|
|
353
|
+
{ id: 0, file: 'host/vfs-snapshot-0.json' },
|
|
354
|
+
{ id: 1, file: 'host/vfs-snapshot-1.json' }
|
|
355
|
+
]
|
|
356
|
+
})];
|
|
357
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
358
|
+
return [2 /*return*/, ({})];
|
|
359
|
+
}); }); } }];
|
|
360
|
+
});
|
|
361
|
+
}); });
|
|
362
|
+
return [4 /*yield*/, runtime.seed()
|
|
363
|
+
// the piece nobody read is still part of the drive
|
|
364
|
+
];
|
|
365
|
+
case 1:
|
|
366
|
+
_a.sent();
|
|
367
|
+
// the piece nobody read is still part of the drive
|
|
368
|
+
expect(runtime.stats()).toEqual(expect.objectContaining({ files: 1, onSite: 1, bytes: 2 + 4096 }));
|
|
369
|
+
return [2 /*return*/];
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}); });
|
|
373
|
+
/**
|
|
374
|
+
* A deletion outlives the page, and the index must not undo it.
|
|
375
|
+
*
|
|
376
|
+
* A sharded site's index lists every file the build ships, so a file the
|
|
377
|
+
* visitor removed would come back with the next page load - the tombstone the
|
|
378
|
+
* page keeps is what stops it.
|
|
379
|
+
*/
|
|
380
|
+
it('keeps a deleted file deleted across a restore', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
381
|
+
var runtime, captured, kept, read;
|
|
382
|
+
var _a;
|
|
383
|
+
return __generator(this, function (_b) {
|
|
384
|
+
switch (_b.label) {
|
|
385
|
+
case 0:
|
|
386
|
+
runtime = loadRuntime();
|
|
387
|
+
captured = "".concat(drivePath('captured.txt'));
|
|
388
|
+
kept = "".concat(drivePath('kept.txt'));
|
|
389
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
390
|
+
var _a, _b;
|
|
391
|
+
return __generator(this, function (_c) {
|
|
392
|
+
if (url.includes('mock-config'))
|
|
393
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 1 })];
|
|
394
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
395
|
+
return [2 /*return*/, jsonResponse({ files: (_a = {}, _a[kept] = 'kept', _a) })];
|
|
396
|
+
if (url.includes('vfs-snapshot'))
|
|
397
|
+
return [2 /*return*/, jsonResponse({
|
|
398
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
399
|
+
sharded: true,
|
|
400
|
+
boot: 0,
|
|
401
|
+
entries: (_b = {},
|
|
402
|
+
_b[captured] = { shard: 0, bytes: 8 },
|
|
403
|
+
_b[kept] = { shard: 0, bytes: 4 },
|
|
404
|
+
_b),
|
|
405
|
+
shards: [{ id: 0, file: 'host/vfs-snapshot-0.json' }]
|
|
406
|
+
})];
|
|
407
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
408
|
+
return [2 /*return*/, ({})];
|
|
409
|
+
}); }); } }];
|
|
410
|
+
});
|
|
411
|
+
}); });
|
|
412
|
+
return [4 /*yield*/, runtime.load((_a = {}, _a[kept] = 'kept', _a), [captured])];
|
|
413
|
+
case 1:
|
|
414
|
+
_b.sent();
|
|
415
|
+
expect(runtime.stats()).toEqual(expect.objectContaining({ files: 1, onSite: 0 }));
|
|
416
|
+
read = runtime.run({
|
|
417
|
+
code: [
|
|
418
|
+
"let seen = 'read'",
|
|
419
|
+
"try { fs.readFileSync('".concat(captured, "') } catch (e) { seen = e.code }"),
|
|
420
|
+
"_webout = [fs.existsSync('".concat(captured, "'), seen].join('|')")
|
|
421
|
+
].join('\n')
|
|
422
|
+
});
|
|
423
|
+
expect(read.webout).toBe('false|ENOENT');
|
|
424
|
+
return [2 /*return*/];
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}); });
|
|
428
|
+
it('asks for no drive index when the build shipped an unsharded capture', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
429
|
+
var runtime, fetched;
|
|
430
|
+
var _a;
|
|
431
|
+
return __generator(this, function (_b) {
|
|
432
|
+
switch (_b.label) {
|
|
433
|
+
case 0:
|
|
434
|
+
runtime = loadRuntime();
|
|
435
|
+
fetched = [];
|
|
436
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
437
|
+
return __generator(this, function (_a) {
|
|
438
|
+
fetched.push(url);
|
|
439
|
+
if (url.includes('mock-config'))
|
|
440
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
|
|
441
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
442
|
+
return [2 /*return*/, ({})];
|
|
443
|
+
}); }); } }];
|
|
444
|
+
});
|
|
445
|
+
}); });
|
|
446
|
+
// a site built before the split carries no count, and the warm path must not
|
|
447
|
+
// pay a request for an index that does not exist
|
|
448
|
+
return [4 /*yield*/, runtime.load((_a = {}, _a[drivePath('a.txt')] = 'a', _a))];
|
|
449
|
+
case 1:
|
|
450
|
+
// a site built before the split carries no count, and the warm path must not
|
|
451
|
+
// pay a request for an index that does not exist
|
|
452
|
+
_b.sent();
|
|
453
|
+
expect(fetched.some(function (url) { return url.includes('vfs-snapshot'); })).toBe(false);
|
|
454
|
+
return [2 /*return*/];
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}); });
|
|
458
|
+
it('restores a drive whose boot shard did not arrive, and says so', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
459
|
+
var runtime, path;
|
|
460
|
+
return __generator(this, function (_a) {
|
|
461
|
+
switch (_a.label) {
|
|
462
|
+
case 0:
|
|
463
|
+
runtime = loadRuntime();
|
|
464
|
+
path = "".concat(drivePath('config.json'));
|
|
465
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
466
|
+
var _a;
|
|
467
|
+
return __generator(this, function (_b) {
|
|
468
|
+
if (url.includes('mock-config'))
|
|
469
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 1 })];
|
|
470
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
471
|
+
return [2 /*return*/, { ok: false, status: 404 }];
|
|
472
|
+
if (url.includes('vfs-snapshot'))
|
|
473
|
+
return [2 /*return*/, jsonResponse({
|
|
474
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
475
|
+
sharded: true,
|
|
476
|
+
boot: 0,
|
|
477
|
+
entries: (_a = {}, _a[path] = { shard: 0, bytes: 2 }, _a),
|
|
478
|
+
shards: [{ id: 0, file: 'host/vfs-snapshot-0.json' }]
|
|
479
|
+
})];
|
|
480
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
481
|
+
return [2 /*return*/, ({})];
|
|
482
|
+
}); }); } }];
|
|
483
|
+
});
|
|
484
|
+
}); });
|
|
485
|
+
// a returning visitor must not be refused where a fresh visitor is only
|
|
486
|
+
// short: the restore reports the failure and keeps the index
|
|
487
|
+
return [4 /*yield*/, expect(runtime.load({})).resolves.toEqual(expect.objectContaining({ booted: true, driveError: expect.any(String) }))];
|
|
488
|
+
case 1:
|
|
489
|
+
// a returning visitor must not be refused where a fresh visitor is only
|
|
490
|
+
// short: the restore reports the failure and keeps the index
|
|
491
|
+
_a.sent();
|
|
492
|
+
return [2 /*return*/];
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}); });
|
|
496
|
+
/**
|
|
497
|
+
* A write over a captured file replaces it, in both directions.
|
|
498
|
+
*
|
|
499
|
+
* The file's bytes are still on the site, so it is in the index as well as in
|
|
500
|
+
* memory unless the write takes it out - and then a listing names it twice, the
|
|
501
|
+
* drive total counts it twice, and a piece fetched later puts the capture's copy
|
|
502
|
+
* back over the visitor's.
|
|
503
|
+
*/
|
|
504
|
+
it('takes a written path out of the capture, so it is counted and listed once', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
505
|
+
var runtime, path, written, read;
|
|
506
|
+
return __generator(this, function (_a) {
|
|
507
|
+
switch (_a.label) {
|
|
508
|
+
case 0:
|
|
509
|
+
runtime = loadRuntime();
|
|
510
|
+
path = "".concat(drivePath('overwritten.txt'));
|
|
511
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
512
|
+
var _a, _b;
|
|
513
|
+
return __generator(this, function (_c) {
|
|
514
|
+
if (url.includes('mock-config'))
|
|
515
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 2 })];
|
|
516
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
517
|
+
return [2 /*return*/, jsonResponse({ files: {} })];
|
|
518
|
+
if (/vfs-snapshot-1\.json/.test(url))
|
|
519
|
+
return [2 /*return*/, jsonResponse({
|
|
520
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
521
|
+
files: (_a = {}, _a[path] = 'captured content', _a)
|
|
522
|
+
})];
|
|
523
|
+
if (url.includes('vfs-snapshot'))
|
|
524
|
+
return [2 /*return*/, jsonResponse({
|
|
525
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
526
|
+
sharded: true,
|
|
527
|
+
boot: 0,
|
|
528
|
+
entries: (_b = {}, _b[path] = { shard: 1, bytes: 16 }, _b),
|
|
529
|
+
shards: [
|
|
530
|
+
{ id: 0, file: 'host/vfs-snapshot-0.json' },
|
|
531
|
+
{ id: 1, file: 'host/vfs-snapshot-1.json' }
|
|
532
|
+
]
|
|
533
|
+
})];
|
|
534
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
535
|
+
return [2 /*return*/, ({})];
|
|
536
|
+
}); }); } }];
|
|
537
|
+
});
|
|
538
|
+
}); });
|
|
539
|
+
return [4 /*yield*/, runtime.seed()];
|
|
540
|
+
case 1:
|
|
541
|
+
_a.sent();
|
|
542
|
+
written = runtime.run({
|
|
543
|
+
code: "fs.writeFileSync('".concat(path, "', 'mine'); _webout = 'ok'")
|
|
544
|
+
});
|
|
545
|
+
expect(written.webout).toBe('ok');
|
|
546
|
+
read = runtime.run({
|
|
547
|
+
code: [
|
|
548
|
+
"const folder = fs.readdirSync('".concat(drivePath(''), "').filter((name) => name === 'overwritten.txt')"),
|
|
549
|
+
"_webout = [folder.length, fs.readFileSync('".concat(path, "')].join('|')")
|
|
550
|
+
].join('\n')
|
|
551
|
+
});
|
|
552
|
+
expect(read.webout).toBe('1|mine');
|
|
553
|
+
// nothing of the capture is left unloaded for that path: the run's own session
|
|
554
|
+
// files are the only other entries, so the count of pieces still on the site
|
|
555
|
+
// is what says the write took the path out of the index
|
|
556
|
+
expect(runtime.stats()).toEqual(expect.objectContaining({ onSite: 0 }));
|
|
557
|
+
return [2 /*return*/];
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}); });
|
|
561
|
+
it('reports an index that failed, rather than restoring the visitor files alone', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
562
|
+
var runtime;
|
|
563
|
+
return __generator(this, function (_a) {
|
|
564
|
+
switch (_a.label) {
|
|
565
|
+
case 0:
|
|
566
|
+
runtime = loadRuntime();
|
|
567
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
568
|
+
return __generator(this, function (_a) {
|
|
569
|
+
if (url.includes('mock-config'))
|
|
570
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 1 })];
|
|
571
|
+
if (url.includes('vfs-snapshot'))
|
|
572
|
+
return [2 /*return*/, { ok: false, status: 500 }];
|
|
573
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
574
|
+
return [2 /*return*/, ({})];
|
|
575
|
+
}); }); } }];
|
|
576
|
+
});
|
|
577
|
+
}); });
|
|
578
|
+
// a 500 is not "this site ships no snapshot": restoring without the index
|
|
579
|
+
// would leave only the files this visitor changed, silently
|
|
580
|
+
return [4 /*yield*/, expect(runtime.load({})).resolves.toEqual(expect.objectContaining({
|
|
581
|
+
booted: true,
|
|
582
|
+
driveError: expect.stringContaining('answered 500')
|
|
583
|
+
}))
|
|
584
|
+
// and 404 still reads as a site with nothing to ship
|
|
585
|
+
];
|
|
586
|
+
case 1:
|
|
587
|
+
// a 500 is not "this site ships no snapshot": restoring without the index
|
|
588
|
+
// would leave only the files this visitor changed, silently
|
|
589
|
+
_a.sent();
|
|
590
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
591
|
+
return __generator(this, function (_a) {
|
|
592
|
+
if (url.includes('mock-config'))
|
|
593
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 1 })];
|
|
594
|
+
if (url.includes('vfs-snapshot'))
|
|
595
|
+
return [2 /*return*/, { ok: false, status: 404 }];
|
|
596
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
597
|
+
return [2 /*return*/, ({})];
|
|
598
|
+
}); }); } }];
|
|
599
|
+
});
|
|
600
|
+
}); });
|
|
601
|
+
return [4 /*yield*/, expect(loadRuntime().load({})).resolves.toEqual(expect.objectContaining({ booted: true }))];
|
|
602
|
+
case 2:
|
|
603
|
+
_a.sent();
|
|
604
|
+
return [2 /*return*/];
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}); });
|
|
608
|
+
it('refuses an append onto a file whose bytes are still on the site', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
609
|
+
var runtime, path, refused, appended;
|
|
610
|
+
return __generator(this, function (_a) {
|
|
611
|
+
switch (_a.label) {
|
|
612
|
+
case 0:
|
|
613
|
+
runtime = loadRuntime();
|
|
614
|
+
path = "".concat(drivePath('appended.txt'));
|
|
615
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
616
|
+
var _a, _b;
|
|
617
|
+
return __generator(this, function (_c) {
|
|
618
|
+
if (url.includes('mock-config'))
|
|
619
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc, snapshotShards: 2 })];
|
|
620
|
+
if (/vfs-snapshot-0\.json/.test(url))
|
|
621
|
+
return [2 /*return*/, jsonResponse({ files: {} })];
|
|
622
|
+
if (/vfs-snapshot-1\.json/.test(url))
|
|
623
|
+
return [2 /*return*/, jsonResponse({
|
|
624
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
625
|
+
files: (_a = {}, _a[path] = 'first', _a)
|
|
626
|
+
})];
|
|
627
|
+
if (url.includes('vfs-snapshot'))
|
|
628
|
+
return [2 /*return*/, jsonResponse({
|
|
629
|
+
format: mockSite_1.DRIVE_FORMAT,
|
|
630
|
+
sharded: true,
|
|
631
|
+
boot: 0,
|
|
632
|
+
entries: (_b = {}, _b[path] = { shard: 1, bytes: 5 }, _b),
|
|
633
|
+
shards: [
|
|
634
|
+
{ id: 0, file: 'host/vfs-snapshot-0.json' },
|
|
635
|
+
{ id: 1, file: 'host/vfs-snapshot-1.json' }
|
|
636
|
+
]
|
|
637
|
+
})];
|
|
638
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
639
|
+
return [2 /*return*/, ({})];
|
|
640
|
+
}); }); } }];
|
|
641
|
+
});
|
|
642
|
+
}); });
|
|
643
|
+
return [4 /*yield*/, runtime.seed()
|
|
644
|
+
// the capture's bytes are still on the site, and appending would replace them
|
|
645
|
+
];
|
|
646
|
+
case 1:
|
|
647
|
+
_a.sent();
|
|
648
|
+
refused = runtime.run({
|
|
649
|
+
code: "fs.appendFileSync('".concat(path, "', 'plus'); _webout = 'ok'")
|
|
650
|
+
});
|
|
651
|
+
// the statement after the append never ran: the write was refused, not done
|
|
652
|
+
expect(refused.webout).not.toBe('ok');
|
|
653
|
+
expect(runtime.stats()).toEqual(expect.objectContaining({ onSite: 1 }));
|
|
654
|
+
// once the piece is here the append lands on top of what was captured
|
|
655
|
+
return [4 /*yield*/, runtime.ensurePaths([path])];
|
|
656
|
+
case 2:
|
|
657
|
+
// once the piece is here the append lands on top of what was captured
|
|
658
|
+
_a.sent();
|
|
659
|
+
appended = runtime.run({
|
|
660
|
+
code: "fs.appendFileSync('".concat(path, "', 'plus'); _webout = fs.readFileSync('").concat(path, "')")
|
|
661
|
+
});
|
|
662
|
+
expect(appended.webout).toBe('firstplus');
|
|
663
|
+
return [2 /*return*/];
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}); });
|
|
121
667
|
it('refuses a drive file whose format it does not speak', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
122
668
|
var runtime, result;
|
|
123
669
|
return __generator(this, function (_a) {
|