@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
|
@@ -55,7 +55,7 @@ mock.module("../lib/assistant-config", () => ({
|
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
57
|
const createBackupMock = mock<typeof backupOps.createBackup>(
|
|
58
|
-
async () => "/tmp/local-pre-upgrade.
|
|
58
|
+
async () => "/tmp/local-pre-upgrade.tar.gz",
|
|
59
59
|
);
|
|
60
60
|
const pruneOldBackupsMock = mock<typeof backupOps.pruneOldBackups>(() => {});
|
|
61
61
|
const restoreBackupMock = mock<typeof backupOps.restoreBackup>(
|
|
@@ -238,7 +238,7 @@ beforeEach(() => {
|
|
|
238
238
|
loadAllAssistantsMock.mockReturnValue([entry]);
|
|
239
239
|
saveAssistantEntryMock.mockReset();
|
|
240
240
|
createBackupMock.mockReset();
|
|
241
|
-
createBackupMock.mockResolvedValue("/tmp/local-pre-upgrade.
|
|
241
|
+
createBackupMock.mockResolvedValue("/tmp/local-pre-upgrade.tar.gz");
|
|
242
242
|
pruneOldBackupsMock.mockReset();
|
|
243
243
|
pruneOldBackupsMock.mockReturnValue(undefined);
|
|
244
244
|
restoreBackupMock.mockReset();
|
|
@@ -372,7 +372,7 @@ describe("cadet upgrade local", () => {
|
|
|
372
372
|
await upgrade();
|
|
373
373
|
|
|
374
374
|
expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
|
|
375
|
-
"http://127.0.0.1:7830/healthz
|
|
375
|
+
"http://127.0.0.1:7830/healthz",
|
|
376
376
|
expect.any(Object),
|
|
377
377
|
);
|
|
378
378
|
expect(createBackupMock).toHaveBeenCalledWith(
|
|
@@ -382,14 +382,6 @@ describe("cadet upgrade local", () => {
|
|
|
382
382
|
prefix: "local-assistant-pre-upgrade",
|
|
383
383
|
}),
|
|
384
384
|
);
|
|
385
|
-
expect(saveAssistantEntryMock).toHaveBeenCalledWith(
|
|
386
|
-
expect.objectContaining({
|
|
387
|
-
previousVersion: "v0.8.11",
|
|
388
|
-
previousDbMigrationVersion: 12,
|
|
389
|
-
previousWorkspaceMigrationId: "011-test",
|
|
390
|
-
preUpgradeBackupPath: "/tmp/local-pre-upgrade.vbundle",
|
|
391
|
-
}),
|
|
392
|
-
);
|
|
393
385
|
expect(stopLocalProcessesMock).toHaveBeenCalledWith(
|
|
394
386
|
expect.objectContaining({
|
|
395
387
|
instanceDir: tempDir,
|
|
@@ -446,6 +438,24 @@ describe("cadet upgrade local", () => {
|
|
|
446
438
|
expect(consoleLogSpy.mock.calls.flat().join("\n")).toContain("upgraded to");
|
|
447
439
|
});
|
|
448
440
|
|
|
441
|
+
test("failed safety backup aborts before runtime installation or service replacement", async () => {
|
|
442
|
+
createBackupMock.mockResolvedValue(null);
|
|
443
|
+
await expect(upgrade()).rejects.toThrow("process.exit(1)");
|
|
444
|
+
expect(ensureLocalRuntimeMock).not.toHaveBeenCalled();
|
|
445
|
+
expect(stopLocalProcessesMock).not.toHaveBeenCalled();
|
|
446
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("failed target startup restores prior runtime selection and all roots before restarting", async () => {
|
|
450
|
+
startLocalDaemonMock.mockRejectedValueOnce(new Error("target startup failed"));
|
|
451
|
+
await expect(upgrade()).rejects.toThrow("process.exit(1)");
|
|
452
|
+
expect(restoreBackupMock).toHaveBeenCalledWith("http://127.0.0.1:7830", "local-assistant", "/tmp/local-pre-upgrade.tar.gz", { resume: false });
|
|
453
|
+
expect(startLocalDaemonMock).toHaveBeenCalledTimes(2);
|
|
454
|
+
expect(startCesMock).toHaveBeenCalledTimes(2);
|
|
455
|
+
expect(stopLocalProcessesMock).toHaveBeenCalledTimes(2);
|
|
456
|
+
expect(waitForReadyMock).toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
449
459
|
test("skips restart when the local assistant is already on the target version", async () => {
|
|
450
460
|
loopbackSafeFetchMock.mockResolvedValue({
|
|
451
461
|
ok: true,
|
|
@@ -156,10 +156,10 @@ describe("buildReplayState", () => {
|
|
|
156
156
|
expect(state.signingKey).toBe("sign-key");
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
test("generates
|
|
159
|
+
test("generates a service token but preserves file-backed signing identity when env is absent", () => {
|
|
160
160
|
const state = buildReplayState({}, {});
|
|
161
161
|
expect(state.bootstrapSecret).toBeUndefined();
|
|
162
162
|
expect(state.cesServiceToken).toMatch(/^[0-9a-f]{64}$/);
|
|
163
|
-
expect(state.signingKey).
|
|
163
|
+
expect(state.signingKey).toBeUndefined();
|
|
164
164
|
});
|
|
165
165
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { afterAll, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as backups from "../lib/backup-ops.js";
|
|
3
|
+
import * as docker from "../lib/docker.js";
|
|
4
|
+
import * as lifecycle from "../lib/upgrade-lifecycle.js";
|
|
5
|
+
import * as config from "../lib/assistant-config.js";
|
|
6
|
+
import { mockCommands } from "./commands-mock.js";
|
|
7
|
+
import * as loopback from "../lib/loopback-fetch.js";
|
|
8
|
+
|
|
9
|
+
const capture = spyOn(backups, "createBackup");
|
|
10
|
+
const images = spyOn(docker, "captureImageRefs");
|
|
11
|
+
const pull = spyOn(docker, "pullImage");
|
|
12
|
+
const wake = spyOn(docker, "wakeContainers");
|
|
13
|
+
const save = spyOn(config, "saveAssistantEntry");
|
|
14
|
+
const restore = spyOn(backups, "restoreBackup");
|
|
15
|
+
const sleep = spyOn(docker, "sleepContainers");
|
|
16
|
+
const stop = spyOn(docker, "stopContainers");
|
|
17
|
+
const start = spyOn(docker, "startContainers");
|
|
18
|
+
const fetch = spyOn(loopback, "loopbackSafeFetch");
|
|
19
|
+
const res = docker.dockerResourceNames("recovery");
|
|
20
|
+
const containerOptions = {
|
|
21
|
+
gatewayPort: 7830,
|
|
22
|
+
assistantPort: 3001,
|
|
23
|
+
imageTags: {
|
|
24
|
+
assistant: "assistant:old",
|
|
25
|
+
gateway: "gateway:old",
|
|
26
|
+
"credential-executor": "ces:old",
|
|
27
|
+
},
|
|
28
|
+
instanceName: "recovery",
|
|
29
|
+
res,
|
|
30
|
+
};
|
|
31
|
+
const events: string[] = [];
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
events.length = 0;
|
|
34
|
+
capture.mockReset();
|
|
35
|
+
images.mockReset();
|
|
36
|
+
pull.mockReset();
|
|
37
|
+
wake.mockReset();
|
|
38
|
+
save.mockReset();
|
|
39
|
+
save.mockImplementation(() => {});
|
|
40
|
+
restore.mockReset();
|
|
41
|
+
sleep.mockReset();
|
|
42
|
+
stop.mockReset();
|
|
43
|
+
start.mockReset();
|
|
44
|
+
fetch.mockReset();
|
|
45
|
+
restore.mockImplementation(async () => {
|
|
46
|
+
events.push("restore-roots");
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
sleep.mockImplementation(async () => {
|
|
50
|
+
events.push("sleep");
|
|
51
|
+
});
|
|
52
|
+
stop.mockImplementation(async () => {
|
|
53
|
+
events.push("remove");
|
|
54
|
+
});
|
|
55
|
+
start.mockImplementation(async () => {
|
|
56
|
+
events.push("start-old-version");
|
|
57
|
+
});
|
|
58
|
+
fetch.mockImplementation(async () => {
|
|
59
|
+
events.push("check-ready");
|
|
60
|
+
return Response.json({ ready: true });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
afterAll(() => {
|
|
64
|
+
for (const spy of [
|
|
65
|
+
restore,
|
|
66
|
+
sleep,
|
|
67
|
+
stop,
|
|
68
|
+
start,
|
|
69
|
+
fetch,
|
|
70
|
+
capture,
|
|
71
|
+
images,
|
|
72
|
+
pull,
|
|
73
|
+
wake,
|
|
74
|
+
save,
|
|
75
|
+
])
|
|
76
|
+
spy.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("failed-state recovery restores tar roots while stopped before restarting old services", async () => {
|
|
80
|
+
expect(
|
|
81
|
+
await lifecycle.attemptFailedStateRestore({
|
|
82
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
83
|
+
assistantId: "recovery",
|
|
84
|
+
backupPath: "pre-upgrade.tar.gz",
|
|
85
|
+
backupLabel: "pre-upgrade",
|
|
86
|
+
res,
|
|
87
|
+
containerOptions,
|
|
88
|
+
}),
|
|
89
|
+
).toEqual({ restored: true, ready: true });
|
|
90
|
+
expect(restore).toHaveBeenCalledWith(
|
|
91
|
+
"http://127.0.0.1:1",
|
|
92
|
+
"recovery",
|
|
93
|
+
"pre-upgrade.tar.gz",
|
|
94
|
+
{ resume: false },
|
|
95
|
+
);
|
|
96
|
+
expect(events).toEqual([
|
|
97
|
+
"restore-roots",
|
|
98
|
+
"sleep",
|
|
99
|
+
"remove",
|
|
100
|
+
"start-old-version",
|
|
101
|
+
"check-ready",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("failed root restore does not restart a partial installation", async () => {
|
|
106
|
+
restore.mockResolvedValue(false);
|
|
107
|
+
expect(
|
|
108
|
+
await lifecycle.attemptFailedStateRestore({
|
|
109
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
110
|
+
assistantId: "recovery",
|
|
111
|
+
backupPath: "pre-upgrade.tar.gz",
|
|
112
|
+
backupLabel: "pre-upgrade",
|
|
113
|
+
res,
|
|
114
|
+
containerOptions,
|
|
115
|
+
}),
|
|
116
|
+
).toEqual({ restored: false, ready: false });
|
|
117
|
+
expect(start).not.toHaveBeenCalled();
|
|
118
|
+
expect(stop).not.toHaveBeenCalled();
|
|
119
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("versioned restore installs roots into stopped target containers before first startup", async () => {
|
|
123
|
+
const commands = mockCommands();
|
|
124
|
+
commands.on(
|
|
125
|
+
"docker",
|
|
126
|
+
(args) => args[0] === "inspect",
|
|
127
|
+
() => ({
|
|
128
|
+
stdout: JSON.stringify(["ACTOR_TOKEN_SIGNING_KEY=" + "01".repeat(32)]),
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
const entry: config.AssistantEntry = {
|
|
132
|
+
assistantId: "recovery",
|
|
133
|
+
cloud: "docker",
|
|
134
|
+
runtimeUrl: "http://127.0.0.1:1",
|
|
135
|
+
};
|
|
136
|
+
capture.mockImplementation(async () => {
|
|
137
|
+
events.push("capture-safety");
|
|
138
|
+
return "/tmp/pre-rollback.tar.gz";
|
|
139
|
+
});
|
|
140
|
+
images.mockResolvedValue({
|
|
141
|
+
assistant: "assistant:current",
|
|
142
|
+
gateway: "gateway:current",
|
|
143
|
+
"credential-executor": "ces:current",
|
|
144
|
+
});
|
|
145
|
+
pull.mockResolvedValue(undefined);
|
|
146
|
+
start.mockImplementation(async (options) => {
|
|
147
|
+
expect(options.createOnly).toBe(true);
|
|
148
|
+
events.push("create-stopped-target");
|
|
149
|
+
});
|
|
150
|
+
wake.mockImplementation(async () => {
|
|
151
|
+
events.push("wake-restored-target");
|
|
152
|
+
});
|
|
153
|
+
fetch.mockImplementation(async (url) => {
|
|
154
|
+
if (String(url).includes("/healthz"))
|
|
155
|
+
return Response.json({ version: "0.12.11" });
|
|
156
|
+
if (String(url).includes("/readyz")) {
|
|
157
|
+
events.push("check-ready");
|
|
158
|
+
return Response.json({ ready: true });
|
|
159
|
+
}
|
|
160
|
+
return Response.json({});
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
await lifecycle.performDockerRollback(entry, {
|
|
164
|
+
targetVersion: "0.12.10",
|
|
165
|
+
backupPath: "/tmp/source.tar.gz",
|
|
166
|
+
});
|
|
167
|
+
expect(restore).toHaveBeenCalledWith(
|
|
168
|
+
entry.runtimeUrl,
|
|
169
|
+
entry.assistantId,
|
|
170
|
+
"/tmp/source.tar.gz",
|
|
171
|
+
{ resume: false },
|
|
172
|
+
);
|
|
173
|
+
expect(events).toEqual([
|
|
174
|
+
"capture-safety",
|
|
175
|
+
"sleep",
|
|
176
|
+
"remove",
|
|
177
|
+
"create-stopped-target",
|
|
178
|
+
"restore-roots",
|
|
179
|
+
"wake-restored-target",
|
|
180
|
+
"check-ready",
|
|
181
|
+
]);
|
|
182
|
+
} finally {
|
|
183
|
+
commands.restore();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
@@ -227,3 +227,28 @@ test("preserves PID tracking when process termination fails", async () => {
|
|
|
227
227
|
expect(isProcessAlive(pidFile)).toEqual({ alive: true, pid: process.pid });
|
|
228
228
|
rmSync(dir, { recursive: true, force: true });
|
|
229
229
|
});
|
|
230
|
+
|
|
231
|
+
test("graceful-only stop times out without SIGKILL or losing PID identity", async () => {
|
|
232
|
+
const originalKill = process.kill;
|
|
233
|
+
const calls: Array<NodeJS.Signals | number | undefined> = [];
|
|
234
|
+
process.kill = ((_pid: number, signal?: NodeJS.Signals | number) => {
|
|
235
|
+
calls.push(signal);
|
|
236
|
+
return true;
|
|
237
|
+
}) as typeof process.kill;
|
|
238
|
+
try {
|
|
239
|
+
expect(
|
|
240
|
+
await stopProcess(
|
|
241
|
+
4812,
|
|
242
|
+
"backup service",
|
|
243
|
+
0,
|
|
244
|
+
"linux",
|
|
245
|
+
undefined,
|
|
246
|
+
() => "cadet-daemon",
|
|
247
|
+
true,
|
|
248
|
+
),
|
|
249
|
+
).toBe(false);
|
|
250
|
+
expect(calls).toEqual([0, "SIGTERM", 0]);
|
|
251
|
+
} finally {
|
|
252
|
+
process.kill = originalKill;
|
|
253
|
+
}
|
|
254
|
+
});
|