@frockbot/plugin-computer 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bot.test.ts CHANGED
@@ -4,17 +4,34 @@ import type {
4
4
  ComputerControlLease,
5
5
  ComputerHandle,
6
6
  } from "@frockbot/computer-core";
7
- import { ComputerError } from "@frockbot/computer-core";
7
+ import { computerBotPathKeyV1, ComputerError } from "@frockbot/computer-core";
8
8
  import {
9
+ COMPUTER_CONNECT_DEFERRAL_MS,
10
+ COMPUTER_CONNECT_START_DELAY_MS,
11
+ COMPUTER_CONNECT_WATCHDOG_MS,
9
12
  COMPUTER_CONTROL_RECORD_KEY,
10
13
  COMPUTER_INTENT_PREFIX,
14
+ COMPUTER_PENDING_CONNECT_KEY,
11
15
  COMPUTER_PROVIDER_RECORD_KEY,
12
16
  COMPUTER_VIEWER_RECORD_KEY,
13
17
  createComputerBotBackendContribution,
14
18
  type ComputerBotStorage,
15
19
  type ComputerBotTransaction,
16
20
  } from "./bot.js";
17
- import type { ComputerCommandV1 } from "./protocol.js";
21
+ import {
22
+ computerCommandFingerprintV1,
23
+ type ComputerCommandV1,
24
+ } from "./protocol.js";
25
+ import { FakeWorkspace } from "./workspace-fixture.js";
26
+
27
+ function png(): Uint8Array {
28
+ const bytes = new Uint8Array(32);
29
+ bytes.set([137, 80, 78, 71, 13, 10, 26, 10], 0);
30
+ const view = new DataView(bytes.buffer);
31
+ view.setUint32(16, 1280);
32
+ view.setUint32(20, 720);
33
+ return bytes;
34
+ }
18
35
 
19
36
  class MemoryStorage implements ComputerBotStorage {
20
37
  readonly values = new Map<string, unknown>();
@@ -75,6 +92,12 @@ function fakeHandle(options: {
75
92
  scope?: string,
76
93
  ): Promise<ComputerControlLease>;
77
94
  release?(lease: ComputerControlLease, scope?: string): Promise<void>;
95
+ capture?(): Promise<{
96
+ bytes: Uint8Array;
97
+ mediaType: "image/png";
98
+ display: string;
99
+ capturedAt: string;
100
+ }>;
78
101
  }): ComputerHandle {
79
102
  return {
80
103
  assignment: { providerId: "fake", generation: 1 },
@@ -96,6 +119,9 @@ function fakeHandle(options: {
96
119
  renew: (lease, request) => options.renew!(lease, request?.scope),
97
120
  release: (lease, request) => options.release!(lease, request?.scope),
98
121
  },
122
+ ...(options.capture
123
+ ? { screenshot: { capture: () => options.capture!() } }
124
+ : {}),
99
125
  close: () => Promise.resolve(),
100
126
  };
101
127
  }
@@ -126,12 +152,27 @@ describe("Computer Bot Durable Object Contribution", () => {
126
152
  label: "Starting the desktop",
127
153
  index: 3,
128
154
  total: 5,
155
+ provisioning: {
156
+ version: 1,
157
+ kind: "provision",
158
+ label: "installing the browser",
159
+ index: 4,
160
+ total: 5,
161
+ resumed: false,
162
+ },
129
163
  });
130
164
  expect(await contribution.read("user-1", "scout")).toMatchObject({
131
165
  phase: "provisioning",
132
166
  progress: {
133
167
  version: 1,
134
168
  kind: "connect",
169
+ provisioning: {
170
+ kind: "provision",
171
+ label: "installing the browser",
172
+ index: 4,
173
+ total: 5,
174
+ resumed: false,
175
+ },
135
176
  steps: [
136
177
  { id: "waking", status: "complete" },
137
178
  { id: "attaching", status: "complete" },
@@ -157,6 +198,7 @@ describe("Computer Bot Durable Object Contribution", () => {
157
198
  "scout",
158
199
  command("connect", "connect-progress"),
159
200
  );
201
+ await contribution.settleScheduledWork();
160
202
 
161
203
  expect(
162
204
  (storage.values.get(COMPUTER_PROVIDER_RECORD_KEY) as { version: number })
@@ -202,12 +244,63 @@ describe("Computer Bot Durable Object Contribution", () => {
202
244
  "scout",
203
245
  command("connect", "connect-after-v1"),
204
246
  );
247
+ await contribution.settleScheduledWork();
205
248
  expect(storage.values.get(COMPUTER_PROVIDER_RECORD_KEY)).toMatchObject({
206
249
  version: 2,
207
250
  phase: "ready",
208
251
  });
209
252
  });
210
253
 
254
+ test("reads a previous V2 progress record without provisioning detail", async () => {
255
+ const storage = new MemoryStorage();
256
+ storage.values.set(COMPUTER_PROVIDER_RECORD_KEY, {
257
+ version: 2,
258
+ phase: "provisioning",
259
+ message: "Attaching the Bot",
260
+ recordedAt: "2026-09-03T00:00:01.000Z",
261
+ progress: {
262
+ version: 1,
263
+ kind: "connect",
264
+ startedAt: "2026-09-03T00:00:00.000Z",
265
+ updatedAt: "2026-09-03T00:00:01.000Z",
266
+ index: 2,
267
+ total: 5,
268
+ steps: [
269
+ {
270
+ version: 1,
271
+ id: "waking",
272
+ label: "Waking the Computer",
273
+ status: "complete",
274
+ },
275
+ {
276
+ version: 1,
277
+ id: "attaching",
278
+ label: "Attaching the Bot",
279
+ status: "active",
280
+ },
281
+ ],
282
+ },
283
+ });
284
+ const contribution = createComputerBotBackendContribution({
285
+ storage,
286
+ configured: true,
287
+ providerLabel: "Fake Computer",
288
+ openComputer: () => Promise.reject(new Error("not used")),
289
+ });
290
+
291
+ expect(await contribution.read("user-1", "scout")).toMatchObject({
292
+ progress: {
293
+ version: 1,
294
+ kind: "connect",
295
+ index: 2,
296
+ total: 5,
297
+ },
298
+ });
299
+ expect(
300
+ (await contribution.read("user-1", "scout")).progress?.provisioning,
301
+ ).toBeUndefined();
302
+ });
303
+
211
304
  test("commits intent before it asks the provider and replays one receipt", async () => {
212
305
  const storage = new MemoryStorage();
213
306
  let calls = 0;
@@ -238,18 +331,175 @@ describe("Computer Bot Durable Object Contribution", () => {
238
331
  "scout",
239
332
  command("connect", "connect-1"),
240
333
  );
334
+ expect(first.status).toBe("accepted");
335
+ await contribution.settleScheduledWork();
241
336
  const replay = await contribution.execute(
242
337
  "user-1",
243
338
  "scout",
244
339
  command("connect", "connect-1"),
245
340
  );
246
- expect(replay).toEqual(first);
341
+ expect(replay.status).toBe("applied");
247
342
  expect(calls).toBe(1);
248
343
  expect(JSON.stringify([...storage.values.values()])).not.toContain(
249
344
  "viewer.invalid",
250
345
  );
251
346
  });
252
347
 
348
+ test("contributes a future deadline for a freshly admitted connect", async () => {
349
+ const storage = new MemoryStorage();
350
+ const now = new Date("2026-09-03T00:00:00.000Z");
351
+ const contribution = createComputerBotBackendContribution({
352
+ storage,
353
+ configured: true,
354
+ providerLabel: "Fake Computer",
355
+ openComputer: () => Promise.reject(new Error("alarm has not fired")),
356
+ now: () => now,
357
+ });
358
+
359
+ expect(
360
+ await contribution.execute(
361
+ "user-1",
362
+ "scout",
363
+ command("connect", "future-connect"),
364
+ ),
365
+ ).toMatchObject({ version: 2, status: "accepted" });
366
+ const [deadline] = await contribution.scheduledDeadlines(storage);
367
+
368
+ expect(deadline).toBe(now.getTime() + COMPUTER_CONNECT_START_DELAY_MS);
369
+ });
370
+
371
+ test("migrates a held connect intent onto a future scheduled deadline", async () => {
372
+ const storage = new MemoryStorage();
373
+ const now = new Date("2026-09-03T00:00:00.000Z");
374
+ const connect = command("connect", "held-connect");
375
+ await storage.put(`${COMPUTER_INTENT_PREFIX}${connect.commandId}`, {
376
+ version: 1,
377
+ fingerprint: computerCommandFingerprintV1(connect),
378
+ command: connect,
379
+ admittedAt: "2026-09-02T00:00:00.000Z",
380
+ });
381
+ const contribution = createComputerBotBackendContribution({
382
+ storage,
383
+ configured: true,
384
+ providerLabel: "Fake Computer",
385
+ openComputer: () => Promise.reject(new Error("alarm has not fired")),
386
+ now: () => now,
387
+ });
388
+
389
+ expect(
390
+ await contribution.execute("user-1", "scout", connect),
391
+ ).toMatchObject({ version: 2, status: "accepted" });
392
+ expect(await contribution.scheduledDeadlines(storage)).toEqual([
393
+ now.getTime() + COMPUTER_CONNECT_START_DELAY_MS,
394
+ ]);
395
+ });
396
+
397
+ test("a cold contribution resumes an accepted connect from durable scheduling", async () => {
398
+ const storage = new MemoryStorage();
399
+ let calls = 0;
400
+ let now = "2026-09-03T00:00:00.000Z";
401
+ const host = {
402
+ storage,
403
+ configured: true,
404
+ providerLabel: "Fake Computer",
405
+ openComputer: () => {
406
+ calls += 1;
407
+ return Promise.resolve(
408
+ fakeHandle({
409
+ presence: () =>
410
+ Promise.resolve({
411
+ id: "viewer-1",
412
+ url: "https://viewer.invalid/secret",
413
+ expiresAt: "2026-09-03T00:01:30.000Z",
414
+ }),
415
+ }),
416
+ );
417
+ },
418
+ now: () => new Date(now),
419
+ };
420
+ const warm = createComputerBotBackendContribution(host);
421
+
422
+ expect(
423
+ await warm.execute(
424
+ "user-1",
425
+ "scout",
426
+ command("connect", "scheduled-connect"),
427
+ ),
428
+ ).toMatchObject({ version: 2, status: "accepted" });
429
+ expect(calls).toBe(0);
430
+ expect(storage.values.has(COMPUTER_PENDING_CONNECT_KEY)).toBe(true);
431
+
432
+ const cold = createComputerBotBackendContribution(host);
433
+ expect(await cold.scheduledDeadlines(storage)).toEqual([
434
+ Date.parse("2026-09-03T00:00:00.000Z") + COMPUTER_CONNECT_START_DELAY_MS,
435
+ ]);
436
+ now = "2026-09-03T00:00:05.000Z";
437
+ await cold.deferScheduledWork(storage);
438
+ expect(await cold.scheduledDeadlines(storage)).toEqual([
439
+ Date.parse(now) + COMPUTER_CONNECT_DEFERRAL_MS,
440
+ ]);
441
+ await cold.settleScheduledWork();
442
+ expect(calls).toBe(1);
443
+ expect(storage.values.has(COMPUTER_PENDING_CONNECT_KEY)).toBe(false);
444
+ expect(
445
+ await cold.execute(
446
+ "user-1",
447
+ "scout",
448
+ command("connect", "scheduled-connect"),
449
+ ),
450
+ ).toMatchObject({ version: 1, status: "applied" });
451
+ });
452
+
453
+ test("leaves a durable watchdog armed while connect is in flight", async () => {
454
+ const storage = new MemoryStorage();
455
+ const now = new Date("2026-09-03T00:00:00.000Z");
456
+ let entered!: () => void;
457
+ const providerEntered = new Promise<void>((resolve) => {
458
+ entered = resolve;
459
+ });
460
+ let release!: () => void;
461
+ const providerFinished = new Promise<{
462
+ id: string;
463
+ url: string;
464
+ expiresAt: string;
465
+ }>((resolve) => {
466
+ release = () =>
467
+ resolve({
468
+ id: "viewer-1",
469
+ url: "https://viewer.invalid/secret",
470
+ expiresAt: "2026-09-03T00:01:30.000Z",
471
+ });
472
+ });
473
+ const contribution = createComputerBotBackendContribution({
474
+ storage,
475
+ configured: true,
476
+ providerLabel: "Fake Computer",
477
+ openComputer: () =>
478
+ Promise.resolve(
479
+ fakeHandle({
480
+ presence: () => {
481
+ entered();
482
+ return providerFinished;
483
+ },
484
+ }),
485
+ ),
486
+ now: () => now,
487
+ });
488
+ await contribution.execute(
489
+ "user-1",
490
+ "scout",
491
+ command("connect", "watched-connect"),
492
+ );
493
+ const settlement = contribution.settleScheduledWork();
494
+ await providerEntered;
495
+
496
+ expect(await contribution.scheduledDeadlines(storage)).toEqual([
497
+ now.getTime() + COMPUTER_CONNECT_WATCHDOG_MS,
498
+ ]);
499
+ release();
500
+ await settlement;
501
+ });
502
+
253
503
  test("records and replays one viewer renewal without storing its bearer URL", async () => {
254
504
  const storage = new MemoryStorage();
255
505
  let renewals = 0;
@@ -287,6 +537,10 @@ describe("Computer Bot Durable Object Contribution", () => {
287
537
  "scout",
288
538
  command("connect", "connect-viewer"),
289
539
  );
540
+ await contribution.settleScheduledWork();
541
+ expect((await contribution.read("user-1", "scout")).screenshots).toEqual(
542
+ [],
543
+ );
290
544
  const first = await contribution.execute(
291
545
  "user-1",
292
546
  "scout",
@@ -332,6 +586,7 @@ describe("Computer Bot Durable Object Contribution", () => {
332
586
  "scout",
333
587
  command("connect", "connect-update"),
334
588
  );
589
+ await contribution.settleScheduledWork();
335
590
 
336
591
  expect(await contribution.read("user-1", "scout")).toMatchObject({
337
592
  phase: "updating",
@@ -363,6 +618,13 @@ describe("Computer Bot Durable Object Contribution", () => {
363
618
  now: () => new Date("2026-09-02T00:00:00.000Z"),
364
619
  });
365
620
 
621
+ const accepted = await contribution.execute(
622
+ "user-1",
623
+ "scout",
624
+ command("connect", "connect-updating"),
625
+ );
626
+ expect(accepted.status).toBe("accepted");
627
+ await contribution.settleScheduledWork();
366
628
  const receipt = await contribution.execute(
367
629
  "user-1",
368
630
  "scout",
@@ -479,6 +741,307 @@ describe("Computer Bot Durable Object Contribution", () => {
479
741
  expect(acquired).toEqual(["human:owner-1", "human:owner-2"]);
480
742
  });
481
743
 
744
+ test("captures the current desktop when a live viewer closes, attributed to the User", async () => {
745
+ const storage = new MemoryStorage();
746
+ const workspace = new FakeWorkspace();
747
+ let opens = 0;
748
+ const contribution = createComputerBotBackendContribution({
749
+ storage,
750
+ workspace,
751
+ configured: true,
752
+ providerLabel: "Fake Computer",
753
+ openComputer: () => {
754
+ opens += 1;
755
+ return Promise.resolve(
756
+ fakeHandle({
757
+ presence: () =>
758
+ Promise.resolve({
759
+ id: "viewer-1",
760
+ url: "https://viewer.invalid/secret",
761
+ expiresAt: "2026-09-03T00:01:30.000Z",
762
+ }),
763
+ capture: () =>
764
+ Promise.resolve({
765
+ bytes: png(),
766
+ mediaType: "image/png",
767
+ display: ":100",
768
+ capturedAt: "2026-09-03T00:00:10.000Z",
769
+ }),
770
+ }),
771
+ );
772
+ },
773
+ now: () => new Date("2026-09-03T00:00:15.000Z"),
774
+ });
775
+ await contribution.execute(
776
+ "user-1",
777
+ "scout",
778
+ command("connect", "connect-viewer"),
779
+ );
780
+ await contribution.settleScheduledWork();
781
+
782
+ const receipt = await contribution.execute(
783
+ "user-1",
784
+ "scout",
785
+ command("closeViewer", "close-viewer"),
786
+ );
787
+ const replay = await contribution.execute(
788
+ "user-1",
789
+ "scout",
790
+ command("closeViewer", "close-viewer"),
791
+ );
792
+
793
+ expect(receipt.status).toBe("applied");
794
+ expect(replay).toEqual(receipt);
795
+ expect(opens).toBe(2);
796
+ expect(workspace.writes).toHaveLength(1);
797
+ expect(workspace.writes[0]?.writer).toEqual({
798
+ kind: "user",
799
+ userId: "user-1",
800
+ });
801
+ expect((await contribution.read("user-1", "scout")).screenshots).toEqual([
802
+ expect.objectContaining({
803
+ path: expect.stringContaining("close-viewer"),
804
+ }),
805
+ ]);
806
+ });
807
+
808
+ test("does not capture a viewer close while the User's control lease is active", async () => {
809
+ const storage = new MemoryStorage();
810
+ const workspace = new FakeWorkspace();
811
+ let captures = 0;
812
+ await storage.put(COMPUTER_CONTROL_RECORD_KEY, {
813
+ version: 1,
814
+ ownerId: "human:owner-1",
815
+ acquiredAt: "2026-09-03T00:00:00.000Z",
816
+ expiresAt: "2026-09-03T00:01:30.000Z",
817
+ });
818
+ const contribution = createComputerBotBackendContribution({
819
+ storage,
820
+ workspace,
821
+ configured: true,
822
+ providerLabel: "Fake Computer",
823
+ openComputer: () =>
824
+ Promise.resolve(
825
+ fakeHandle({
826
+ presence: () =>
827
+ Promise.resolve({
828
+ id: "viewer-1",
829
+ url: "https://viewer.invalid/secret",
830
+ expiresAt: "2026-09-03T00:01:30.000Z",
831
+ }),
832
+ capture: () => {
833
+ captures += 1;
834
+ return Promise.reject(
835
+ new ComputerError("human-control-active", "held by User"),
836
+ );
837
+ },
838
+ }),
839
+ ),
840
+ now: () => new Date("2026-09-03T00:00:15.000Z"),
841
+ });
842
+ await contribution.execute(
843
+ "user-1",
844
+ "scout",
845
+ command("connect", "connect-controlled-viewer"),
846
+ );
847
+ await contribution.settleScheduledWork();
848
+
849
+ const receipt = await contribution.execute(
850
+ "user-1",
851
+ "scout",
852
+ command("closeViewer", "close-controlled-viewer"),
853
+ );
854
+
855
+ expect(receipt.status).toBe("applied");
856
+ expect(captures).toBe(0);
857
+ expect(workspace.writes).toHaveLength(0);
858
+ });
859
+
860
+ test("does not wake a Computer to close a viewer after Bot DO eviction", async () => {
861
+ const storage = new MemoryStorage();
862
+ const workspace = new FakeWorkspace();
863
+ let opens = 0;
864
+ const host = {
865
+ storage,
866
+ workspace,
867
+ configured: true,
868
+ providerLabel: "Fake Computer",
869
+ openComputer: () => {
870
+ opens += 1;
871
+ return Promise.resolve(
872
+ fakeHandle({
873
+ presence: () =>
874
+ Promise.resolve({
875
+ id: "viewer-1",
876
+ url: "https://viewer.invalid/secret",
877
+ expiresAt: "2026-09-03T00:01:30.000Z",
878
+ }),
879
+ capture: () =>
880
+ Promise.resolve({
881
+ bytes: png(),
882
+ mediaType: "image/png" as const,
883
+ display: ":100",
884
+ capturedAt: "2026-09-03T00:00:10.000Z",
885
+ }),
886
+ }),
887
+ );
888
+ },
889
+ now: () => new Date("2026-09-03T00:00:15.000Z"),
890
+ };
891
+ const resident = createComputerBotBackendContribution(host);
892
+ await resident.execute(
893
+ "user-1",
894
+ "scout",
895
+ command("connect", "connect-before-eviction"),
896
+ );
897
+ await resident.settleScheduledWork();
898
+
899
+ const reconstructed = createComputerBotBackendContribution(host);
900
+ const receipt = await reconstructed.execute(
901
+ "user-1",
902
+ "scout",
903
+ command("closeViewer", "close-after-eviction"),
904
+ );
905
+
906
+ expect(receipt.status).toBe("applied");
907
+ expect(opens).toBe(1);
908
+ expect(workspace.writes).toHaveLength(0);
909
+ });
910
+
911
+ test("serves repeated projection file reads from the resident cache", async () => {
912
+ const storage = new MemoryStorage();
913
+ const workspace = new FakeWorkspace();
914
+ const contribution = createComputerBotBackendContribution({
915
+ storage,
916
+ workspace,
917
+ configured: true,
918
+ providerLabel: "Fake Computer",
919
+ openComputer: () => Promise.reject(new Error("must stay wake-free")),
920
+ now: () => new Date("2026-09-03T00:00:00.000Z"),
921
+ });
922
+
923
+ await contribution.read("user-1", "scout");
924
+ await contribution.read("user-1", "scout");
925
+
926
+ expect(workspace.lists).toHaveLength(1);
927
+ expect(workspace.reads).toHaveLength(1);
928
+ });
929
+
930
+ test("invalidates the doctor cache when a User-run report is written", async () => {
931
+ const storage = new MemoryStorage();
932
+ const workspace = new FakeWorkspace();
933
+ const contribution = createComputerBotBackendContribution({
934
+ storage,
935
+ workspace,
936
+ configured: true,
937
+ providerLabel: "Fake Computer",
938
+ openComputer: () =>
939
+ Promise.resolve({
940
+ assignment: { providerId: "fake", generation: 1 },
941
+ identity: { userId: "user-1" },
942
+ tenant: { botId: "scout" },
943
+ doctor: {
944
+ run: () =>
945
+ Promise.resolve({
946
+ schemaVersion: 2,
947
+ generation: 1,
948
+ capturedAt: "2026-09-03T00:00:05.000Z",
949
+ checks: [{ name: "disk", status: "pass", detail: "healthy" }],
950
+ summary: "1 check, 1 passed, 0 failed",
951
+ }),
952
+ },
953
+ close: () => Promise.resolve(),
954
+ }),
955
+ now: () => new Date("2026-09-03T00:00:10.000Z"),
956
+ });
957
+ await contribution.read("user-1", "scout");
958
+
959
+ await contribution.execute(
960
+ "user-1",
961
+ "scout",
962
+ command("runDoctor", "doctor-write"),
963
+ );
964
+ const projected = await contribution.read("user-1", "scout");
965
+
966
+ expect(workspace.reads).toHaveLength(2);
967
+ expect(projected.doctor?.summary).toBe("1 check, 1 passed, 0 failed");
968
+ });
969
+
970
+ test("expires projection file caches so out-of-band durable-root writes surface", async () => {
971
+ const storage = new MemoryStorage();
972
+ const workspace = new FakeWorkspace();
973
+ let now = new Date("2026-09-03T00:00:00.000Z");
974
+ const contribution = createComputerBotBackendContribution({
975
+ storage,
976
+ workspace,
977
+ configured: true,
978
+ providerLabel: "Fake Computer",
979
+ openComputer: () => Promise.reject(new Error("must stay wake-free")),
980
+ now: () => now,
981
+ });
982
+ await contribution.read("user-1", "scout");
983
+ now = new Date("2026-09-03T00:00:30.001Z");
984
+
985
+ await contribution.read("user-1", "scout");
986
+
987
+ expect(workspace.lists).toHaveLength(2);
988
+ expect(workspace.reads).toHaveLength(2);
989
+ });
990
+
991
+ test("never shares a projection file cache across Bots or Users", async () => {
992
+ const storage = new MemoryStorage();
993
+ const workspace = new FakeWorkspace();
994
+ const contribution = createComputerBotBackendContribution({
995
+ storage,
996
+ workspace,
997
+ configured: true,
998
+ providerLabel: "Fake Computer",
999
+ openComputer: () => Promise.reject(new Error("must stay wake-free")),
1000
+ now: () => new Date("2026-09-03T00:00:00.000Z"),
1001
+ });
1002
+
1003
+ await contribution.read("user-1", "scout");
1004
+ await contribution.read("user-1", "builder");
1005
+ await contribution.read("user-2", "scout");
1006
+ await contribution.read("user-1", "scout");
1007
+
1008
+ expect(workspace.lists).toHaveLength(3);
1009
+ expect(workspace.reads).toHaveLength(3);
1010
+ });
1011
+
1012
+ test("a cold instance projects the same files as a warm instance", async () => {
1013
+ const storage = new MemoryStorage();
1014
+ const workspace = new FakeWorkspace();
1015
+ await workspace.write({
1016
+ path: {
1017
+ root: {
1018
+ kind: "package-declared",
1019
+ userId: "user-1",
1020
+ packageId: "computer",
1021
+ rootId: "screenshots",
1022
+ },
1023
+ path: `${computerBotPathKeyV1("scout")}/capture.png`,
1024
+ },
1025
+ bytes: png(),
1026
+ writer: { kind: "user", userId: "user-1" },
1027
+ });
1028
+ const host = {
1029
+ storage,
1030
+ workspace,
1031
+ configured: true,
1032
+ providerLabel: "Fake Computer",
1033
+ openComputer: () => Promise.reject(new Error("must stay wake-free")),
1034
+ now: () => new Date("2026-09-03T00:00:00.000Z"),
1035
+ };
1036
+ const warm = createComputerBotBackendContribution(host);
1037
+ const expected = await warm.read("user-1", "scout");
1038
+ await warm.read("user-1", "scout");
1039
+
1040
+ const cold = createComputerBotBackendContribution(host);
1041
+ expect(await cold.read("user-1", "scout")).toEqual(expected);
1042
+ expect(workspace.lists).toHaveLength(2);
1043
+ });
1044
+
482
1045
  test("projects reconstructed durable state without opening the Computer", async () => {
483
1046
  const storage = new MemoryStorage();
484
1047
  await storage.put({