@sasjs/cli 6.27.0 → 6.27.2

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.
@@ -457,7 +457,48 @@ const transcriptFor = async (programPath) => {
457
457
  return undefined
458
458
  }
459
459
 
460
+ /**
461
+ * The program a Studio code run came from, from the line the interface puts in
462
+ * front of the buffer.
463
+ *
464
+ * Run in Studio posts the buffer to `/SASjsApi/code/execute` - not the file's
465
+ * path - and the interface prepends `%let _program = <path>;` (its
466
+ * `programPathInjection`) so a program can still see where it lives. That line is
467
+ * the only thing in such a request that names the file, and it is what lets the
468
+ * recording answer a Press Run on the program it recorded.
469
+ *
470
+ * The line is written whatever was sent - the whole buffer or a selection of it -
471
+ * so the recording answers the program its path names, and a visitor who edits
472
+ * the code before pressing Run still sees the session recorded for that file. A
473
+ * browser cannot run SAS at all, so the alternative is a refusal for every SAS
474
+ * run; the demo's program says what it is, and its comment says where the answer
475
+ * comes from.
476
+ */
477
+ const recordedProgramIn = (code, runTime) => {
478
+ if (String(runTime || '').toLowerCase() !== 'sas') return undefined
479
+
480
+ // anchored: the interface writes the line FIRST (`%let _program = ${path};\n`
481
+ // + the buffer), so a `%let _program` further down is the program's own code -
482
+ // a stored program that sets it deliberately, or a datalines row that looks
483
+ // like it - and is not what this host answers. Without the anchor, a caller's
484
+ // own SAS program could name the recorded file and be handed its session.
485
+ const match = /^\s*%let\s+_program\s*=\s*([^;]+);/i.exec(String(code || ''))
486
+
487
+ return match ? match[1].trim() : undefined
488
+ }
489
+
460
490
  // ---------------------------------------------------------------- the runner
491
+ /**
492
+ * What a SAS run this host cannot answer is told.
493
+ *
494
+ * `sas` is one of the host's run times, so the message must not claim the run
495
+ * time is missing: what is missing is the binary, because this is a browser. It
496
+ * names the one thing that does answer SAS here, so a reader whose program is
497
+ * refused knows why the demo's own program is not.
498
+ */
499
+ const SAS_UNAVAILABLE =
500
+ 'SAS is not available on this host: a browser has no SAS binary, and this demo answers only the program it ships a recorded session of.'
501
+
461
502
  /**
462
503
  * The work a run does, over the browser's drive.
463
504
  *
@@ -477,17 +518,19 @@ class BrowserRuntime extends API.MemorySessionHost {
477
518
  const program = request.program || {}
478
519
  const params = Object.assign({}, request.vars || {})
479
520
 
480
- // A SAS program the recording does not name. This host has no SAS binary, so
481
- // the answer is the refusal a client already understands - not a JavaScript
482
- // SyntaxError quoting the SAS source, which is what handing it to the runner
483
- // would produce. The file is resolved because the request may name the
484
- // program without its extension.
485
- if (program.code === undefined) {
486
- const resolved = R.resolveProgram(program.path)
521
+ // SAS never reaches the runner. `sas` is one of this host's run times - the
522
+ // recording answers the program it names - but the answer for anything else
523
+ // is the refusal a client already understands, not a JavaScript SyntaxError
524
+ // quoting the SAS source, which is what handing it to the runner would
525
+ // produce. The file is resolved because the request may name the program
526
+ // without its extension.
527
+ const resolved =
528
+ program.code === undefined ? R.resolveProgram(program.path) : undefined
529
+ const isSas =
530
+ String(request.runTime || '').toLowerCase() === 'sas' ||
531
+ Boolean(resolved && /\.sas$/i.test(resolved))
487
532
 
488
- if (resolved && /\.sas$/i.test(resolved))
489
- throw runFailure('"runTime" must be one of [js]')
490
- }
533
+ if (isSas) throw runFailure(SAS_UNAVAILABLE)
491
534
 
492
535
  const payload = { params, tables: tablesOf(request.files) }
493
536
  if (program.code !== undefined) payload.code = program.code
@@ -580,6 +623,23 @@ const withoutBase = (pathname) =>
580
623
  ? pathname.slice(BASE.length) || '/'
581
624
  : pathname
582
625
 
626
+ // ------------------------------------------------------------- what it can run
627
+ /**
628
+ * The run times this host answers, in the order a client should try them.
629
+ *
630
+ * `js` is executed here, in the page's own JavaScript engine. `sas` is answered
631
+ * from the recorded session (`host/sas-transcript.json`) for the one program the
632
+ * recording names; any other SAS program is refused by `SAS_UNAVAILABLE`, which
633
+ * names the missing binary rather than a missing run time - a browser has none.
634
+ *
635
+ * Both the `/SASjsApi/info` answer and the runtime service read this list, so the
636
+ * run times the host states and the ones its service accepts cannot drift apart.
637
+ * `sas` is stated because that is what offers Run on a `.sas` file at all; a
638
+ * program the recording does not name is then refused with its reason, not by the
639
+ * run time being absent from a list the host publishes.
640
+ */
641
+ const RUN_TIMES = ['js', 'sas']
642
+
583
643
  // ------------------------------------------------------------- host reports
584
644
  // The Admin routes ask a host what it is doing. A browser has no process, but it
585
645
  // does have a page, a heap and this worker's own activity, so the reports are
@@ -626,7 +686,7 @@ const adminReports = {
626
686
  cpuCount: navigator.hardwareConcurrency || 1,
627
687
  cpuModel: 'unknown',
628
688
  loadAverage: [],
629
- runTimes: ['js', 'sas'],
689
+ runTimes: RUN_TIMES,
630
690
  sasjsRoot: '/'
631
691
  },
632
692
  memory: {
@@ -772,16 +832,21 @@ const homeService = API.createHomeService(async () => {
772
832
 
773
833
  const SERVICES = {
774
834
  drive: API.createDriveService(DRIVE_STORE),
775
- info: API.createInfoService(API.BROWSER_INFO),
835
+ // The package's browser fixture states one run time; this host serves two, and
836
+ // `info` is what the Studio reads to decide whether a file may be run at all,
837
+ // so the answer is the host's own list rather than the fixture's.
838
+ info: API.createInfoService(
839
+ Object.assign({}, API.BROWSER_INFO, { runTimes: RUN_TIMES })
840
+ ),
776
841
  identity: IDENTITY,
777
842
  records: RECORDS,
778
843
  runtime: API.createRuntimeService(IDENTITY_STORE, RUNTIME, {
779
- // `js` is executed here; `sas` is offered so a SAS program can be run at all
780
- // in the demo, and answered from the recording when one names it. A `.sas`
781
- // program with no recording is refused in the runner, deliberately - it must
782
- // not be handed to the JavaScript runtime as source, which is what the
783
- // resolveProgram guard in BrowserRuntime.run prevents.
784
- runTimes: ['js', 'sas']
844
+ // `js` is executed here; `sas` is offered because the recording answers the
845
+ // program it names, and the Studio offers Run on a `.sas` file only for a run
846
+ // time the host states. A `.sas` program with no recording is refused in the
847
+ // runner, deliberately - it must not be handed to the JavaScript runtime as
848
+ // source, which is what the guard in BrowserRuntime.run prevents.
849
+ runTimes: RUN_TIMES
785
850
  }),
786
851
  session: API.createSessionService(IDENTITY_STORE, RUNTIME),
787
852
  authConfig: API.createAuthConfigService({}),
@@ -1146,15 +1211,17 @@ const runRoute = async (routeId, payload, subject) => {
1146
1211
  runRequest.program = query._program || body._program
1147
1212
  }
1148
1213
 
1149
- // A recorded SAS session is answered here, ahead of the run time check: a
1150
- // transcript is data, not a run time, and this host holds only `js`. Accepting
1151
- // `sas` instead would make the package compile SAS source as JavaScript and
1152
- // hand back a SyntaxError where every other .sas program gets a refusal.
1153
- // Neither a code run nor a trigger: a trigger has to answer with the session
1154
- // record the route builds, which a client parses as JSON, so a recording is not
1155
- // an answer there.
1156
- if (!code && !trigger) {
1157
- const recording = await transcriptFor(runRequest.program)
1214
+ // A recorded SAS session is answered here, ahead of the run. `sas` IS one of
1215
+ // this host's run times - it is what the Studio reads to offer Run on a .sas
1216
+ // file - and the recording is how it is answered, so the check comes before
1217
+ // the package would compile the source as JavaScript. A trigger is excluded:
1218
+ // it has to answer with the session record the route builds, which a client
1219
+ // parses as JSON, so a recording is not an answer there.
1220
+ if (!trigger) {
1221
+ const recorded = code
1222
+ ? recordedProgramIn(body.code, body.runTime)
1223
+ : runRequest.program
1224
+ const recording = recorded ? await transcriptFor(recorded) : undefined
1158
1225
 
1159
1226
  if (recording)
1160
1227
  return artifactResponse(
@@ -1168,7 +1235,10 @@ const runRoute = async (routeId, payload, subject) => {
1168
1235
  // the same rule runtime-core applies to a service that declares none
1169
1236
  headers: SEEDED_HEADER_LINE
1170
1237
  },
1171
- vars._debug !== undefined
1238
+ // a code run carries no `_debug` and still answers with its log: the
1239
+ // package compiles a code run at its own debug level, and the Studio
1240
+ // splits the three parts on the separator it writes
1241
+ code ? true : vars._debug !== undefined
1172
1242
  )
1173
1243
  )
1174
1244
  }
@@ -1357,12 +1427,14 @@ const routeRequest = async (request, url) => {
1357
1427
 
1358
1428
  if (!refusal) broadcast({ type: 'sasjs-host-error', path, message })
1359
1429
  // A refusal the API decided on carries its status and, where the route
1360
- // answers an envelope, the body to send. The drive's routes send the message
1361
- // itself as plain text - the server answers them with `res.status(code)
1362
- // .send(err.message)` - so those stay text; everything else keeps the JSON
1363
- // envelope the other routes already answer with.
1430
+ // answers an envelope, the body to send. The deploy routes state one in the
1431
+ // document - a refusal there is `{status: 'failure', message}` - while the
1432
+ // other drive routes send the message itself as plain text, which is what the
1433
+ // server's `res.status(code).send(err.message)` does.
1364
1434
  if (API.isApiError(e)) {
1365
1435
  if (e.body !== undefined) return json(e.body, e.status)
1436
+ if (/\/SASjsApi\/drive\/deploy(\/|$)/.test(apiPath))
1437
+ return json({ status: 'failure', message: e.message }, e.status)
1366
1438
  if (apiPath.includes('/SASjsApi/drive/')) return text(e.message, e.status)
1367
1439
  return json({ message: e.message }, e.status)
1368
1440
  }
@@ -90,6 +90,7 @@ var path_1 = __importDefault(require("path"));
90
90
  var api_1 = require("@sasjs/api");
91
91
  var os_1 = __importDefault(require("os"));
92
92
  var yazl_1 = __importDefault(require("yazl"));
93
+ var utils_1 = require("@sasjs/utils");
93
94
  var mockSite_1 = require("../mockSite");
94
95
  /**
95
96
  * The other half of the parity claim: the service worker is what turns an
@@ -122,8 +123,9 @@ var lintBundle = function () {
122
123
  }
123
124
  return lintBundlePath;
124
125
  };
125
- var loadWorker = function (base) {
126
+ var loadWorker = function (base, options) {
126
127
  if (base === void 0) { base = ''; }
128
+ if (options === void 0) { options = {}; }
127
129
  jest.resetModules();
128
130
  var handlers = {};
129
131
  var refusingWrites = false;
@@ -205,6 +207,22 @@ var loadWorker = function (base) {
205
207
  fixture = global.__sasTranscript;
206
208
  return [2 /*return*/, fixture ? jsonResponse(fixture) : new Response('', { status: 404 })];
207
209
  }
210
+ // the webout() block the build writes beside the worker, from the same
211
+ // @sasjs/utils generator sasjs/server uses. Absent unless a test asks for
212
+ // it: a site built before the block existed keeps the `_webout`-only path.
213
+ if (options.prelude && target.includes('webout-prelude'))
214
+ return [2 /*return*/, jsonResponse({
215
+ sentinels: mockSite_1.WEBOUT_SENTINELS,
216
+ template: (0, utils_1.getWeboutPrelude)({
217
+ weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
218
+ headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
219
+ program: mockSite_1.WEBOUT_SENTINELS.program,
220
+ debug: mockSite_1.WEBOUT_SENTINELS.debug,
221
+ username: mockSite_1.WEBOUT_SENTINELS.username,
222
+ userId: mockSite_1.WEBOUT_SENTINELS.userId,
223
+ apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
224
+ })
225
+ })];
208
226
  return [2 /*return*/, target.includes('vfs-seed')
209
227
  ? jsonResponse({
210
228
  files: (_a = {},
@@ -260,7 +278,8 @@ var jsonResponse = function (body) { return ({
260
278
  return [2 /*return*/, body];
261
279
  }); }); }
262
280
  }); };
263
- /** A zip archive of text members, written the way a deploy upload ships one.
281
+ /** A zip archive of members - text, or bytes for a base64 member - written the way
282
+ * a deploy upload ships one.
264
283
  * The first member is stored rather than deflated, so both methods the reader
265
284
  * accepts are exercised by the one case. */
266
285
  var zipOf = function (files) {
@@ -392,6 +411,164 @@ describe('service worker host', function () {
392
411
  }
393
412
  });
394
413
  }); });
414
+ it('runs a JS program written the way sasjs/server runs one', function () { return __awaiter(void 0, void 0, void 0, function () {
415
+ var worker, response, body, _a, _b;
416
+ var _c;
417
+ return __generator(this, function (_d) {
418
+ switch (_d.label) {
419
+ case 0:
420
+ worker = loadWorker('', { prelude: true });
421
+ worker.runtime.load((_c = {},
422
+ _c[drivePath('services/public/run.js')] = [
423
+ "const rows = [{ name: 'Alice', age: 14 }, { name: 'Bob', age: 15 }]",
424
+ 'console.log(`Read ${rows.length} rows`)',
425
+ "webout('OPEN')",
426
+ "webout('OBJ', 'adults', rows.filter((row) => row.age > 13))",
427
+ "webout('CLOSE')"
428
+ ].join('\n'),
429
+ _c));
430
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
431
+ encodeURIComponent("".concat(appLoc, "/services/public/run")))];
432
+ case 1:
433
+ response = _d.sent();
434
+ expect(response.status).toBe(200);
435
+ _b = (_a = JSON).parse;
436
+ return [4 /*yield*/, response.text()];
437
+ case 2:
438
+ body = _b.apply(_a, [_d.sent()]);
439
+ expect(body.adults).toEqual([
440
+ { name: 'Alice', age: 14 },
441
+ { name: 'Bob', age: 15 }
442
+ ]);
443
+ return [2 /*return*/];
444
+ }
445
+ });
446
+ }); });
447
+ it('answers a Run in Studio on the recorded program, as a code run', function () { return __awaiter(void 0, void 0, void 0, function () {
448
+ var worker, response, parts;
449
+ var _a, _b;
450
+ return __generator(this, function (_c) {
451
+ switch (_c.label) {
452
+ case 0:
453
+ // Press Run in Studio posts the buffer to /code/execute with the file's own
454
+ // path prepended as `%let _program` - the path is not a query parameter, as
455
+ // it is for stp/execute. The recording is what answers it: without this the
456
+ // demo's .sas program has a Run button (the host states `sas`) and no way to
457
+ // answer it, because a browser cannot compile SAS.
458
+ ;
459
+ global.__sasTranscript = (_a = {},
460
+ _a["".concat(appLoc, "/services/public/program.sas")] = {
461
+ webout: '{"SYSSITE":"00000000"}',
462
+ log: 'NOTE: recorded',
463
+ print: 'The SAS System'
464
+ },
465
+ _a);
466
+ worker = loadWorker();
467
+ worker.runtime.load((_b = {},
468
+ _b[drivePath('services/public/program.sas')] = 'data work.class; run;',
469
+ _b));
470
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
471
+ code: "%let _program = ".concat(appLoc, "/services/public/program.sas;\ndata work.class; run;"),
472
+ runTime: 'sas'
473
+ }))];
474
+ case 1:
475
+ response = _c.sent();
476
+ expect(response.status).toBe(200);
477
+ return [4 /*yield*/, response.text()];
478
+ case 2:
479
+ parts = (_c.sent()).split(LOGS_SEPARATOR);
480
+ expect(parts[0].trim()).toBe('{"SYSSITE":"00000000"}');
481
+ expect(parts[1]).toContain('NOTE: recorded');
482
+ expect(parts[2]).toContain('The SAS System');
483
+ delete global.__sasTranscript;
484
+ return [2 /*return*/];
485
+ }
486
+ });
487
+ }); });
488
+ it('answers only the line the interface writes, not one further down', function () { return __awaiter(void 0, void 0, void 0, function () {
489
+ var worker, response, _a;
490
+ var _b, _c;
491
+ return __generator(this, function (_d) {
492
+ switch (_d.label) {
493
+ case 0:
494
+ // The interface puts `%let _program` first; a line further down belongs to
495
+ // the program. Answered, it would hand a caller's own SAS program the session
496
+ // recorded for another file - the "inventing a session" the refusal exists to
497
+ // avoid, with a different file's log.
498
+ ;
499
+ global.__sasTranscript = (_b = {},
500
+ _b["".concat(appLoc, "/services/public/program.sas")] = {
501
+ webout: '{"SYSSITE":"00000000"}',
502
+ log: 'NOTE: recorded',
503
+ print: 'The SAS System'
504
+ },
505
+ _b);
506
+ worker = loadWorker();
507
+ worker.runtime.load((_c = {},
508
+ _c[drivePath('services/public/mine.sas')] = 'data work.class; run;',
509
+ _c));
510
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
511
+ code: "data _null_;\n%let _program = ".concat(appLoc, "/services/public/program.sas;\nrun;"),
512
+ runTime: 'sas'
513
+ }))];
514
+ case 1:
515
+ response = _d.sent();
516
+ expect(response.status).toBe(400);
517
+ _a = expect;
518
+ return [4 /*yield*/, response.text()];
519
+ case 2:
520
+ _a.apply(void 0, [_d.sent()]).toContain('SAS is not available');
521
+ delete global.__sasTranscript;
522
+ return [2 /*return*/];
523
+ }
524
+ });
525
+ }); });
526
+ it('refuses SAS code that names no recorded program', function () { return __awaiter(void 0, void 0, void 0, function () {
527
+ var worker, _a, _b, code, response, body, e_1_1;
528
+ var e_1, _c;
529
+ return __generator(this, function (_d) {
530
+ switch (_d.label) {
531
+ case 0:
532
+ worker = loadWorker();
533
+ _d.label = 1;
534
+ case 1:
535
+ _d.trys.push([1, 7, 8, 9]);
536
+ _a = __values([
537
+ 'data work.class; run;',
538
+ "%let _program = ".concat(appLoc, "/services/public/other.sas;\ndata work.class; run;")
539
+ ]), _b = _a.next();
540
+ _d.label = 2;
541
+ case 2:
542
+ if (!!_b.done) return [3 /*break*/, 6];
543
+ code = _b.value;
544
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({ code: code, runTime: 'sas' }))];
545
+ case 3:
546
+ response = _d.sent();
547
+ expect(response.status).toBe(400);
548
+ return [4 /*yield*/, response.text()];
549
+ case 4:
550
+ body = _d.sent();
551
+ expect(body).toContain('SAS is not available');
552
+ expect(body).not.toContain('SyntaxError');
553
+ _d.label = 5;
554
+ case 5:
555
+ _b = _a.next();
556
+ return [3 /*break*/, 2];
557
+ case 6: return [3 /*break*/, 9];
558
+ case 7:
559
+ e_1_1 = _d.sent();
560
+ e_1 = { error: e_1_1 };
561
+ return [3 /*break*/, 9];
562
+ case 8:
563
+ try {
564
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
565
+ }
566
+ finally { if (e_1) throw e_1.error; }
567
+ return [7 /*endfinally*/];
568
+ case 9: return [2 /*return*/];
569
+ }
570
+ });
571
+ }); });
395
572
  it('refuses a .sas program no recording names, as it always did', function () { return __awaiter(void 0, void 0, void 0, function () {
396
573
  var worker, response, body;
397
574
  var _a;
@@ -410,7 +587,8 @@ describe('service worker host', function () {
410
587
  return [4 /*yield*/, response.text()];
411
588
  case 2:
412
589
  body = _b.sent();
413
- expect(body).toContain('runTime');
590
+ expect(body).toContain('SAS is not available');
591
+ expect(body).toContain('recorded session');
414
592
  expect(body).not.toContain('SyntaxError');
415
593
  return [2 /*return*/];
416
594
  }
@@ -1266,10 +1444,17 @@ describe('service worker host', function () {
1266
1444
  case 1:
1267
1445
  empty = _c.sent();
1268
1446
  expect(empty.status).toBe(400);
1447
+ // the deploy routes answer a refusal as the envelope the document states for
1448
+ // them, which is what the corpus pins for the same route
1269
1449
  _a = expect;
1270
- return [4 /*yield*/, empty.text()];
1450
+ return [4 /*yield*/, empty.json()];
1271
1451
  case 2:
1272
- _a.apply(void 0, [_c.sent()]).toBe('"file" is not present.');
1452
+ // the deploy routes answer a refusal as the envelope the document states for
1453
+ // them, which is what the corpus pins for the same route
1454
+ _a.apply(void 0, [_c.sent()]).toEqual({
1455
+ status: 'failure',
1456
+ message: '"file" is not present.'
1457
+ });
1273
1458
  form = new FormData();
1274
1459
  form.set('file', new Blob([new Uint8Array([1, 2, 3])]), 'not-a-zip.zip');
1275
1460
  return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
@@ -1280,9 +1465,12 @@ describe('service worker host', function () {
1280
1465
  notZip = _c.sent();
1281
1466
  expect(notZip.status).toBe(400);
1282
1467
  _b = expect;
1283
- return [4 /*yield*/, notZip.text()];
1468
+ return [4 /*yield*/, notZip.json()];
1284
1469
  case 4:
1285
- _b.apply(void 0, [_c.sent()]).toBe('The archive is not a zip file.');
1470
+ _b.apply(void 0, [_c.sent()]).toEqual({
1471
+ status: 'failure',
1472
+ message: 'The archive is not a zip file.'
1473
+ });
1286
1474
  return [2 /*return*/];
1287
1475
  }
1288
1476
  });
@@ -1536,8 +1724,8 @@ describe('service worker host', function () {
1536
1724
  });
1537
1725
  }); });
1538
1726
  it('publishes, lists, serves and removes an app through the package surface', function () { return __awaiter(void 0, void 0, void 0, function () {
1539
- var dispatch, _a, _b, path_2, listing, _c, e_1_1, _d, _e, path_3, landing, e_2_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_3_1, _j, _k, path_5, page, _l, e_4_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
1540
- var e_1, _q, e_2, _r, e_3, _s, e_4, _t;
1727
+ var dispatch, _a, _b, path_2, listing, _c, e_2_1, _d, _e, path_3, landing, e_3_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_4_1, _j, _k, path_5, page, _l, e_5_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
1728
+ var e_2, _q, e_3, _r, e_4, _s, e_5, _t;
1541
1729
  return __generator(this, function (_u) {
1542
1730
  switch (_u.label) {
1543
1731
  case 0:
@@ -1564,14 +1752,14 @@ describe('service worker host', function () {
1564
1752
  return [3 /*break*/, 2];
1565
1753
  case 6: return [3 /*break*/, 9];
1566
1754
  case 7:
1567
- e_1_1 = _u.sent();
1568
- e_1 = { error: e_1_1 };
1755
+ e_2_1 = _u.sent();
1756
+ e_2 = { error: e_2_1 };
1569
1757
  return [3 /*break*/, 9];
1570
1758
  case 8:
1571
1759
  try {
1572
1760
  if (_b && !_b.done && (_q = _a.return)) _q.call(_a);
1573
1761
  }
1574
- finally { if (e_1) throw e_1.error; }
1762
+ finally { if (e_2) throw e_2.error; }
1575
1763
  return [7 /*endfinally*/];
1576
1764
  case 9:
1577
1765
  _u.trys.push([9, 14, 15, 16]);
@@ -1591,14 +1779,14 @@ describe('service worker host', function () {
1591
1779
  return [3 /*break*/, 10];
1592
1780
  case 13: return [3 /*break*/, 16];
1593
1781
  case 14:
1594
- e_2_1 = _u.sent();
1595
- e_2 = { error: e_2_1 };
1782
+ e_3_1 = _u.sent();
1783
+ e_3 = { error: e_3_1 };
1596
1784
  return [3 /*break*/, 16];
1597
1785
  case 15:
1598
1786
  try {
1599
1787
  if (_e && !_e.done && (_r = _d.return)) _r.call(_d);
1600
1788
  }
1601
- finally { if (e_2) throw e_2.error; }
1789
+ finally { if (e_3) throw e_3.error; }
1602
1790
  return [7 /*endfinally*/];
1603
1791
  case 16: return [4 /*yield*/, dispatch('/AppStream')];
1604
1792
  case 17:
@@ -1651,14 +1839,14 @@ describe('service worker host', function () {
1651
1839
  return [3 /*break*/, 22];
1652
1840
  case 26: return [3 /*break*/, 29];
1653
1841
  case 27:
1654
- e_3_1 = _u.sent();
1655
- e_3 = { error: e_3_1 };
1842
+ e_4_1 = _u.sent();
1843
+ e_4 = { error: e_4_1 };
1656
1844
  return [3 /*break*/, 29];
1657
1845
  case 28:
1658
1846
  try {
1659
1847
  if (_g && !_g.done && (_s = _f.return)) _s.call(_f);
1660
1848
  }
1661
- finally { if (e_3) throw e_3.error; }
1849
+ finally { if (e_4) throw e_4.error; }
1662
1850
  return [7 /*endfinally*/];
1663
1851
  case 29:
1664
1852
  _u.trys.push([29, 35, 36, 37]);
@@ -1685,14 +1873,14 @@ describe('service worker host', function () {
1685
1873
  return [3 /*break*/, 30];
1686
1874
  case 34: return [3 /*break*/, 37];
1687
1875
  case 35:
1688
- e_4_1 = _u.sent();
1689
- e_4 = { error: e_4_1 };
1876
+ e_5_1 = _u.sent();
1877
+ e_5 = { error: e_5_1 };
1690
1878
  return [3 /*break*/, 37];
1691
1879
  case 36:
1692
1880
  try {
1693
1881
  if (_k && !_k.done && (_t = _j.return)) _t.call(_j);
1694
1882
  }
1695
- finally { if (e_4) throw e_4.error; }
1883
+ finally { if (e_5) throw e_5.error; }
1696
1884
  return [7 /*endfinally*/];
1697
1885
  case 37: return [4 /*yield*/, dispatch("/app/".concat(app.id))];
1698
1886
  case 38:
@@ -1995,30 +2183,33 @@ describe('service worker host', function () {
1995
2183
  case 0:
1996
2184
  dispatch = seeded().dispatch;
1997
2185
  return [4 /*yield*/, dispatch('/SASjsApi/info')
1998
- // the documented body, field for field, in the values @sasjs/api's
1999
- // BROWSER_INFO states: a property the document does not state, or a login
2000
- // form told there are accounts when there are none, is this host describing
2001
- // an API the server does not serve
2186
+ // the documented body, field for field: a property the document does not
2187
+ // state, or a login form told there are accounts when there are none, is
2188
+ // this host describing an API the server does not serve. `runTimes` is the
2189
+ // host's own, not the package's browser fixture: `sas` is what makes the
2190
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2002
2191
  ];
2003
2192
  case 1:
2004
2193
  info = _f.sent();
2005
- // the documented body, field for field, in the values @sasjs/api's
2006
- // BROWSER_INFO states: a property the document does not state, or a login
2007
- // form told there are accounts when there are none, is this host describing
2008
- // an API the server does not serve
2194
+ // the documented body, field for field: a property the document does not
2195
+ // state, or a login form told there are accounts when there are none, is
2196
+ // this host describing an API the server does not serve. `runTimes` is the
2197
+ // host's own, not the package's browser fixture: `sas` is what makes the
2198
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2009
2199
  _a = expect;
2010
2200
  return [4 /*yield*/, info.json()];
2011
2201
  case 2:
2012
- // the documented body, field for field, in the values @sasjs/api's
2013
- // BROWSER_INFO states: a property the document does not state, or a login
2014
- // form told there are accounts when there are none, is this host describing
2015
- // an API the server does not serve
2202
+ // the documented body, field for field: a property the document does not
2203
+ // state, or a login form told there are accounts when there are none, is
2204
+ // this host describing an API the server does not serve. `runTimes` is the
2205
+ // host's own, not the package's browser fixture: `sas` is what makes the
2206
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2016
2207
  _a.apply(void 0, [_f.sent()]).toEqual({
2017
2208
  mode: 'desktop',
2018
2209
  cors: 'disable',
2019
2210
  httpPort: null,
2020
2211
  httpsPort: null,
2021
- runTimes: ['js'],
2212
+ runTimes: ['js', 'sas'],
2022
2213
  authProviders: [],
2023
2214
  localLoginEnabled: false,
2024
2215
  version: 'dev'
@@ -2631,7 +2822,7 @@ describe('service worker host', function () {
2631
2822
  }); });
2632
2823
  });
2633
2824
  describe('conformance corpus', function () {
2634
- var e_5, _a;
2825
+ var e_6, _a, e_7, _b;
2635
2826
  /**
2636
2827
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
2637
2828
  * its own services, and against a live sasjs/server, run here against this
@@ -2640,7 +2831,7 @@ describe('service worker host', function () {
2640
2831
  * used to have - and it is a test rather than a review.
2641
2832
  */
2642
2833
  var httpFor = function (testCase) {
2643
- var e_6, _a;
2834
+ var e_8, _a;
2644
2835
  var _b;
2645
2836
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
2646
2837
  var query = new URLSearchParams();
@@ -2651,12 +2842,12 @@ describe('service worker host', function () {
2651
2842
  query.set(key, String(value));
2652
2843
  }
2653
2844
  }
2654
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
2845
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
2655
2846
  finally {
2656
2847
  try {
2657
2848
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
2658
2849
  }
2659
- finally { if (e_6) throw e_6.error; }
2850
+ finally { if (e_8) throw e_8.error; }
2660
2851
  }
2661
2852
  var suffix = query.toString() ? "?".concat(query) : '';
2662
2853
  var init = { method: method };
@@ -2687,8 +2878,8 @@ describe('service worker host', function () {
2687
2878
  * does.
2688
2879
  */
2689
2880
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
2690
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_7_1;
2691
- var e_8, _g, e_7, _h;
2881
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_9_1;
2882
+ var e_10, _g, e_9, _h;
2692
2883
  return __generator(this, function (_j) {
2693
2884
  switch (_j.label) {
2694
2885
  case 0:
@@ -2700,12 +2891,12 @@ describe('service worker host', function () {
2700
2891
  text["/sasjs/drive/files".concat(file)] = contents;
2701
2892
  }
2702
2893
  }
2703
- catch (e_8_1) { e_8 = { error: e_8_1 }; }
2894
+ catch (e_10_1) { e_10 = { error: e_10_1 }; }
2704
2895
  finally {
2705
2896
  try {
2706
2897
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
2707
2898
  }
2708
- finally { if (e_8) throw e_8.error; }
2899
+ finally { if (e_10) throw e_10.error; }
2709
2900
  }
2710
2901
  return [4 /*yield*/, runtime.load(text)];
2711
2902
  case 1:
@@ -2728,87 +2919,393 @@ describe('service worker host', function () {
2728
2919
  return [3 /*break*/, 3];
2729
2920
  case 6: return [3 /*break*/, 9];
2730
2921
  case 7:
2731
- e_7_1 = _j.sent();
2732
- e_7 = { error: e_7_1 };
2922
+ e_9_1 = _j.sent();
2923
+ e_9 = { error: e_9_1 };
2733
2924
  return [3 /*break*/, 9];
2734
2925
  case 8:
2735
2926
  try {
2736
2927
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
2737
2928
  }
2738
- finally { if (e_7) throw e_7.error; }
2929
+ finally { if (e_9) throw e_9.error; }
2739
2930
  return [7 /*endfinally*/];
2740
2931
  case 9: return [2 /*return*/];
2741
2932
  }
2742
2933
  });
2743
2934
  }); };
2935
+ /**
2936
+ * The expectation kinds this harness asserts.
2937
+ *
2938
+ * A case that states one it does not implement fails rather than passing
2939
+ * unproven - `keys`, `shape` and `encoding` are asserted by the package's own
2940
+ * corpus run and not here, and a case that starts using them has to be told
2941
+ * so by this list rather than by silence.
2942
+ */
2943
+ var ASSERTED = ['status', 'error', 'partial', 'body', 'contents'];
2944
+ var assertExpectations = function (response, testCase) { return __awaiter(void 0, void 0, void 0, function () {
2945
+ var unasserted, bytes, _a, _b, text, _c, isJson, body;
2946
+ return __generator(this, function (_d) {
2947
+ switch (_d.label) {
2948
+ case 0:
2949
+ unasserted = Object.keys(testCase.expect).filter(function (kind) { return !ASSERTED.includes(kind); });
2950
+ if (unasserted.length)
2951
+ throw new Error("".concat(testCase.id, ": this harness does not assert ").concat(unasserted.join(', ')));
2952
+ expect(response.status).toBe(testCase.expect.status);
2953
+ if (!(testCase.expect.contents instanceof Uint8Array)) return [3 /*break*/, 2];
2954
+ _b = Uint8Array.bind;
2955
+ return [4 /*yield*/, response.arrayBuffer()];
2956
+ case 1:
2957
+ _a = new (_b.apply(Uint8Array, [void 0, _d.sent()]))();
2958
+ return [3 /*break*/, 3];
2959
+ case 2:
2960
+ _a = undefined;
2961
+ _d.label = 3;
2962
+ case 3:
2963
+ bytes = _a;
2964
+ if (!(bytes === undefined)) return [3 /*break*/, 5];
2965
+ return [4 /*yield*/, response.text()];
2966
+ case 4:
2967
+ _c = _d.sent();
2968
+ return [3 /*break*/, 6];
2969
+ case 5:
2970
+ _c = undefined;
2971
+ _d.label = 6;
2972
+ case 6:
2973
+ text = _c;
2974
+ isJson = (response.headers.get('content-type') || '').includes('application/json');
2975
+ if (testCase.expect.error !== undefined) {
2976
+ // the routes that answer an envelope - the two deploy routes - send
2977
+ // `{status, message}`; the rest send the message itself as text, which is
2978
+ // what the server's `res.status(code).send(err.message)` does
2979
+ if (isJson) {
2980
+ body = JSON.parse(text);
2981
+ expect(body.message).toBe(testCase.expect.error);
2982
+ // a refusal that states the envelope it expects is compared on the whole
2983
+ // of it, not only on the message
2984
+ if (testCase.expect.body !== undefined)
2985
+ expect(body).toEqual(testCase.expect.body);
2986
+ }
2987
+ else {
2988
+ // a case cannot state an envelope and be answered with text: one of the
2989
+ // two is wrong, and silence would hide which
2990
+ if (testCase.expect.body !== undefined)
2991
+ throw new Error("".concat(testCase.id, ": the answer is text, so its body cannot be asserted"));
2992
+ expect(text).toBe(testCase.expect.error);
2993
+ }
2994
+ return [2 /*return*/];
2995
+ }
2996
+ if (testCase.expect.contents !== undefined) {
2997
+ if (bytes !== undefined)
2998
+ expect(bytes).toEqual(testCase.expect.contents);
2999
+ else
3000
+ expect(text).toEqual(testCase.expect.contents);
3001
+ return [2 /*return*/];
3002
+ }
3003
+ if (testCase.expect.body !== undefined)
3004
+ expect(JSON.parse(text)).toEqual(testCase.expect.body);
3005
+ if (testCase.expect.partial !== undefined)
3006
+ expect(JSON.parse(text)).toMatchObject(testCase.expect.partial);
3007
+ return [2 /*return*/];
3008
+ }
3009
+ });
3010
+ }); };
2744
3011
  var _loop_1 = function (testCase) {
2745
3012
  it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
2746
- var worker, _a, requestPath, init, response, body, want, _b, _c, _d;
2747
- return __generator(this, function (_e) {
2748
- switch (_e.label) {
3013
+ var worker, _a, requestPath, init, response;
3014
+ return __generator(this, function (_b) {
3015
+ switch (_b.label) {
2749
3016
  case 0:
2750
3017
  worker = loadWorker();
2751
3018
  return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
2752
3019
  case 1:
2753
- _e.sent();
3020
+ _b.sent();
2754
3021
  worker.refuseWrites(Boolean(testCase.unwritable));
2755
3022
  _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
2756
3023
  return [4 /*yield*/, worker.dispatch(requestPath, init)];
2757
3024
  case 2:
2758
- response = _e.sent();
2759
- expect(response.status).toBe(testCase.expect.status);
2760
- if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
2761
- return [4 /*yield*/, response.text()
2762
- // the routes that answer an envelope - the two deploy routes - send
2763
- // `{status, message}`; the rest send the message itself as text, which
2764
- // is what the server's `res.status(code).send(err.message)` does
2765
- ];
3025
+ response = _b.sent();
3026
+ return [4 /*yield*/, assertExpectations(response, testCase)];
2766
3027
  case 3:
2767
- body = _e.sent();
2768
- // the routes that answer an envelope - the two deploy routes - send
2769
- // `{status, message}`; the rest send the message itself as text, which
2770
- // is what the server's `res.status(code).send(err.message)` does
2771
- if ((response.headers.get('content-type') || '').includes('application/json'))
2772
- expect(JSON.parse(body)).toEqual({
2773
- status: 'failure',
2774
- message: testCase.expect.error
2775
- });
3028
+ _b.sent();
3029
+ return [2 /*return*/];
3030
+ }
3031
+ });
3032
+ }); });
3033
+ };
3034
+ try {
3035
+ for (var DRIVE_CORPUS_1 = __values(api_1.DRIVE_CORPUS), DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next(); !DRIVE_CORPUS_1_1.done; DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next()) {
3036
+ var testCase = DRIVE_CORPUS_1_1.value;
3037
+ _loop_1(testCase);
3038
+ }
3039
+ }
3040
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
3041
+ finally {
3042
+ try {
3043
+ if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
3044
+ }
3045
+ finally { if (e_6) throw e_6.error; }
3046
+ }
3047
+ /** The app a removal case names, as the corpus states it. */
3048
+ var CORPUS_APP = {
3049
+ appLoc: '/Public/myApp',
3050
+ streamWebFolder: 'web',
3051
+ publishedAt: '2026-01-01T00:00:00.000Z'
3052
+ };
3053
+ /**
3054
+ * A second app that no removal case names.
3055
+ *
3056
+ * Without it, a removal that ignored the id and deleted whatever was
3057
+ * published would pass both removal cases - the unknown-id one sees an empty
3058
+ * index either way. With it, the assertion after the answer is that the app
3059
+ * the id did *not* name is still listed.
3060
+ */
3061
+ var BYSTANDER = {
3062
+ id: '0b9a1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d',
3063
+ name: 'bystander',
3064
+ appLoc: '/Public/bystander',
3065
+ streamWebFolder: 'web',
3066
+ publishedAt: '2026-01-01T00:00:00.000Z'
3067
+ };
3068
+ /**
3069
+ * The index a removal case starts from - built from the id the case asks to
3070
+ * remove, not from the answer it expects, so a host that removed some other
3071
+ * app would answer a different id and fail.
3072
+ */
3073
+ var seedIndex = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
3074
+ var id, named, entries;
3075
+ var _a, _b, _c;
3076
+ return __generator(this, function (_d) {
3077
+ switch (_d.label) {
3078
+ case 0:
3079
+ if (testCase.route !== 'DELETE /SASjsApi/drive/deploy')
3080
+ return [2 /*return*/];
3081
+ id = (_a = testCase.request.body) === null || _a === void 0 ? void 0 : _a.id;
3082
+ named = (_b = testCase.expect.partial) === null || _b === void 0 ? void 0 : _b.app;
3083
+ entries = [BYSTANDER];
3084
+ // the app the case expects to remove is published only when the case names
3085
+ // the id it removes: the unknown-id case asks for one that is not there, and
3086
+ // publishing it would make that case pass by accident. It goes in *after* the
3087
+ // bystander, so a removal that takes the first entry rather than the named
3088
+ // one removes the bystander and both assertions below fail.
3089
+ if (id !== undefined && (named === null || named === void 0 ? void 0 : named.id) === id)
3090
+ entries.push(__assign({ id: id, name: (_c = named.name) !== null && _c !== void 0 ? _c : 'myApp' }, CORPUS_APP));
3091
+ return [4 /*yield*/, runtime.load({
3092
+ '/sasjs/drive/files/.sasjs/apps.json': JSON.stringify(entries)
3093
+ })];
3094
+ case 1:
3095
+ _d.sent();
3096
+ return [2 /*return*/];
3097
+ }
3098
+ });
3099
+ }); };
3100
+ /**
3101
+ * The archive a client uploads, built from the tree the case states.
3102
+ *
3103
+ * The corpus is transport-agnostic: an upload case gives the tree the archive
3104
+ * has to expand to, and the folder in the query where the document puts it.
3105
+ * This host takes an archive, so the harness makes one - which is the only way
3106
+ * the mock's own expansion, and the tree it produces, gets exercised by the
3107
+ * cases that describe it.
3108
+ */
3109
+ var archiveOf = function (tree) { return __awaiter(void 0, void 0, void 0, function () {
3110
+ var files, walk;
3111
+ return __generator(this, function (_a) {
3112
+ files = {};
3113
+ walk = function (members, prefix) {
3114
+ var e_11, _a;
3115
+ var _b;
3116
+ try {
3117
+ for (var _c = __values(members || []), _d = _c.next(); !_d.done; _d = _c.next()) {
3118
+ var member = _d.value;
3119
+ var at = prefix ? "".concat(prefix, "/").concat(member.name) : member.name;
3120
+ if (member.type === 'folder')
3121
+ walk(member.members, at);
2776
3122
  else
2777
- expect(body).toBe(testCase.expect.error);
2778
- return [3 /*break*/, 8];
3123
+ files[at] =
3124
+ member.encoding === 'base64'
3125
+ ? new Uint8Array(Buffer.from(String(member.code), 'base64'))
3126
+ : String((_b = member.code) !== null && _b !== void 0 ? _b : '');
3127
+ }
3128
+ }
3129
+ catch (e_11_1) { e_11 = { error: e_11_1 }; }
3130
+ finally {
3131
+ try {
3132
+ if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
3133
+ }
3134
+ finally { if (e_11) throw e_11.error; }
3135
+ }
3136
+ };
3137
+ walk(tree === null || tree === void 0 ? void 0 : tree.members, '');
3138
+ return [2 /*return*/, zipOf(files)];
3139
+ });
3140
+ }); };
3141
+ /**
3142
+ * The request a deploy case describes, as HTTP.
3143
+ *
3144
+ * The JSON and removal routes carry their fields as they are stated. The upload
3145
+ * route's body is an archive, so the tree becomes one and the fields the case
3146
+ * states beside it travel as form fields - a host's multipart body, with the
3147
+ * folder also in the query where the document puts it.
3148
+ */
3149
+ var deployRequest = function (testCase) { return __awaiter(void 0, void 0, void 0, function () {
3150
+ var _a, requestPath, init, body, archive, form, _b, _c, _d, field, value;
3151
+ var e_12, _e;
3152
+ return __generator(this, function (_f) {
3153
+ switch (_f.label) {
3154
+ case 0:
3155
+ _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
3156
+ if (!testCase.route.endsWith('/deploy/upload'))
3157
+ return [2 /*return*/, { requestPath: requestPath, init: init }];
3158
+ body = (testCase.request.body || {});
3159
+ return [4 /*yield*/, archiveOf(body.fileTree)];
3160
+ case 1:
3161
+ archive = _f.sent();
3162
+ form = new FormData();
3163
+ try {
3164
+ for (_b = __values(Object.entries(body)), _c = _b.next(); !_c.done; _c = _b.next()) {
3165
+ _d = __read(_c.value, 2), field = _d[0], value = _d[1];
3166
+ if (field !== 'fileTree' && value !== undefined)
3167
+ form.set(field, String(value));
3168
+ }
3169
+ }
3170
+ catch (e_12_1) { e_12 = { error: e_12_1 }; }
3171
+ finally {
3172
+ try {
3173
+ if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
3174
+ }
3175
+ finally { if (e_12) throw e_12.error; }
3176
+ }
3177
+ form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
3178
+ return [2 /*return*/, { requestPath: requestPath, init: { method: 'POST', body: form } }];
3179
+ }
3180
+ });
3181
+ }); };
3182
+ var _loop_2 = function (testCase) {
3183
+ it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
3184
+ var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_13_1, _d, _e, path_7, file, e_14_1;
3185
+ var e_13, _f, e_14, _g;
3186
+ var _h, _j, _k;
3187
+ return __generator(this, function (_l) {
3188
+ switch (_l.label) {
3189
+ case 0:
3190
+ worker = loadWorker();
3191
+ return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
3192
+ case 1:
3193
+ _l.sent();
3194
+ return [4 /*yield*/, seedIndex(worker.runtime, testCase)];
3195
+ case 2:
3196
+ _l.sent();
3197
+ worker.refuseWrites(Boolean(testCase.unwritable));
3198
+ return [4 /*yield*/, deployRequest(testCase)];
3199
+ case 3:
3200
+ _a = _l.sent(), requestPath = _a.requestPath, init = _a.init;
3201
+ return [4 /*yield*/, worker.dispatch(requestPath, init)];
2779
3202
  case 4:
2780
- want = testCase.expect.contents;
2781
- if (!(typeof want === 'string')) return [3 /*break*/, 6];
2782
- _b = expect;
2783
- return [4 /*yield*/, response.text()];
3203
+ response = _l.sent();
3204
+ return [4 /*yield*/, assertExpectations(response, testCase)
3205
+ // a removal takes the app the id names, and leaves the rest published
3206
+ ];
2784
3207
  case 5:
2785
- _b.apply(void 0, [_e.sent()]).toBe(want);
2786
- return [3 /*break*/, 8];
3208
+ _l.sent();
3209
+ if (!(testCase.route === 'DELETE /SASjsApi/drive/deploy')) return [3 /*break*/, 8];
3210
+ return [4 /*yield*/, worker.dispatch('/app/apps.json')];
2787
3211
  case 6:
2788
- if (!(want !== undefined)) return [3 /*break*/, 8];
2789
- _c = expect;
2790
- _d = Uint8Array.bind;
2791
- return [4 /*yield*/, response.arrayBuffer()];
3212
+ listing = _l.sent();
3213
+ return [4 /*yield*/, listing.json()];
2792
3214
  case 7:
2793
- _c.apply(void 0, [new (_d.apply(Uint8Array, [void 0, _e.sent()]))()]).toEqual(want);
2794
- _e.label = 8;
2795
- case 8: return [2 /*return*/];
3215
+ ids = (_l.sent()).apps.map(function (app) { return app.id; });
3216
+ expect(ids).toContain(BYSTANDER.id);
3217
+ if (response.status === 200)
3218
+ expect(ids).not.toContain((_h = testCase.request.body) === null || _h === void 0 ? void 0 : _h.id);
3219
+ _l.label = 8;
3220
+ case 8:
3221
+ _l.trys.push([8, 13, 14, 15]);
3222
+ _b = __values(((_j = testCase.expectDrive) === null || _j === void 0 ? void 0 : _j.present) || []), _c = _b.next();
3223
+ _l.label = 9;
3224
+ case 9:
3225
+ if (!!_c.done) return [3 /*break*/, 12];
3226
+ path_6 = _c.value;
3227
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_6))];
3228
+ case 10:
3229
+ file = _l.sent();
3230
+ expect({ path: path_6, status: file.status }).toEqual({ path: path_6, status: 200 });
3231
+ _l.label = 11;
3232
+ case 11:
3233
+ _c = _b.next();
3234
+ return [3 /*break*/, 9];
3235
+ case 12: return [3 /*break*/, 15];
3236
+ case 13:
3237
+ e_13_1 = _l.sent();
3238
+ e_13 = { error: e_13_1 };
3239
+ return [3 /*break*/, 15];
3240
+ case 14:
3241
+ try {
3242
+ if (_c && !_c.done && (_f = _b.return)) _f.call(_b);
3243
+ }
3244
+ finally { if (e_13) throw e_13.error; }
3245
+ return [7 /*endfinally*/];
3246
+ case 15:
3247
+ _l.trys.push([15, 20, 21, 22]);
3248
+ _d = __values(((_k = testCase.expectDrive) === null || _k === void 0 ? void 0 : _k.missing) || []), _e = _d.next();
3249
+ _l.label = 16;
3250
+ case 16:
3251
+ if (!!_e.done) return [3 /*break*/, 19];
3252
+ path_7 = _e.value;
3253
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_7))];
3254
+ case 17:
3255
+ file = _l.sent();
3256
+ expect({ path: path_7, status: file.status }).toEqual({ path: path_7, status: 404 });
3257
+ _l.label = 18;
3258
+ case 18:
3259
+ _e = _d.next();
3260
+ return [3 /*break*/, 16];
3261
+ case 19: return [3 /*break*/, 22];
3262
+ case 20:
3263
+ e_14_1 = _l.sent();
3264
+ e_14 = { error: e_14_1 };
3265
+ return [3 /*break*/, 22];
3266
+ case 21:
3267
+ try {
3268
+ if (_e && !_e.done && (_g = _d.return)) _g.call(_d);
3269
+ }
3270
+ finally { if (e_14) throw e_14.error; }
3271
+ return [7 /*endfinally*/];
3272
+ case 22: return [2 /*return*/];
2796
3273
  }
2797
3274
  });
2798
3275
  }); });
2799
3276
  };
2800
3277
  try {
2801
- for (var DRIVE_CORPUS_1 = __values(api_1.DRIVE_CORPUS), DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next(); !DRIVE_CORPUS_1_1.done; DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next()) {
2802
- var testCase = DRIVE_CORPUS_1_1.value;
2803
- _loop_1(testCase);
3278
+ /**
3279
+ * The deploy routes: the JSON form and the removal, plus the upload route
3280
+ * through an archive this harness builds from the tree the case states.
3281
+ *
3282
+ * The corpus describes an upload by what the archive has to expand to, so a
3283
+ * host that takes bytes can run the same case as one that is handed an
3284
+ * expansion - `deployRequest` makes the archive, and what it asserts is this
3285
+ * worker's own unzipping and tree-building rather than the corpus's
3286
+ * assumption. The one case that cannot travel this way is the expansion that is
3287
+ * not a tree, which the corpus marks `transport: 'expansion'`.
3288
+ *
3289
+ * The removal cases need an app to remove, so the index is seeded with the one
3290
+ * the case names and a bystander that must survive it. The state a case leaves
3291
+ * is checked too - `expectDrive` - because both deploy answers are `200` and
3292
+ * replacing a folder only differs from keeping it in what the drive holds
3293
+ * afterwards.
3294
+ */
3295
+ for (var _c = __values(api_1.DEPLOY_CORPUS.filter(
3296
+ // a case a host that takes an archive cannot produce, which the corpus marks
3297
+ // on the case itself rather than leaving each host to name it
3298
+ function (entry) { return entry.transport !== 'expansion'; })), _d = _c.next(); !_d.done; _d = _c.next()) {
3299
+ var testCase = _d.value;
3300
+ _loop_2(testCase);
2804
3301
  }
2805
3302
  }
2806
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
3303
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
2807
3304
  finally {
2808
3305
  try {
2809
- if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
3306
+ if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
2810
3307
  }
2811
- finally { if (e_5) throw e_5.error; }
3308
+ finally { if (e_7) throw e_7.error; }
2812
3309
  }
2813
3310
  });
2814
3311
  /**
@@ -2820,7 +3317,7 @@ describe('service worker host', function () {
2820
3317
  * are the package's.
2821
3318
  */
2822
3319
  describe('identity and record conformance', function () {
2823
- var e_9, _a;
3320
+ var e_15, _a;
2824
3321
  /**
2825
3322
  * Boots the worker from a fixture, the way the page does: the case's own
2826
3323
  * identity, and `subject: null` for no implicit caller, so a case that
@@ -2853,7 +3350,7 @@ describe('service worker host', function () {
2853
3350
  };
2854
3351
  /** The request a case describes, as HTTP: path parameters filled, token sent. */
2855
3352
  var httpForCase = function (testCase) {
2856
- var e_10, _a;
3353
+ var e_16, _a;
2857
3354
  var _b = __read(testCase.route.split(' '), 2), method = _b[0], routePath = _b[1];
2858
3355
  var filled = routePath.replace(/\{(\w+)\}/g, function (_match, name) {
2859
3356
  return encodeURIComponent(String((testCase.request.params || {})[name]));
@@ -2866,12 +3363,12 @@ describe('service worker host', function () {
2866
3363
  query.set(key, String(value));
2867
3364
  }
2868
3365
  }
2869
- catch (e_10_1) { e_10 = { error: e_10_1 }; }
3366
+ catch (e_16_1) { e_16 = { error: e_16_1 }; }
2870
3367
  finally {
2871
3368
  try {
2872
3369
  if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
2873
3370
  }
2874
- finally { if (e_10) throw e_10.error; }
3371
+ finally { if (e_16) throw e_16.error; }
2875
3372
  }
2876
3373
  var suffix = query.toString() ? "?".concat(query) : '';
2877
3374
  var headers = {};
@@ -2965,7 +3462,7 @@ describe('service worker host', function () {
2965
3462
  }
2966
3463
  });
2967
3464
  }); };
2968
- var _loop_2 = function (testCase) {
3465
+ var _loop_3 = function (testCase) {
2969
3466
  it("".concat(testCase.id, ": ").concat(testCase.description), function () { return __awaiter(void 0, void 0, void 0, function () {
2970
3467
  var worker, _a, requestPath, init, _b, _c;
2971
3468
  return __generator(this, function (_d) {
@@ -2990,15 +3487,15 @@ describe('service worker host', function () {
2990
3487
  try {
2991
3488
  for (var _b = __values(__spreadArray(__spreadArray([], __read(api_1.IDENTITY_CORPUS), false), __read(api_1.RECORD_CORPUS), false)), _c = _b.next(); !_c.done; _c = _b.next()) {
2992
3489
  var testCase = _c.value;
2993
- _loop_2(testCase);
3490
+ _loop_3(testCase);
2994
3491
  }
2995
3492
  }
2996
- catch (e_9_1) { e_9 = { error: e_9_1 }; }
3493
+ catch (e_15_1) { e_15 = { error: e_15_1 }; }
2997
3494
  finally {
2998
3495
  try {
2999
3496
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
3000
3497
  }
3001
- finally { if (e_9) throw e_9.error; }
3498
+ finally { if (e_15) throw e_15.error; }
3002
3499
  }
3003
3500
  it('answers as the fixture administrator when no subject was chosen', function () { return __awaiter(void 0, void 0, void 0, function () {
3004
3501
  var worker, response, rules;
package/package.json CHANGED
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@sasjs/adapter": "6.0.2",
74
- "@sasjs/api": "0.27.0",
74
+ "@sasjs/api": "0.28.0",
75
75
  "@sasjs/core": "7.1.12",
76
76
  "@sasjs/lint": "5.1.1",
77
77
  "@sasjs/utils": "4.11.0",
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.27.0"
128
+ "version": "6.27.2"
129
129
  }