@mycadet/cli 0.12.9 → 0.12.11
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/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/devices.test.ts +62 -0
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +58 -26
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +6 -4
- package/src/__tests__/setup.test.ts +14 -4
- package/src/__tests__/teleport.test.ts +0 -15
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +265 -70
- package/src/__tests__/uninstall.test.ts +140 -8
- package/src/commands/client.ts +4 -6
- package/src/commands/devices.ts +66 -30
- package/src/commands/hatch.ts +14 -53
- package/src/commands/pair.ts +147 -38
- package/src/commands/ps.ts +10 -20
- package/src/commands/teleport.ts +2 -21
- package/src/commands/tunnel.ts +67 -28
- package/src/commands/uninstall.ts +106 -71
- package/src/commands/upgrade.ts +6 -1
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/assistant-config.ts +20 -5
- package/src/lib/cloud-client.ts +0 -3
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +3 -2
- package/src/lib/color.ts +4 -0
- package/src/lib/docker.ts +46 -70
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/environments/__tests__/paths.test.ts +27 -1
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +2 -4
- package/src/lib/hatch-local.ts +16 -59
- package/src/lib/hatch-next-steps.ts +18 -9
- package/src/lib/live-voice/tunnel.ts +2 -2
- package/src/lib/local.ts +42 -9
- package/src/lib/nginx-ingress.ts +124 -24
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
beforeEach,
|
|
5
5
|
describe,
|
|
6
6
|
expect,
|
|
7
|
+
mock,
|
|
7
8
|
spyOn,
|
|
8
9
|
test,
|
|
9
10
|
} from "bun:test";
|
|
@@ -19,9 +20,36 @@ process.env.CADET_LOCKFILE_DIR = testDir;
|
|
|
19
20
|
const originalEnvironment = process.env.CADET_ENVIRONMENT;
|
|
20
21
|
process.env.CADET_ENVIRONMENT = "production";
|
|
21
22
|
|
|
22
|
-
import { buildAppConnectUrl, pair } from "../commands/pair.js";
|
|
23
23
|
import { withPreconnect } from "./helpers/http.js";
|
|
24
24
|
|
|
25
|
+
// The gateway-CLI mint runs in a SUBPROCESS for local and docker assistants;
|
|
26
|
+
// answer it in-process and record the invocations (the fetch stubs below
|
|
27
|
+
// still serve the loopback HTTP mint the other topologies keep using).
|
|
28
|
+
const mintCalls: Array<{ command: string; args: readonly string[] }> = [];
|
|
29
|
+
const actualStepRunner = await import("../lib/step-runner.js");
|
|
30
|
+
const mockExecOutput = mock(
|
|
31
|
+
(command: string, args: readonly string[]): Promise<string> => {
|
|
32
|
+
mintCalls.push({ command, args });
|
|
33
|
+
const mintedUrl = args[args.length - 1] ?? "";
|
|
34
|
+
return Promise.resolve(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
deviceCode: "device-code",
|
|
37
|
+
userCode: "ABCD-EFGH",
|
|
38
|
+
verificationUri: `${mintedUrl}/assistant/pair`,
|
|
39
|
+
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
40
|
+
expiresInSeconds: 600,
|
|
41
|
+
intervalSeconds: 5,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
mock.module("../lib/step-runner.js", () => ({
|
|
47
|
+
...actualStepRunner,
|
|
48
|
+
execOutput: mockExecOutput,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const { buildAppConnectUrl, pair } = await import("../commands/pair.js");
|
|
52
|
+
|
|
25
53
|
// Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
|
|
26
54
|
const LOCAL_URL = "http://127.0.0.1:7830";
|
|
27
55
|
const RUNTIME_URL = "http://192.168.1.50:7830";
|
|
@@ -93,6 +121,7 @@ const ORIGINAL_ARGV = [...process.argv];
|
|
|
93
121
|
|
|
94
122
|
describe("pair command", () => {
|
|
95
123
|
beforeEach(() => {
|
|
124
|
+
mintCalls.length = 0;
|
|
96
125
|
writeLockfile();
|
|
97
126
|
});
|
|
98
127
|
|
|
@@ -130,18 +159,16 @@ describe("pair command", () => {
|
|
|
130
159
|
globalThis.fetch = origFetch;
|
|
131
160
|
}
|
|
132
161
|
|
|
133
|
-
// Mint
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
162
|
+
// Mint through the gateway CLI subprocess: process execution IS the
|
|
163
|
+
// local-presence proof and the approval, so the link alone completes
|
|
164
|
+
// the pairing — no HTTP at all for a local assistant.
|
|
165
|
+
expect(mintCalls).toHaveLength(1);
|
|
166
|
+
expect(mintCalls[0]?.args.slice(-3)).toEqual([
|
|
167
|
+
"pairing",
|
|
168
|
+
"mint",
|
|
169
|
+
PUBLIC_URL,
|
|
138
170
|
]);
|
|
139
|
-
expect(
|
|
140
|
-
publicBaseUrl: PUBLIC_URL,
|
|
141
|
-
});
|
|
142
|
-
expect(JSON.parse(String(calls[1][1]?.body))).toEqual({
|
|
143
|
-
userCode: "ABCD-EFGH",
|
|
144
|
-
});
|
|
171
|
+
expect(calls).toHaveLength(0);
|
|
145
172
|
|
|
146
173
|
const output = logs.join("\n");
|
|
147
174
|
// One artifact, two renderings: the link and the same link as a QR.
|
|
@@ -391,10 +418,9 @@ describe("pair command", () => {
|
|
|
391
418
|
}
|
|
392
419
|
|
|
393
420
|
// Resolution succeeded (no exit), so the challenge was minted.
|
|
394
|
-
expect(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
]);
|
|
421
|
+
expect(mintCalls).toHaveLength(1);
|
|
422
|
+
expect(mintCalls[0]?.args.at(-1)).toBe(PUBLIC_URL);
|
|
423
|
+
expect(calls).toHaveLength(0);
|
|
398
424
|
});
|
|
399
425
|
|
|
400
426
|
test("rejects an unknown --flag before any network call", async () => {
|
|
@@ -587,11 +613,8 @@ describe("pair command", () => {
|
|
|
587
613
|
globalThis.fetch = origFetch;
|
|
588
614
|
}
|
|
589
615
|
|
|
590
|
-
// Mint still
|
|
591
|
-
expect(
|
|
592
|
-
expect(JSON.parse(String(calls[0][1]?.body))).toEqual({
|
|
593
|
-
publicBaseUrl: OVERRIDE,
|
|
594
|
-
});
|
|
616
|
+
// Mint still through the gateway CLI; the link advertises the override.
|
|
617
|
+
expect(mintCalls[0]?.args.at(-1)).toBe(OVERRIDE);
|
|
595
618
|
expect(JSON.parse(logs.join("\n")).pairUrl).toBe(
|
|
596
619
|
`${OVERRIDE}/assistant/pair#device_code=device-code`,
|
|
597
620
|
);
|
|
@@ -1069,35 +1092,8 @@ describe("pair command", () => {
|
|
|
1069
1092
|
|
|
1070
1093
|
const calls: string[] = [];
|
|
1071
1094
|
const origFetch = globalThis.fetch;
|
|
1072
|
-
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL
|
|
1073
|
-
|
|
1074
|
-
calls.push(url);
|
|
1075
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1076
|
-
// The minted challenge must advertise the entry's recorded URL.
|
|
1077
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
1078
|
-
publicBaseUrl: "https://saved.example.ts.net",
|
|
1079
|
-
});
|
|
1080
|
-
return new Response(
|
|
1081
|
-
JSON.stringify({
|
|
1082
|
-
deviceCode: "device-code",
|
|
1083
|
-
userCode: "ABCD-EFGH",
|
|
1084
|
-
verificationUri: "https://saved.example.ts.net/assistant/pair",
|
|
1085
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1086
|
-
expiresInSeconds: 600,
|
|
1087
|
-
}),
|
|
1088
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1092
|
-
return new Response(
|
|
1093
|
-
JSON.stringify({
|
|
1094
|
-
status: "approved",
|
|
1095
|
-
verificationUri: "https://saved.example.ts.net/assistant/pair",
|
|
1096
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1097
|
-
}),
|
|
1098
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1095
|
+
globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
|
|
1096
|
+
calls.push(String(input));
|
|
1101
1097
|
return new Response("not found", { status: 404 });
|
|
1102
1098
|
});
|
|
1103
1099
|
|
|
@@ -1116,7 +1112,9 @@ describe("pair command", () => {
|
|
|
1116
1112
|
globalThis.fetch = origFetch;
|
|
1117
1113
|
}
|
|
1118
1114
|
|
|
1119
|
-
|
|
1115
|
+
// The minted challenge must advertise the entry's recorded URL.
|
|
1116
|
+
expect(mintCalls[0]?.args.at(-1)).toBe("https://saved.example.ts.net");
|
|
1117
|
+
expect(calls).toHaveLength(0);
|
|
1120
1118
|
const output = logs.join("\n");
|
|
1121
1119
|
expect(output).toContain(
|
|
1122
1120
|
"Using saved ingress URL https://saved.example.ts.net",
|
|
@@ -1130,33 +1128,7 @@ describe("pair command", () => {
|
|
|
1130
1128
|
writeLockfileWithIngress("https://saved.example.ts.net");
|
|
1131
1129
|
|
|
1132
1130
|
const origFetch = globalThis.fetch;
|
|
1133
|
-
globalThis.fetch = withPreconnect(async (
|
|
1134
|
-
const url = String(input);
|
|
1135
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1136
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
1137
|
-
publicBaseUrl: "https://explicit.example.ts.net",
|
|
1138
|
-
});
|
|
1139
|
-
return new Response(
|
|
1140
|
-
JSON.stringify({
|
|
1141
|
-
deviceCode: "device-code",
|
|
1142
|
-
userCode: "ABCD-EFGH",
|
|
1143
|
-
verificationUri: "https://explicit.example.ts.net/assistant/pair",
|
|
1144
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1145
|
-
expiresInSeconds: 600,
|
|
1146
|
-
}),
|
|
1147
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1148
|
-
);
|
|
1149
|
-
}
|
|
1150
|
-
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1151
|
-
return new Response(
|
|
1152
|
-
JSON.stringify({
|
|
1153
|
-
status: "approved",
|
|
1154
|
-
verificationUri: "https://explicit.example.ts.net/assistant/pair",
|
|
1155
|
-
expiresAt: "2026-06-04T00:10:00.000Z",
|
|
1156
|
-
}),
|
|
1157
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1131
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
1160
1132
|
return new Response("not found", { status: 404 });
|
|
1161
1133
|
});
|
|
1162
1134
|
|
|
@@ -1182,6 +1154,8 @@ describe("pair command", () => {
|
|
|
1182
1154
|
}
|
|
1183
1155
|
|
|
1184
1156
|
expect(logs.join("\n")).not.toContain("Using saved ingress URL");
|
|
1157
|
+
// The override reached the mint.
|
|
1158
|
+
expect(mintCalls[0]?.args.at(-1)).toBe("https://explicit.example.ts.net");
|
|
1185
1159
|
});
|
|
1186
1160
|
|
|
1187
1161
|
test("a non-https recorded ingress URL is ignored", async () => {
|
|
@@ -1263,4 +1237,101 @@ describe("pair command", () => {
|
|
|
1263
1237
|
expect(errors.join("\n")).toContain(RUNTIME_URL);
|
|
1264
1238
|
expect(minted).toBe(false);
|
|
1265
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
test("refuses a paired (connect-import) assistant before minting", async () => {
|
|
1242
|
+
writeFileSync(
|
|
1243
|
+
join(testDir, "lockfile.json"),
|
|
1244
|
+
JSON.stringify({
|
|
1245
|
+
assistants: [
|
|
1246
|
+
{
|
|
1247
|
+
assistantId: "pair-test",
|
|
1248
|
+
runtimeUrl: RUNTIME_URL,
|
|
1249
|
+
cloud: "paired",
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
activeAssistant: "pair-test",
|
|
1253
|
+
}),
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
const errors: string[] = [];
|
|
1257
|
+
const errSpy = spyOn(console, "error").mockImplementation(
|
|
1258
|
+
(...a: unknown[]) => {
|
|
1259
|
+
errors.push(a.join(" "));
|
|
1260
|
+
},
|
|
1261
|
+
);
|
|
1262
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1263
|
+
(code?: number | string | undefined): never => {
|
|
1264
|
+
throw new Error(`exit:${code}`);
|
|
1265
|
+
},
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
|
|
1269
|
+
let exited = false;
|
|
1270
|
+
try {
|
|
1271
|
+
await pair();
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
exited = e instanceof Error && e.message === "exit:1";
|
|
1274
|
+
} finally {
|
|
1275
|
+
errSpy.mockRestore();
|
|
1276
|
+
exitSpy.mockRestore();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
expect(exited).toBe(true);
|
|
1280
|
+
expect(errors.join("\n")).toContain("was paired as a remote");
|
|
1281
|
+
// Keyed on the entry's cloud, not on which fields happen to exist.
|
|
1282
|
+
expect(mintCalls).toHaveLength(0);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("a custom (ssh-managed) assistant keeps the loopback HTTP mint", async () => {
|
|
1286
|
+
writeFileSync(
|
|
1287
|
+
join(testDir, "lockfile.json"),
|
|
1288
|
+
JSON.stringify({
|
|
1289
|
+
assistants: [
|
|
1290
|
+
{
|
|
1291
|
+
assistantId: "pair-test",
|
|
1292
|
+
runtimeUrl: LOCAL_URL,
|
|
1293
|
+
cloud: "custom",
|
|
1294
|
+
},
|
|
1295
|
+
],
|
|
1296
|
+
activeAssistant: "pair-test",
|
|
1297
|
+
}),
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
const calls: Array<[string, RequestInit | undefined]> = [];
|
|
1301
|
+
const origFetch = globalThis.fetch;
|
|
1302
|
+
globalThis.fetch = withPreconnect(
|
|
1303
|
+
async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
1304
|
+
const url = String(input);
|
|
1305
|
+
calls.push([url, init]);
|
|
1306
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
|
|
1307
|
+
return challengeResponse(PUBLIC_URL);
|
|
1308
|
+
}
|
|
1309
|
+
if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
|
|
1310
|
+
return approvalResponse(PUBLIC_URL);
|
|
1311
|
+
}
|
|
1312
|
+
return new Response("not found", { status: 404 });
|
|
1313
|
+
},
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
1317
|
+
|
|
1318
|
+
process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
|
|
1319
|
+
try {
|
|
1320
|
+
await pair();
|
|
1321
|
+
} finally {
|
|
1322
|
+
logSpy.mockRestore();
|
|
1323
|
+
globalThis.fetch = origFetch;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Not local/docker: no CLI execution — the mint rides the loopback HTTP
|
|
1327
|
+
// route, keyed on the entry's cloud, not on containerInfo.
|
|
1328
|
+
expect(mintCalls).toHaveLength(0);
|
|
1329
|
+
expect(calls.map((c) => c[0])).toEqual([
|
|
1330
|
+
`${LOCAL_URL}/v1/remote-web/pairing-challenge`,
|
|
1331
|
+
`${LOCAL_URL}/v1/remote-web/pairing-verification`,
|
|
1332
|
+
]);
|
|
1333
|
+
expect(JSON.parse(String(calls[0]?.[1]?.body))).toEqual({
|
|
1334
|
+
publicBaseUrl: PUBLIC_URL,
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1266
1337
|
});
|
|
@@ -258,10 +258,12 @@ describe("cadet ps — platform status line", () => {
|
|
|
258
258
|
await listAllAssistants(false);
|
|
259
259
|
|
|
260
260
|
const output = stdout.join(NL);
|
|
261
|
-
// The tunnel
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(output).toContain("
|
|
261
|
+
// The tunnel and the edge render machine-level — root resources, never
|
|
262
|
+
// children of an assistant row…
|
|
263
|
+
expect(output).toContain("ngrok");
|
|
264
|
+
expect(output).not.toContain("└ ngrok");
|
|
265
|
+
expect(output).toContain("nginx");
|
|
266
|
+
expect(output).toContain("port ");
|
|
265
267
|
// …the type column distinguishes the three process kinds…
|
|
266
268
|
expect(output).toContain("🧑🚀 Assistant");
|
|
267
269
|
expect(output).toContain("🚇 Tunnel");
|
|
@@ -62,7 +62,7 @@ function writeLockfile(entry: AssistantEntry): void {
|
|
|
62
62
|
const lockfileDir = process.env.CADET_LOCKFILE_DIR!;
|
|
63
63
|
mkdirSync(lockfileDir, { recursive: true });
|
|
64
64
|
writeFileSync(
|
|
65
|
-
join(lockfileDir, ".
|
|
65
|
+
join(lockfileDir, "lockfile.json"),
|
|
66
66
|
JSON.stringify(
|
|
67
67
|
{
|
|
68
68
|
assistants: [entry],
|
|
@@ -119,6 +119,10 @@ function installFetchStub(
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
if (url.endsWith("/v1/config/llm/default-provider")) {
|
|
123
|
+
return Response.json({ provider: "anthropic" });
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
if (url.endsWith("/v1/secrets/read")) {
|
|
123
127
|
return new Response(JSON.stringify({ found: false }), {
|
|
124
128
|
status: 200,
|
|
@@ -189,7 +193,9 @@ describe("setup command", () => {
|
|
|
189
193
|
|
|
190
194
|
await setup();
|
|
191
195
|
|
|
192
|
-
expect(fetchCalls[0].url).toBe(
|
|
196
|
+
expect(fetchCalls[0].url).toBe(
|
|
197
|
+
"http://127.0.0.1:3000/v1/config/llm/default-provider",
|
|
198
|
+
);
|
|
193
199
|
expect(fetchCalls[0].headers.get("Authorization")).toBe(
|
|
194
200
|
"Bearer guardian-token",
|
|
195
201
|
);
|
|
@@ -245,7 +251,9 @@ describe("setup command", () => {
|
|
|
245
251
|
// gate instead of burning retry attempts against it.
|
|
246
252
|
expect(fetchCalls[0].url).toBe("http://127.0.0.1:3000/readyz");
|
|
247
253
|
expect(fetchCalls[1].url).toBe("http://127.0.0.1:3000/v1/guardian/init");
|
|
248
|
-
expect(fetchCalls[2].url).toBe(
|
|
254
|
+
expect(fetchCalls[2].url).toBe(
|
|
255
|
+
"http://127.0.0.1:3000/v1/config/llm/default-provider",
|
|
256
|
+
);
|
|
249
257
|
expect(fetchCalls[2].headers.get("Authorization")).toBe(
|
|
250
258
|
"Bearer leased-guardian-token",
|
|
251
259
|
);
|
|
@@ -288,7 +296,9 @@ describe("setup command", () => {
|
|
|
288
296
|
|
|
289
297
|
await setup();
|
|
290
298
|
|
|
291
|
-
expect(fetchCalls[0].url).toBe(
|
|
299
|
+
expect(fetchCalls[0].url).toBe(
|
|
300
|
+
"https://assistant.example/v1/config/llm/default-provider",
|
|
301
|
+
);
|
|
292
302
|
expect(fetchCalls[0].headers.get("Authorization")).toBe(
|
|
293
303
|
"Bearer entry-token",
|
|
294
304
|
);
|
|
@@ -2567,7 +2567,6 @@ describe("platform credential injection", () => {
|
|
|
2567
2567
|
bearerToken: "local-bearer",
|
|
2568
2568
|
assistantApiKey: "api-key-123",
|
|
2569
2569
|
cloudAssistantId: "platform-assistant-1",
|
|
2570
|
-
cloudBaseUrl: "https://cloud.mycadet.ai",
|
|
2571
2570
|
organizationId: "org-1",
|
|
2572
2571
|
userId: "user-1",
|
|
2573
2572
|
webhookSecret: "webhook-secret-123",
|
|
@@ -3008,17 +3007,3 @@ describe("upload signed-URL records source runtime version (not CLI version)", (
|
|
|
3008
3007
|
});
|
|
3009
3008
|
});
|
|
3010
3009
|
|
|
3011
|
-
// ---------------------------------------------------------------------------
|
|
3012
|
-
// Misc: legacy --to deprecation
|
|
3013
|
-
// ---------------------------------------------------------------------------
|
|
3014
|
-
|
|
3015
|
-
describe("misc", () => {
|
|
3016
|
-
test("legacy --to flag shows deprecation message", async () => {
|
|
3017
|
-
setArgv("--from", "source", "--to", "target");
|
|
3018
|
-
|
|
3019
|
-
await expect(teleport()).rejects.toThrow("process.exit:1");
|
|
3020
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
3021
|
-
expect.stringContaining("--to is deprecated"),
|
|
3022
|
-
);
|
|
3023
|
-
});
|
|
3024
|
-
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
|
|
4
|
+
import { findAvailableIngressPort } from "../lib/nginx-ingress";
|
|
5
|
+
import { edgeServedRemote } from "../lib/nginx-ingress";
|
|
6
|
+
import { summarizeStartupFailure } from "../lib/tunnel";
|
|
7
|
+
|
|
8
|
+
function portOf(server: ReturnType<typeof createServer>): number {
|
|
9
|
+
const addr = server.address();
|
|
10
|
+
return typeof addr === "object" && addr !== null ? addr.port : -1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("summarizeStartupFailure", () => {
|
|
14
|
+
test("names an nginx emerg line over generic log noise", () => {
|
|
15
|
+
const log = [
|
|
16
|
+
"Starting ngrok tunnel to localhost:7840...",
|
|
17
|
+
"nginx: [emerg] bind() to 0.0.0.0:7840 failed (98: Address already in use)",
|
|
18
|
+
].join("\n");
|
|
19
|
+
expect(summarizeStartupFailure(log)).toContain("[emerg]");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("names an ngrok error code", () => {
|
|
23
|
+
expect(
|
|
24
|
+
summarizeStartupFailure(
|
|
25
|
+
"agent started\nERR_NGROK_4018: Authentication failed",
|
|
26
|
+
),
|
|
27
|
+
).toContain("ERR_NGROK_4018");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the last matching line wins", () => {
|
|
31
|
+
const log = [
|
|
32
|
+
"nginx: [emerg] bind() to 0.0.0.0:7840 failed (98: Address already in use)",
|
|
33
|
+
"nginx: [emerg] bind() to 0.0.0.0:7841 failed (98: Address already in use)",
|
|
34
|
+
].join("\n");
|
|
35
|
+
expect(summarizeStartupFailure(log)).toContain(":7841");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("falls back to the last non-empty line when nothing error-shaped appears", () => {
|
|
39
|
+
expect(summarizeStartupFailure("first line\nsome plain output")).toBe(
|
|
40
|
+
"some plain output",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("an empty log summarizes to null", () => {
|
|
45
|
+
expect(summarizeStartupFailure("")).toBeNull();
|
|
46
|
+
expect(summarizeStartupFailure("\n \n")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("long lines are capped", () => {
|
|
50
|
+
const long = `nginx: [emerg] ${"x".repeat(400)}`;
|
|
51
|
+
const summary = summarizeStartupFailure(long) ?? "";
|
|
52
|
+
expect(summary.length).toBeLessThanOrEqual(201);
|
|
53
|
+
expect(summary.endsWith("…")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("findAvailableIngressPort", () => {
|
|
58
|
+
test("skips a port a foreign listener already holds", async () => {
|
|
59
|
+
const blocker = createServer();
|
|
60
|
+
await new Promise<void>((resolve) => {
|
|
61
|
+
blocker.listen(0, "127.0.0.1", resolve);
|
|
62
|
+
});
|
|
63
|
+
const occupied = portOf(blocker);
|
|
64
|
+
try {
|
|
65
|
+
const port = await findAvailableIngressPort(occupied);
|
|
66
|
+
expect(port).toBe(occupied + 1);
|
|
67
|
+
} finally {
|
|
68
|
+
await new Promise<void>((resolve) => blocker.close(() => resolve()));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns null when the whole band is busy", async () => {
|
|
73
|
+
const scout = createServer();
|
|
74
|
+
await new Promise<void>((resolve) => scout.listen(0, "127.0.0.1", resolve));
|
|
75
|
+
const base = portOf(scout);
|
|
76
|
+
await new Promise<void>((resolve) => scout.close(() => resolve()));
|
|
77
|
+
// Hold both ports of a 2-port band with fresh listeners.
|
|
78
|
+
const first = createServer();
|
|
79
|
+
const second = createServer();
|
|
80
|
+
await new Promise<void>((resolve) =>
|
|
81
|
+
first.listen(base, "127.0.0.1", resolve),
|
|
82
|
+
);
|
|
83
|
+
await new Promise<void>((resolve) =>
|
|
84
|
+
second.listen(base + 1, "127.0.0.1", resolve),
|
|
85
|
+
);
|
|
86
|
+
try {
|
|
87
|
+
expect(await findAvailableIngressPort(base, 2)).toBeNull();
|
|
88
|
+
} finally {
|
|
89
|
+
await new Promise<void>((resolve) => first.close(() => resolve()));
|
|
90
|
+
await new Promise<void>((resolve) => second.close(() => resolve()));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("edgeServedRemote", () => {
|
|
96
|
+
test("a docker cadet is stamped truthfully", () => {
|
|
97
|
+
expect(edgeServedRemote("docker")).toBe("docker");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a custom cadet keeps its kind", () => {
|
|
101
|
+
expect(edgeServedRemote("custom")).toBe("custom");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("local, paired, cloud, and unknown entries read as custom so the remote-gateway machinery always runs", () => {
|
|
105
|
+
expect(edgeServedRemote("local")).toBe("custom");
|
|
106
|
+
expect(edgeServedRemote("paired")).toBe("custom");
|
|
107
|
+
expect(edgeServedRemote("cloud")).toBe("custom");
|
|
108
|
+
expect(edgeServedRemote(undefined)).toBe("custom");
|
|
109
|
+
});
|
|
110
|
+
});
|