@mycadet/cli 0.12.11 → 0.13.0-dev.202610171626.d97b8b8
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/channels.ts +0 -1
- 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/channels.ts +0 -1
- 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/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/package.json +8 -3
- 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 +536 -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__/shell-mode.test.ts +83 -0
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +10 -2
- 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__/wake.test.ts +4 -1
- 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/tunnel.ts +48 -7
- 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 +23 -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/ingress-config.ts +31 -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 +454 -79
- 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/tunnel.ts +1 -0
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import * as tar from "tar";
|
|
8
|
+
import {
|
|
9
|
+
restoreHostTeleportTarget,
|
|
10
|
+
TeleportRecoveryError,
|
|
11
|
+
} from "../lib/host-teleport.js";
|
|
12
|
+
import { dockerResourceNames } from "../lib/docker.js";
|
|
13
|
+
import { DOCKER_STATEFUL_SET_SPEC } from "../lib/statefulset.js";
|
|
14
|
+
import { packRoot, STATE_ROOTS } from "../lib/root-backup.js";
|
|
15
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
16
|
+
import { mockCommands } from "./commands-mock.js";
|
|
17
|
+
|
|
18
|
+
const entry: AssistantEntry = {
|
|
19
|
+
assistantId: "destination",
|
|
20
|
+
cloud: "docker",
|
|
21
|
+
runtimeUrl: "http://localhost:7830",
|
|
22
|
+
containerInfo: {
|
|
23
|
+
assistantImage: "image-assistant",
|
|
24
|
+
gatewayImage: "image-gateway",
|
|
25
|
+
cesImage: "image-ces",
|
|
26
|
+
assistantPort: 7821,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const names = dockerResourceNames(entry.assistantId);
|
|
30
|
+
const containers = {
|
|
31
|
+
assistant: names.assistantContainer,
|
|
32
|
+
gateway: names.gatewayContainer,
|
|
33
|
+
"credential-executor": names.cesContainer,
|
|
34
|
+
};
|
|
35
|
+
const services: Array<keyof typeof containers> = ["assistant", "gateway", "credential-executor"];
|
|
36
|
+
const rootContainer = {
|
|
37
|
+
workspace: names.assistantContainer,
|
|
38
|
+
"gateway-security": names.gatewayContainer,
|
|
39
|
+
"ces-security": names.cesContainer,
|
|
40
|
+
"ces-data": names.cesContainer,
|
|
41
|
+
};
|
|
42
|
+
let directory: string, archive: string, recovery: string;
|
|
43
|
+
let commands: ReturnType<typeof mockCommands>;
|
|
44
|
+
let log: ReturnType<typeof spyOn>;
|
|
45
|
+
let failReadiness: boolean, failEveryWrite: boolean, writes: number;
|
|
46
|
+
let states: Map<string, ReturnType<typeof inspection>>;
|
|
47
|
+
function inspection(service: keyof typeof containers) {
|
|
48
|
+
const spec = DOCKER_STATEFUL_SET_SPEC.containers.find(
|
|
49
|
+
(item) => item.internalName === service,
|
|
50
|
+
)!;
|
|
51
|
+
return {
|
|
52
|
+
Id: `${service}-id`,
|
|
53
|
+
Image: `image-${service}`,
|
|
54
|
+
State: { Status: "exited", Running: false },
|
|
55
|
+
Config: {
|
|
56
|
+
User: "1001:1001",
|
|
57
|
+
Env: [
|
|
58
|
+
"ACTOR_TOKEN_SIGNING_KEY=" + "07".repeat(32),
|
|
59
|
+
"CES_SERVICE_TOKEN=internal-test-token",
|
|
60
|
+
"RUNTIME_HTTP_PORT=7821",
|
|
61
|
+
"GATEWAY_PORT=7830",
|
|
62
|
+
"GUARDIAN_BOOTSTRAP_SECRET=bootstrap-test-secret",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
HostConfig: {
|
|
66
|
+
NetworkMode:
|
|
67
|
+
service === "assistant" ? names.network : "container:assistant-id",
|
|
68
|
+
},
|
|
69
|
+
Mounts: spec.volumeMounts.map((mount) => ({
|
|
70
|
+
Type: "volume",
|
|
71
|
+
Name: DOCKER_STATEFUL_SET_SPEC.volumeClaimTemplates
|
|
72
|
+
.find((volume) => volume.name === mount.volumeName)!
|
|
73
|
+
.dockerVolume(entry.assistantId),
|
|
74
|
+
Source: "/remote/volume",
|
|
75
|
+
Destination: mount.mountPath,
|
|
76
|
+
RW: !mount.readOnly,
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const previousDataHome = process.env.XDG_DATA_HOME;
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
directory = await mkdtemp(join(tmpdir(), "teleport-docker-"));
|
|
83
|
+
process.env.XDG_DATA_HOME = directory;
|
|
84
|
+
archive = join(directory, "source.tar.gz");
|
|
85
|
+
recovery = join(directory, "recovery.tar.gz");
|
|
86
|
+
for (const flavor of ["source", "original", "volumes"]) {
|
|
87
|
+
for (const root of STATE_ROOTS) {
|
|
88
|
+
await mkdir(join(directory, flavor, root), { recursive: true });
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(directory, flavor, root, "value"),
|
|
91
|
+
flavor === "volumes" ? "original" : flavor,
|
|
92
|
+
{ mode: 0o600 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
await writeFile(
|
|
96
|
+
join(directory, flavor, "gateway-security", "actor-token-signing-key"),
|
|
97
|
+
Buffer.alloc(32, flavor === "source" ? 9 : 7),
|
|
98
|
+
);
|
|
99
|
+
await writeFile(
|
|
100
|
+
join(directory, flavor, "ces-security", "keys.enc"),
|
|
101
|
+
flavor === "source" ? "source ciphertext" : "original ciphertext",
|
|
102
|
+
{ mode: 0o600 },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
await packRoot(join(directory, "source"), archive);
|
|
106
|
+
await packRoot(join(directory, "original"), recovery);
|
|
107
|
+
states = new Map(
|
|
108
|
+
services.map((service) => [
|
|
109
|
+
containers[service],
|
|
110
|
+
inspection(service),
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
failReadiness = false;
|
|
114
|
+
failEveryWrite = false;
|
|
115
|
+
writes = 0;
|
|
116
|
+
log = spyOn(console, "log").mockImplementation(() => {});
|
|
117
|
+
commands = mockCommands();
|
|
118
|
+
commands.on(
|
|
119
|
+
"docker",
|
|
120
|
+
(args) => args[0] === "inspect",
|
|
121
|
+
({ args }) => {
|
|
122
|
+
const state = states.get(args.at(-1)!);
|
|
123
|
+
if (!state) {
|
|
124
|
+
return { code: 1, stderr: "No such container" };
|
|
125
|
+
}
|
|
126
|
+
if (args.includes("--format")) {
|
|
127
|
+
return {
|
|
128
|
+
stdout:
|
|
129
|
+
args[2] === "{{.State.Status}}"
|
|
130
|
+
? state.State.Status
|
|
131
|
+
: String(state.State.Running),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { stdout: JSON.stringify([state]) };
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
commands.on(
|
|
138
|
+
"docker",
|
|
139
|
+
(args) => args[0] === "rm",
|
|
140
|
+
({ args }) => {
|
|
141
|
+
const state = states.get(args[1]!);
|
|
142
|
+
if (!state) {
|
|
143
|
+
return { code: 1, stderr: "No such container" };
|
|
144
|
+
}
|
|
145
|
+
expect(state.State.Running).toBe(false);
|
|
146
|
+
states.delete(args[1]!);
|
|
147
|
+
return {};
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
commands.on(
|
|
151
|
+
"docker",
|
|
152
|
+
(args) => args[0] === "create",
|
|
153
|
+
({ args }) => {
|
|
154
|
+
const container = args[args.indexOf("--name") + 1]!;
|
|
155
|
+
const service = services.find((service) => containers[service] === container)!;
|
|
156
|
+
const state = inspection(service);
|
|
157
|
+
state.Config.Env = args.flatMap((value, index) =>
|
|
158
|
+
value === "-e" ? [args[index + 1]!] : [],
|
|
159
|
+
);
|
|
160
|
+
states.set(container, state);
|
|
161
|
+
return {};
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
commands.on(
|
|
165
|
+
"docker",
|
|
166
|
+
(args) => args[0] === "start",
|
|
167
|
+
({ args }) => {
|
|
168
|
+
const state = states.get(args[1]!)!;
|
|
169
|
+
state.State.Running = true;
|
|
170
|
+
state.State.Status = "running";
|
|
171
|
+
return {};
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
commands.on(
|
|
175
|
+
"docker",
|
|
176
|
+
(args) => args[0] === "kill",
|
|
177
|
+
({ args }) => {
|
|
178
|
+
expect(args[1]).toBe("--signal=SIGTERM");
|
|
179
|
+
const state = states.get(args.at(-1)!)!;
|
|
180
|
+
state.State.Running = false;
|
|
181
|
+
state.State.Status = "exited";
|
|
182
|
+
return {};
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
commands.on(
|
|
186
|
+
"docker",
|
|
187
|
+
(args) => args[0] === "cp",
|
|
188
|
+
async ({ args }) => {
|
|
189
|
+
const root = STATE_ROOTS.find((root) => args[1]!.includes(`:/${root}/`))!;
|
|
190
|
+
expect(
|
|
191
|
+
states.get(rootContainer[root])!.State
|
|
192
|
+
.Running,
|
|
193
|
+
).toBe(false);
|
|
194
|
+
await cp(join(directory, "volumes", root), args[2]!, { recursive: true });
|
|
195
|
+
return {};
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
commands.on(
|
|
199
|
+
"docker",
|
|
200
|
+
(args) => args[0] === "run" && args.includes("-i"),
|
|
201
|
+
async ({ args, input }) => {
|
|
202
|
+
const mount = args[args.indexOf("--mount") + 1]!;
|
|
203
|
+
const root = STATE_ROOTS.find(
|
|
204
|
+
(root) =>
|
|
205
|
+
states
|
|
206
|
+
.get(rootContainer[root])!
|
|
207
|
+
.Mounts.find((item) => item.Destination === `/${root}`)?.Name ===
|
|
208
|
+
mount.split("src=")[1]!.split(",")[0]!,
|
|
209
|
+
)!;
|
|
210
|
+
expect([...states.values()].every((state) => !state.State.Running)).toBe(
|
|
211
|
+
true,
|
|
212
|
+
);
|
|
213
|
+
expect(args).toContain("--network=none");
|
|
214
|
+
expect(args).toContain("--user=0");
|
|
215
|
+
expect(args).toContain("1001:1001");
|
|
216
|
+
expect(args[args.indexOf("-ec") + 1]).toContain('chown -R "$1" /restore');
|
|
217
|
+
writes++;
|
|
218
|
+
if (failEveryWrite) {
|
|
219
|
+
return { code: 1, stderr: "injected volume write failure" };
|
|
220
|
+
}
|
|
221
|
+
const rootDir = join(directory, "volumes", root);
|
|
222
|
+
await rm(rootDir, { recursive: true });
|
|
223
|
+
await mkdir(rootDir);
|
|
224
|
+
await pipeline(
|
|
225
|
+
Readable.from([input]),
|
|
226
|
+
tar.x({ cwd: rootDir, preserveOwner: false }),
|
|
227
|
+
);
|
|
228
|
+
return {};
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
commands.on(
|
|
232
|
+
"docker",
|
|
233
|
+
(args) => args[0] === "exec",
|
|
234
|
+
() =>
|
|
235
|
+
failReadiness
|
|
236
|
+
? { code: 2, stdout: "migration_failed", stderr: "migration_failed" }
|
|
237
|
+
: { stdout: "ready" },
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
afterEach(async () => {
|
|
241
|
+
if (previousDataHome === undefined) {
|
|
242
|
+
delete process.env.XDG_DATA_HOME;
|
|
243
|
+
} else {
|
|
244
|
+
process.env.XDG_DATA_HOME = previousDataHome;
|
|
245
|
+
}
|
|
246
|
+
commands.restore();
|
|
247
|
+
log.mockRestore();
|
|
248
|
+
await rm(directory, { recursive: true, force: true });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("recreates immutable identity overrides and imports all roots before first start", async () => {
|
|
252
|
+
await restoreHostTeleportTarget(entry, archive, recovery);
|
|
253
|
+
for (const root of STATE_ROOTS) {
|
|
254
|
+
expect(
|
|
255
|
+
await readFile(join(directory, "volumes", root, "value"), "utf8"),
|
|
256
|
+
).toBe("source");
|
|
257
|
+
}
|
|
258
|
+
expect(
|
|
259
|
+
await readFile(
|
|
260
|
+
join(directory, "volumes", "ces-security", "keys.enc"),
|
|
261
|
+
"utf8",
|
|
262
|
+
),
|
|
263
|
+
).toBe("source ciphertext");
|
|
264
|
+
expect(
|
|
265
|
+
await readFile(
|
|
266
|
+
join(directory, "volumes", "gateway-security", "actor-token-signing-key"),
|
|
267
|
+
),
|
|
268
|
+
).toEqual(Buffer.alloc(32, 9));
|
|
269
|
+
expect(
|
|
270
|
+
[...states.values()].every(
|
|
271
|
+
(state) =>
|
|
272
|
+
!state.Config.Env.some((value) =>
|
|
273
|
+
value.startsWith("ACTOR_TOKEN_SIGNING_KEY="),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
).toBe(true);
|
|
277
|
+
const firstStart = commands.calls.findIndex(
|
|
278
|
+
(request) => request.args[0] === "start",
|
|
279
|
+
);
|
|
280
|
+
const installs = commands.calls
|
|
281
|
+
.map((request, index) => ({ request, index }))
|
|
282
|
+
.filter(
|
|
283
|
+
({ request }) => request.args[0] === "run" && request.args.includes("-i"),
|
|
284
|
+
);
|
|
285
|
+
expect(installs).toHaveLength(4);
|
|
286
|
+
expect(installs.every(({ index }) => index < firstStart)).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
test("readiness failure restores original ciphertext, gateway identity, and stopped containers", async () => {
|
|
289
|
+
failReadiness = true;
|
|
290
|
+
await expect(
|
|
291
|
+
restoreHostTeleportTarget(entry, archive, recovery),
|
|
292
|
+
).rejects.toThrow("did not become ready");
|
|
293
|
+
for (const root of STATE_ROOTS) {
|
|
294
|
+
expect(
|
|
295
|
+
await readFile(join(directory, "volumes", root, "value"), "utf8"),
|
|
296
|
+
).toBe("original");
|
|
297
|
+
}
|
|
298
|
+
expect(
|
|
299
|
+
await readFile(
|
|
300
|
+
join(directory, "volumes", "ces-security", "keys.enc"),
|
|
301
|
+
"utf8",
|
|
302
|
+
),
|
|
303
|
+
).toBe("original ciphertext");
|
|
304
|
+
expect([...states.values()].every((state) => !state.State.Running)).toBe(
|
|
305
|
+
true,
|
|
306
|
+
);
|
|
307
|
+
expect(states.get(names.gatewayContainer)!.Config.Env).toContain(
|
|
308
|
+
"ACTOR_TOKEN_SIGNING_KEY=" + "07".repeat(32),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
test("unrecoverable writes leave services stopped and report the retained recovery tar", async () => {
|
|
312
|
+
failEveryWrite = true;
|
|
313
|
+
await expect(
|
|
314
|
+
restoreHostTeleportTarget(entry, archive, recovery),
|
|
315
|
+
).rejects.toBeInstanceOf(TeleportRecoveryError);
|
|
316
|
+
expect([...states.values()].every((state) => !state.State.Running)).toBe(
|
|
317
|
+
true,
|
|
318
|
+
);
|
|
319
|
+
expect(writes).toBeGreaterThan(1);
|
|
320
|
+
});
|
|
321
|
+
test("customized persistent mounts are rejected before container removal or volume writes", async () => {
|
|
322
|
+
states.get(names.cesContainer)!.Mounts[0]!.Name = "custom-security";
|
|
323
|
+
await expect(
|
|
324
|
+
restoreHostTeleportTarget(entry, archive, recovery),
|
|
325
|
+
).rejects.toThrow("customized Docker mounts");
|
|
326
|
+
expect(commands.calls.some((request) => request.args[0] === "rm")).toBe(
|
|
327
|
+
false,
|
|
328
|
+
);
|
|
329
|
+
expect(writes).toBe(0);
|
|
330
|
+
});
|
|
331
|
+
test("legacy vbundle is rejected before touching destination containers", async () => {
|
|
332
|
+
await expect(
|
|
333
|
+
restoreHostTeleportTarget(
|
|
334
|
+
entry,
|
|
335
|
+
join(directory, "legacy.vbundle"),
|
|
336
|
+
recovery,
|
|
337
|
+
),
|
|
338
|
+
).rejects.toThrow("vellum-to-cadet");
|
|
339
|
+
expect(commands.calls).toHaveLength(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("reads the remote Docker runtime version through in-container health instead of host ports", async () => {
|
|
343
|
+
const { readHostRuntimeVersion } = await import("../lib/host-teleport.js");
|
|
344
|
+
commands.on(
|
|
345
|
+
"docker",
|
|
346
|
+
(args) => args[0] === "exec" && args.at(-1)?.endsWith("/healthz") === true,
|
|
347
|
+
({ args }) => {
|
|
348
|
+
expect(args[1]).toBe(names.gatewayContainer);
|
|
349
|
+
expect(args.at(-1)).toBe("http://127.0.0.1:7830/healthz");
|
|
350
|
+
return { stdout: JSON.stringify({ version: "0.12.9" }) };
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
expect(await readHostRuntimeVersion(entry)).toBe("0.12.9");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("sleeping remote Docker runtime uses its recorded image tag for the version guard", async () => {
|
|
357
|
+
const { readHostRuntimeVersion } = await import("../lib/host-teleport.js");
|
|
358
|
+
commands.on("docker", (args) => args[0] === "exec", {
|
|
359
|
+
code: 1,
|
|
360
|
+
stderr: "container is stopped",
|
|
361
|
+
});
|
|
362
|
+
expect(
|
|
363
|
+
await readHostRuntimeVersion({
|
|
364
|
+
...entry,
|
|
365
|
+
containerInfo: {
|
|
366
|
+
...entry.containerInfo!,
|
|
367
|
+
gatewayImage: "mycadet/gateway:0.12.8",
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
).toBe("0.12.8");
|
|
371
|
+
});
|