@mycadet/cli 0.12.6-dev.202610091338.ee7f3e3 → 0.12.7-dev.202610102207.24bf931
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/package.json +5 -4
- package/src/__tests__/backup.test.ts +40 -45
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +24 -24
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +78 -86
- package/src/__tests__/pair.test.ts +128 -121
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/teleport.test.ts +55 -71
- package/src/__tests__/tunnel.test.ts +135 -154
- package/src/__tests__/uninstall.test.ts +180 -0
- package/src/__tests__/wake.test.ts +242 -32
- package/src/commands/client.ts +76 -34
- package/src/commands/ps.ts +1 -0
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +125 -7
- package/src/commands/rollback.ts +9 -15
- package/src/commands/terminal.ts +153 -13
- package/src/commands/uninstall.ts +276 -0
- package/src/commands/upgrade.ts +2 -19
- package/src/commands/wake.ts +157 -37
- package/src/index.ts +5 -9
- package/src/lib/assistant-config.ts +10 -22
- package/src/lib/backup-ops.ts +3 -4
- package/src/lib/client-identity.ts +8 -10
- package/src/lib/docker.ts +107 -6
- package/src/lib/guardian-token.ts +104 -18
- package/src/lib/hatch-local.ts +13 -17
- package/src/lib/health-check.ts +24 -15
- package/src/lib/local-hatch-lock.ts +19 -27
- package/src/lib/local-runtime-client.ts +25 -14
- package/src/lib/nginx-ingress.ts +9 -3
- package/src/lib/ngrok.ts +48 -17
- package/src/lib/platform-client.ts +141 -72
- package/src/lib/retire-archive.ts +4 -4
- package/src/lib/retire-local.ts +2 -25
- package/src/lib/teleport-backup.ts +22 -13
- package/src/lib/terminal-client.ts +19 -12
- package/src/lib/terminal-session.ts +4 -4
- package/src/lib/tunnel-edge.ts +11 -13
- package/src/lib/upgrade-lifecycle.ts +16 -18
- package/src/lib/workos-pkce.ts +51 -25
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/json.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mycadet/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7-dev.202610102207.24bf931",
|
|
4
4
|
"description": "CLI tools for mycadet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"./src/commands/*": "./src/commands/*.ts"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"cadet": "./src/index.ts"
|
|
18
|
+
"cadet-dev": "./src/index.ts"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"format": "prettier --write .",
|
|
22
22
|
"format:check": "prettier --check .",
|
|
23
|
-
"lint": "eslint --max-warnings
|
|
23
|
+
"lint": "eslint --max-warnings 326",
|
|
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",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"nanoid": "5.1.7",
|
|
41
41
|
"qrcode-terminal": "0.12.0",
|
|
42
42
|
"react": "19.2.6",
|
|
43
|
-
"react-devtools-core": "7.0.1"
|
|
43
|
+
"react-devtools-core": "7.0.1",
|
|
44
|
+
"zod": "4.3.6"
|
|
44
45
|
},
|
|
45
46
|
"bundledDependencies": [
|
|
46
47
|
"@mycadet/assistant-api",
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
describe,
|
|
6
6
|
expect,
|
|
7
7
|
mock,
|
|
8
|
+
type Mock,
|
|
8
9
|
spyOn,
|
|
9
10
|
test,
|
|
10
11
|
} from "bun:test";
|
|
@@ -79,6 +80,7 @@ const localRuntimePollJobStatusMock = spyOn(
|
|
|
79
80
|
result: { manifest_sha256: "abc123def456" },
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
/** A complete-but-fake guardian token; the SUT only reads the fields it sets. */
|
|
82
84
|
// Mode 1 (runtime-direct local backup) uses guardian tokens. Don't exercise
|
|
83
85
|
// it here, but the spies need to exist so the module under test can import
|
|
84
86
|
// them without surprises. Saved to variables so afterAll can restore them —
|
|
@@ -87,31 +89,25 @@ const localRuntimePollJobStatusMock = spyOn(
|
|
|
87
89
|
const loadGuardianTokenSpy = spyOn(
|
|
88
90
|
guardianToken,
|
|
89
91
|
"loadGuardianToken",
|
|
90
|
-
).mockReturnValue({
|
|
91
|
-
accessToken: "local-token",
|
|
92
|
-
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
93
|
-
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
92
|
+
).mockReturnValue(makeGuardianTokenData({ accessToken: "local-token" }));
|
|
94
93
|
const leaseGuardianTokenSpy = spyOn(
|
|
95
94
|
guardianToken,
|
|
96
95
|
"leaseGuardianToken",
|
|
97
|
-
).mockResolvedValue({
|
|
98
|
-
accessToken: "leased-token",
|
|
99
|
-
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
100
|
-
} as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
|
|
96
|
+
).mockResolvedValue(makeGuardianTokenData({ accessToken: "leased-token" }));
|
|
101
97
|
|
|
102
98
|
const getBackupsDirMock = spyOn(backupOps, "getBackupsDir").mockReturnValue(
|
|
103
99
|
"/tmp/backups-default",
|
|
104
100
|
);
|
|
105
101
|
|
|
106
102
|
const mkdirSyncMock = spyOn(fs, "mkdirSync").mockImplementation(
|
|
107
|
-
(
|
|
103
|
+
() => undefined,
|
|
108
104
|
);
|
|
109
105
|
const writeFileSyncMock = spyOn(fs, "writeFileSync").mockImplementation(
|
|
110
106
|
() => undefined,
|
|
111
107
|
);
|
|
112
108
|
|
|
113
109
|
let originalFetch: typeof globalThis.fetch;
|
|
114
|
-
let exitMock:
|
|
110
|
+
let exitMock: Mock<(code?: number | string | undefined) => never>;
|
|
115
111
|
|
|
116
112
|
const CADET_ENTRY = {
|
|
117
113
|
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
@@ -126,10 +122,10 @@ function setArgv(...rest: string[]) {
|
|
|
126
122
|
|
|
127
123
|
beforeEach(() => {
|
|
128
124
|
originalFetch = globalThis.fetch;
|
|
129
|
-
exitMock = mock((code?: number) => {
|
|
125
|
+
exitMock = mock((code?: number | string | undefined): never => {
|
|
130
126
|
throw new Error(`process.exit:${code}`);
|
|
131
127
|
});
|
|
132
|
-
process.exit = exitMock
|
|
128
|
+
process.exit = exitMock;
|
|
133
129
|
|
|
134
130
|
findAssistantByNameMock.mockReset();
|
|
135
131
|
findAssistantByNameMock.mockReturnValue(null);
|
|
@@ -162,17 +158,15 @@ beforeEach(() => {
|
|
|
162
158
|
getBackupsDirMock.mockReset();
|
|
163
159
|
getBackupsDirMock.mockReturnValue("/tmp/backups-default");
|
|
164
160
|
loadGuardianTokenSpy.mockReset();
|
|
165
|
-
loadGuardianTokenSpy.mockReturnValue(
|
|
166
|
-
accessToken: "local-token",
|
|
167
|
-
|
|
168
|
-
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
161
|
+
loadGuardianTokenSpy.mockReturnValue(
|
|
162
|
+
makeGuardianTokenData({ accessToken: "local-token" }),
|
|
163
|
+
);
|
|
169
164
|
leaseGuardianTokenSpy.mockReset();
|
|
170
|
-
leaseGuardianTokenSpy.mockResolvedValue(
|
|
171
|
-
accessToken: "leased-token",
|
|
172
|
-
|
|
173
|
-
} as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
|
|
165
|
+
leaseGuardianTokenSpy.mockResolvedValue(
|
|
166
|
+
makeGuardianTokenData({ accessToken: "leased-token" }),
|
|
167
|
+
);
|
|
174
168
|
mkdirSyncMock.mockReset();
|
|
175
|
-
mkdirSyncMock.mockImplementation((
|
|
169
|
+
mkdirSyncMock.mockImplementation(() => undefined);
|
|
176
170
|
writeFileSyncMock.mockReset();
|
|
177
171
|
writeFileSyncMock.mockImplementation(() => undefined);
|
|
178
172
|
});
|
|
@@ -200,20 +194,26 @@ afterAll(() => {
|
|
|
200
194
|
});
|
|
201
195
|
|
|
202
196
|
import { backup } from "../commands/backup.js";
|
|
197
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
198
|
+
import { makeGuardianTokenData } from "./helpers/guardian-token.js";
|
|
203
199
|
|
|
204
200
|
// ---------------------------------------------------------------------------
|
|
205
201
|
// Helper: simulated GCS download response
|
|
206
202
|
// ---------------------------------------------------------------------------
|
|
207
|
-
function mockGcsDownload(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
203
|
+
function mockGcsDownload(
|
|
204
|
+
body: Uint8Array<ArrayBuffer>,
|
|
205
|
+
ok = true,
|
|
206
|
+
status = 200,
|
|
207
|
+
) {
|
|
208
|
+
const gcsFetch = mock(async () => {
|
|
209
|
+
const responseBody: BodyInit = ok ? new Blob([body]) : "boom";
|
|
212
210
|
return new Response(responseBody, {
|
|
213
211
|
status,
|
|
214
212
|
statusText: ok ? "OK" : "Error",
|
|
215
213
|
});
|
|
216
|
-
})
|
|
214
|
+
});
|
|
215
|
+
globalThis.fetch = withPreconnect(gcsFetch);
|
|
216
|
+
return gcsFetch;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
describe("cadet backup <local>: guardian bootstrap secret", () => {
|
|
@@ -228,11 +228,9 @@ describe("cadet backup <local>: guardian bootstrap secret", () => {
|
|
|
228
228
|
loadGuardianTokenSpy.mockReturnValue(null);
|
|
229
229
|
setArgv("my-local", "--output", "/tmp/local-backup.vbundle");
|
|
230
230
|
|
|
231
|
-
globalThis.fetch =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
});
|
|
235
|
-
}) as unknown as typeof globalThis.fetch;
|
|
231
|
+
globalThis.fetch = withPreconnect(
|
|
232
|
+
async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
233
|
+
);
|
|
236
234
|
|
|
237
235
|
await backup();
|
|
238
236
|
|
|
@@ -294,9 +292,9 @@ describe("cadet backup: --export-timeout flag", () => {
|
|
|
294
292
|
"/tmp/local-backup.vbundle",
|
|
295
293
|
);
|
|
296
294
|
|
|
297
|
-
globalThis.fetch =
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
globalThis.fetch = withPreconnect(
|
|
296
|
+
async () => new Response(new Uint8Array([1, 2, 3]), { status: 200 }),
|
|
297
|
+
);
|
|
300
298
|
|
|
301
299
|
await backup();
|
|
302
300
|
|
|
@@ -313,7 +311,7 @@ describe("cadet backup <platform-managed>: GCS happy path", () => {
|
|
|
313
311
|
setArgv("my-platform");
|
|
314
312
|
|
|
315
313
|
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
316
|
-
mockGcsDownload(bytes);
|
|
314
|
+
const gcsFetch = mockGcsDownload(bytes);
|
|
317
315
|
|
|
318
316
|
await backup();
|
|
319
317
|
|
|
@@ -366,13 +364,12 @@ describe("cadet backup <platform-managed>: GCS happy path", () => {
|
|
|
366
364
|
"https://cloud.mycadet.ai",
|
|
367
365
|
);
|
|
368
366
|
const downloadCall = platformRequestSignedUrlMock.mock.calls.find(
|
|
369
|
-
(c) =>
|
|
367
|
+
(c) => c[0].operation === "download",
|
|
370
368
|
);
|
|
371
369
|
expect(downloadCall).toBeDefined();
|
|
372
370
|
expect(downloadCall![0]).not.toHaveProperty("targetRuntimeVersion");
|
|
373
371
|
|
|
374
372
|
// GCS fetch went directly to the signed download URL with no auth.
|
|
375
|
-
const gcsFetch = globalThis.fetch as unknown as ReturnType<typeof mock>;
|
|
376
373
|
expect(gcsFetch).toHaveBeenCalledWith(
|
|
377
374
|
"https://storage.googleapis.com/bucket/signed-download",
|
|
378
375
|
);
|
|
@@ -382,7 +379,7 @@ describe("cadet backup <platform-managed>: GCS happy path", () => {
|
|
|
382
379
|
const [outputPath, written] = writeFileSyncMock.mock.calls[0]!;
|
|
383
380
|
expect(written).toEqual(bytes);
|
|
384
381
|
expect(typeof outputPath).toBe("string");
|
|
385
|
-
expect(outputPath
|
|
382
|
+
expect(outputPath).toMatch(
|
|
386
383
|
/\/tmp\/backups-default\/my-platform-.*\.vbundle$/,
|
|
387
384
|
);
|
|
388
385
|
expect(mkdirSyncMock).toHaveBeenCalled();
|
|
@@ -411,10 +408,8 @@ describe("cadet backup <platform-managed>: GCS happy path", () => {
|
|
|
411
408
|
await backup();
|
|
412
409
|
|
|
413
410
|
const [outputPath] = writeFileSyncMock.mock.calls[0]!;
|
|
414
|
-
expect(outputPath
|
|
415
|
-
|
|
416
|
-
);
|
|
417
|
-
expect(outputPath as string).toMatch(/\.vbundle$/);
|
|
411
|
+
expect(outputPath).toMatch(/^\/tmp\/backups-default\/my-platform-/);
|
|
412
|
+
expect(outputPath).toMatch(/\.vbundle$/);
|
|
418
413
|
});
|
|
419
414
|
|
|
420
415
|
test("signed-URL requests target entry.runtimeUrl, not getPlatformUrl() — regression for staging/dev assistants", async () => {
|
|
@@ -502,10 +497,10 @@ describe("cadet backup <platform-managed>: GCS happy path", () => {
|
|
|
502
497
|
// `platformToken` parameter instead of `exportPlatformToken`, a
|
|
503
498
|
// future poll-loop refresh would silently break this invariant.
|
|
504
499
|
const uploadCallToken = platformRequestSignedUrlMock.mock.calls.find(
|
|
505
|
-
(c) =>
|
|
500
|
+
(c) => c[0].operation === "upload",
|
|
506
501
|
)![1];
|
|
507
502
|
const downloadCallToken = platformRequestSignedUrlMock.mock.calls.find(
|
|
508
|
-
(c) =>
|
|
503
|
+
(c) => c[0].operation === "download",
|
|
509
504
|
)![1];
|
|
510
505
|
expect(downloadCallToken).toBe(uploadCallToken);
|
|
511
506
|
const kickoffToken = localRuntimeExportToGcsMock.mock.calls[0]![1];
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
findAssistantByName,
|
|
28
28
|
saveAssistantEntry,
|
|
29
29
|
} from "../lib/assistant-config.js";
|
|
30
|
+
import { z } from "zod";
|
|
30
31
|
import { loadGuardianToken } from "../lib/guardian-token.js";
|
|
32
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
31
33
|
|
|
32
34
|
const HOST = "https://host.example.ts.net";
|
|
33
35
|
const PAIRING_LINK = `${HOST}/assistant/pair#device_code=dev-code`;
|
|
@@ -69,6 +71,9 @@ function approvedBody(overrides: Record<string, unknown> = {}) {
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** The commands always send flat string-keyed JSON bodies. */
|
|
75
|
+
const JsonRecord = z.record(z.string(), z.unknown());
|
|
76
|
+
|
|
72
77
|
interface FetchCall {
|
|
73
78
|
url: string;
|
|
74
79
|
body: Record<string, unknown>;
|
|
@@ -79,17 +84,20 @@ function stubFetch(
|
|
|
79
84
|
replies: (call: FetchCall, index: number) => Response,
|
|
80
85
|
): FetchCall[] {
|
|
81
86
|
const calls: FetchCall[] = [];
|
|
82
|
-
globalThis.fetch = (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
string
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
globalThis.fetch = withPreconnect(
|
|
88
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
89
|
+
const call = {
|
|
90
|
+
url: String(input),
|
|
91
|
+
// The command always sends a JSON string body; anything else records
|
|
92
|
+
// as an empty record.
|
|
93
|
+
body: JsonRecord.parse(
|
|
94
|
+
JSON.parse(typeof init?.body === "string" ? init.body : "{}"),
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
calls.push(call);
|
|
98
|
+
return replies(call, calls.length - 1);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
93
101
|
return calls;
|
|
94
102
|
}
|
|
95
103
|
|
|
@@ -112,14 +120,16 @@ async function runExpectingExit(): Promise<{ exited: boolean; out: string }> {
|
|
|
112
120
|
const logSpy = spyOn(console, "log").mockImplementation((...a: unknown[]) => {
|
|
113
121
|
messages.push(a.join(" "));
|
|
114
122
|
});
|
|
115
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
124
|
+
(c?: number | string | undefined): never => {
|
|
125
|
+
throw new Error(`exit:${c}`);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
118
128
|
let exited = false;
|
|
119
129
|
try {
|
|
120
130
|
await connectImport();
|
|
121
131
|
} catch (e) {
|
|
122
|
-
exited =
|
|
132
|
+
exited = e instanceof Error && e.message === "exit:1";
|
|
123
133
|
} finally {
|
|
124
134
|
errSpy.mockRestore();
|
|
125
135
|
logSpy.mockRestore();
|
|
@@ -144,10 +154,13 @@ describe("connect import", () => {
|
|
|
144
154
|
// a multi-second cadence is exercised in full without being spent, and
|
|
145
155
|
// both the cadence and the backoff are assertable rather than implicit.
|
|
146
156
|
setSystemTime(new Date());
|
|
147
|
-
sleepSpy = spyOn(Bun, "sleep").mockImplementation(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
sleepSpy = spyOn(Bun, "sleep").mockImplementation(
|
|
158
|
+
async (ms: number | Date) => {
|
|
159
|
+
const waitMs = typeof ms === "number" ? ms : ms.getTime() - Date.now();
|
|
160
|
+
waits.push(waitMs);
|
|
161
|
+
setSystemTime(new Date(Date.now() + waitMs));
|
|
162
|
+
},
|
|
163
|
+
);
|
|
151
164
|
});
|
|
152
165
|
|
|
153
166
|
afterEach(() => {
|
|
@@ -312,10 +325,10 @@ describe("connect import", () => {
|
|
|
312
325
|
"http://insecure.example.com",
|
|
313
326
|
];
|
|
314
327
|
let fetched = false;
|
|
315
|
-
globalThis.fetch = (async () => {
|
|
328
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
316
329
|
fetched = true;
|
|
317
330
|
return jsonResponse({});
|
|
318
|
-
})
|
|
331
|
+
});
|
|
319
332
|
|
|
320
333
|
const { exited, out } = await runExpectingExit();
|
|
321
334
|
|
|
@@ -333,10 +346,10 @@ describe("connect import", () => {
|
|
|
333
346
|
"https://127.0.0.1:7830",
|
|
334
347
|
];
|
|
335
348
|
let fetched = false;
|
|
336
|
-
globalThis.fetch = (async () => {
|
|
349
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
337
350
|
fetched = true;
|
|
338
351
|
return jsonResponse({});
|
|
339
|
-
})
|
|
352
|
+
});
|
|
340
353
|
|
|
341
354
|
const { exited, out } = await runExpectingExit();
|
|
342
355
|
|
|
@@ -361,10 +374,10 @@ describe("connect import", () => {
|
|
|
361
374
|
"legacy-box",
|
|
362
375
|
];
|
|
363
376
|
let fetched = false;
|
|
364
|
-
globalThis.fetch = (async () => {
|
|
377
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
365
378
|
fetched = true;
|
|
366
379
|
return jsonResponse({});
|
|
367
|
-
})
|
|
380
|
+
});
|
|
368
381
|
const { logs, restore } = captureLogs();
|
|
369
382
|
try {
|
|
370
383
|
await connectImport();
|
|
@@ -385,10 +398,10 @@ describe("connect import", () => {
|
|
|
385
398
|
test("a string that is neither an address nor a bundle keeps the address error", async () => {
|
|
386
399
|
process.argv = ["bun", "cadet", "connect", "import", "not-a-url"];
|
|
387
400
|
let fetched = false;
|
|
388
|
-
globalThis.fetch = (async () => {
|
|
401
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
389
402
|
fetched = true;
|
|
390
403
|
return jsonResponse({});
|
|
391
|
-
})
|
|
404
|
+
});
|
|
392
405
|
|
|
393
406
|
const { exited, out } = await runExpectingExit();
|
|
394
407
|
|
|
@@ -410,7 +423,8 @@ describe("connect import", () => {
|
|
|
410
423
|
"flaky-box",
|
|
411
424
|
];
|
|
412
425
|
let attempt = 0;
|
|
413
|
-
globalThis.fetch = (async (
|
|
426
|
+
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
|
|
427
|
+
const url = String(input);
|
|
414
428
|
if (url === CHALLENGE_URL) {
|
|
415
429
|
return jsonResponse(challengeBody());
|
|
416
430
|
}
|
|
@@ -420,7 +434,7 @@ describe("connect import", () => {
|
|
|
420
434
|
throw new Error("connect ECONNRESET");
|
|
421
435
|
}
|
|
422
436
|
return jsonResponse(approvedBody());
|
|
423
|
-
})
|
|
437
|
+
});
|
|
424
438
|
const { logs, restore } = captureLogs();
|
|
425
439
|
try {
|
|
426
440
|
await connectImport();
|
|
@@ -448,7 +462,8 @@ describe("connect import", () => {
|
|
|
448
462
|
"repairing-box",
|
|
449
463
|
];
|
|
450
464
|
let attempt = 0;
|
|
451
|
-
globalThis.fetch = (async (
|
|
465
|
+
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
|
|
466
|
+
const url = String(input);
|
|
452
467
|
if (url === CHALLENGE_URL) {
|
|
453
468
|
return jsonResponse(challengeBody());
|
|
454
469
|
}
|
|
@@ -459,7 +474,7 @@ describe("connect import", () => {
|
|
|
459
474
|
return jsonResponse({ error: "guardian repair required" }, 503);
|
|
460
475
|
}
|
|
461
476
|
return jsonResponse(approvedBody());
|
|
462
|
-
})
|
|
477
|
+
});
|
|
463
478
|
const { logs, restore } = captureLogs();
|
|
464
479
|
try {
|
|
465
480
|
await connectImport();
|
|
@@ -477,7 +492,8 @@ describe("connect import", () => {
|
|
|
477
492
|
test("reports expiry once transient failures outlast the code", async () => {
|
|
478
493
|
process.argv = ["bun", "cadet", "connect", "import", HOST];
|
|
479
494
|
let exchanges = 0;
|
|
480
|
-
globalThis.fetch = (async (
|
|
495
|
+
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
|
|
496
|
+
const url = String(input);
|
|
481
497
|
if (url === CHALLENGE_URL) {
|
|
482
498
|
return jsonResponse(
|
|
483
499
|
challengeBody({
|
|
@@ -488,7 +504,7 @@ describe("connect import", () => {
|
|
|
488
504
|
}
|
|
489
505
|
exchanges += 1;
|
|
490
506
|
throw new Error("connect ECONNREFUSED");
|
|
491
|
-
})
|
|
507
|
+
});
|
|
492
508
|
|
|
493
509
|
const { exited, out } = await runExpectingExit();
|
|
494
510
|
|
|
@@ -507,10 +523,10 @@ describe("connect import", () => {
|
|
|
507
523
|
// ten-minute TTL.
|
|
508
524
|
process.argv = ["bun", "cadet", "connect", "import", PAIRING_LINK];
|
|
509
525
|
let attempts = 0;
|
|
510
|
-
globalThis.fetch = (async () => {
|
|
526
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
511
527
|
attempts += 1;
|
|
512
528
|
throw new Error("connect ECONNREFUSED");
|
|
513
|
-
})
|
|
529
|
+
});
|
|
514
530
|
|
|
515
531
|
const { exited, out } = await runExpectingExit();
|
|
516
532
|
|
|
@@ -532,10 +548,10 @@ describe("connect import", () => {
|
|
|
532
548
|
test("rejects an unknown --flag instead of pairing against it", async () => {
|
|
533
549
|
process.argv = ["bun", "cadet", "connect", "import", "--qr", HOST];
|
|
534
550
|
let fetched = false;
|
|
535
|
-
globalThis.fetch = (async () => {
|
|
551
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
536
552
|
fetched = true;
|
|
537
553
|
return jsonResponse({});
|
|
538
|
-
})
|
|
554
|
+
});
|
|
539
555
|
|
|
540
556
|
const { exited, out } = await runExpectingExit();
|
|
541
557
|
|
|
@@ -560,10 +576,10 @@ describe("connect import", () => {
|
|
|
560
576
|
"--frobnicate",
|
|
561
577
|
];
|
|
562
578
|
let fetched = false;
|
|
563
|
-
globalThis.fetch = (async () => {
|
|
579
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
564
580
|
fetched = true;
|
|
565
581
|
return jsonResponse({});
|
|
566
|
-
})
|
|
582
|
+
});
|
|
567
583
|
|
|
568
584
|
const { exited, out } = await runExpectingExit();
|
|
569
585
|
|
|
@@ -20,6 +20,8 @@ import * as assistantConfig from "../lib/assistant-config.js";
|
|
|
20
20
|
import * as guardianToken from "../lib/guardian-token.js";
|
|
21
21
|
import * as localRuntimeClient from "../lib/local-runtime-client.js";
|
|
22
22
|
import * as platformClient from "../lib/platform-client.js";
|
|
23
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
24
|
+
import { makeGuardianTokenData } from "./helpers/guardian-token.js";
|
|
23
25
|
|
|
24
26
|
const resolveTargetMock = spyOn(
|
|
25
27
|
assistantConfig,
|
|
@@ -46,23 +48,25 @@ const pollMock = spyOn(
|
|
|
46
48
|
jobId: "job-1",
|
|
47
49
|
type: "export",
|
|
48
50
|
status: "complete",
|
|
49
|
-
}
|
|
50
|
-
ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
|
|
51
|
-
>);
|
|
51
|
+
});
|
|
52
52
|
const loadGuardianTokenSpy = spyOn(
|
|
53
53
|
guardianToken,
|
|
54
54
|
"loadGuardianToken",
|
|
55
|
-
).mockReturnValue(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
).mockReturnValue(
|
|
56
|
+
makeGuardianTokenData({
|
|
57
|
+
accessToken: "local-token",
|
|
58
|
+
accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
59
61
|
const leaseGuardianTokenSpy = spyOn(
|
|
60
62
|
guardianToken,
|
|
61
63
|
"leaseGuardianToken",
|
|
62
|
-
).mockResolvedValue(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
).mockResolvedValue(
|
|
65
|
+
makeGuardianTokenData({
|
|
66
|
+
accessToken: "leased-token",
|
|
67
|
+
accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
66
70
|
|
|
67
71
|
const { debug } = await import("../commands/debug.js");
|
|
68
72
|
|
|
@@ -75,7 +79,7 @@ const LOCAL_ENTRY = {
|
|
|
75
79
|
platformAssistantId: "11111111-2222-3333-4444-555555555555",
|
|
76
80
|
platformBaseUrl: "https://cloud.mycadet.ai",
|
|
77
81
|
platformOrganizationId: "org-1",
|
|
78
|
-
}
|
|
82
|
+
} satisfies assistantConfig.AssistantEntry;
|
|
79
83
|
|
|
80
84
|
let originalFetch: typeof globalThis.fetch;
|
|
81
85
|
let originalExit: typeof process.exit;
|
|
@@ -86,12 +90,12 @@ beforeEach(() => {
|
|
|
86
90
|
process.argv = ["bun", "cadet", "debug"];
|
|
87
91
|
originalFetch = globalThis.fetch;
|
|
88
92
|
originalExit = process.exit;
|
|
89
|
-
process.exit = mock((code?: number) => {
|
|
93
|
+
process.exit = mock((code?: number | string | undefined): never => {
|
|
90
94
|
throw new Error(`process.exit:${code}`);
|
|
91
|
-
})
|
|
95
|
+
});
|
|
92
96
|
fetchCalls = [];
|
|
93
97
|
uploadUrlStatus = 201;
|
|
94
|
-
globalThis.fetch =
|
|
98
|
+
globalThis.fetch = withPreconnect(
|
|
95
99
|
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
96
100
|
fetchCalls.push({ url: String(input), init });
|
|
97
101
|
return new Response(
|
|
@@ -102,7 +106,7 @@ beforeEach(() => {
|
|
|
102
106
|
},
|
|
103
107
|
);
|
|
104
108
|
},
|
|
105
|
-
)
|
|
109
|
+
);
|
|
106
110
|
resolveTargetMock.mockReset();
|
|
107
111
|
resolveTargetMock.mockReturnValue(LOCAL_ENTRY);
|
|
108
112
|
identityMock.mockReset();
|
|
@@ -140,9 +144,7 @@ describe("cadet debug", () => {
|
|
|
140
144
|
);
|
|
141
145
|
expect(fetchCalls[0].init?.method).toBe("POST");
|
|
142
146
|
expect(
|
|
143
|
-
(fetchCalls[0].init?.headers
|
|
144
|
-
"Cadet-Organization-Id"
|
|
145
|
-
],
|
|
147
|
+
new Headers(fetchCalls[0].init?.headers).get("Cadet-Organization-Id"),
|
|
146
148
|
).toBe("org-1");
|
|
147
149
|
|
|
148
150
|
expect(exportMock).toHaveBeenCalledTimes(1);
|
|
@@ -196,9 +198,7 @@ describe("cadet debug", () => {
|
|
|
196
198
|
jobId: "job-1",
|
|
197
199
|
type: "export",
|
|
198
200
|
status: "complete",
|
|
199
|
-
}
|
|
200
|
-
ReturnType<typeof localRuntimeClient.localRuntimePollJobStatus>
|
|
201
|
-
>;
|
|
201
|
+
};
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
await debug();
|
|
@@ -226,7 +226,7 @@ describe("cadet debug", () => {
|
|
|
226
226
|
resolveTargetMock.mockReturnValue({
|
|
227
227
|
...LOCAL_ENTRY,
|
|
228
228
|
cloud: "cadet",
|
|
229
|
-
}
|
|
229
|
+
});
|
|
230
230
|
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
231
231
|
try {
|
|
232
232
|
await expect(debug()).rejects.toThrow("process.exit:1");
|
|
@@ -240,7 +240,7 @@ describe("cadet debug", () => {
|
|
|
240
240
|
resolveTargetMock.mockReturnValue({
|
|
241
241
|
...LOCAL_ENTRY,
|
|
242
242
|
platformAssistantId: undefined,
|
|
243
|
-
}
|
|
243
|
+
});
|
|
244
244
|
const errors: string[] = [];
|
|
245
245
|
const errorSpy = spyOn(console, "error").mockImplementation((msg) => {
|
|
246
246
|
errors.push(String(msg));
|