@mycadet/cli 0.12.11 → 0.13.0
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/README.md +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/package.json +3 -2
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +2 -2
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/nginx-ingress.test.ts +182 -23
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +6 -6
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +3 -3
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +1 -1
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +51 -17
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/pair.ts +2 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/upgrade.ts +78 -156
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +0 -18
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login.ts +2 -1
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +59 -22
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +17 -1
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +1 -1
- package/src/lib/local.ts +4 -2
- package/src/lib/nginx-ingress.ts +134 -18
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterEach,
|
|
3
|
-
beforeEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
mock,
|
|
7
|
-
spyOn,
|
|
8
|
-
test,
|
|
9
|
-
} from "bun:test";
|
|
10
|
-
import {
|
|
11
|
-
existsSync,
|
|
12
|
-
mkdirSync,
|
|
13
|
-
mkdtempSync,
|
|
14
|
-
readFileSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
writeFileSync,
|
|
17
|
-
} from "node:fs";
|
|
18
|
-
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
|
|
21
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
22
|
-
import { restoreBackup } from "../lib/backup-ops.js";
|
|
23
|
-
import {
|
|
24
|
-
bundleFileSizeBytes,
|
|
25
|
-
formatBundleSizeMb,
|
|
26
|
-
importStagedBundle,
|
|
27
|
-
RESTORE_STAGING_DIRNAME,
|
|
28
|
-
stageBundleForRestore,
|
|
29
|
-
stagingTargetFromEntry,
|
|
30
|
-
} from "../lib/bundle-staging.js";
|
|
31
|
-
import * as guardianToken from "../lib/guardian-token.js";
|
|
32
|
-
import * as loopbackFetch from "../lib/loopback-fetch.js";
|
|
33
|
-
import * as stepRunner from "../lib/step-runner.js";
|
|
34
|
-
|
|
35
|
-
function makeEntry(overrides: Partial<AssistantEntry> = {}): AssistantEntry {
|
|
36
|
-
return {
|
|
37
|
-
assistantId: "assistant-123",
|
|
38
|
-
runtimeUrl: "http://127.0.0.1:7831",
|
|
39
|
-
cloud: "local",
|
|
40
|
-
hatchedAt: new Date().toISOString(),
|
|
41
|
-
...overrides,
|
|
42
|
-
} as AssistantEntry;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let tmpRoot: string;
|
|
46
|
-
let fetchSpy: ReturnType<
|
|
47
|
-
typeof spyOn<typeof loopbackFetch, "loopbackSafeFetch">
|
|
48
|
-
>;
|
|
49
|
-
let execSpy: ReturnType<typeof spyOn<typeof stepRunner, "exec">>;
|
|
50
|
-
|
|
51
|
-
beforeEach(() => {
|
|
52
|
-
tmpRoot = mkdtempSync(join(tmpdir(), "cli-bundle-staging-"));
|
|
53
|
-
fetchSpy = spyOn(loopbackFetch, "loopbackSafeFetch");
|
|
54
|
-
execSpy = spyOn(stepRunner, "exec").mockResolvedValue(undefined);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
fetchSpy.mockRestore();
|
|
59
|
-
execSpy.mockRestore();
|
|
60
|
-
rmSync(tmpRoot, { recursive: true, force: true });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("stagingTargetFromEntry", () => {
|
|
64
|
-
test("returns docker staging for docker topology", () => {
|
|
65
|
-
expect(stagingTargetFromEntry(makeEntry({ cloud: "docker" }))).toEqual({
|
|
66
|
-
kind: "docker",
|
|
67
|
-
assistantId: "assistant-123",
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("returns local staging when instanceDir is present", () => {
|
|
72
|
-
expect(
|
|
73
|
-
stagingTargetFromEntry(
|
|
74
|
-
makeEntry({
|
|
75
|
-
cloud: "local",
|
|
76
|
-
resources: {
|
|
77
|
-
instanceDir: "/tmp/instance",
|
|
78
|
-
} as AssistantEntry["resources"],
|
|
79
|
-
}),
|
|
80
|
-
),
|
|
81
|
-
).toEqual({ kind: "local", instanceDir: "/tmp/instance" });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("returns null for unknown topologies", () => {
|
|
85
|
-
expect(
|
|
86
|
-
stagingTargetFromEntry(makeEntry({ cloud: "custom", sshUser: "user" })),
|
|
87
|
-
).toBeNull();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("stageBundleForRestore", () => {
|
|
92
|
-
test("copies a host bundle into the local workspace staging directory", async () => {
|
|
93
|
-
const instanceDir = join(tmpRoot, "instance");
|
|
94
|
-
const hostBundle = join(tmpRoot, "backup.vbundle");
|
|
95
|
-
writeFileSync(hostBundle, "bundle-bytes");
|
|
96
|
-
|
|
97
|
-
const staged = await stageBundleForRestore(
|
|
98
|
-
{ kind: "local", instanceDir },
|
|
99
|
-
hostBundle,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const dest = join(instanceDir, "workspace", staged.relativePath);
|
|
103
|
-
expect(staged.relativePath.startsWith(`${RESTORE_STAGING_DIRNAME}/`)).toBe(
|
|
104
|
-
true,
|
|
105
|
-
);
|
|
106
|
-
expect(staged.relativePath.endsWith(".vbundle")).toBe(true);
|
|
107
|
-
expect(readFileSync(dest, "utf8")).toBe("bundle-bytes");
|
|
108
|
-
expect(execSpy).not.toHaveBeenCalled();
|
|
109
|
-
|
|
110
|
-
await staged.cleanup();
|
|
111
|
-
expect(existsSync(dest)).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("copies a host bundle into the docker workspace via docker cp", async () => {
|
|
115
|
-
const hostBundle = join(tmpRoot, "backup.vbundle");
|
|
116
|
-
writeFileSync(hostBundle, "bundle-bytes");
|
|
117
|
-
|
|
118
|
-
const staged = await stageBundleForRestore(
|
|
119
|
-
{ kind: "docker", assistantId: "assistant-123" },
|
|
120
|
-
hostBundle,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
expect(execSpy).toHaveBeenCalledWith("docker", [
|
|
124
|
-
"exec",
|
|
125
|
-
"assistant-123-assistant",
|
|
126
|
-
"mkdir",
|
|
127
|
-
"-p",
|
|
128
|
-
`/workspace/${RESTORE_STAGING_DIRNAME}`,
|
|
129
|
-
]);
|
|
130
|
-
expect(execSpy).toHaveBeenCalledWith("docker", [
|
|
131
|
-
"cp",
|
|
132
|
-
hostBundle,
|
|
133
|
-
`assistant-123-assistant:/workspace/${staged.relativePath}`,
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
await staged.cleanup();
|
|
137
|
-
expect(execSpy).toHaveBeenCalledWith("docker", [
|
|
138
|
-
"exec",
|
|
139
|
-
"assistant-123-assistant",
|
|
140
|
-
"rm",
|
|
141
|
-
"-f",
|
|
142
|
-
`/workspace/${staged.relativePath}`,
|
|
143
|
-
]);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe("importStagedBundle", () => {
|
|
148
|
-
test("returns a direct 200 assistant response", async () => {
|
|
149
|
-
const payload = { success: true, summary: { total_files: 1 } };
|
|
150
|
-
fetchSpy.mockResolvedValue(
|
|
151
|
-
new Response(JSON.stringify(payload), { status: 200 }),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const response = await importStagedBundle(
|
|
155
|
-
"http://127.0.0.1:7831",
|
|
156
|
-
"token",
|
|
157
|
-
`${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
expect(response.status).toBe(200);
|
|
161
|
-
expect(await response.json()).toEqual(payload);
|
|
162
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
163
|
-
const [url, init] = fetchSpy.mock.calls[0];
|
|
164
|
-
expect(url).toBe("http://127.0.0.1:7831/v1/migrations/import");
|
|
165
|
-
expect(init?.headers).toMatchObject({
|
|
166
|
-
Authorization: "Bearer token",
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
});
|
|
169
|
-
expect(init?.body).toBe(
|
|
170
|
-
JSON.stringify({
|
|
171
|
-
path: `${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("polls a 202 gateway job until complete", async () => {
|
|
177
|
-
const realSetTimeout = globalThis.setTimeout;
|
|
178
|
-
const timeoutSpy = mock((fn: (...args: unknown[]) => void) =>
|
|
179
|
-
realSetTimeout(fn, 0),
|
|
180
|
-
);
|
|
181
|
-
globalThis.setTimeout = timeoutSpy as unknown as typeof setTimeout;
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
fetchSpy
|
|
185
|
-
.mockResolvedValueOnce(
|
|
186
|
-
new Response(JSON.stringify({ job_id: "job-1", status: "pending" }), {
|
|
187
|
-
status: 202,
|
|
188
|
-
}),
|
|
189
|
-
)
|
|
190
|
-
.mockResolvedValueOnce(
|
|
191
|
-
new Response(JSON.stringify({ status: "processing" }), {
|
|
192
|
-
status: 200,
|
|
193
|
-
}),
|
|
194
|
-
)
|
|
195
|
-
.mockResolvedValueOnce(
|
|
196
|
-
new Response(
|
|
197
|
-
JSON.stringify({
|
|
198
|
-
status: "complete",
|
|
199
|
-
result: { success: true, summary: { total_files: 2 } },
|
|
200
|
-
}),
|
|
201
|
-
{ status: 200 },
|
|
202
|
-
),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const response = await importStagedBundle(
|
|
206
|
-
"http://127.0.0.1:7831",
|
|
207
|
-
"token",
|
|
208
|
-
`${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
expect(response.status).toBe(200);
|
|
212
|
-
expect(await response.json()).toEqual({
|
|
213
|
-
success: true,
|
|
214
|
-
summary: { total_files: 2 },
|
|
215
|
-
});
|
|
216
|
-
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
217
|
-
expect(String(fetchSpy.mock.calls[1][0])).toBe(
|
|
218
|
-
"http://127.0.0.1:7831/v1/migrations/import/job-1/status",
|
|
219
|
-
);
|
|
220
|
-
} finally {
|
|
221
|
-
globalThis.setTimeout = realSetTimeout;
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe("restoreBackup staged path", () => {
|
|
227
|
-
test("stages a local bundle and posts JSON { path }", async () => {
|
|
228
|
-
const instanceDir = join(tmpRoot, "instance");
|
|
229
|
-
mkdirSync(join(instanceDir, "workspace"), { recursive: true });
|
|
230
|
-
const backupPath = join(tmpRoot, "backup.vbundle");
|
|
231
|
-
writeFileSync(backupPath, "bundle-bytes");
|
|
232
|
-
|
|
233
|
-
const loadSpy = spyOn(guardianToken, "loadGuardianToken").mockReturnValue({
|
|
234
|
-
accessToken: "local-token",
|
|
235
|
-
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
236
|
-
} as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
237
|
-
|
|
238
|
-
fetchSpy.mockResolvedValue(
|
|
239
|
-
new Response(JSON.stringify({ success: true }), { status: 200 }),
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const ok = await restoreBackup(
|
|
244
|
-
"http://127.0.0.1:7831",
|
|
245
|
-
"assistant-123",
|
|
246
|
-
backupPath,
|
|
247
|
-
{ kind: "local", instanceDir },
|
|
248
|
-
);
|
|
249
|
-
expect(ok).toBe(true);
|
|
250
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
251
|
-
const init = fetchSpy.mock.calls[0][1];
|
|
252
|
-
expect(init?.headers).toMatchObject({
|
|
253
|
-
"Content-Type": "application/json",
|
|
254
|
-
});
|
|
255
|
-
const posted = JSON.parse(String(init?.body)) as { path: string };
|
|
256
|
-
expect(posted.path.startsWith(`${RESTORE_STAGING_DIRNAME}/`)).toBe(true);
|
|
257
|
-
} finally {
|
|
258
|
-
loadSpy.mockRestore();
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
describe("bundle size helpers", () => {
|
|
264
|
-
test("formatBundleSizeMb reports two decimal megabytes", () => {
|
|
265
|
-
expect(formatBundleSizeMb(1024 * 1024)).toBe("1.00");
|
|
266
|
-
expect(bundleFileSizeBytes(join(tmpRoot, "missing.vbundle"))).toBe(0);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `cadet unpair <name>`: forget a paired (cloud:"paired") assistant
|
|
3
|
-
* by removing its lockfile entry + guardian token. Refuses non-paired entries.
|
|
4
|
-
*/
|
|
5
|
-
import {
|
|
6
|
-
afterAll,
|
|
7
|
-
afterEach,
|
|
8
|
-
beforeEach,
|
|
9
|
-
describe,
|
|
10
|
-
expect,
|
|
11
|
-
spyOn,
|
|
12
|
-
test,
|
|
13
|
-
} from "bun:test";
|
|
14
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
|
|
18
|
-
const testDir = mkdtempSync(join(tmpdir(), "unpair-test-"));
|
|
19
|
-
const ORIGINAL_LOCKFILE_DIR = process.env.CADET_LOCKFILE_DIR;
|
|
20
|
-
const ORIGINAL_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
|
|
21
|
-
const ORIGINAL_ARGV = [...process.argv];
|
|
22
|
-
|
|
23
|
-
import { unpair } from "../commands/unpair.js";
|
|
24
|
-
import {
|
|
25
|
-
findAssistantByName,
|
|
26
|
-
saveAssistantEntry,
|
|
27
|
-
} from "../lib/assistant-config.js";
|
|
28
|
-
import {
|
|
29
|
-
deleteGuardianToken,
|
|
30
|
-
loadGuardianToken,
|
|
31
|
-
saveGuardianToken,
|
|
32
|
-
} from "../lib/guardian-token.js";
|
|
33
|
-
|
|
34
|
-
function seedToken(assistantId: string): void {
|
|
35
|
-
saveGuardianToken(assistantId, {
|
|
36
|
-
guardianPrincipalId: "imported",
|
|
37
|
-
accessToken: "acc",
|
|
38
|
-
accessTokenExpiresAt: Date.now() + 3_600_000,
|
|
39
|
-
refreshToken: "ref",
|
|
40
|
-
refreshTokenExpiresAt: Date.now() + 3_600_000,
|
|
41
|
-
refreshAfter: "",
|
|
42
|
-
isNew: false,
|
|
43
|
-
deviceId: "dev",
|
|
44
|
-
leasedAt: new Date().toISOString(),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Run unpair with console.error + process.exit spied. */
|
|
49
|
-
async function runUnpair(): Promise<{ exited: boolean; errors: string }> {
|
|
50
|
-
const errors: string[] = [];
|
|
51
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
52
|
-
const errSpy = spyOn(console, "error").mockImplementation(
|
|
53
|
-
(...a: unknown[]) => {
|
|
54
|
-
errors.push(a.join(" "));
|
|
55
|
-
},
|
|
56
|
-
);
|
|
57
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
|
|
58
|
-
throw new Error(`exit:${c}`);
|
|
59
|
-
}) as never);
|
|
60
|
-
let exited = false;
|
|
61
|
-
try {
|
|
62
|
-
await unpair();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
exited = (e as Error).message === "exit:1";
|
|
65
|
-
} finally {
|
|
66
|
-
logSpy.mockRestore();
|
|
67
|
-
errSpy.mockRestore();
|
|
68
|
-
exitSpy.mockRestore();
|
|
69
|
-
}
|
|
70
|
-
return { exited, errors: errors.join("\n") };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
describe("cadet unpair", () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
process.env.CADET_LOCKFILE_DIR = testDir;
|
|
76
|
-
process.env.XDG_CONFIG_HOME = testDir;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
process.argv = [...ORIGINAL_ARGV];
|
|
81
|
-
if (ORIGINAL_LOCKFILE_DIR === undefined)
|
|
82
|
-
delete process.env.CADET_LOCKFILE_DIR;
|
|
83
|
-
else process.env.CADET_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
|
|
84
|
-
if (ORIGINAL_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
85
|
-
else process.env.XDG_CONFIG_HOME = ORIGINAL_CONFIG_HOME;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
afterAll(() => {
|
|
89
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("removes a paired entry's lockfile entry and guardian token", async () => {
|
|
93
|
-
saveAssistantEntry({
|
|
94
|
-
assistantId: "px",
|
|
95
|
-
name: "Paired Box",
|
|
96
|
-
runtimeUrl: "http://10.0.0.9:7830",
|
|
97
|
-
cloud: "paired",
|
|
98
|
-
paired: true,
|
|
99
|
-
});
|
|
100
|
-
seedToken("px");
|
|
101
|
-
expect(findAssistantByName("px")).not.toBeNull();
|
|
102
|
-
expect(loadGuardianToken("px")).not.toBeNull();
|
|
103
|
-
|
|
104
|
-
// Non-interactive test env → use --yes to bypass the confirmation prompt.
|
|
105
|
-
process.argv = ["bun", "cadet", "unpair", "px", "--yes"];
|
|
106
|
-
const { exited } = await runUnpair();
|
|
107
|
-
|
|
108
|
-
expect(exited).toBe(false);
|
|
109
|
-
expect(findAssistantByName("px")).toBeNull();
|
|
110
|
-
expect(loadGuardianToken("px")).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("refuses to unpair without --yes in a non-interactive terminal", async () => {
|
|
114
|
-
saveAssistantEntry({
|
|
115
|
-
assistantId: "py",
|
|
116
|
-
name: "Paired Two",
|
|
117
|
-
runtimeUrl: "http://10.0.0.9:7830",
|
|
118
|
-
cloud: "paired",
|
|
119
|
-
paired: true,
|
|
120
|
-
});
|
|
121
|
-
seedToken("py");
|
|
122
|
-
|
|
123
|
-
process.argv = ["bun", "cadet", "unpair", "py"]; // no --yes
|
|
124
|
-
const { exited, errors } = await runUnpair();
|
|
125
|
-
|
|
126
|
-
expect(exited).toBe(true);
|
|
127
|
-
expect(errors).toContain("--yes");
|
|
128
|
-
// Not removed — confirmation was required.
|
|
129
|
-
expect(findAssistantByName("py")).not.toBeNull();
|
|
130
|
-
expect(loadGuardianToken("py")).not.toBeNull();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("refuses a non-paired (local) assistant and leaves it intact", async () => {
|
|
134
|
-
saveAssistantEntry({
|
|
135
|
-
assistantId: "desk",
|
|
136
|
-
name: "Desk",
|
|
137
|
-
runtimeUrl: "http://127.0.0.1:7830",
|
|
138
|
-
cloud: "local",
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
process.argv = ["bun", "cadet", "unpair", "desk"];
|
|
142
|
-
const { exited, errors } = await runUnpair();
|
|
143
|
-
|
|
144
|
-
expect(exited).toBe(true);
|
|
145
|
-
expect(errors).toContain("cadet retire");
|
|
146
|
-
expect(findAssistantByName("desk")).not.toBeNull(); // untouched
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("errors on an unknown name", async () => {
|
|
150
|
-
process.argv = ["bun", "cadet", "unpair", "nope"];
|
|
151
|
-
const { exited } = await runUnpair();
|
|
152
|
-
expect(exited).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("deleteGuardianToken is a no-op when the token is absent", () => {
|
|
156
|
-
// No token seeded for this id; calling (twice) must not throw.
|
|
157
|
-
expect(() => deleteGuardianToken("ghost")).not.toThrow();
|
|
158
|
-
expect(() => deleteGuardianToken("ghost")).not.toThrow();
|
|
159
|
-
});
|
|
160
|
-
});
|