@frockbot/plugin-computer 0.2.3 → 0.2.4

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.
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ ComputerProtocolDecodeError,
4
+ decodeComputerProjectionV1,
5
+ type ComputerProgressViewV1,
6
+ type ComputerProjectionV1,
7
+ } from "./protocol.js";
8
+
9
+ const projection: ComputerProjectionV1 & { progress: ComputerProgressViewV1 } =
10
+ {
11
+ version: 1,
12
+ botId: "scout",
13
+ providerLabel: "Fake Computer",
14
+ phase: "provisioning",
15
+ message: "Starting the desktop…",
16
+ progress: {
17
+ version: 1,
18
+ kind: "connect",
19
+ startedAt: "2026-09-03T00:00:00.000Z",
20
+ updatedAt: "2026-09-03T00:00:02.000Z",
21
+ index: 2,
22
+ total: 3,
23
+ steps: [
24
+ {
25
+ version: 1,
26
+ id: "waking",
27
+ label: "Waking the Computer",
28
+ status: "complete",
29
+ },
30
+ {
31
+ version: 1,
32
+ id: "starting-desktop",
33
+ label: "Starting the desktop",
34
+ status: "active",
35
+ },
36
+ {
37
+ version: 1,
38
+ id: "minting-viewer",
39
+ label: "Minting the viewer",
40
+ status: "pending",
41
+ },
42
+ ],
43
+ },
44
+ screenshots: [],
45
+ };
46
+
47
+ describe("Computer projection progress", () => {
48
+ test("decodes the exact ordered V1 progress shape", () => {
49
+ expect(decodeComputerProjectionV1(projection).progress).toEqual(
50
+ projection.progress,
51
+ );
52
+ });
53
+
54
+ test("refuses malformed or extended progress at the client seam", () => {
55
+ expect(() =>
56
+ decodeComputerProjectionV1({
57
+ ...projection,
58
+ progress: {
59
+ ...projection.progress,
60
+ secret: "must not cross",
61
+ },
62
+ }),
63
+ ).toThrow(ComputerProtocolDecodeError);
64
+ expect(() =>
65
+ decodeComputerProjectionV1({
66
+ ...projection,
67
+ progress: {
68
+ ...projection.progress,
69
+ steps: projection.progress.steps.map((step) => ({
70
+ ...step,
71
+ status: "working",
72
+ })),
73
+ },
74
+ }),
75
+ ).toThrow(ComputerProtocolDecodeError);
76
+ });
77
+ });
package/src/protocol.ts CHANGED
@@ -61,6 +61,26 @@ export interface ComputerDoctorViewV1 {
61
61
  checks: ComputerDoctorCheckViewV1[];
62
62
  }
63
63
 
64
+ export type ComputerProgressStepStatusV1 = "pending" | "active" | "complete";
65
+
66
+ export interface ComputerProgressStepViewV1 {
67
+ version: 1;
68
+ id: string;
69
+ label: string;
70
+ status: ComputerProgressStepStatusV1;
71
+ }
72
+
73
+ /** Durable progress projected from the Bot authority; it contains no secrets. */
74
+ export interface ComputerProgressViewV1 {
75
+ version: 1;
76
+ kind: "connect" | "update";
77
+ startedAt: string;
78
+ updatedAt: string;
79
+ index: number;
80
+ total: number;
81
+ steps: ComputerProgressStepViewV1[];
82
+ }
83
+
64
84
  export const COMPUTER_PHASES = [
65
85
  "unconfigured",
66
86
  "idle",
@@ -92,6 +112,7 @@ export interface ComputerProjectionV1 {
92
112
  providerLabel: string;
93
113
  phase: ComputerPhase;
94
114
  message: string;
115
+ progress?: ComputerProgressViewV1;
95
116
  viewerSession?: ComputerViewerSessionViewV1;
96
117
  controlLease?: ComputerControlLeaseViewV1;
97
118
  screenshots: ComputerScreenshotViewV1[];
@@ -176,6 +197,95 @@ function phase(value: unknown): ComputerPhase {
176
197
  return decoded;
177
198
  }
178
199
 
200
+ function boundedInteger(
201
+ value: unknown,
202
+ minimum: number,
203
+ maximum: number,
204
+ label: string,
205
+ ): number {
206
+ if (
207
+ typeof value !== "number" ||
208
+ !Number.isSafeInteger(value) ||
209
+ value < minimum ||
210
+ value > maximum
211
+ ) {
212
+ throw new ComputerProtocolDecodeError(`${label} is invalid`);
213
+ }
214
+ return value;
215
+ }
216
+
217
+ function decodeProgressStepV1(value: unknown): ComputerProgressStepViewV1 {
218
+ const candidate = record(value, "Computer progress step");
219
+ exactKeys(
220
+ candidate,
221
+ ["version", "id", "label", "status"],
222
+ [],
223
+ "Computer progress step",
224
+ );
225
+ if (
226
+ candidate.version !== 1 ||
227
+ (candidate.status !== "pending" &&
228
+ candidate.status !== "active" &&
229
+ candidate.status !== "complete")
230
+ ) {
231
+ throw new ComputerProtocolDecodeError("Computer progress step is invalid");
232
+ }
233
+ return {
234
+ version: 1,
235
+ id: text(candidate.id, "Computer progress step id"),
236
+ label: text(candidate.label, "Computer progress step label"),
237
+ status: candidate.status,
238
+ };
239
+ }
240
+
241
+ export function decodeComputerProgressViewV1(
242
+ value: unknown,
243
+ ): ComputerProgressViewV1 {
244
+ const candidate = record(value, "Computer progress");
245
+ exactKeys(
246
+ candidate,
247
+ ["version", "kind", "startedAt", "updatedAt", "index", "total", "steps"],
248
+ [],
249
+ "Computer progress",
250
+ );
251
+ if (
252
+ candidate.version !== 1 ||
253
+ (candidate.kind !== "connect" && candidate.kind !== "update") ||
254
+ !Array.isArray(candidate.steps) ||
255
+ candidate.steps.length === 0 ||
256
+ candidate.steps.length > 20
257
+ ) {
258
+ throw new ComputerProtocolDecodeError("Computer progress is invalid");
259
+ }
260
+ const total = boundedInteger(
261
+ candidate.total,
262
+ 1,
263
+ 1_000,
264
+ "Computer progress total",
265
+ );
266
+ const index = boundedInteger(
267
+ candidate.index,
268
+ 0,
269
+ total,
270
+ "Computer progress index",
271
+ );
272
+ const steps = candidate.steps.map(decodeProgressStepV1);
273
+ if (steps.filter((step) => step.status === "active").length > 1) {
274
+ throw new ComputerProtocolDecodeError(
275
+ "Computer progress has multiple active steps",
276
+ );
277
+ }
278
+ return {
279
+ version: 1,
280
+ kind: candidate.kind,
281
+ startedAt: timestamp(candidate.startedAt, "Computer progress startedAt"),
282
+ updatedAt: timestamp(candidate.updatedAt, "Computer progress updatedAt"),
283
+ index,
284
+ total,
285
+ steps,
286
+ };
287
+ }
288
+
179
289
  export function decodeComputerCommandV1(value: unknown): ComputerCommandV1 {
180
290
  const candidate = record(value, "Computer command");
181
291
  exactKeys(
@@ -328,7 +438,7 @@ export function decodeComputerProjectionV1(
328
438
  exactKeys(
329
439
  candidate,
330
440
  ["version", "botId", "providerLabel", "phase", "message", "screenshots"],
331
- ["viewerSession", "controlLease", "doctor"],
441
+ ["viewerSession", "controlLease", "doctor", "progress"],
332
442
  "Computer projection",
333
443
  );
334
444
  if (candidate.version !== 1 || !Array.isArray(candidate.screenshots)) {
@@ -343,6 +453,9 @@ export function decodeComputerProjectionV1(
343
453
  ),
344
454
  phase: phase(candidate.phase),
345
455
  message: text(candidate.message, "Computer projection message"),
456
+ ...(candidate.progress === undefined
457
+ ? {}
458
+ : { progress: decodeComputerProgressViewV1(candidate.progress) }),
346
459
  ...(candidate.viewerSession === undefined
347
460
  ? {}
348
461
  : { viewerSession: decodeViewerSessionV1(candidate.viewerSession) }),
package/src/shared.ts CHANGED
@@ -2,6 +2,7 @@ import type { InjectionKey, Ref } from "vue";
2
2
  import type {
3
3
  ComputerDoctorViewV1,
4
4
  ComputerPhase,
5
+ ComputerProgressViewV1,
5
6
  ComputerScreenshotViewV1,
6
7
  } from "./protocol.js";
7
8
 
@@ -30,6 +31,7 @@ export interface ComputerState {
30
31
  botId: string;
31
32
  providerLabel: string;
32
33
  message: string;
34
+ progress?: ComputerProgressViewV1;
33
35
  viewerUrl?: string;
34
36
  /** Whether the one live viewer is open over the hosted shell. */
35
37
  expanded: boolean;