@mycadet/cli 0.12.9-dev.202610131757.9cfb396 → 0.12.9-dev.202610142154.883240

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.
Files changed (78) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  8. package/package.json +3 -2
  9. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  10. package/src/__tests__/backup-ops.test.ts +105 -0
  11. package/src/__tests__/backup.test.ts +114 -673
  12. package/src/__tests__/bundle-staging.test.ts +0 -40
  13. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  14. package/src/__tests__/cloud-releases.test.ts +82 -0
  15. package/src/__tests__/commands-mock.ts +101 -0
  16. package/src/__tests__/devices.test.ts +64 -2
  17. package/src/__tests__/ensure-binary.test.ts +52 -0
  18. package/src/__tests__/gateway-token.test.ts +267 -0
  19. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  20. package/src/__tests__/multi-local.test.ts +3 -3
  21. package/src/__tests__/nginx-ingress.test.ts +55 -17
  22. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  23. package/src/__tests__/ps-cloud-status.test.ts +2 -1
  24. package/src/__tests__/root-backup-docker.test.ts +294 -0
  25. package/src/__tests__/root-backup.test.ts +490 -0
  26. package/src/__tests__/root-restore-command.test.ts +146 -0
  27. package/src/__tests__/setup.test.ts +14 -4
  28. package/src/__tests__/sleep.test.ts +11 -0
  29. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  30. package/src/__tests__/tunnel-stop.test.ts +226 -0
  31. package/src/__tests__/tunnel.test.ts +12 -2
  32. package/src/__tests__/upgrade-local.test.ts +21 -11
  33. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  34. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  35. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  36. package/src/commands/backup.ts +44 -399
  37. package/src/commands/client.ts +3 -3
  38. package/src/commands/completions.ts +0 -2
  39. package/src/commands/devices.ts +117 -47
  40. package/src/commands/gateway/token.ts +77 -23
  41. package/src/commands/hatch.ts +10 -52
  42. package/src/commands/pair.ts +9 -3
  43. package/src/commands/ps.ts +4 -2
  44. package/src/commands/restore.ts +83 -375
  45. package/src/commands/retire.ts +2 -3
  46. package/src/commands/sleep.ts +15 -2
  47. package/src/commands/tunnel.ts +43 -4
  48. package/src/commands/upgrade.ts +84 -157
  49. package/src/commands/wake.ts +1 -1
  50. package/src/index.ts +0 -4
  51. package/src/lib/assistant-config.ts +20 -23
  52. package/src/lib/backup-ops.ts +37 -251
  53. package/src/lib/cloud-login-poll.ts +75 -0
  54. package/src/lib/cloud-login.ts +3 -1
  55. package/src/lib/cloud-releases.ts +54 -10
  56. package/src/lib/confirm-action.ts +2 -2
  57. package/src/lib/docker.ts +26 -17
  58. package/src/lib/ensure-binary.ts +65 -6
  59. package/src/lib/github-repository.ts +2 -0
  60. package/src/lib/guardian-token.ts +1 -23
  61. package/src/lib/hatch-local.ts +1 -51
  62. package/src/lib/hatch-next-steps.ts +15 -2
  63. package/src/lib/live-voice/tunnel.ts +2 -2
  64. package/src/lib/local.ts +10 -8
  65. package/src/lib/nginx-ingress.ts +123 -21
  66. package/src/lib/process.ts +13 -0
  67. package/src/lib/root-backup.ts +887 -0
  68. package/src/lib/tunnel.ts +86 -34
  69. package/src/lib/tunnels/cadet.ts +12 -1
  70. package/src/lib/tunnels/cloudflare.ts +36 -1
  71. package/src/lib/tunnels/ngrok.ts +16 -2
  72. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  73. package/src/lib/tunnels/tailscale.ts +79 -1
  74. package/src/lib/upgrade-lifecycle.ts +35 -110
  75. package/src/lib/xdg-log.ts +11 -0
  76. package/src/__tests__/unpair.test.ts +0 -160
  77. package/src/commands/rollback.ts +0 -479
  78. package/src/commands/unpair.ts +0 -122
package/README.md CHANGED
@@ -14,6 +14,19 @@ bun run ./src/index.ts <command> [options]
14
14
 
15
15
  ## Commands
16
16
 
17
+ ### Version changes: `upgrade`, `restore`
18
+
19
+ Use `cadet upgrade` to move to a newer release. Rollbacks must restore a backup
20
+ through `cadet restore`; version changes and backup data belong to the same
21
+ restore operation. The CLI has no standalone `rollback` command.
22
+
23
+ ```bash
24
+ cadet restore my-assistant --from backup.tar.gz --version v1.2.3
25
+ ```
26
+
27
+ Automatic failure recovery during an upgrade or restore uses the images and
28
+ migration checkpoints captured in memory for that operation.
29
+
17
30
  ### Lifecycle: `ps`, `sleep`, `wake`
18
31
 
19
32
  Day-to-day process management for the assistant and gateway.
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
31
  "tunnel-staging.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
34
  "tunnel-dev.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -6,7 +6,7 @@
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
31
  "tunnel-staging.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
34
  "tunnel-dev.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -6,7 +6,7 @@
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
@@ -10,7 +10,7 @@ import {
10
10
  trimmedNonEmptyString,
11
11
  TUNNEL_PROVIDERS,
12
12
  type TunnelRecord,
13
- tunnelHostForPlatformHost,
13
+ tunnelHostForCloudHost,
14
14
  } from "../ingress.js";
15
15
  import {
16
16
  buildTwilioMediaStreamUrl,
@@ -19,27 +19,27 @@ import {
19
19
  resolveTwilioPublicBaseUrl,
20
20
  } from "../twilio-ingress.js";
21
21
 
22
- describe("tunnelHostForPlatformHost", () => {
22
+ describe("tunnelHostForCloudHost", () => {
23
23
  test("maps the prod platform host to the prod tunnel", () => {
24
- expect(tunnelHostForPlatformHost("cloud.mycadet.ai")).toBe(
24
+ expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
25
25
  "tunnel.mycadet.ai",
26
26
  );
27
27
  });
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
- expect(tunnelHostForPlatformHost("staging-cloud.mycadet.ai")).toBe(
30
+ expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
31
  "tunnel-staging.mycadet.ai",
32
32
  );
33
- expect(tunnelHostForPlatformHost("dev-cloud.mycadet.ai")).toBe(
33
+ expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
34
  "tunnel-dev.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
38
38
  test("returns null for hosts outside the deployment convention", () => {
39
- expect(tunnelHostForPlatformHost("localhost")).toBeNull();
40
- expect(tunnelHostForPlatformHost("platform.example.com")).toBeNull();
39
+ expect(tunnelHostForCloudHost("localhost")).toBeNull();
40
+ expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
41
41
  expect(
42
- tunnelHostForPlatformHost("staging-cloud.mycadet.ai.evil.com"),
42
+ tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
43
43
  ).toBeNull();
44
44
  });
45
45
  });
@@ -6,7 +6,7 @@
6
6
  * gateway speech relay and the web live-voice client so the two ends of the
7
7
  * cloud-speech transport can't drift.
8
8
  */
9
- export function tunnelHostForPlatformHost(host: string): string | null {
9
+ export function tunnelHostForCloudHost(host: string): string | null {
10
10
  if (host === "cloud.mycadet.ai") {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.9-dev.202610131757.9cfb396",
3
+ "version": "0.12.9-dev.202610142154.0883240",
4
4
  "description": "CLI tools for mycadet",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "format": "prettier --write .",
22
22
  "format:check": "prettier --check .",
23
- "lint": "eslint --max-warnings 323",
23
+ "lint": "eslint --max-warnings 319",
24
24
  "lint:unused": "knip --include files,dependencies,unlisted",
25
25
  "compile:check": "bun build --compile src/index.ts --outfile /dev/null",
26
26
  "test": "bun run scripts/run-tests.ts",
@@ -41,6 +41,7 @@
41
41
  "qrcode-terminal": "0.12.0",
42
42
  "react": "19.2.6",
43
43
  "react-devtools-core": "7.0.1",
44
+ "tar": "7.5.22",
44
45
  "yaml": "2.8.2",
45
46
  "zod": "4.3.6"
46
47
  },
@@ -7,42 +7,44 @@ const STAMP = "2026-09-15T14-27-28-123Z";
7
7
  describe("assistantBackupFilenamePattern", () => {
8
8
  test("matches this CLI's backups for exactly the given assistant", () => {
9
9
  const pattern = assistantBackupFilenamePattern("alpha");
10
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(true);
11
- expect(pattern.test(`alpha-pre-teleport-${STAMP}.vbundle`)).toBe(true);
10
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(true);
11
+ expect(pattern.test(`alpha-pre-teleport-${STAMP}.tar.gz`)).toBe(true);
12
+ expect(pattern.test(`alpha-pre-rollback-${STAMP}.tar.gz`)).toBe(true);
13
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
12
14
  });
13
15
 
14
16
  test("does not match an assistant whose id extends this one", () => {
15
17
  const pattern = assistantBackupFilenamePattern("alpha");
16
- expect(pattern.test(`alpha-prod-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
17
- expect(pattern.test(`alpha-prod-pre-teleport-${STAMP}.vbundle`)).toBe(
18
+ expect(pattern.test(`alpha-prod-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
19
+ expect(pattern.test(`alpha-prod-pre-teleport-${STAMP}.tar.gz`)).toBe(
18
20
  false,
19
21
  );
20
22
  });
21
23
 
22
24
  test("does not match a shorter id's backups from a longer id", () => {
23
25
  const pattern = assistantBackupFilenamePattern("alpha-prod");
24
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
26
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
25
27
  });
26
28
 
27
29
  test("ignores user-named cadet backup files and other kinds", () => {
28
30
  const pattern = assistantBackupFilenamePattern("alpha");
29
- expect(pattern.test(`alpha-${STAMP}.vbundle`)).toBe(false);
30
- expect(pattern.test(`alpha-manual-${STAMP}.vbundle`)).toBe(false);
31
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle.enc`)).toBe(false);
31
+ expect(pattern.test(`alpha-${STAMP}.tar.gz`)).toBe(false);
32
+ expect(pattern.test(`alpha-manual-${STAMP}.tar.gz`)).toBe(false);
33
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz.enc`)).toBe(false);
32
34
  });
33
35
 
34
36
  test("restricts to the requested kinds", () => {
35
37
  const pattern = assistantBackupFilenamePattern("alpha", ["pre-teleport"]);
36
- expect(pattern.test(`alpha-pre-teleport-${STAMP}.vbundle`)).toBe(true);
37
- expect(pattern.test(`alpha-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
38
+ expect(pattern.test(`alpha-pre-teleport-${STAMP}.tar.gz`)).toBe(true);
39
+ expect(pattern.test(`alpha-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
38
40
  expect(
39
- pattern.test(`alpha-pre-teleport-prod-pre-teleport-${STAMP}.vbundle`),
41
+ pattern.test(`alpha-pre-teleport-prod-pre-teleport-${STAMP}.tar.gz`),
40
42
  ).toBe(false);
41
43
  });
42
44
 
43
45
  test("escapes regex metacharacters in ids", () => {
44
46
  const pattern = assistantBackupFilenamePattern("a.b");
45
- expect(pattern.test(`a.b-pre-upgrade-${STAMP}.vbundle`)).toBe(true);
46
- expect(pattern.test(`axb-pre-upgrade-${STAMP}.vbundle`)).toBe(false);
47
+ expect(pattern.test(`a.b-pre-upgrade-${STAMP}.tar.gz`)).toBe(true);
48
+ expect(pattern.test(`axb-pre-upgrade-${STAMP}.tar.gz`)).toBe(false);
47
49
  });
48
50
  });
@@ -0,0 +1,105 @@
1
+ import { afterAll, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import {
3
+ mkdir,
4
+ mkdtemp,
5
+ readFile,
6
+ readdir,
7
+ rm,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import * as config from "../lib/assistant-config.js";
13
+ import * as backups from "../lib/backup-ops.js";
14
+ import * as guardian from "../lib/guardian-token.js";
15
+ import { stageRootArchive, STATE_ROOTS } from "../lib/root-backup.js";
16
+
17
+ const directory = await mkdtemp(join(tmpdir(), "safety-root-backup-"));
18
+ const root = join(directory, "instance");
19
+ const backupDir = join(directory, "backups");
20
+ const entry: config.AssistantEntry = {
21
+ assistantId: "alpha",
22
+ cloud: "local",
23
+ runtimeUrl: "http://127.0.0.1:1",
24
+ resources: {
25
+ instanceDir: root,
26
+ daemonPort: 1,
27
+ gatewayPort: 2,
28
+ cesPort: 3,
29
+ qdrantPort: 4,
30
+ signingKey: "07".repeat(32),
31
+ },
32
+ };
33
+ const lookup = spyOn(config, "findAssistantByName").mockReturnValue(entry);
34
+ const location = spyOn(backups, "getBackupsDir").mockReturnValue(backupDir);
35
+ const save = spyOn(config, "saveAssistantEntry").mockImplementation(() => {});
36
+ const tokens = spyOn(guardian, "loadGuardianToken").mockImplementation(() => {
37
+ throw new Error("no guardian token access allowed");
38
+ });
39
+ beforeEach(async () => {
40
+ for (const name of STATE_ROOTS)
41
+ await mkdir(join(root, name), { recursive: true });
42
+ await writeFile(join(root, "workspace", "notes"), "original");
43
+ await writeFile(join(root, "gateway-security", "trust"), "gateway trust");
44
+ await writeFile(join(root, "ces-security", "keys.enc"), "opaque ciphertext", {
45
+ mode: 0o600,
46
+ });
47
+ await writeFile(join(root, "ces-data", "grants"), "CES policy");
48
+ lookup.mockReturnValue(entry);
49
+ tokens.mockClear();
50
+ });
51
+ afterAll(async () => {
52
+ for (const spy of [lookup, location, save, tokens]) spy.mockRestore();
53
+ await rm(directory, { recursive: true, force: true });
54
+ });
55
+
56
+ test("pre-upgrade safety backup captures all roots in XDG without a guardian token", async () => {
57
+ const path = await backups.createBackup(entry.runtimeUrl, entry.assistantId, {
58
+ prefix: "alpha-pre-upgrade",
59
+ });
60
+ expect(path).toMatch(
61
+ new RegExp(`^${backupDir}/alpha-pre-upgrade-.*\\.tar\\.gz$`),
62
+ );
63
+ const stage = await stageRootArchive(path!, directory);
64
+ for (const name of STATE_ROOTS)
65
+ expect(await readdir(join(stage, "state", name))).not.toHaveLength(0);
66
+ expect(
67
+ await readFile(join(stage, "state", "ces-security", "keys.enc"), "utf8"),
68
+ ).toBe("opaque ciphertext");
69
+ expect(tokens).not.toHaveBeenCalled();
70
+ });
71
+
72
+ test("rollback restores gateway and CES state as well as workspace", async () => {
73
+ const path = await backups.createBackup(entry.runtimeUrl, entry.assistantId, {
74
+ prefix: "alpha-pre-rollback",
75
+ });
76
+ for (const name of STATE_ROOTS)
77
+ await writeFile(join(root, name, "after-upgrade"), "stale");
78
+ await writeFile(join(root, "workspace", "notes"), "modified");
79
+ expect(
80
+ await backups.restoreBackup(entry.runtimeUrl, entry.assistantId, path!, {
81
+ resume: false,
82
+ }),
83
+ ).toBe(true);
84
+ expect(await readFile(join(root, "workspace", "notes"), "utf8")).toBe(
85
+ "original",
86
+ );
87
+ expect(await readFile(join(root, "gateway-security", "trust"), "utf8")).toBe(
88
+ "gateway trust",
89
+ );
90
+ expect(await readFile(join(root, "ces-data", "grants"), "utf8")).toBe(
91
+ "CES policy",
92
+ );
93
+ for (const name of STATE_ROOTS)
94
+ expect(await readdir(join(root, name))).not.toContain("after-upgrade");
95
+ expect(tokens).not.toHaveBeenCalled();
96
+ });
97
+
98
+ test("missing registry identity fails before capture instead of trying runtime export", async () => {
99
+ lookup.mockReturnValue(null);
100
+ expect(await backups.createBackup(entry.runtimeUrl, "missing")).toBeNull();
101
+ expect(
102
+ await backups.restoreBackup(entry.runtimeUrl, "missing", "missing.tar.gz"),
103
+ ).toBe(false);
104
+ expect(tokens).not.toHaveBeenCalled();
105
+ });