@mycadet/cli 0.12.11 → 0.13.0-dev.202610171617.9ea363e

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 (94) 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 +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  4. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  6. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  7. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  9. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  10. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  11. package/package.json +8 -3
  12. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  13. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  14. package/src/__tests__/backup-ops.test.ts +105 -0
  15. package/src/__tests__/backup.test.ts +114 -673
  16. package/src/__tests__/cloud-releases.test.ts +82 -0
  17. package/src/__tests__/commands-mock.ts +101 -0
  18. package/src/__tests__/debug.test.ts +17 -91
  19. package/src/__tests__/devices.test.ts +2 -2
  20. package/src/__tests__/gateway-token.test.ts +267 -0
  21. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  22. package/src/__tests__/host-teleport.test.ts +405 -0
  23. package/src/__tests__/nginx-ingress.test.ts +536 -23
  24. package/src/__tests__/orphan-detection.test.ts +1 -1
  25. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  26. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  27. package/src/__tests__/root-backup-docker.test.ts +294 -0
  28. package/src/__tests__/root-backup.test.ts +527 -0
  29. package/src/__tests__/root-restore-command.test.ts +147 -0
  30. package/src/__tests__/root-signing-key.test.ts +50 -0
  31. package/src/__tests__/setup.test.ts +3 -3
  32. package/src/__tests__/shell-mode.test.ts +83 -0
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel.test.ts +10 -2
  36. package/src/__tests__/upgrade-local.test.ts +21 -11
  37. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  38. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  39. package/src/__tests__/wake.test.ts +4 -1
  40. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  41. package/src/commands/backup.ts +44 -399
  42. package/src/commands/client.ts +3 -3
  43. package/src/commands/completions.ts +0 -2
  44. package/src/commands/debug.ts +75 -131
  45. package/src/commands/devices.ts +51 -17
  46. package/src/commands/gateway/token.ts +77 -23
  47. package/src/commands/pair.ts +2 -2
  48. package/src/commands/restore.ts +77 -444
  49. package/src/commands/retire.ts +2 -3
  50. package/src/commands/sleep.ts +15 -2
  51. package/src/commands/teleport.ts +47 -1559
  52. package/src/commands/tunnel.ts +48 -7
  53. package/src/commands/upgrade.ts +78 -156
  54. package/src/commands/wake.ts +1 -1
  55. package/src/index.ts +0 -4
  56. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  57. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  58. package/src/lib/__tests__/web-dist.test.ts +27 -1
  59. package/src/lib/assistant-client.ts +1 -1
  60. package/src/lib/assistant-config.ts +23 -18
  61. package/src/lib/backup-ops.ts +37 -251
  62. package/src/lib/cloud-client.ts +10 -13
  63. package/src/lib/cloud-login.ts +2 -1
  64. package/src/lib/cloud-releases.ts +54 -10
  65. package/src/lib/cloud-values.ts +1 -1
  66. package/src/lib/confirm-action.ts +2 -2
  67. package/src/lib/docker.ts +59 -22
  68. package/src/lib/github-repository.ts +2 -0
  69. package/src/lib/guardian-token.ts +1 -23
  70. package/src/lib/hatch-local.ts +17 -1
  71. package/src/lib/host-teleport.ts +506 -0
  72. package/src/lib/http-client.ts +25 -0
  73. package/src/lib/ingress-config.ts +31 -0
  74. package/src/lib/live-voice/tunnel.test.ts +1 -1
  75. package/src/lib/live-voice/tunnel.ts +1 -1
  76. package/src/lib/local.ts +4 -2
  77. package/src/lib/nginx-ingress.ts +454 -79
  78. package/src/lib/process.ts +13 -0
  79. package/src/lib/root-backup.ts +917 -0
  80. package/src/lib/root-signing-key.ts +31 -0
  81. package/src/lib/runtime-url.ts +0 -29
  82. package/src/lib/teleport-backup.ts +3 -16
  83. package/src/lib/tunnel.ts +1 -0
  84. package/src/lib/upgrade-lifecycle.ts +63 -141
  85. package/src/lib/web-dist.ts +79 -1
  86. package/src/__tests__/bundle-staging.test.ts +0 -268
  87. package/src/__tests__/unpair.test.ts +0 -160
  88. package/src/commands/rollback.ts +0 -479
  89. package/src/commands/unpair.ts +0 -122
  90. package/src/lib/__tests__/job-polling.test.ts +0 -278
  91. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  92. package/src/lib/bundle-staging.ts +0 -220
  93. package/src/lib/job-polling.ts +0 -195
  94. package/src/lib/local-runtime-client.ts +0 -374
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.
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
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.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
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.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
@@ -28,10 +28,10 @@ describe("tunnelHostForCloudHost", () => {
28
28
 
29
29
  test("maps env-prefixed platform hosts to their env tunnel", () => {
30
30
  expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
31
- "tunnel-staging.mycadet.ai",
31
+ "staging-tunnel.mycadet.ai",
32
32
  );
33
33
  expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
34
- "tunnel-dev.mycadet.ai",
34
+ "dev-tunnel.mycadet.ai",
35
35
  );
36
36
  });
37
37
 
@@ -39,7 +39,6 @@ export const CHANNEL_IDS = [
39
39
  "slack",
40
40
  "email",
41
41
  "platform",
42
- "a2a",
43
42
  "discord",
44
43
  "plugin",
45
44
  // Internal id for conversations a guardian shares with a contact. It has no
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Map a Cadet Cloud host onto its environment's tunnel host, following the
3
3
  * deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
4
- * `{env}-cloud.mycadet.ai` → `tunnel-{env}.mycadet.ai`). Returns null for
4
+ * `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
5
5
  * hosts outside that convention (localhost, custom domains). Shared by the
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.
@@ -11,7 +11,7 @@ export function tunnelHostForCloudHost(host: string): string | null {
11
11
  return "tunnel.mycadet.ai";
12
12
  }
13
13
  const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
14
- return match ? `tunnel-${match[1]}.mycadet.ai` : null;
14
+ return match ? `${match[1]}-tunnel.mycadet.ai` : null;
15
15
  }
16
16
 
17
17
  export function normalizePublicBaseUrl(value: unknown): string | undefined {
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.11",
3
+ "version": "0.13.0-dev.202610171617.9ea363e",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/vargasjr-dev/mycadet-temp.git"
7
+ },
4
8
  "description": "CLI tools for mycadet",
5
9
  "type": "module",
6
10
  "exports": {
@@ -15,12 +19,12 @@
15
19
  "./src/commands/*": "./src/commands/*.ts"
16
20
  },
17
21
  "bin": {
18
- "cadet": "./src/index.ts"
22
+ "cadet-dev": "./src/index.ts"
19
23
  },
20
24
  "scripts": {
21
25
  "format": "prettier --write .",
22
26
  "format:check": "prettier --check .",
23
- "lint": "eslint --max-warnings 323",
27
+ "lint": "eslint --max-warnings 319",
24
28
  "lint:unused": "knip --include files,dependencies,unlisted",
25
29
  "compile:check": "bun build --compile src/index.ts --outfile /dev/null",
26
30
  "test": "bun run scripts/run-tests.ts",
@@ -41,6 +45,7 @@
41
45
  "qrcode-terminal": "0.12.0",
42
46
  "react": "19.2.6",
43
47
  "react-devtools-core": "7.0.1",
48
+ "tar": "7.5.22",
44
49
  "yaml": "2.8.2",
45
50
  "zod": "4.3.6"
46
51
  },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tests for AssistantClient's cloud-managed (`cloud: "cadet"`) auth path.
2
+ * Tests for AssistantClient's cloud-managed (`cloud: "cloud"`) auth path.
3
3
  * These entries have no guardian token on disk, so the client must authenticate
4
4
  * with the stored platform credential instead of sending an anonymous request
5
5
  * (which the cloud answers with a bare 403).
@@ -38,7 +38,7 @@ function seedCloud(token?: string): void {
38
38
  assistantId: ASSISTANT_ID,
39
39
  name: "Credence",
40
40
  runtimeUrl: PLATFORM_URL,
41
- cloud: "cadet",
41
+ cloud: "cloud",
42
42
  });
43
43
  if (token) {
44
44
  saveCloudToken(token);
@@ -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
+ });