@mycadet/cli 0.12.7 → 0.12.8
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 +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/{show-platform-login.ts → show-cloud-login.ts} +3 -5
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +3 -2
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +2 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +5 -5
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +18 -18
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -196
- package/src/__tests__/tunnel.test.ts +255 -227
- package/src/__tests__/uninstall.test.ts +13 -4
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +258 -41
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +22 -22
- package/src/commands/client.ts +76 -76
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +5 -5
- package/src/commands/hatch.ts +22 -22
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +1 -1
- package/src/commands/ps.ts +188 -79
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +138 -20
- package/src/commands/retire.ts +46 -148
- package/src/commands/rollback.ts +18 -24
- package/src/commands/setup.ts +2 -2
- package/src/commands/teleport.ts +93 -93
- package/src/commands/terminal.ts +157 -17
- package/src/commands/tunnel.ts +80 -122
- package/src/commands/uninstall.ts +55 -7
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +22 -46
- package/src/commands/wake.ts +162 -46
- package/src/index.ts +3 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/job-polling.test.ts +1 -1
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +111 -10
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/guardian-token.ts +36 -4
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +9 -30
- package/src/lib/nginx-ingress.ts +41 -14
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/sync-cloud-assistants.ts +18 -18
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
- package/src/lib/upgrade-lifecycle.ts +3 -17
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- 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/managed-cloud.ts +0 -3
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
test,
|
|
11
11
|
} from "bun:test";
|
|
12
12
|
import * as childProcess from "node:child_process";
|
|
13
|
-
import type { ChildProcess } from "node:child_process";
|
|
14
|
-
import { EventEmitter } from "node:events";
|
|
13
|
+
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
15
14
|
import {
|
|
16
15
|
appendFileSync,
|
|
17
16
|
mkdirSync,
|
|
@@ -24,42 +23,62 @@ import { tmpdir } from "node:os";
|
|
|
24
23
|
import { join } from "node:path";
|
|
25
24
|
|
|
26
25
|
import { TUNNEL_PROVIDERS } from "@mycadet/service-contracts/ingress";
|
|
26
|
+
import { z } from "zod";
|
|
27
27
|
|
|
28
|
-
import * as cloudflareTunnel from "../lib/cloudflare
|
|
29
|
-
import * as
|
|
28
|
+
import * as cloudflareTunnel from "../lib/tunnels/cloudflare.js";
|
|
29
|
+
import * as gatewayNotify from "../lib/tunnels/gateway-notify.js";
|
|
30
|
+
import * as ngrok from "../lib/tunnels/ngrok.js";
|
|
30
31
|
import * as nginxIngress from "../lib/nginx-ingress.js";
|
|
31
|
-
import * as tailscaleTunnel from "../lib/tailscale
|
|
32
|
+
import * as tailscaleTunnel from "../lib/tunnels/tailscale.js";
|
|
32
33
|
import { getLogDir } from "../lib/xdg-log.js";
|
|
33
34
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
35
|
+
import { withPreconnect } from "./helpers/http.js";
|
|
34
36
|
|
|
35
37
|
const realCloudflareTunnel = { ...cloudflareTunnel };
|
|
38
|
+
const realGatewayNotify = { ...gatewayNotify };
|
|
36
39
|
const realNgrok = { ...ngrok };
|
|
37
40
|
const realNginxIngress = { ...nginxIngress };
|
|
38
41
|
const realTailscaleTunnel = { ...tailscaleTunnel };
|
|
39
42
|
const realChildProcess = { ...childProcess };
|
|
40
43
|
|
|
44
|
+
/** What a mocked provider run hands back: a URL and an already-settled exit. */
|
|
45
|
+
const fakeTunnelResult = (publicUrl: string) => ({
|
|
46
|
+
publicUrl,
|
|
47
|
+
waitForExit: Promise.resolve(),
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
const runCloudflareTunnelMock = mock<
|
|
42
51
|
typeof cloudflareTunnel.runCloudflareTunnel
|
|
43
|
-
>(async () =>
|
|
44
|
-
mock.module("../lib/cloudflare
|
|
52
|
+
>(async () => fakeTunnelResult("https://test.example.cloudflare.dev"));
|
|
53
|
+
mock.module("../lib/tunnels/cloudflare.js", () => ({
|
|
45
54
|
...realCloudflareTunnel,
|
|
46
55
|
runCloudflareTunnel: runCloudflareTunnelMock,
|
|
47
56
|
}));
|
|
48
57
|
|
|
49
|
-
const runNgrokTunnelMock = mock<typeof ngrok.runNgrokTunnel>(async () =>
|
|
50
|
-
|
|
58
|
+
const runNgrokTunnelMock = mock<typeof ngrok.runNgrokTunnel>(async () =>
|
|
59
|
+
fakeTunnelResult("https://test.example.ngrok.dev"),
|
|
60
|
+
);
|
|
61
|
+
mock.module("../lib/tunnels/ngrok", () => ({
|
|
51
62
|
...realNgrok,
|
|
52
63
|
runNgrokTunnel: runNgrokTunnelMock,
|
|
53
64
|
}));
|
|
54
65
|
|
|
55
66
|
const runTailscaleTunnelMock = mock<typeof tailscaleTunnel.runTailscaleTunnel>(
|
|
56
|
-
async () =>
|
|
67
|
+
async () => fakeTunnelResult("https://test-machine.tailnet.ts.net"),
|
|
57
68
|
);
|
|
58
|
-
mock.module("../lib/tailscale
|
|
69
|
+
mock.module("../lib/tunnels/tailscale.js", () => ({
|
|
59
70
|
...realTailscaleTunnel,
|
|
60
71
|
runTailscaleTunnel: runTailscaleTunnelMock,
|
|
61
72
|
}));
|
|
62
73
|
|
|
74
|
+
const notifyGatewayOfPublicUrlMock = mock<
|
|
75
|
+
typeof gatewayNotify.notifyGatewayOfPublicUrl
|
|
76
|
+
>(async () => {});
|
|
77
|
+
mock.module("../lib/tunnels/gateway-notify.js", () => ({
|
|
78
|
+
...realGatewayNotify,
|
|
79
|
+
notifyGatewayOfPublicUrl: notifyGatewayOfPublicUrlMock,
|
|
80
|
+
}));
|
|
81
|
+
|
|
63
82
|
const EDGE_PORT = 18080;
|
|
64
83
|
|
|
65
84
|
const ensureTunnelEdgeMock = mock<typeof nginxIngress.ensureTunnelEdge>(
|
|
@@ -74,6 +93,7 @@ const { tunnel } = await import("../commands/tunnel.js");
|
|
|
74
93
|
|
|
75
94
|
const originalArgv = [...process.argv];
|
|
76
95
|
const originalFetch = globalThis.fetch;
|
|
96
|
+
const originalConfigHome = process.env.XDG_CONFIG_HOME;
|
|
77
97
|
const originalLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
78
98
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
79
99
|
const tempDirs: string[] = [];
|
|
@@ -126,10 +146,7 @@ function writeEntryWorkspaceConfig(
|
|
|
126
146
|
entry: AssistantEntry,
|
|
127
147
|
config: Record<string, unknown>,
|
|
128
148
|
): string {
|
|
129
|
-
const workspaceDir = join(
|
|
130
|
-
entry.resources!.instanceDir,
|
|
131
|
-
"workspace",
|
|
132
|
-
);
|
|
149
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
133
150
|
mkdirSync(workspaceDir, { recursive: true });
|
|
134
151
|
writeFileSync(
|
|
135
152
|
join(workspaceDir, "config.json"),
|
|
@@ -158,7 +175,7 @@ function writeLockfile(
|
|
|
158
175
|
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
159
176
|
mkdirSync(lockfileDir, { recursive: true });
|
|
160
177
|
writeFileSync(
|
|
161
|
-
join(lockfileDir, ".
|
|
178
|
+
join(lockfileDir, "lockfile.json"),
|
|
162
179
|
JSON.stringify(
|
|
163
180
|
{
|
|
164
181
|
activeAssistant: activeAssistant ?? entries[0].assistantId,
|
|
@@ -170,6 +187,15 @@ function writeLockfile(
|
|
|
170
187
|
);
|
|
171
188
|
}
|
|
172
189
|
|
|
190
|
+
const IngressTestConfigSchema = z.object({
|
|
191
|
+
ingress: z
|
|
192
|
+
.looseObject({
|
|
193
|
+
publicBaseUrl: z.string().optional(),
|
|
194
|
+
ngrok: z.object({ domain: z.string().optional() }).optional(),
|
|
195
|
+
})
|
|
196
|
+
.optional(),
|
|
197
|
+
});
|
|
198
|
+
|
|
173
199
|
/** Run tunnel() expecting exit(1); returns the joined console.error output. */
|
|
174
200
|
async function runTunnelExpectingExit1(): Promise<{
|
|
175
201
|
exited: boolean;
|
|
@@ -181,17 +207,17 @@ async function runTunnelExpectingExit1(): Promise<{
|
|
|
181
207
|
errors.push(a.join(" "));
|
|
182
208
|
},
|
|
183
209
|
);
|
|
184
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
185
|
-
code?: number
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
211
|
+
(code?: number | string | undefined): never => {
|
|
212
|
+
throw new Error(`exit:${code}`);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
189
215
|
|
|
190
216
|
let exited = false;
|
|
191
217
|
try {
|
|
192
218
|
await tunnel();
|
|
193
219
|
} catch (e) {
|
|
194
|
-
exited =
|
|
220
|
+
exited = e instanceof Error && e.message === "exit:1";
|
|
195
221
|
} finally {
|
|
196
222
|
errSpy.mockRestore();
|
|
197
223
|
exitSpy.mockRestore();
|
|
@@ -217,15 +243,27 @@ describe("tunnel edge targeting", () => {
|
|
|
217
243
|
beforeEach(() => {
|
|
218
244
|
process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
|
|
219
245
|
writeLockfile(makeLocalEntry());
|
|
220
|
-
globalThis.fetch = (async () => {
|
|
246
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
221
247
|
throw new Error("gateway unavailable");
|
|
222
|
-
})
|
|
248
|
+
});
|
|
249
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
250
|
+
// test keeps the saved-domain state hermetic.
|
|
251
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
|
|
252
|
+
tempDirs.push(configHome);
|
|
253
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
223
254
|
runCloudflareTunnelMock.mockReset();
|
|
224
|
-
runCloudflareTunnelMock.mockResolvedValue(
|
|
255
|
+
runCloudflareTunnelMock.mockResolvedValue(
|
|
256
|
+
fakeTunnelResult("https://test.example.cloudflare.dev"),
|
|
257
|
+
);
|
|
225
258
|
runNgrokTunnelMock.mockReset();
|
|
226
|
-
runNgrokTunnelMock.mockResolvedValue(
|
|
259
|
+
runNgrokTunnelMock.mockResolvedValue(
|
|
260
|
+
fakeTunnelResult("https://test.example.ngrok.dev"),
|
|
261
|
+
);
|
|
227
262
|
runTailscaleTunnelMock.mockReset();
|
|
228
|
-
runTailscaleTunnelMock.mockResolvedValue(
|
|
263
|
+
runTailscaleTunnelMock.mockResolvedValue(
|
|
264
|
+
fakeTunnelResult("https://test-machine.tailnet.ts.net"),
|
|
265
|
+
);
|
|
266
|
+
notifyGatewayOfPublicUrlMock.mockReset();
|
|
229
267
|
ensureTunnelEdgeMock.mockReset();
|
|
230
268
|
ensureTunnelEdgeMock.mockResolvedValue({
|
|
231
269
|
port: EDGE_PORT,
|
|
@@ -237,6 +275,12 @@ describe("tunnel edge targeting", () => {
|
|
|
237
275
|
afterEach(() => {
|
|
238
276
|
process.argv = originalArgv;
|
|
239
277
|
globalThis.fetch = originalFetch;
|
|
278
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
279
|
+
if (originalConfigHome === undefined) {
|
|
280
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
281
|
+
} else {
|
|
282
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
283
|
+
}
|
|
240
284
|
if (originalLockfileDir === undefined) {
|
|
241
285
|
delete process.env.CADET_LOCKFILE_DIR;
|
|
242
286
|
} else {
|
|
@@ -253,9 +297,9 @@ describe("tunnel edge targeting", () => {
|
|
|
253
297
|
});
|
|
254
298
|
|
|
255
299
|
afterAll(() => {
|
|
256
|
-
mock.module("../lib/cloudflare
|
|
257
|
-
mock.module("../lib/ngrok", () => realNgrok);
|
|
258
|
-
mock.module("../lib/tailscale
|
|
300
|
+
mock.module("../lib/tunnels/cloudflare.js", () => realCloudflareTunnel);
|
|
301
|
+
mock.module("../lib/tunnels/ngrok", () => realNgrok);
|
|
302
|
+
mock.module("../lib/tunnels/tailscale.js", () => realTailscaleTunnel);
|
|
259
303
|
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
260
304
|
});
|
|
261
305
|
|
|
@@ -267,10 +311,7 @@ describe("tunnel edge targeting", () => {
|
|
|
267
311
|
|
|
268
312
|
const logs = await runTunnelCapturingLogs();
|
|
269
313
|
|
|
270
|
-
const workspaceDir = join(
|
|
271
|
-
entry.resources!.instanceDir,
|
|
272
|
-
"workspace",
|
|
273
|
-
);
|
|
314
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
274
315
|
expect(ensureTunnelEdgeMock).toHaveBeenCalledWith({
|
|
275
316
|
assistantId: "assistant-1",
|
|
276
317
|
workspaceDir,
|
|
@@ -286,6 +327,20 @@ describe("tunnel edge targeting", () => {
|
|
|
286
327
|
expect(logs).toContain("serves remote web + webhooks");
|
|
287
328
|
});
|
|
288
329
|
|
|
330
|
+
test("the tunnel level notifies the gateway of the provider's public URL", async () => {
|
|
331
|
+
writeLockfile(makeLocalEntry());
|
|
332
|
+
process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
|
|
333
|
+
|
|
334
|
+
await runTunnelCapturingLogs();
|
|
335
|
+
|
|
336
|
+
// The provider hands its URL back; the notification is the tunnel
|
|
337
|
+
// level's job, so it holds no matter which provider ran.
|
|
338
|
+
expect(notifyGatewayOfPublicUrlMock).toHaveBeenCalledWith(
|
|
339
|
+
"https://test.example.ngrok.dev",
|
|
340
|
+
"assistant-1",
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
289
344
|
test("an active cloud assistant falls back to the sole local entry with a note", async () => {
|
|
290
345
|
const cloud = makeCloudEntry();
|
|
291
346
|
const local = makeLocalEntry();
|
|
@@ -294,10 +349,7 @@ describe("tunnel edge targeting", () => {
|
|
|
294
349
|
|
|
295
350
|
const logs = await runTunnelCapturingLogs();
|
|
296
351
|
|
|
297
|
-
const workspaceDir = join(
|
|
298
|
-
local.resources!.instanceDir,
|
|
299
|
-
"workspace",
|
|
300
|
-
);
|
|
352
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
301
353
|
expect(logs).toContain(
|
|
302
354
|
"Assistant 'cloud-1' runs on Cadet Cloud and needs no tunnel. " +
|
|
303
355
|
"Tunneling the local assistant 'assistant-1' instead.",
|
|
@@ -419,14 +471,7 @@ describe("tunnel edge targeting", () => {
|
|
|
419
471
|
test("a positional cloud assistant name errors without auto-fallback", async () => {
|
|
420
472
|
const local = makeLocalEntry();
|
|
421
473
|
writeLockfile([makeCloudEntry(), local], local.assistantId);
|
|
422
|
-
process.argv = [
|
|
423
|
-
"bun",
|
|
424
|
-
"cadet",
|
|
425
|
-
"tunnel",
|
|
426
|
-
"cloud-1",
|
|
427
|
-
"--provider",
|
|
428
|
-
"ngrok",
|
|
429
|
-
];
|
|
474
|
+
process.argv = ["bun", "cadet", "tunnel", "cloud-1", "--provider", "ngrok"];
|
|
430
475
|
|
|
431
476
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
432
477
|
|
|
@@ -449,10 +494,7 @@ describe("tunnel edge targeting", () => {
|
|
|
449
494
|
|
|
450
495
|
await runTunnelCapturingLogs();
|
|
451
496
|
|
|
452
|
-
const workspaceDir = join(
|
|
453
|
-
local.resources!.instanceDir,
|
|
454
|
-
"workspace",
|
|
455
|
-
);
|
|
497
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
456
498
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
457
499
|
port: EDGE_PORT,
|
|
458
500
|
assistantId: "assistant-1",
|
|
@@ -476,10 +518,7 @@ describe("tunnel edge targeting", () => {
|
|
|
476
518
|
|
|
477
519
|
await runTunnelCapturingLogs();
|
|
478
520
|
|
|
479
|
-
const workspaceDir = join(
|
|
480
|
-
local.resources!.instanceDir,
|
|
481
|
-
"workspace",
|
|
482
|
-
);
|
|
521
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
483
522
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
484
523
|
port: EDGE_PORT,
|
|
485
524
|
assistantId: "assistant-1",
|
|
@@ -491,14 +530,7 @@ describe("tunnel edge targeting", () => {
|
|
|
491
530
|
const workspaceDir = useTempDefaultWorkspaceDir();
|
|
492
531
|
const local = makeLocalEntry();
|
|
493
532
|
writeLockfile([local, makeAppleContainerEntry()], local.assistantId);
|
|
494
|
-
process.argv = [
|
|
495
|
-
"bun",
|
|
496
|
-
"cadet",
|
|
497
|
-
"tunnel",
|
|
498
|
-
"apple-1",
|
|
499
|
-
"--provider",
|
|
500
|
-
"ngrok",
|
|
501
|
-
];
|
|
533
|
+
process.argv = ["bun", "cadet", "tunnel", "apple-1", "--provider", "ngrok"];
|
|
502
534
|
|
|
503
535
|
await runTunnelCapturingLogs();
|
|
504
536
|
|
|
@@ -644,46 +676,40 @@ describe("tunnel edge targeting", () => {
|
|
|
644
676
|
expect(logs).toContain("serves webhooks only");
|
|
645
677
|
});
|
|
646
678
|
|
|
647
|
-
test("a bare invocation
|
|
648
|
-
|
|
679
|
+
test("a bare invocation tunnels the default provider", async () => {
|
|
680
|
+
const entry = makeLocalEntry();
|
|
681
|
+
writeLockfile(entry);
|
|
649
682
|
process.argv = ["bun", "cadet", "tunnel"];
|
|
650
683
|
|
|
651
|
-
const
|
|
684
|
+
const logs = await runTunnelCapturingLogs();
|
|
652
685
|
|
|
653
|
-
expect(
|
|
654
|
-
expect(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
expect(ensureTunnelEdgeMock).
|
|
686
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
687
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
688
|
+
port: EDGE_PORT,
|
|
689
|
+
assistantId: "assistant-1",
|
|
690
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
691
|
+
});
|
|
692
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
660
693
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
661
|
-
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
662
694
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
663
695
|
});
|
|
664
696
|
|
|
665
|
-
test("a named assistant without --provider
|
|
666
|
-
|
|
697
|
+
test("a named assistant without --provider tunnels it with the default provider", async () => {
|
|
698
|
+
const entry = makeLocalEntry();
|
|
699
|
+
entry.name = "Ada";
|
|
700
|
+
writeLockfile(entry);
|
|
667
701
|
process.argv = ["bun", "cadet", "tunnel", "Ada"];
|
|
668
702
|
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
expect(exited).toBe(true);
|
|
672
|
-
expect(errors).toContain("--provider is required");
|
|
673
|
-
// The suggestions keep the assistant that was asked for, so pasting one
|
|
674
|
-
// cannot tunnel the active assistant instead.
|
|
675
|
-
expect(errors).toContain("cadet tunnel Ada --provider ngrok");
|
|
676
|
-
expect(errors).toContain("cadet tunnel Ada --provider tailscale");
|
|
677
|
-
expect(ensureTunnelEdgeMock).not.toHaveBeenCalled();
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
test("a shell-sensitive assistant name is quoted in the suggestions", async () => {
|
|
681
|
-
writeLockfile(makeLocalEntry());
|
|
682
|
-
process.argv = ["bun", "cadet", "tunnel", "Bob&Alice"];
|
|
683
|
-
|
|
684
|
-
const { errors } = await runTunnelExpectingExit1();
|
|
703
|
+
const logs = await runTunnelCapturingLogs();
|
|
685
704
|
|
|
686
|
-
expect(
|
|
705
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
706
|
+
// The assistant that was asked for is the one tunneled.
|
|
707
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
708
|
+
port: EDGE_PORT,
|
|
709
|
+
assistantId: entry.assistantId,
|
|
710
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
711
|
+
});
|
|
712
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
687
713
|
});
|
|
688
714
|
|
|
689
715
|
test("--provider tailscale keeps the webhook callback base and says so", async () => {
|
|
@@ -838,7 +864,7 @@ describe("tunnel edge targeting", () => {
|
|
|
838
864
|
|
|
839
865
|
expect(exited).toBe(true);
|
|
840
866
|
expect(errors).toContain(
|
|
841
|
-
"--domain is
|
|
867
|
+
"--domain is not supported with --provider cloudflare",
|
|
842
868
|
);
|
|
843
869
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
844
870
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -847,15 +873,9 @@ describe("tunnel edge targeting", () => {
|
|
|
847
873
|
test("--clear-domain drops the saved domain and runs domainless", async () => {
|
|
848
874
|
const entry = makeLocalEntry();
|
|
849
875
|
writeLockfile(entry);
|
|
850
|
-
const workspaceDir = join(
|
|
851
|
-
entry.resources!.instanceDir,
|
|
852
|
-
"workspace",
|
|
853
|
-
);
|
|
876
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
854
877
|
mkdirSync(workspaceDir, { recursive: true });
|
|
855
|
-
|
|
856
|
-
join(workspaceDir, "config.json"),
|
|
857
|
-
JSON.stringify({ ingress: { ngrok: { domain: "dead.ngrok.app" } } }),
|
|
858
|
-
);
|
|
878
|
+
realNgrok.saveNgrokDomain("dead.ngrok.app");
|
|
859
879
|
process.argv = [
|
|
860
880
|
"bun",
|
|
861
881
|
"cadet",
|
|
@@ -867,11 +887,9 @@ describe("tunnel edge targeting", () => {
|
|
|
867
887
|
|
|
868
888
|
const logs = await runTunnelCapturingLogs();
|
|
869
889
|
|
|
870
|
-
expect(logs).toContain("Cleared the saved
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
) as { ingress?: { ngrok?: { domain?: string } } };
|
|
874
|
-
expect(config.ingress?.ngrok).toBeUndefined();
|
|
890
|
+
expect(logs).toContain("Cleared the saved reserved domain");
|
|
891
|
+
// The saved domain lives in the XDG ngrok config, not the workspace config.
|
|
892
|
+
expect(realNgrok.loadNgrokDomain()).toBeNull();
|
|
875
893
|
// The run proceeds domainless: no `domain` key reaches runNgrokTunnel.
|
|
876
894
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
877
895
|
port: EDGE_PORT,
|
|
@@ -913,7 +931,7 @@ describe("tunnel edge targeting", () => {
|
|
|
913
931
|
|
|
914
932
|
expect(exited).toBe(true);
|
|
915
933
|
expect(errors).toContain(
|
|
916
|
-
"--clear-domain is
|
|
934
|
+
"--clear-domain is not supported with --provider cloudflare",
|
|
917
935
|
);
|
|
918
936
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
919
937
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -945,7 +963,7 @@ describe("tunnel edge targeting", () => {
|
|
|
945
963
|
try {
|
|
946
964
|
await tunnel();
|
|
947
965
|
} catch (e) {
|
|
948
|
-
err = e
|
|
966
|
+
err = e instanceof Error ? e : undefined;
|
|
949
967
|
}
|
|
950
968
|
|
|
951
969
|
expect(err?.message).toContain("is not yet implemented");
|
|
@@ -959,20 +977,26 @@ describe("tunnel edge targeting", () => {
|
|
|
959
977
|
|
|
960
978
|
describe("ngrok --domain spawn args", () => {
|
|
961
979
|
const originalContainerized = process.env.IS_CONTAINERIZED;
|
|
962
|
-
let lastChild:
|
|
963
|
-
|
|
964
|
-
const spawnMock = mock(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
980
|
+
let lastChild: ChildProcess | null = null;
|
|
981
|
+
|
|
982
|
+
const spawnMock = mock(
|
|
983
|
+
(
|
|
984
|
+
_command: string,
|
|
985
|
+
_args: readonly string[],
|
|
986
|
+
_options?: SpawnOptions,
|
|
987
|
+
): ChildProcess => {
|
|
988
|
+
const child = new realChildProcess.ChildProcess();
|
|
989
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
990
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
991
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
992
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
993
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
994
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
995
|
+
const fake = Object.assign(child, { pid: 4242, kill: () => true });
|
|
996
|
+
lastChild = fake;
|
|
997
|
+
return fake;
|
|
998
|
+
},
|
|
999
|
+
);
|
|
976
1000
|
const execFileSyncMock = mock(() => "ngrok version 3.9.0");
|
|
977
1001
|
|
|
978
1002
|
beforeAll(() => {
|
|
@@ -991,11 +1015,22 @@ describe("ngrok --domain spawn args", () => {
|
|
|
991
1015
|
spawnMock.mockClear();
|
|
992
1016
|
lastChild = null;
|
|
993
1017
|
delete process.env.IS_CONTAINERIZED;
|
|
1018
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
1019
|
+
// test keeps the saved-domain state hermetic.
|
|
1020
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-ngrok-xdg-"));
|
|
1021
|
+
tempDirs.push(configHome);
|
|
1022
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
994
1023
|
mockNgrokApiFetch([{ tunnels: [] }]);
|
|
995
1024
|
});
|
|
996
1025
|
|
|
997
1026
|
afterEach(() => {
|
|
998
1027
|
globalThis.fetch = originalFetch;
|
|
1028
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
1029
|
+
if (originalConfigHome === undefined) {
|
|
1030
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
1031
|
+
} else {
|
|
1032
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
1033
|
+
}
|
|
999
1034
|
if (originalContainerized === undefined) {
|
|
1000
1035
|
delete process.env.IS_CONTAINERIZED;
|
|
1001
1036
|
} else {
|
|
@@ -1021,22 +1056,20 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1021
1056
|
/** ngrok local API stub replaying one response per call, last one repeated. */
|
|
1022
1057
|
function mockNgrokApiFetch(responses: { tunnels: StubTunnel[] }[]): void {
|
|
1023
1058
|
let call = 0;
|
|
1024
|
-
globalThis.fetch = (async () => {
|
|
1059
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
1025
1060
|
const body = responses[Math.min(call, responses.length - 1)];
|
|
1026
1061
|
call++;
|
|
1027
1062
|
return new Response(JSON.stringify(body), {
|
|
1028
1063
|
status: 200,
|
|
1029
1064
|
headers: { "Content-Type": "application/json" },
|
|
1030
1065
|
});
|
|
1031
|
-
})
|
|
1066
|
+
});
|
|
1032
1067
|
}
|
|
1033
1068
|
|
|
1034
1069
|
function readIngress(ws: string): Record<string, unknown> {
|
|
1035
|
-
const config =
|
|
1036
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1037
|
-
)
|
|
1038
|
-
ingress?: Record<string, unknown>;
|
|
1039
|
-
};
|
|
1070
|
+
const config = IngressTestConfigSchema.parse(
|
|
1071
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1072
|
+
);
|
|
1040
1073
|
return config.ingress ?? {};
|
|
1041
1074
|
}
|
|
1042
1075
|
|
|
@@ -1048,7 +1081,7 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1048
1081
|
const lockfileDir = mkdtempSync(join(tmpdir(), "cadet-ngrok-lockfile-"));
|
|
1049
1082
|
tempDirs.push(lockfileDir);
|
|
1050
1083
|
writeFileSync(
|
|
1051
|
-
join(lockfileDir, ".
|
|
1084
|
+
join(lockfileDir, "lockfile.json"),
|
|
1052
1085
|
JSON.stringify({
|
|
1053
1086
|
activeAssistant: assistantId,
|
|
1054
1087
|
assistants: [
|
|
@@ -1104,33 +1137,33 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1104
1137
|
}
|
|
1105
1138
|
|
|
1106
1139
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1107
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1108
|
-
string,
|
|
1109
|
-
string[],
|
|
1110
|
-
];
|
|
1140
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1111
1141
|
expect(cmd).toBe("ngrok");
|
|
1112
1142
|
expect(args).toEqual([
|
|
1113
|
-
"
|
|
1114
|
-
"
|
|
1143
|
+
"start",
|
|
1144
|
+
"cadet",
|
|
1145
|
+
`--config=${realNgrok.ngrokConfigPath()}`,
|
|
1115
1146
|
"--log=stdout",
|
|
1116
|
-
"--domain=foo.ngrok.app",
|
|
1117
1147
|
]);
|
|
1148
|
+
// The reserved domain rides the XDG config the agent loads, not a flag.
|
|
1149
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1118
1150
|
|
|
1119
|
-
const config =
|
|
1120
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1121
|
-
)
|
|
1122
|
-
expect(config.ingress
|
|
1123
|
-
expect(config.ingress.ngrok?.domain).toBe("foo.ngrok.app");
|
|
1151
|
+
const config = IngressTestConfigSchema.parse(
|
|
1152
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1153
|
+
);
|
|
1154
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1124
1155
|
});
|
|
1125
1156
|
|
|
1126
1157
|
function mockTunnelListFetch(publicUrl: string, addr: string): void {
|
|
1127
|
-
globalThis.fetch = (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1158
|
+
globalThis.fetch = withPreconnect(
|
|
1159
|
+
async () =>
|
|
1160
|
+
new Response(
|
|
1161
|
+
JSON.stringify({
|
|
1162
|
+
tunnels: [{ public_url: publicUrl, config: { addr } }],
|
|
1163
|
+
}),
|
|
1164
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
1165
|
+
),
|
|
1166
|
+
);
|
|
1134
1167
|
}
|
|
1135
1168
|
|
|
1136
1169
|
test("runNgrokTunnel rejects an existing tunnel that mismatches the requested domain", async () => {
|
|
@@ -1143,11 +1176,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1143
1176
|
errors.push(a.join(" "));
|
|
1144
1177
|
},
|
|
1145
1178
|
);
|
|
1146
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1147
|
-
code?: number
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1179
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1180
|
+
(code?: number | string | undefined): never => {
|
|
1181
|
+
throw new Error(`exit:${code}`);
|
|
1182
|
+
},
|
|
1183
|
+
);
|
|
1151
1184
|
|
|
1152
1185
|
try {
|
|
1153
1186
|
await expect(
|
|
@@ -1170,9 +1203,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1170
1203
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1171
1204
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1172
1205
|
|
|
1173
|
-
const config =
|
|
1174
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1175
|
-
)
|
|
1206
|
+
const config = IngressTestConfigSchema.parse(
|
|
1207
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1208
|
+
);
|
|
1176
1209
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1177
1210
|
expect(config.ingress?.ngrok).toBeUndefined();
|
|
1178
1211
|
});
|
|
@@ -1189,6 +1222,8 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1189
1222
|
// The adopt path blocks until SIGINT/SIGTERM; pump SIGINT until the
|
|
1190
1223
|
// listener is registered and the promise settles. Earlier tests leak
|
|
1191
1224
|
// SIGINT handlers that call process.exit, so no-op it while pumping.
|
|
1225
|
+
// LOAD-BEARING cast: a no-op exit can't honestly be typed `: never` — it
|
|
1226
|
+
// must return so leaked SIGINT handlers can't crash the pump below.
|
|
1192
1227
|
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1193
1228
|
(() => undefined) as never,
|
|
1194
1229
|
);
|
|
@@ -1201,18 +1236,16 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1201
1236
|
}
|
|
1202
1237
|
|
|
1203
1238
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1204
|
-
const config =
|
|
1205
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1206
|
-
)
|
|
1207
|
-
expect(config.ingress
|
|
1208
|
-
expect(
|
|
1239
|
+
const config = IngressTestConfigSchema.parse(
|
|
1240
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1241
|
+
);
|
|
1242
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1243
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1209
1244
|
});
|
|
1210
1245
|
|
|
1211
1246
|
test("maybeStartNgrokTunnel rejects a mismatched existing tunnel without adopting or spawning", async () => {
|
|
1212
|
-
const ws = makeWorkspace({
|
|
1213
|
-
|
|
1214
|
-
ingress: { ngrok: { domain: "foo.ngrok.app" } },
|
|
1215
|
-
});
|
|
1247
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1248
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1216
1249
|
mockTunnelListFetch("https://other.ngrok-free.app", "localhost:7830");
|
|
1217
1250
|
|
|
1218
1251
|
const warnings: string[] = [];
|
|
@@ -1240,13 +1273,13 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1240
1273
|
"cadet tunnel --provider ngrok --domain foo.ngrok.app",
|
|
1241
1274
|
);
|
|
1242
1275
|
|
|
1243
|
-
const config =
|
|
1244
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1245
|
-
)
|
|
1276
|
+
const config = IngressTestConfigSchema.parse(
|
|
1277
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1278
|
+
);
|
|
1246
1279
|
// The mismatched tunnel is not blessed in config…
|
|
1247
|
-
expect(config.ingress
|
|
1280
|
+
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1248
1281
|
// …and the reserved domain stays saved as standing intent.
|
|
1249
|
-
expect(
|
|
1282
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1250
1283
|
});
|
|
1251
1284
|
|
|
1252
1285
|
test("an adopted automatic tunnel records the assistant it fronts", async () => {
|
|
@@ -1329,9 +1362,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1329
1362
|
expect(combined).toContain("not 18080");
|
|
1330
1363
|
expect(combined).toContain("cadet tunnel --provider ngrok");
|
|
1331
1364
|
|
|
1332
|
-
const config =
|
|
1333
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1334
|
-
)
|
|
1365
|
+
const config = IngressTestConfigSchema.parse(
|
|
1366
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1367
|
+
);
|
|
1335
1368
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1336
1369
|
});
|
|
1337
1370
|
|
|
@@ -1345,11 +1378,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1345
1378
|
errors.push(a.join(" "));
|
|
1346
1379
|
},
|
|
1347
1380
|
);
|
|
1348
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1349
|
-
code?: number
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1381
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1382
|
+
(code?: number | string | undefined): never => {
|
|
1383
|
+
throw new Error(`exit:${code}`);
|
|
1384
|
+
},
|
|
1385
|
+
);
|
|
1353
1386
|
|
|
1354
1387
|
try {
|
|
1355
1388
|
await expect(
|
|
@@ -1369,17 +1402,15 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1369
1402
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1370
1403
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1371
1404
|
|
|
1372
|
-
const config =
|
|
1373
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1374
|
-
)
|
|
1405
|
+
const config = IngressTestConfigSchema.parse(
|
|
1406
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1407
|
+
);
|
|
1375
1408
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1376
1409
|
});
|
|
1377
1410
|
|
|
1378
1411
|
test("maybeStartNgrokTunnel passes the saved domain to the spawn args", async () => {
|
|
1379
|
-
const ws = makeWorkspace({
|
|
1380
|
-
|
|
1381
|
-
ingress: { ngrok: { domain: "foo.ngrok.app" } },
|
|
1382
|
-
});
|
|
1412
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1413
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1383
1414
|
mockNgrokApiFetch([
|
|
1384
1415
|
{ tunnels: [] },
|
|
1385
1416
|
{
|
|
@@ -1397,28 +1428,24 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1397
1428
|
|
|
1398
1429
|
expect(child).not.toBeNull();
|
|
1399
1430
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1400
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1401
|
-
string,
|
|
1402
|
-
string[],
|
|
1403
|
-
];
|
|
1431
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1404
1432
|
expect(cmd).toBe("ngrok");
|
|
1405
1433
|
expect(args).toEqual([
|
|
1406
|
-
"
|
|
1407
|
-
"
|
|
1434
|
+
"start",
|
|
1435
|
+
"cadet",
|
|
1436
|
+
`--config=${realNgrok.ngrokConfigPath()}`,
|
|
1408
1437
|
"--log=stdout",
|
|
1409
|
-
"--domain=foo.ngrok.app",
|
|
1410
1438
|
]);
|
|
1411
1439
|
|
|
1412
|
-
const config =
|
|
1413
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1414
|
-
)
|
|
1415
|
-
expect(config.ingress
|
|
1440
|
+
const config = IngressTestConfigSchema.parse(
|
|
1441
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1442
|
+
);
|
|
1443
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1416
1444
|
});
|
|
1417
1445
|
|
|
1418
1446
|
test("runNgrokTunnel without --domain reuses the saved domain and does not delete it", async () => {
|
|
1419
|
-
const ws = makeWorkspace({
|
|
1420
|
-
|
|
1421
|
-
});
|
|
1447
|
+
const ws = makeWorkspace({});
|
|
1448
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1422
1449
|
mockNgrokApiFetch([
|
|
1423
1450
|
{ tunnels: [] },
|
|
1424
1451
|
{
|
|
@@ -1440,19 +1467,19 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1440
1467
|
}
|
|
1441
1468
|
|
|
1442
1469
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1443
|
-
const [, args] = spawnMock.mock.calls[0]
|
|
1470
|
+
const [, args] = spawnMock.mock.calls[0];
|
|
1444
1471
|
expect(args).toEqual([
|
|
1445
|
-
"
|
|
1446
|
-
"
|
|
1472
|
+
"start",
|
|
1473
|
+
"cadet",
|
|
1474
|
+
`--config=${realNgrok.ngrokConfigPath()}`,
|
|
1447
1475
|
"--log=stdout",
|
|
1448
|
-
"--domain=foo.ngrok.app",
|
|
1449
1476
|
]);
|
|
1450
1477
|
|
|
1451
|
-
const config =
|
|
1452
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1453
|
-
)
|
|
1454
|
-
expect(config.ingress
|
|
1455
|
-
expect(
|
|
1478
|
+
const config = IngressTestConfigSchema.parse(
|
|
1479
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1480
|
+
);
|
|
1481
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1482
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1456
1483
|
});
|
|
1457
1484
|
});
|
|
1458
1485
|
|
|
@@ -1460,18 +1487,23 @@ describe("tunnel --detach", () => {
|
|
|
1460
1487
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
1461
1488
|
|
|
1462
1489
|
function makeFakeChild(pid: number): ChildProcess {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}) as unknown as ChildProcess;
|
|
1490
|
+
const child = new realChildProcess.ChildProcess();
|
|
1491
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
1492
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
1493
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
1494
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
1495
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
1496
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
1497
|
+
return Object.assign(child, { pid, kill: () => true });
|
|
1472
1498
|
}
|
|
1473
1499
|
|
|
1474
|
-
const spawnMock = mock(
|
|
1500
|
+
const spawnMock = mock(
|
|
1501
|
+
(
|
|
1502
|
+
_command: string,
|
|
1503
|
+
_args: readonly string[],
|
|
1504
|
+
_options?: SpawnOptions,
|
|
1505
|
+
): ChildProcess => makeFakeChild(9999),
|
|
1506
|
+
);
|
|
1475
1507
|
|
|
1476
1508
|
// spawnDetachedTunnel() scopes the log filename to its own (parent) pid,
|
|
1477
1509
|
// which in-process is this test runner's pid.
|
|
@@ -1532,17 +1564,13 @@ describe("tunnel --detach", () => {
|
|
|
1532
1564
|
const logs = await runTunnelCapturingLogs();
|
|
1533
1565
|
|
|
1534
1566
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1535
|
-
const [, args, opts] = spawnMock.mock.calls[0]
|
|
1536
|
-
string,
|
|
1537
|
-
string[],
|
|
1538
|
-
Record<string, unknown>,
|
|
1539
|
-
];
|
|
1567
|
+
const [, args, opts] = spawnMock.mock.calls[0];
|
|
1540
1568
|
expect(args).toContain("tunnel");
|
|
1541
1569
|
expect(args).toContain("--provider");
|
|
1542
1570
|
expect(args).toContain("ngrok");
|
|
1543
1571
|
expect(args).not.toContain("--detach");
|
|
1544
1572
|
expect(args).not.toContain("-d");
|
|
1545
|
-
expect(opts
|
|
1573
|
+
expect(opts?.detached).toBe(true);
|
|
1546
1574
|
|
|
1547
1575
|
expect(logs).toContain("Tunnel established: https://foo.ngrok.app");
|
|
1548
1576
|
expect(logs).toContain("Running in background (pid 9999)");
|
|
@@ -1566,7 +1594,7 @@ describe("tunnel --detach", () => {
|
|
|
1566
1594
|
|
|
1567
1595
|
await runTunnelCapturingLogs();
|
|
1568
1596
|
|
|
1569
|
-
const [, args] = spawnMock.mock.calls[0]
|
|
1597
|
+
const [, args] = spawnMock.mock.calls[0];
|
|
1570
1598
|
expect(args).not.toContain("-d");
|
|
1571
1599
|
expect(args).not.toContain("--detach");
|
|
1572
1600
|
});
|
|
@@ -1584,7 +1612,7 @@ describe("tunnel --detach", () => {
|
|
|
1584
1612
|
const child = makeFakeChild(4242);
|
|
1585
1613
|
// Listeners attach synchronously right after spawn() returns; defer the
|
|
1586
1614
|
// exit so they're in place before it fires.
|
|
1587
|
-
setTimeout(() =>
|
|
1615
|
+
setTimeout(() => child.emit("exit", 1), 0);
|
|
1588
1616
|
return child;
|
|
1589
1617
|
});
|
|
1590
1618
|
|