@reventlessdev/reventless-aws 3.0.0-alpha.290 → 3.0.0-alpha.292

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.
@@ -32,18 +32,33 @@ The config.json field set.
32
32
 
33
33
  `computed` arrives in wire order and keeps it. `viewModes` unset ⇒ no
34
34
  `viewModes` key and no per-mode key, so a deployment that wants no optional mode
35
- gets byte-identical output to before this input existed. `shellConfig` merges in
36
- last; a key it shares with one already present fails the deploy naming it,
37
- because silently resolving the collision either way produces an app pointed
35
+ gets byte-identical output to before this input existed. `bakedManifest` unset
36
+ no `manifestUrl`, and every caller keeps the admin discovery path. `shellConfig`
37
+ merges in last; a key it shares with one already present fails the deploy naming
38
+ it, because silently resolving the collision either way produces an app pointed
38
39
  somewhere unintended with nothing in the diff to say so.
39
40
  */
40
41
  let fields = (
41
42
  ~computed: array<(string, JSON.t)>,
42
43
  ~viewModes: option<array<Platform.viewMode>>=?,
44
+ ~bakedManifest: option<Platform.bakedManifest>=?,
43
45
  ~shellConfig: option<dict<JSON.t>>=?,
44
46
  ): dict<JSON.t> => {
45
47
  let out = computed->Dict.fromArray
46
48
 
49
+ // A declared bake is what turns the shell's non-elevated audience on: an
50
+ // operator keeps the admin queries, everyone else discovers from this file.
51
+ // Computed rather than left to `shellConfig` because the deploy is what
52
+ // decides where the file goes — a passthrough could point the shell at a key
53
+ // nothing writes, and a statically-discovered shell has no admin API behind it
54
+ // to notice.
55
+ bakedManifest->Option.forEach(bake =>
56
+ out->Dict.set(
57
+ "manifestUrl",
58
+ JSON.Encode.string(ReventlessCore.Platform_BakedManifest.urlForKey(bake.key)),
59
+ )
60
+ )
61
+
47
62
  switch viewModes {
48
63
  | Some(modes) =>
49
64
  out->Dict.set(
@@ -4,6 +4,7 @@ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
4
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
7
+ import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
7
8
 
8
9
  function modeOptions(mode) {
9
10
  if (mode.TAG === "Map") {
@@ -28,8 +29,11 @@ function modeOptions(mode) {
28
29
  }
29
30
  }
30
31
 
31
- function fields(computed, viewModes, shellConfig) {
32
+ function fields(computed, viewModes, bakedManifest, shellConfig) {
32
33
  let out = Object.fromEntries(computed);
34
+ Stdlib_Option.forEach(bakedManifest, bake => {
35
+ out["manifestUrl"] = Platform_BakedManifest$ReventlessCore.urlForKey(bake.key);
36
+ });
33
37
  if (viewModes !== undefined) {
34
38
  out["viewModes"] = viewModes.map(Platform$ReventlessInfra.viewModeToString);
35
39
  viewModes.forEach(m => {
@@ -176,3 +176,177 @@ describe("toEntryWith carries Internal queryables on the filtered field", () =>
176
176
  expect(names)->toEqual([])
177
177
  })
178
178
  })
179
+
180
+ // ── Bake mode ────────────────────────────────────────────────────────────────
181
+ // The same scan, written out instead of answered with. Two inputs decide what a
182
+ // bake does and neither is checkable at deploy time: the include-list the
183
+ // platform put in the environment, and the target its caller supplied. Both are
184
+ // pure, so both are assertable without a Lambda.
185
+
186
+ describe("bakeTargetOf", () => {
187
+ let target = raw => Platform_ComponentDefinitions_Lambda_Ops.bakeTargetOf(JSON.parseOrThrow(raw))
188
+
189
+ // The read paths must stay read paths: an AppSync resolver invokes with
190
+ // `{}` or `{complete: true}`, and neither may write an object.
191
+ testSync("a query invocation is not a bake", () => {
192
+ expect(target(`{}`)->Option.isNone)->toBe(true)
193
+ expect(target(`{"complete": true}`)->Option.isNone)->toBe(true)
194
+ })
195
+
196
+ // No bucket, no default. Guessing one would write the manifest somewhere
197
+ // nothing serves it, which reads as a bake that worked.
198
+ testSync("a bake without a target is not a bake", () =>
199
+ expect(target(`{"bake": true}`)->Option.isNone)->toBe(true)
200
+ )
201
+
202
+ testSync("defaults the key to the one the deploy tells the shell to fetch", () =>
203
+ expect(target(`{"bake": true, "bucket": "host-ui"}`)->Option.map(t => t.key))->toEqual(
204
+ Some("component-manifest.json"),
205
+ )
206
+ )
207
+
208
+ testSync("takes an explicit key", () =>
209
+ expect(
210
+ target(`{"bake": true, "bucket": "host-ui", "key": "storefront.json"}`)->Option.map(t => (
211
+ t.bucket,
212
+ t.key,
213
+ )),
214
+ )->toEqual(Some(("host-ui", "storefront.json")))
215
+ )
216
+ })
217
+
218
+ describe("bakeSelection", () => {
219
+ let sel = raw => Platform_ComponentDefinitions_Lambda_Ops.bakeSelection(JSON.parseOrThrow(raw))
220
+
221
+ // Absent is not empty. `views` absent means every public component of that
222
+ // plugin; `views: []` means none of them, and a decoder that collapsed the two
223
+ // would silently bake an empty section.
224
+ testSync("keeps absent and empty apart", () => {
225
+ expect(sel(`{"plugin": "Catalog"}`)->Option.flatMap(s => s.views))->toEqual(None)
226
+ expect(sel(`{"plugin": "Catalog", "views": []}`)->Option.flatMap(s => s.views))->toEqual(
227
+ Some([]),
228
+ )
229
+ })
230
+
231
+ testSync("reads the named components", () =>
232
+ expect(
233
+ sel(`{"plugin": "Ordering", "views": ["Orders"], "commands": ["PlaceOrder"]}`)->Option.map(
234
+ s => (s.plugin, s.views, s.commands),
235
+ ),
236
+ )->toEqual(Some(("Ordering", Some(["Orders"]), Some(["PlaceOrder"]))))
237
+ )
238
+
239
+ testSync("refuses an entry naming no plugin", () =>
240
+ expect(sel(`{"views": ["Orders"]}`)->Option.isNone)->toBe(true)
241
+ )
242
+ })
243
+
244
+ // The bake reads a read model the deploy updates asynchronously, so "is this the
245
+ // deployment I was asked to bake" is a question it has to be able to answer. The
246
+ // answer is an equality check against the key each plugin stack just wrote.
247
+ describe("pendingRegistrations", () => {
248
+ let row = (~name, ~key=?, ()) => {
249
+ let item = Dict.fromArray([("name", JSON.Encode.string(name))])
250
+ key->Option.forEach(k =>
251
+ item->Dict.set(
252
+ "structure",
253
+ JSON.parseOrThrow(`{"$offload": {"store": "pluginStructures", "key": "${k}"}}`),
254
+ )
255
+ )
256
+ item
257
+ }
258
+
259
+ let pending = (rows, expect) =>
260
+ Platform_ComponentDefinitions_Lambda_Ops.pendingRegistrations(
261
+ rows,
262
+ ~expect=Dict.fromArray(expect),
263
+ )
264
+
265
+ testSync("nothing is pending when every row carries the key the deploy wrote", () =>
266
+ expect(
267
+ pending(
268
+ [row(~name="Catalog", ~key="sha256/a", ()), row(~name="Ordering", ~key="sha256/b", ())],
269
+ [("Catalog", "sha256/a"), ("Ordering", "sha256/b")],
270
+ ),
271
+ )->toEqual([])
272
+ )
273
+
274
+ // The window this exists for: the stack wrote a new structure, the row still
275
+ // points at the one before it.
276
+ testSync("names a plugin still carrying the previous structure", () =>
277
+ expect(
278
+ pending(
279
+ [row(~name="Catalog", ~key="sha256/a", ()), row(~name="Ordering", ~key="sha256/old", ())],
280
+ [("Catalog", "sha256/a"), ("Ordering", "sha256/new")],
281
+ ),
282
+ )->toEqual(["Ordering"])
283
+ )
284
+
285
+ // A plugin that has never registered is behind, not absent — baking without it
286
+ // would ship a shop missing a section.
287
+ testSync("names a plugin with no row at all", () =>
288
+ expect(pending([], [("Ordering", "sha256/new")]))->toEqual(["Ordering"])
289
+ )
290
+
291
+ // Deployed before the stack exported its key, or invoked by hand: bake what is
292
+ // current rather than wait for an expectation nobody stated.
293
+ testSync("waits for nothing when the caller expects nothing", () =>
294
+ expect(pending([row(~name="Ordering", ~key="sha256/old", ())], []))->toEqual([])
295
+ )
296
+
297
+ testSync("reads the expectations off the invocation payload", () =>
298
+ expect(
299
+ Platform_ComponentDefinitions_Lambda_Ops.bakeExpectations(
300
+ JSON.parseOrThrow(`{"bake": true, "expect": {"Ordering": "sha256/b"}}`),
301
+ )->Dict.get("Ordering"),
302
+ )->toEqual(Some("sha256/b"))
303
+ )
304
+ })
305
+
306
+ describe("resolveStructure", () => {
307
+ let item = (structure: string) =>
308
+ Dict.fromArray([
309
+ ("name", JSON.Encode.string("Ordering")),
310
+ ("structure", JSON.parseOrThrow(structure)),
311
+ ])
312
+
313
+ let offloaded = `{"$offload": {"store": "pluginStructures", "key": "sha256/abc", "bytes": 4}}`
314
+
315
+ test("substitutes the fetched bytes for the reference", async () => {
316
+ let resolved = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
317
+ _ => Promise.resolve(`{"aggregates": []}`),
318
+ item(offloaded),
319
+ )
320
+ expect(resolved->Dict.get("structure")->Option.map(j => JSON.stringify(j)))->toEqual(
321
+ Some(`{"aggregates":[]}`),
322
+ )
323
+ })
324
+
325
+ // One offload bucket serves every plugin, so the S3 error names only the key —
326
+ // which plugin's row carries the unreadable reference is the part worth having.
327
+ // A missing object is the shape a deploy leaves behind while the read model
328
+ // still points at the previous structure.
329
+ test("names the plugin whose reference cannot be read", async () => {
330
+ let failed = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
331
+ _ => Promise.reject(JsExn.anyToExnInternal(JsError.make("AccessDenied"))),
332
+ item(offloaded),
333
+ )
334
+ ->Promise.thenResolve(_ => None)
335
+ ->Promise.catch(e =>
336
+ Promise.resolve(e->JsExn.fromException->Option.flatMap(JsExn.message))
337
+ )
338
+ expect(failed)->toEqual(
339
+ Some("offloaded structure for plugin Ordering is unreadable at sha256/abc: AccessDenied"),
340
+ )
341
+ })
342
+
343
+ test("passes an inline structure through untouched", async () => {
344
+ let resolved = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
345
+ _ => Promise.reject(JsExn.anyToExnInternal(JsError.make("must not fetch"))),
346
+ item(`{"aggregates": []}`),
347
+ )
348
+ expect(resolved->Dict.get("structure")->Option.map(j => JSON.stringify(j)))->toEqual(
349
+ Some(`{"aggregates":[]}`),
350
+ )
351
+ })
352
+ })
@@ -2,7 +2,10 @@
2
2
 
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
8
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
9
  import * as Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws from "../src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs";
7
10
 
8
11
  function entry(filter, structure) {
@@ -108,6 +111,133 @@ globalThis.describe("toEntryWith carries Internal queryables on the filtered fie
108
111
  });
109
112
  });
110
113
 
114
+ globalThis.describe("bakeTargetOf", () => {
115
+ globalThis.test("a query invocation is not a bake", () => {
116
+ globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{}`)))).toBe(true);
117
+ globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"complete": true}`)))).toBe(true);
118
+ });
119
+ globalThis.test("a bake without a target is not a bake", () => {
120
+ globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true}`)))).toBe(true);
121
+ });
122
+ globalThis.test("defaults the key to the one the deploy tells the shell to fetch", () => {
123
+ globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true, "bucket": "host-ui"}`)), t => t.key)).toEqual("component-manifest.json");
124
+ });
125
+ globalThis.test("takes an explicit key", () => {
126
+ globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true, "bucket": "host-ui", "key": "storefront.json"}`)), t => [
127
+ t.bucket,
128
+ t.key
129
+ ])).toEqual([
130
+ "host-ui",
131
+ "storefront.json"
132
+ ]);
133
+ });
134
+ });
135
+
136
+ globalThis.describe("bakeSelection", () => {
137
+ globalThis.test("keeps absent and empty apart", () => {
138
+ globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog"}`)), s => s.views)).toEqual(undefined);
139
+ globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog", "views": []}`)), s => s.views)).toEqual([]);
140
+ });
141
+ globalThis.test("reads the named components", () => {
142
+ globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Ordering", "views": ["Orders"], "commands": ["PlaceOrder"]}`)), s => [
143
+ s.plugin,
144
+ s.views,
145
+ s.commands
146
+ ])).toEqual([
147
+ "Ordering",
148
+ ["Orders"],
149
+ ["PlaceOrder"]
150
+ ]);
151
+ });
152
+ globalThis.test("refuses an entry naming no plugin", () => {
153
+ globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"views": ["Orders"]}`)))).toBe(true);
154
+ });
155
+ });
156
+
157
+ globalThis.describe("pendingRegistrations", () => {
158
+ let row = (name, key, param) => {
159
+ let item = Object.fromEntries([[
160
+ "name",
161
+ name
162
+ ]]);
163
+ Stdlib_Option.forEach(key, k => {
164
+ item["structure"] = JSON.parse(`{"$offload": {"store": "pluginStructures", "key": "` + k + `"}}`);
165
+ });
166
+ return item;
167
+ };
168
+ globalThis.test("nothing is pending when every row carries the key the deploy wrote", () => {
169
+ let rows = [
170
+ row("Catalog", "sha256/a", undefined),
171
+ row("Ordering", "sha256/b", undefined)
172
+ ];
173
+ globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([
174
+ [
175
+ "Catalog",
176
+ "sha256/a"
177
+ ],
178
+ [
179
+ "Ordering",
180
+ "sha256/b"
181
+ ]
182
+ ]))).toEqual([]);
183
+ });
184
+ globalThis.test("names a plugin still carrying the previous structure", () => {
185
+ let rows = [
186
+ row("Catalog", "sha256/a", undefined),
187
+ row("Ordering", "sha256/old", undefined)
188
+ ];
189
+ globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([
190
+ [
191
+ "Catalog",
192
+ "sha256/a"
193
+ ],
194
+ [
195
+ "Ordering",
196
+ "sha256/new"
197
+ ]
198
+ ]))).toEqual(["Ordering"]);
199
+ });
200
+ globalThis.test("names a plugin with no row at all", () => {
201
+ globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations([], Object.fromEntries([[
202
+ "Ordering",
203
+ "sha256/new"
204
+ ]]))).toEqual(["Ordering"]);
205
+ });
206
+ globalThis.test("waits for nothing when the caller expects nothing", () => {
207
+ let rows = [row("Ordering", "sha256/old", undefined)];
208
+ globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([]))).toEqual([]);
209
+ });
210
+ globalThis.test("reads the expectations off the invocation payload", () => {
211
+ globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeExpectations(JSON.parse(`{"bake": true, "expect": {"Ordering": "sha256/b"}}`))["Ordering"]).toEqual("sha256/b");
212
+ });
213
+ });
214
+
215
+ globalThis.describe("resolveStructure", () => {
216
+ let item = structure => Object.fromEntries([
217
+ [
218
+ "name",
219
+ "Ordering"
220
+ ],
221
+ [
222
+ "structure",
223
+ JSON.parse(structure)
224
+ ]
225
+ ]);
226
+ let offloaded = `{"$offload": {"store": "pluginStructures", "key": "sha256/abc", "bytes": 4}}`;
227
+ globalThis.test("substitutes the fetched bytes for the reference", async () => {
228
+ let resolved = await Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.resolve(`{"aggregates": []}`), item(offloaded));
229
+ globalThis.expect(Stdlib_Option.map(resolved["structure"], j => JSON.stringify(j))).toEqual(`{"aggregates":[]}`);
230
+ });
231
+ globalThis.test("names the plugin whose reference cannot be read", async () => {
232
+ let failed = await Stdlib_Promise.$$catch(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.reject(Primitive_exceptions.internalToException(new Error("AccessDenied"))), item(offloaded)).then(param => {}), e => Promise.resolve(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(e), Stdlib_JsExn.message)));
233
+ globalThis.expect(failed).toEqual("offloaded structure for plugin Ordering is unreadable at sha256/abc: AccessDenied");
234
+ });
235
+ globalThis.test("passes an inline structure through untouched", async () => {
236
+ let resolved = await Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.reject(Primitive_exceptions.internalToException(new Error("must not fetch"))), item(`{"aggregates": []}`));
237
+ globalThis.expect(Stdlib_Option.map(resolved["structure"], j => JSON.stringify(j))).toEqual(`{"aggregates":[]}`);
238
+ });
239
+ });
240
+
111
241
  export {
112
242
  entry,
113
243
  members,
@@ -40,6 +40,47 @@ describe("Util_ShellConfig.fields — viewModes", () => {
40
40
  })
41
41
  })
42
42
 
43
+ describe("Util_ShellConfig.fields — bakedManifest", () => {
44
+ let bake = (~key: option<string>=?): ReventlessInfra.Platform.bakedManifest => {
45
+ components: [{plugin: "Catalog", views: ["Products"], commands: []}],
46
+ key: ?key,
47
+ }
48
+
49
+ testSync("unset ⇒ no manifestUrl, so every caller keeps the admin path", () => {
50
+ let out = Util_ShellConfig.fields(~computed)
51
+ expect(out->Dict.get("manifestUrl")->Option.isNone)->toBe(true)
52
+ })
53
+
54
+ testSync("declared ⇒ the URL the deploy writes the file to", () => {
55
+ let out = Util_ShellConfig.fields(~computed, ~bakedManifest=bake())
56
+ expect(out->get("manifestUrl"))->toEqual(JSON.Encode.string("/component-manifest.json"))
57
+ })
58
+
59
+ // The key and the URL are one string; a renamed bake that kept the default
60
+ // URL would 404 on a shell with no admin API to fall back to.
61
+ testSync("follows a renamed bake", () => {
62
+ let out = Util_ShellConfig.fields(~computed, ~bakedManifest=bake(~key="storefront.json"))
63
+ expect(out->get("manifestUrl"))->toEqual(JSON.Encode.string("/storefront.json"))
64
+ })
65
+
66
+ testSync("a shellConfig manifestUrl fails the deploy rather than redirecting it", () => {
67
+ let failure = try {
68
+ let _ = Util_ShellConfig.fields(
69
+ ~computed,
70
+ ~bakedManifest=bake(),
71
+ ~shellConfig=Dict.fromArray([("manifestUrl", JSON.Encode.string("/elsewhere.json"))]),
72
+ )
73
+ None
74
+ } catch {
75
+ | Failure(message) => Some(message)
76
+ }
77
+ switch failure {
78
+ | Some(message) => expect(message->String.includes("manifestUrl"))->toBe(true)
79
+ | None => fail("a shellConfig key redirecting the computed manifest must fail the deploy")
80
+ }
81
+ })
82
+ })
83
+
43
84
  describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
44
85
  testSync("shell-owned keys land verbatim, under the computed ones", () => {
45
86
  let out = Util_ShellConfig.fields(
@@ -22,7 +22,7 @@ function get(out, key) {
22
22
 
23
23
  globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
24
24
  globalThis.test("unset ⇒ no viewModes key and the computed set is untouched", () => {
25
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined);
25
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined);
26
26
  globalThis.expect(Object.keys(out)).toEqual([
27
27
  "apiEndpoint",
28
28
  "region"
@@ -33,7 +33,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
33
33
  let out = Util_ShellConfig$ReventlessAws.fields(computed, [{
34
34
  TAG: "Map",
35
35
  _0: {}
36
- }], undefined);
36
+ }], undefined, undefined);
37
37
  globalThis.expect(get(out, "viewModes")).toEqual(["map"]);
38
38
  globalThis.expect(Stdlib_Option.isNone(out["mapStyle"])).toBe(true);
39
39
  });
@@ -51,7 +51,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
51
51
  layout: "dagre"
52
52
  }
53
53
  }
54
- ], undefined);
54
+ ], undefined, undefined);
55
55
  globalThis.expect(get(out, "viewModes")).toEqual([
56
56
  "map",
57
57
  "graph"
@@ -61,9 +61,55 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
61
61
  });
62
62
  });
63
63
 
64
+ globalThis.describe("Util_ShellConfig.fields — bakedManifest", () => {
65
+ let bake = key => ({
66
+ components: [{
67
+ plugin: "Catalog",
68
+ views: ["Products"],
69
+ commands: []
70
+ }],
71
+ key: key
72
+ });
73
+ globalThis.test("unset ⇒ no manifestUrl, so every caller keeps the admin path", () => {
74
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined);
75
+ globalThis.expect(Stdlib_Option.isNone(out["manifestUrl"])).toBe(true);
76
+ });
77
+ globalThis.test("declared ⇒ the URL the deploy writes the file to", () => {
78
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), undefined);
79
+ globalThis.expect(get(out, "manifestUrl")).toEqual("/component-manifest.json");
80
+ });
81
+ globalThis.test("follows a renamed bake", () => {
82
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake("storefront.json"), undefined);
83
+ globalThis.expect(get(out, "manifestUrl")).toEqual("/storefront.json");
84
+ });
85
+ globalThis.test("a shellConfig manifestUrl fails the deploy rather than redirecting it", () => {
86
+ let failure;
87
+ try {
88
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), Object.fromEntries([[
89
+ "manifestUrl",
90
+ "/elsewhere.json"
91
+ ]]));
92
+ failure = undefined;
93
+ } catch (raw_message) {
94
+ let message = Primitive_exceptions.internalToException(raw_message);
95
+ if (message.RE_EXN_ID === "Failure") {
96
+ failure = message._1;
97
+ } else {
98
+ throw message;
99
+ }
100
+ }
101
+ if (failure !== undefined) {
102
+ globalThis.expect(failure.includes("manifestUrl")).toBe(true);
103
+ return;
104
+ } else {
105
+ return JestGlobals.fail("a shellConfig key redirecting the computed manifest must fail the deploy");
106
+ }
107
+ });
108
+ });
109
+
64
110
  globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
65
111
  globalThis.test("shell-owned keys land verbatim, under the computed ones", () => {
66
- let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([
112
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, Object.fromEntries([
67
113
  [
68
114
  "platformName",
69
115
  "Online Shop"
@@ -84,7 +130,7 @@ globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () =>
84
130
  globalThis.test("a key the deploy computes fails the deploy, naming it", () => {
85
131
  let failure;
86
132
  try {
87
- Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([[
133
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, Object.fromEntries([[
88
134
  "apiEndpoint",
89
135
  "https://wrong.example/graphql"
90
136
  ]]));