@serverkgg/bridge 0.0.9 → 0.0.11
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/package.json +1 -1
- package/src/cli/validate.test.ts +116 -1
- package/src/cli/validate.ts +50 -3
- package/src/context/bridgeContext.ts +8 -1
- package/src/core/bridgeKind.ts +1 -0
- package/src/core/bridgeProbeError.ts +16 -0
- package/src/core/bridgeSetup.ts +20 -0
- package/src/core/bridgeValue.test.ts +29 -0
- package/src/core/bridgeValue.ts +12 -0
- package/src/core/index.ts +2 -0
- package/src/guides/blocks.ts +8 -0
- package/src/guides/markdown.test.ts +18 -0
- package/src/guides/markdown.ts +7 -1
- package/src/manifest/guide.ts +1 -0
- package/src/manifest/manifest.test.ts +49 -2
- package/src/manifest/presence.ts +9 -1
- package/src/modules/bridge.ts +20 -0
- package/src/modules/bridgeDriver.ts +2 -0
- package/src/modules/bridgeModule.ts +7 -0
- package/src/panel/bridgeControl.ts +1 -0
- package/src/panel/bridgeDetail.ts +6 -5
- package/src/panel/bridgePanel.ts +2 -0
- package/src/probes/a2s.test.ts +71 -0
- package/src/probes/a2s.ts +38 -11
- package/src/protocol/bridgeFrame.ts +5 -0
- package/src/protocol/bridgeLimits.ts +10 -0
- package/src/protocol/bridgeManifest.ts +2 -0
- package/src/protocol/bridgeSetup.ts +97 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeConfigReader.ts +12 -8
- package/src/runtime/bridgeContext.ts +28 -2
- package/src/runtime/bridgeDriver.test.ts +512 -2
- package/src/runtime/bridgeDriver.ts +219 -1
- package/src/runtime/bridgeEvents.test.ts +96 -0
- package/src/runtime/bridgeEvents.ts +4 -1
- package/src/runtime/bridgeRegistry.test.ts +38 -1
- package/src/runtime/bridgeRegistry.ts +14 -1
- package/src/runtime/bridgeSecrets.test.ts +121 -0
- package/src/runtime/bridgeSecrets.ts +43 -0
- package/src/runtime/bridgeServer.test.ts +262 -2
- package/src/runtime/bridgeServer.ts +17 -0
- package/src/runtime/bridgeSetup.test.ts +575 -0
- package/src/runtime/bridgeSetup.ts +303 -0
- package/src/runtime/bridgeSetupHandlers.test.ts +303 -0
- package/src/runtime/bridgeSetupHandlers.ts +71 -0
- package/src/runtime/bridgeSignal.test.ts +114 -0
- package/src/runtime/bridgeSignal.ts +31 -0
- package/src/runtime/bridgeSupervisor.test.ts +111 -0
- package/src/runtime/bridgeSupervisor.ts +37 -5
- package/src/runtime/serve.ts +19 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/stripAnsi.ts +12 -0
- package/src/wire/panel.test.ts +155 -0
- package/src/wire/panel.ts +43 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BridgeSetupPromptKind, BridgeSetupStepState } from "../core";
|
|
3
|
+
import type { BridgeSetupRuntime } from "../protocol";
|
|
4
|
+
import { failedSetup, normalizeSetupRuntime, setupSummaryOf, unsupportedSetup } from "./bridgeSetup";
|
|
5
|
+
|
|
6
|
+
const text = (value: string) => ({
|
|
7
|
+
ar: value,
|
|
8
|
+
en: value,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const runtimeWith = (prompt: BridgeSetupRuntime["prompt"]): BridgeSetupRuntime => ({
|
|
12
|
+
steps: [
|
|
13
|
+
{
|
|
14
|
+
id: "sign-in",
|
|
15
|
+
state: BridgeSetupStepState.Active,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "download",
|
|
19
|
+
state: BridgeSetupStepState.Pending,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
prompt,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("the setup summary", () => {
|
|
26
|
+
test("has nothing to summarise while the driver reported no runtime", () => {
|
|
27
|
+
expect(setupSummaryOf(null)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("names the active driver step", () => {
|
|
31
|
+
expect(
|
|
32
|
+
setupSummaryOf(
|
|
33
|
+
runtimeWith({
|
|
34
|
+
kind: BridgeSetupPromptKind.Wait,
|
|
35
|
+
message: text("starting your server"),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
).toEqual({
|
|
39
|
+
step: "sign-in",
|
|
40
|
+
attention: false,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("names no step once every driver step is done", () => {
|
|
45
|
+
expect(
|
|
46
|
+
setupSummaryOf({
|
|
47
|
+
steps: [
|
|
48
|
+
{
|
|
49
|
+
id: "sign-in",
|
|
50
|
+
state: BridgeSetupStepState.Done,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
prompt: null,
|
|
54
|
+
}),
|
|
55
|
+
).toEqual({
|
|
56
|
+
step: null,
|
|
57
|
+
attention: false,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("asks for attention while a link prompt waits on the customer", () => {
|
|
62
|
+
expect(
|
|
63
|
+
setupSummaryOf(
|
|
64
|
+
runtimeWith({
|
|
65
|
+
kind: BridgeSetupPromptKind.Link,
|
|
66
|
+
message: text("sign in with your account"),
|
|
67
|
+
url: "https://hytale.com/device",
|
|
68
|
+
code: "ABCD-EFGH",
|
|
69
|
+
expiresAt: null,
|
|
70
|
+
actions: [],
|
|
71
|
+
}),
|
|
72
|
+
)?.attention,
|
|
73
|
+
).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("asks for attention on an action prompt and on a failed one", () => {
|
|
77
|
+
expect(
|
|
78
|
+
setupSummaryOf(
|
|
79
|
+
runtimeWith({
|
|
80
|
+
kind: BridgeSetupPromptKind.Action,
|
|
81
|
+
message: text("get a new code"),
|
|
82
|
+
actions: [],
|
|
83
|
+
}),
|
|
84
|
+
)?.attention,
|
|
85
|
+
).toBe(true);
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
setupSummaryOf(
|
|
89
|
+
runtimeWith({
|
|
90
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
91
|
+
message: text("the sign-in expired"),
|
|
92
|
+
actions: [],
|
|
93
|
+
}),
|
|
94
|
+
)?.attention,
|
|
95
|
+
).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("waits quietly while a progress prompt runs", () => {
|
|
99
|
+
expect(
|
|
100
|
+
setupSummaryOf(
|
|
101
|
+
runtimeWith({
|
|
102
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
103
|
+
message: text("downloading the game"),
|
|
104
|
+
percent: 10,
|
|
105
|
+
detail: "146.6 MiB / 1.4 GiB",
|
|
106
|
+
}),
|
|
107
|
+
)?.attention,
|
|
108
|
+
).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("the unsupported sentinel", () => {
|
|
113
|
+
test("answers a driver that declares no setup", () => {
|
|
114
|
+
expect(unsupportedSetup()).toEqual({
|
|
115
|
+
supported: false,
|
|
116
|
+
runtime: null,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("a rejected submit", () => {
|
|
122
|
+
test("turns the user error into a failed prompt keeping the previous actions", () => {
|
|
123
|
+
const result = failedSetup(
|
|
124
|
+
runtimeWith({
|
|
125
|
+
kind: BridgeSetupPromptKind.Link,
|
|
126
|
+
message: text("sign in with your account"),
|
|
127
|
+
url: "https://hytale.com/device",
|
|
128
|
+
code: "ABCD-EFGH",
|
|
129
|
+
expiresAt: null,
|
|
130
|
+
actions: [
|
|
131
|
+
{
|
|
132
|
+
id: "renew",
|
|
133
|
+
label: text("Get a new code"),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
text("this code cannot be renewed any more"),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result).toEqual({
|
|
141
|
+
supported: true,
|
|
142
|
+
runtime: {
|
|
143
|
+
steps: [
|
|
144
|
+
{
|
|
145
|
+
id: "sign-in",
|
|
146
|
+
state: BridgeSetupStepState.Active,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "download",
|
|
150
|
+
state: BridgeSetupStepState.Pending,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
prompt: {
|
|
154
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
155
|
+
message: text("this code cannot be renewed any more"),
|
|
156
|
+
actions: [
|
|
157
|
+
{
|
|
158
|
+
id: "renew",
|
|
159
|
+
label: text("Get a new code"),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("carries no actions over from a prompt that had none", () => {
|
|
168
|
+
const result = failedSetup(
|
|
169
|
+
runtimeWith({
|
|
170
|
+
kind: BridgeSetupPromptKind.Wait,
|
|
171
|
+
message: text("starting your server"),
|
|
172
|
+
}),
|
|
173
|
+
text("the server never came up"),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(result.runtime?.prompt).toEqual({
|
|
177
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
178
|
+
message: text("the server never came up"),
|
|
179
|
+
actions: [],
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("stays empty when the driver never reported a runtime", () => {
|
|
184
|
+
expect(failedSetup(null, text("nothing to fail"))).toEqual({
|
|
185
|
+
supported: true,
|
|
186
|
+
runtime: null,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const declared: ReadonlySet<string> = new Set([
|
|
192
|
+
"sign-in",
|
|
193
|
+
"download",
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
const normalizing = (runtime: BridgeSetupRuntime | null) => {
|
|
197
|
+
const reasons: string[] = [];
|
|
198
|
+
|
|
199
|
+
const normalized = normalizeSetupRuntime(runtime, {
|
|
200
|
+
declared,
|
|
201
|
+
warn: (_message, fields) => {
|
|
202
|
+
reasons.push(String(fields?.reason));
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
normalized,
|
|
208
|
+
reasons,
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const withSteps = (steps: BridgeSetupRuntime["steps"]): BridgeSetupRuntime => ({
|
|
213
|
+
steps,
|
|
214
|
+
prompt: null,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const link = (
|
|
218
|
+
prompt: Partial<
|
|
219
|
+
Extract<
|
|
220
|
+
BridgeSetupRuntime["prompt"],
|
|
221
|
+
{
|
|
222
|
+
kind: BridgeSetupPromptKind.Link;
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
>,
|
|
226
|
+
) => {
|
|
227
|
+
return runtimeWith({
|
|
228
|
+
kind: BridgeSetupPromptKind.Link,
|
|
229
|
+
message: text("sign in with your account"),
|
|
230
|
+
url: "https://hytale.com/device",
|
|
231
|
+
code: "ABCD-EFGH",
|
|
232
|
+
expiresAt: null,
|
|
233
|
+
actions: [],
|
|
234
|
+
...prompt,
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
describe("normalizing a reported runtime", () => {
|
|
239
|
+
test("has nothing to normalize while the driver reported no runtime", () => {
|
|
240
|
+
expect(
|
|
241
|
+
normalizeSetupRuntime(null, {
|
|
242
|
+
declared,
|
|
243
|
+
warn: () => {
|
|
244
|
+
return;
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("drops a step the driver never declared", () => {
|
|
251
|
+
const { normalized, reasons } = normalizing(
|
|
252
|
+
withSteps([
|
|
253
|
+
{
|
|
254
|
+
id: "sign-in",
|
|
255
|
+
state: BridgeSetupStepState.Active,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "invent",
|
|
259
|
+
state: BridgeSetupStepState.Pending,
|
|
260
|
+
},
|
|
261
|
+
]),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(normalized?.steps).toEqual([
|
|
265
|
+
{
|
|
266
|
+
id: "sign-in",
|
|
267
|
+
state: BridgeSetupStepState.Active,
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
expect(reasons).toEqual([
|
|
271
|
+
"unknown-step",
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("drops the second report of the same step", () => {
|
|
276
|
+
const { normalized, reasons } = normalizing(
|
|
277
|
+
withSteps([
|
|
278
|
+
{
|
|
279
|
+
id: "sign-in",
|
|
280
|
+
state: BridgeSetupStepState.Done,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "sign-in",
|
|
284
|
+
state: BridgeSetupStepState.Failed,
|
|
285
|
+
},
|
|
286
|
+
]),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect(normalized?.steps).toEqual([
|
|
290
|
+
{
|
|
291
|
+
id: "sign-in",
|
|
292
|
+
state: BridgeSetupStepState.Done,
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
expect(reasons).toEqual([
|
|
296
|
+
"duplicate-step",
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("keeps the first active step and pends the rest", () => {
|
|
301
|
+
const { normalized, reasons } = normalizing(
|
|
302
|
+
withSteps([
|
|
303
|
+
{
|
|
304
|
+
id: "sign-in",
|
|
305
|
+
state: BridgeSetupStepState.Active,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "download",
|
|
309
|
+
state: BridgeSetupStepState.Active,
|
|
310
|
+
},
|
|
311
|
+
]),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(normalized?.steps).toEqual([
|
|
315
|
+
{
|
|
316
|
+
id: "sign-in",
|
|
317
|
+
state: BridgeSetupStepState.Active,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "download",
|
|
321
|
+
state: BridgeSetupStepState.Pending,
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
expect(reasons).toEqual([
|
|
325
|
+
"extra-active-step",
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("clamps a percent that left the scale", () => {
|
|
330
|
+
expect(
|
|
331
|
+
normalizing(
|
|
332
|
+
runtimeWith({
|
|
333
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
334
|
+
message: text("downloading the game"),
|
|
335
|
+
percent: 140,
|
|
336
|
+
detail: null,
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
).toEqual({
|
|
340
|
+
normalized: {
|
|
341
|
+
steps: [
|
|
342
|
+
{
|
|
343
|
+
id: "sign-in",
|
|
344
|
+
state: BridgeSetupStepState.Active,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "download",
|
|
348
|
+
state: BridgeSetupStepState.Pending,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
prompt: {
|
|
352
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
353
|
+
message: text("downloading the game"),
|
|
354
|
+
percent: 100,
|
|
355
|
+
detail: null,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
reasons: [
|
|
359
|
+
"percent-out-of-range",
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("drops a percent that is not a number", () => {
|
|
365
|
+
const { normalized, reasons } = normalizing(
|
|
366
|
+
runtimeWith({
|
|
367
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
368
|
+
message: text("downloading the game"),
|
|
369
|
+
percent: Number.NaN,
|
|
370
|
+
detail: null,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(normalized?.prompt).toEqual({
|
|
375
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
376
|
+
message: text("downloading the game"),
|
|
377
|
+
percent: null,
|
|
378
|
+
detail: null,
|
|
379
|
+
});
|
|
380
|
+
expect(reasons).toEqual([
|
|
381
|
+
"unusable-percent",
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("cuts a detail line to the length the platform takes", () => {
|
|
386
|
+
const { normalized, reasons } = normalizing(
|
|
387
|
+
runtimeWith({
|
|
388
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
389
|
+
message: text("downloading the game"),
|
|
390
|
+
percent: null,
|
|
391
|
+
detail: "d".repeat(300),
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(normalized?.prompt).toEqual({
|
|
396
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
397
|
+
message: text("downloading the game"),
|
|
398
|
+
percent: null,
|
|
399
|
+
detail: "d".repeat(256),
|
|
400
|
+
});
|
|
401
|
+
expect(reasons).toEqual([
|
|
402
|
+
"long-detail",
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("drops an empty detail line", () => {
|
|
407
|
+
const { normalized, reasons } = normalizing(
|
|
408
|
+
runtimeWith({
|
|
409
|
+
kind: BridgeSetupPromptKind.Progress,
|
|
410
|
+
message: text("downloading the game"),
|
|
411
|
+
percent: null,
|
|
412
|
+
detail: "",
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(normalized?.prompt).toMatchObject({
|
|
417
|
+
detail: null,
|
|
418
|
+
});
|
|
419
|
+
expect(reasons).toEqual([
|
|
420
|
+
"empty-detail",
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("drops an action whose id is not kebab-case and the duplicate beside it", () => {
|
|
425
|
+
const { normalized, reasons } = normalizing(
|
|
426
|
+
runtimeWith({
|
|
427
|
+
kind: BridgeSetupPromptKind.Action,
|
|
428
|
+
message: text("get a new code"),
|
|
429
|
+
actions: [
|
|
430
|
+
{
|
|
431
|
+
id: "renew",
|
|
432
|
+
label: text("Get a new code"),
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "Renew Now",
|
|
436
|
+
label: text("Get a new code"),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "renew",
|
|
440
|
+
label: text("Get a new code"),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(normalized?.prompt).toEqual({
|
|
447
|
+
kind: BridgeSetupPromptKind.Action,
|
|
448
|
+
message: text("get a new code"),
|
|
449
|
+
actions: [
|
|
450
|
+
{
|
|
451
|
+
id: "renew",
|
|
452
|
+
label: text("Get a new code"),
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
expect(reasons).toEqual([
|
|
457
|
+
"unknown-action-id",
|
|
458
|
+
"duplicate-action",
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("keeps only as many actions as the platform renders", () => {
|
|
463
|
+
const { normalized, reasons } = normalizing(
|
|
464
|
+
runtimeWith({
|
|
465
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
466
|
+
message: text("the sign-in expired"),
|
|
467
|
+
actions: Array.from({
|
|
468
|
+
length: 6,
|
|
469
|
+
}).map((_entry, index) => ({
|
|
470
|
+
id: `action-${index}`,
|
|
471
|
+
label: text(`Action ${index}`),
|
|
472
|
+
})),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(normalized?.prompt?.kind).toBe(BridgeSetupPromptKind.Failed);
|
|
477
|
+
expect(normalized?.prompt && "actions" in normalized.prompt ? normalized.prompt.actions.length : 0).toBe(4);
|
|
478
|
+
expect(reasons).toEqual([
|
|
479
|
+
"extra-actions",
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("cuts a long code and drops an empty one", () => {
|
|
484
|
+
expect(
|
|
485
|
+
normalizing(
|
|
486
|
+
link({
|
|
487
|
+
code: "c".repeat(80),
|
|
488
|
+
}),
|
|
489
|
+
).normalized?.prompt,
|
|
490
|
+
).toMatchObject({
|
|
491
|
+
code: "c".repeat(64),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(
|
|
495
|
+
normalizing(
|
|
496
|
+
link({
|
|
497
|
+
code: "",
|
|
498
|
+
}),
|
|
499
|
+
).reasons,
|
|
500
|
+
).toEqual([
|
|
501
|
+
"empty-code",
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("re-emits an expiry as an iso stamp and drops one it cannot read", () => {
|
|
506
|
+
expect(
|
|
507
|
+
normalizing(
|
|
508
|
+
link({
|
|
509
|
+
expiresAt: "2026-09-04T10:00:00+03:00",
|
|
510
|
+
}),
|
|
511
|
+
).normalized?.prompt,
|
|
512
|
+
).toMatchObject({
|
|
513
|
+
expiresAt: "2026-09-04T07:00:00.000Z",
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(
|
|
517
|
+
normalizing(
|
|
518
|
+
link({
|
|
519
|
+
expiresAt: "soon",
|
|
520
|
+
}),
|
|
521
|
+
).reasons,
|
|
522
|
+
).toEqual([
|
|
523
|
+
"unusable-expiry",
|
|
524
|
+
]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("turns a link the browser cannot open into a failed prompt keeping its actions", () => {
|
|
528
|
+
const { normalized, reasons } = normalizing(
|
|
529
|
+
link({
|
|
530
|
+
url: "javascript:alert(1)",
|
|
531
|
+
actions: [
|
|
532
|
+
{
|
|
533
|
+
id: "renew",
|
|
534
|
+
label: text("Get a new code"),
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(normalized?.prompt).toEqual({
|
|
541
|
+
kind: BridgeSetupPromptKind.Failed,
|
|
542
|
+
message: {
|
|
543
|
+
ar: "اللعبة رجّعت رابط دخول ما يشتغل. جرّب مرة ثانية.",
|
|
544
|
+
en: "The game reported a broken sign-in link. Try again.",
|
|
545
|
+
},
|
|
546
|
+
actions: [
|
|
547
|
+
{
|
|
548
|
+
id: "renew",
|
|
549
|
+
label: text("Get a new code"),
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
expect(reasons).toEqual([
|
|
554
|
+
"unusable-link",
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("turns a url it cannot parse into a failed prompt", () => {
|
|
559
|
+
expect(
|
|
560
|
+
normalizing(
|
|
561
|
+
link({
|
|
562
|
+
url: "not a url",
|
|
563
|
+
}),
|
|
564
|
+
).normalized?.prompt?.kind,
|
|
565
|
+
).toBe(BridgeSetupPromptKind.Failed);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("leaves a healthy runtime exactly as the driver reported it", () => {
|
|
569
|
+
const source = link({});
|
|
570
|
+
const { normalized, reasons } = normalizing(source);
|
|
571
|
+
|
|
572
|
+
expect(normalized).toEqual(source);
|
|
573
|
+
expect(reasons).toEqual([]);
|
|
574
|
+
});
|
|
575
|
+
});
|