@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
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-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +6 -4
- 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 +1 -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} +28 -18
- 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__/ensure-binary.test.ts +182 -0
- 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 +15 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- 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} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- 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 +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -78
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- 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} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- 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 +46 -34
- package/src/lib/nginx-ingress.ts +40 -15
- package/src/lib/pair.ts +1 -1
- 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/step-runner.ts +6 -1
- package/src/lib/sync-cloud-assistants.ts +19 -19
- 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} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- 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/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- 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,56 +23,81 @@ 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>(
|
|
66
85
|
async () => ({ port: EDGE_PORT, started: true, includesWebApp: true }),
|
|
67
86
|
);
|
|
87
|
+
const getNginxVersionMock = mock<typeof nginxIngress.getNginxVersion>(
|
|
88
|
+
() => "nginx version (test)",
|
|
89
|
+
);
|
|
68
90
|
mock.module("../lib/nginx-ingress.js", () => ({
|
|
69
91
|
...realNginxIngress,
|
|
70
92
|
ensureTunnelEdge: ensureTunnelEdgeMock,
|
|
93
|
+
getNginxVersion: getNginxVersionMock,
|
|
71
94
|
}));
|
|
72
95
|
|
|
73
96
|
const { tunnel } = await import("../commands/tunnel.js");
|
|
74
97
|
|
|
75
98
|
const originalArgv = [...process.argv];
|
|
76
99
|
const originalFetch = globalThis.fetch;
|
|
100
|
+
const originalConfigHome = process.env.XDG_CONFIG_HOME;
|
|
77
101
|
const originalLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
78
102
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
79
103
|
const tempDirs: string[] = [];
|
|
@@ -126,10 +150,7 @@ function writeEntryWorkspaceConfig(
|
|
|
126
150
|
entry: AssistantEntry,
|
|
127
151
|
config: Record<string, unknown>,
|
|
128
152
|
): string {
|
|
129
|
-
const workspaceDir = join(
|
|
130
|
-
entry.resources!.instanceDir,
|
|
131
|
-
"workspace",
|
|
132
|
-
);
|
|
153
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
133
154
|
mkdirSync(workspaceDir, { recursive: true });
|
|
134
155
|
writeFileSync(
|
|
135
156
|
join(workspaceDir, "config.json"),
|
|
@@ -158,7 +179,7 @@ function writeLockfile(
|
|
|
158
179
|
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
159
180
|
mkdirSync(lockfileDir, { recursive: true });
|
|
160
181
|
writeFileSync(
|
|
161
|
-
join(lockfileDir, ".
|
|
182
|
+
join(lockfileDir, "lockfile.json"),
|
|
162
183
|
JSON.stringify(
|
|
163
184
|
{
|
|
164
185
|
activeAssistant: activeAssistant ?? entries[0].assistantId,
|
|
@@ -170,6 +191,15 @@ function writeLockfile(
|
|
|
170
191
|
);
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
const IngressTestConfigSchema = z.object({
|
|
195
|
+
ingress: z
|
|
196
|
+
.looseObject({
|
|
197
|
+
publicBaseUrl: z.string().optional(),
|
|
198
|
+
ngrok: z.object({ domain: z.string().optional() }).optional(),
|
|
199
|
+
})
|
|
200
|
+
.optional(),
|
|
201
|
+
});
|
|
202
|
+
|
|
173
203
|
/** Run tunnel() expecting exit(1); returns the joined console.error output. */
|
|
174
204
|
async function runTunnelExpectingExit1(): Promise<{
|
|
175
205
|
exited: boolean;
|
|
@@ -181,17 +211,17 @@ async function runTunnelExpectingExit1(): Promise<{
|
|
|
181
211
|
errors.push(a.join(" "));
|
|
182
212
|
},
|
|
183
213
|
);
|
|
184
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
185
|
-
code?: number
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
214
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
215
|
+
(code?: number | string | undefined): never => {
|
|
216
|
+
throw new Error(`exit:${code}`);
|
|
217
|
+
},
|
|
218
|
+
);
|
|
189
219
|
|
|
190
220
|
let exited = false;
|
|
191
221
|
try {
|
|
192
222
|
await tunnel();
|
|
193
223
|
} catch (e) {
|
|
194
|
-
exited =
|
|
224
|
+
exited = e instanceof Error && e.message === "exit:1";
|
|
195
225
|
} finally {
|
|
196
226
|
errSpy.mockRestore();
|
|
197
227
|
exitSpy.mockRestore();
|
|
@@ -215,17 +245,36 @@ async function runTunnelCapturingLogs(): Promise<string> {
|
|
|
215
245
|
|
|
216
246
|
describe("tunnel edge targeting", () => {
|
|
217
247
|
beforeEach(() => {
|
|
218
|
-
process.argv = [
|
|
248
|
+
process.argv = [
|
|
249
|
+
"bun",
|
|
250
|
+
"cadet",
|
|
251
|
+
"tunnel",
|
|
252
|
+
"--provider",
|
|
253
|
+
"tailscale",
|
|
254
|
+
"--foreground",
|
|
255
|
+
];
|
|
219
256
|
writeLockfile(makeLocalEntry());
|
|
220
|
-
globalThis.fetch = (async () => {
|
|
257
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
221
258
|
throw new Error("gateway unavailable");
|
|
222
|
-
})
|
|
259
|
+
});
|
|
260
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
261
|
+
// test keeps the saved-domain state hermetic.
|
|
262
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
|
|
263
|
+
tempDirs.push(configHome);
|
|
264
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
223
265
|
runCloudflareTunnelMock.mockReset();
|
|
224
|
-
runCloudflareTunnelMock.mockResolvedValue(
|
|
266
|
+
runCloudflareTunnelMock.mockResolvedValue(
|
|
267
|
+
fakeTunnelResult("https://test.example.cloudflare.dev"),
|
|
268
|
+
);
|
|
225
269
|
runNgrokTunnelMock.mockReset();
|
|
226
|
-
runNgrokTunnelMock.mockResolvedValue(
|
|
270
|
+
runNgrokTunnelMock.mockResolvedValue(
|
|
271
|
+
fakeTunnelResult("https://test.example.ngrok.dev"),
|
|
272
|
+
);
|
|
227
273
|
runTailscaleTunnelMock.mockReset();
|
|
228
|
-
runTailscaleTunnelMock.mockResolvedValue(
|
|
274
|
+
runTailscaleTunnelMock.mockResolvedValue(
|
|
275
|
+
fakeTunnelResult("https://test-machine.tailnet.ts.net"),
|
|
276
|
+
);
|
|
277
|
+
notifyGatewayOfPublicUrlMock.mockReset();
|
|
229
278
|
ensureTunnelEdgeMock.mockReset();
|
|
230
279
|
ensureTunnelEdgeMock.mockResolvedValue({
|
|
231
280
|
port: EDGE_PORT,
|
|
@@ -237,6 +286,12 @@ describe("tunnel edge targeting", () => {
|
|
|
237
286
|
afterEach(() => {
|
|
238
287
|
process.argv = originalArgv;
|
|
239
288
|
globalThis.fetch = originalFetch;
|
|
289
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
290
|
+
if (originalConfigHome === undefined) {
|
|
291
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
292
|
+
} else {
|
|
293
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
294
|
+
}
|
|
240
295
|
if (originalLockfileDir === undefined) {
|
|
241
296
|
delete process.env.CADET_LOCKFILE_DIR;
|
|
242
297
|
} else {
|
|
@@ -253,9 +308,9 @@ describe("tunnel edge targeting", () => {
|
|
|
253
308
|
});
|
|
254
309
|
|
|
255
310
|
afterAll(() => {
|
|
256
|
-
mock.module("../lib/cloudflare
|
|
257
|
-
mock.module("../lib/ngrok", () => realNgrok);
|
|
258
|
-
mock.module("../lib/tailscale
|
|
311
|
+
mock.module("../lib/tunnels/cloudflare.js", () => realCloudflareTunnel);
|
|
312
|
+
mock.module("../lib/tunnels/ngrok", () => realNgrok);
|
|
313
|
+
mock.module("../lib/tunnels/tailscale.js", () => realTailscaleTunnel);
|
|
259
314
|
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
260
315
|
});
|
|
261
316
|
|
|
@@ -263,14 +318,18 @@ describe("tunnel edge targeting", () => {
|
|
|
263
318
|
const entry = makeLocalEntry();
|
|
264
319
|
entry.runtimeUrl = "https://stale-tunnel.ngrok-free.dev";
|
|
265
320
|
writeLockfile(entry);
|
|
266
|
-
process.argv = [
|
|
321
|
+
process.argv = [
|
|
322
|
+
"bun",
|
|
323
|
+
"cadet",
|
|
324
|
+
"tunnel",
|
|
325
|
+
"--provider",
|
|
326
|
+
"ngrok",
|
|
327
|
+
"--foreground",
|
|
328
|
+
];
|
|
267
329
|
|
|
268
330
|
const logs = await runTunnelCapturingLogs();
|
|
269
331
|
|
|
270
|
-
const workspaceDir = join(
|
|
271
|
-
entry.resources!.instanceDir,
|
|
272
|
-
"workspace",
|
|
273
|
-
);
|
|
332
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
274
333
|
expect(ensureTunnelEdgeMock).toHaveBeenCalledWith({
|
|
275
334
|
assistantId: "assistant-1",
|
|
276
335
|
workspaceDir,
|
|
@@ -283,21 +342,45 @@ describe("tunnel edge targeting", () => {
|
|
|
283
342
|
});
|
|
284
343
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
285
344
|
expect(logs).toContain(`Started the nginx edge on 127.0.0.1:${EDGE_PORT}`);
|
|
286
|
-
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("the tunnel level notifies the gateway of the provider's public URL", async () => {
|
|
348
|
+
writeLockfile(makeLocalEntry());
|
|
349
|
+
process.argv = [
|
|
350
|
+
"bun",
|
|
351
|
+
"cadet",
|
|
352
|
+
"tunnel",
|
|
353
|
+
"--provider",
|
|
354
|
+
"ngrok",
|
|
355
|
+
"--foreground",
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
await runTunnelCapturingLogs();
|
|
359
|
+
|
|
360
|
+
// The provider hands its URL back; the notification is the tunnel
|
|
361
|
+
// level's job, so it holds no matter which provider ran.
|
|
362
|
+
expect(notifyGatewayOfPublicUrlMock).toHaveBeenCalledWith(
|
|
363
|
+
"https://test.example.ngrok.dev",
|
|
364
|
+
"assistant-1",
|
|
365
|
+
);
|
|
287
366
|
});
|
|
288
367
|
|
|
289
368
|
test("an active cloud assistant falls back to the sole local entry with a note", async () => {
|
|
290
369
|
const cloud = makeCloudEntry();
|
|
291
370
|
const local = makeLocalEntry();
|
|
292
371
|
writeLockfile([cloud, local], cloud.assistantId);
|
|
293
|
-
process.argv = [
|
|
372
|
+
process.argv = [
|
|
373
|
+
"bun",
|
|
374
|
+
"cadet",
|
|
375
|
+
"tunnel",
|
|
376
|
+
"--provider",
|
|
377
|
+
"ngrok",
|
|
378
|
+
"--foreground",
|
|
379
|
+
];
|
|
294
380
|
|
|
295
381
|
const logs = await runTunnelCapturingLogs();
|
|
296
382
|
|
|
297
|
-
const workspaceDir = join(
|
|
298
|
-
local.resources!.instanceDir,
|
|
299
|
-
"workspace",
|
|
300
|
-
);
|
|
383
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
301
384
|
expect(logs).toContain(
|
|
302
385
|
"Assistant 'cloud-1' runs on Cadet Cloud and needs no tunnel. " +
|
|
303
386
|
"Tunneling the local assistant 'assistant-1' instead.",
|
|
@@ -325,6 +408,7 @@ describe("tunnel edge targeting", () => {
|
|
|
325
408
|
"docker-1",
|
|
326
409
|
"--provider",
|
|
327
410
|
"ngrok",
|
|
411
|
+
"--foreground",
|
|
328
412
|
];
|
|
329
413
|
|
|
330
414
|
await runTunnelCapturingLogs();
|
|
@@ -345,7 +429,14 @@ describe("tunnel edge targeting", () => {
|
|
|
345
429
|
const workspaceDir = useTempDefaultWorkspaceDir();
|
|
346
430
|
const docker = makeDockerEntry();
|
|
347
431
|
writeLockfile(docker, docker.assistantId);
|
|
348
|
-
process.argv = [
|
|
432
|
+
process.argv = [
|
|
433
|
+
"bun",
|
|
434
|
+
"cadet",
|
|
435
|
+
"tunnel",
|
|
436
|
+
"--provider",
|
|
437
|
+
"ngrok",
|
|
438
|
+
"--foreground",
|
|
439
|
+
];
|
|
349
440
|
|
|
350
441
|
await runTunnelCapturingLogs();
|
|
351
442
|
|
|
@@ -365,7 +456,14 @@ describe("tunnel edge targeting", () => {
|
|
|
365
456
|
const workspaceDir = useTempDefaultWorkspaceDir();
|
|
366
457
|
const cloud = makeCloudEntry();
|
|
367
458
|
writeLockfile([cloud, makeDockerEntry()], cloud.assistantId);
|
|
368
|
-
process.argv = [
|
|
459
|
+
process.argv = [
|
|
460
|
+
"bun",
|
|
461
|
+
"cadet",
|
|
462
|
+
"tunnel",
|
|
463
|
+
"--provider",
|
|
464
|
+
"ngrok",
|
|
465
|
+
"--foreground",
|
|
466
|
+
];
|
|
369
467
|
|
|
370
468
|
const logs = await runTunnelCapturingLogs();
|
|
371
469
|
|
|
@@ -382,7 +480,14 @@ describe("tunnel edge targeting", () => {
|
|
|
382
480
|
|
|
383
481
|
test("an active cloud assistant with no local entries exits with an error", async () => {
|
|
384
482
|
writeLockfile(makeCloudEntry());
|
|
385
|
-
process.argv = [
|
|
483
|
+
process.argv = [
|
|
484
|
+
"bun",
|
|
485
|
+
"cadet",
|
|
486
|
+
"tunnel",
|
|
487
|
+
"--provider",
|
|
488
|
+
"ngrok",
|
|
489
|
+
"--foreground",
|
|
490
|
+
];
|
|
386
491
|
|
|
387
492
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
388
493
|
|
|
@@ -401,7 +506,14 @@ describe("tunnel edge targeting", () => {
|
|
|
401
506
|
[cloud, makeLocalEntry("assistant-a"), makeLocalEntry("assistant-b")],
|
|
402
507
|
cloud.assistantId,
|
|
403
508
|
);
|
|
404
|
-
process.argv = [
|
|
509
|
+
process.argv = [
|
|
510
|
+
"bun",
|
|
511
|
+
"cadet",
|
|
512
|
+
"tunnel",
|
|
513
|
+
"--provider",
|
|
514
|
+
"ngrok",
|
|
515
|
+
"--foreground",
|
|
516
|
+
];
|
|
405
517
|
|
|
406
518
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
407
519
|
|
|
@@ -426,6 +538,7 @@ describe("tunnel edge targeting", () => {
|
|
|
426
538
|
"cloud-1",
|
|
427
539
|
"--provider",
|
|
428
540
|
"ngrok",
|
|
541
|
+
"--foreground",
|
|
429
542
|
];
|
|
430
543
|
|
|
431
544
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
@@ -445,14 +558,19 @@ describe("tunnel edge targeting", () => {
|
|
|
445
558
|
const local = makeLocalEntry();
|
|
446
559
|
local.name = "Ada";
|
|
447
560
|
writeLockfile([makeCloudEntry(), local], "cloud-1");
|
|
448
|
-
process.argv = [
|
|
561
|
+
process.argv = [
|
|
562
|
+
"bun",
|
|
563
|
+
"cadet",
|
|
564
|
+
"tunnel",
|
|
565
|
+
"Ada",
|
|
566
|
+
"--provider",
|
|
567
|
+
"ngrok",
|
|
568
|
+
"--foreground",
|
|
569
|
+
];
|
|
449
570
|
|
|
450
571
|
await runTunnelCapturingLogs();
|
|
451
572
|
|
|
452
|
-
const workspaceDir = join(
|
|
453
|
-
local.resources!.instanceDir,
|
|
454
|
-
"workspace",
|
|
455
|
-
);
|
|
573
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
456
574
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
457
575
|
port: EDGE_PORT,
|
|
458
576
|
assistantId: "assistant-1",
|
|
@@ -472,14 +590,12 @@ describe("tunnel edge targeting", () => {
|
|
|
472
590
|
"Lovelace",
|
|
473
591
|
"--provider",
|
|
474
592
|
"ngrok",
|
|
593
|
+
"--foreground",
|
|
475
594
|
];
|
|
476
595
|
|
|
477
596
|
await runTunnelCapturingLogs();
|
|
478
597
|
|
|
479
|
-
const workspaceDir = join(
|
|
480
|
-
local.resources!.instanceDir,
|
|
481
|
-
"workspace",
|
|
482
|
-
);
|
|
598
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
483
599
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
484
600
|
port: EDGE_PORT,
|
|
485
601
|
assistantId: "assistant-1",
|
|
@@ -498,6 +614,7 @@ describe("tunnel edge targeting", () => {
|
|
|
498
614
|
"apple-1",
|
|
499
615
|
"--provider",
|
|
500
616
|
"ngrok",
|
|
617
|
+
"--foreground",
|
|
501
618
|
];
|
|
502
619
|
|
|
503
620
|
await runTunnelCapturingLogs();
|
|
@@ -518,7 +635,14 @@ describe("tunnel edge targeting", () => {
|
|
|
518
635
|
const workspaceDir = useTempDefaultWorkspaceDir();
|
|
519
636
|
const cloud = makeCloudEntry();
|
|
520
637
|
writeLockfile([cloud, makeAppleContainerEntry()], cloud.assistantId);
|
|
521
|
-
process.argv = [
|
|
638
|
+
process.argv = [
|
|
639
|
+
"bun",
|
|
640
|
+
"cadet",
|
|
641
|
+
"tunnel",
|
|
642
|
+
"--provider",
|
|
643
|
+
"ngrok",
|
|
644
|
+
"--foreground",
|
|
645
|
+
];
|
|
522
646
|
|
|
523
647
|
const logs = await runTunnelCapturingLogs();
|
|
524
648
|
|
|
@@ -550,6 +674,7 @@ describe("tunnel edge targeting", () => {
|
|
|
550
674
|
"assistant-b",
|
|
551
675
|
"--provider",
|
|
552
676
|
"ngrok",
|
|
677
|
+
"--foreground",
|
|
553
678
|
];
|
|
554
679
|
|
|
555
680
|
await runTunnelCapturingLogs();
|
|
@@ -564,7 +689,15 @@ describe("tunnel edge targeting", () => {
|
|
|
564
689
|
cloud.name = "Cloudy";
|
|
565
690
|
const local = makeLocalEntry();
|
|
566
691
|
writeLockfile([cloud, local], local.assistantId);
|
|
567
|
-
process.argv = [
|
|
692
|
+
process.argv = [
|
|
693
|
+
"bun",
|
|
694
|
+
"cadet",
|
|
695
|
+
"tunnel",
|
|
696
|
+
"Cloudy",
|
|
697
|
+
"--provider",
|
|
698
|
+
"ngrok",
|
|
699
|
+
"--foreground",
|
|
700
|
+
];
|
|
568
701
|
|
|
569
702
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
570
703
|
|
|
@@ -585,7 +718,15 @@ describe("tunnel edge targeting", () => {
|
|
|
585
718
|
const second = makeLocalEntry("assistant-b");
|
|
586
719
|
second.name = "Ada";
|
|
587
720
|
writeLockfile([first, second], "assistant-a");
|
|
588
|
-
process.argv = [
|
|
721
|
+
process.argv = [
|
|
722
|
+
"bun",
|
|
723
|
+
"cadet",
|
|
724
|
+
"tunnel",
|
|
725
|
+
"Ada",
|
|
726
|
+
"--provider",
|
|
727
|
+
"ngrok",
|
|
728
|
+
"--foreground",
|
|
729
|
+
];
|
|
589
730
|
|
|
590
731
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
591
732
|
|
|
@@ -598,7 +739,15 @@ describe("tunnel edge targeting", () => {
|
|
|
598
739
|
});
|
|
599
740
|
|
|
600
741
|
test("an unknown positional name errors", async () => {
|
|
601
|
-
process.argv = [
|
|
742
|
+
process.argv = [
|
|
743
|
+
"bun",
|
|
744
|
+
"cadet",
|
|
745
|
+
"tunnel",
|
|
746
|
+
"nope",
|
|
747
|
+
"--provider",
|
|
748
|
+
"ngrok",
|
|
749
|
+
"--foreground",
|
|
750
|
+
];
|
|
602
751
|
|
|
603
752
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
604
753
|
|
|
@@ -611,7 +760,14 @@ describe("tunnel edge targeting", () => {
|
|
|
611
760
|
test("targets the edge port for cloudflare", async () => {
|
|
612
761
|
const entry = makeLocalEntry();
|
|
613
762
|
writeLockfile(entry);
|
|
614
|
-
process.argv = [
|
|
763
|
+
process.argv = [
|
|
764
|
+
"bun",
|
|
765
|
+
"cadet",
|
|
766
|
+
"tunnel",
|
|
767
|
+
"--provider",
|
|
768
|
+
"cloudflare",
|
|
769
|
+
"--foreground",
|
|
770
|
+
];
|
|
615
771
|
|
|
616
772
|
await runTunnelCapturingLogs();
|
|
617
773
|
|
|
@@ -626,7 +782,14 @@ describe("tunnel edge targeting", () => {
|
|
|
626
782
|
test("targets the edge port for tailscale and notes a reused webhooks-only edge", async () => {
|
|
627
783
|
const entry = makeLocalEntry();
|
|
628
784
|
writeLockfile(entry);
|
|
629
|
-
process.argv = [
|
|
785
|
+
process.argv = [
|
|
786
|
+
"bun",
|
|
787
|
+
"cadet",
|
|
788
|
+
"tunnel",
|
|
789
|
+
"--provider",
|
|
790
|
+
"tailscale",
|
|
791
|
+
"--foreground",
|
|
792
|
+
];
|
|
630
793
|
ensureTunnelEdgeMock.mockResolvedValue({
|
|
631
794
|
port: EDGE_PORT,
|
|
632
795
|
started: false,
|
|
@@ -641,49 +804,42 @@ describe("tunnel edge targeting", () => {
|
|
|
641
804
|
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
642
805
|
});
|
|
643
806
|
expect(logs).toContain(`Reusing the nginx edge on 127.0.0.1:${EDGE_PORT}`);
|
|
644
|
-
expect(logs).toContain("serves webhooks only");
|
|
645
807
|
});
|
|
646
808
|
|
|
647
|
-
test("a bare invocation
|
|
648
|
-
|
|
649
|
-
|
|
809
|
+
test("a bare invocation tunnels the default provider", async () => {
|
|
810
|
+
const entry = makeLocalEntry();
|
|
811
|
+
writeLockfile(entry);
|
|
812
|
+
process.argv = ["bun", "cadet", "tunnel", "--foreground"];
|
|
650
813
|
|
|
651
|
-
const
|
|
814
|
+
const logs = await runTunnelCapturingLogs();
|
|
652
815
|
|
|
653
|
-
expect(
|
|
654
|
-
expect(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
expect(ensureTunnelEdgeMock).
|
|
816
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
817
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
818
|
+
port: EDGE_PORT,
|
|
819
|
+
assistantId: "assistant-1",
|
|
820
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
821
|
+
});
|
|
822
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
660
823
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
661
|
-
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
662
824
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
663
825
|
});
|
|
664
826
|
|
|
665
|
-
test("a named assistant without --provider
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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"];
|
|
827
|
+
test("a named assistant without --provider tunnels it with the default provider", async () => {
|
|
828
|
+
const entry = makeLocalEntry();
|
|
829
|
+
entry.name = "Ada";
|
|
830
|
+
writeLockfile(entry);
|
|
831
|
+
process.argv = ["bun", "cadet", "tunnel", "Ada", "--foreground"];
|
|
683
832
|
|
|
684
|
-
const
|
|
833
|
+
const logs = await runTunnelCapturingLogs();
|
|
685
834
|
|
|
686
|
-
expect(
|
|
835
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
836
|
+
// The assistant that was asked for is the one tunneled.
|
|
837
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
838
|
+
port: EDGE_PORT,
|
|
839
|
+
assistantId: entry.assistantId,
|
|
840
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
841
|
+
});
|
|
842
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
687
843
|
});
|
|
688
844
|
|
|
689
845
|
test("--provider tailscale keeps the webhook callback base and says so", async () => {
|
|
@@ -692,7 +848,14 @@ describe("tunnel edge targeting", () => {
|
|
|
692
848
|
const workspaceDir = writeEntryWorkspaceConfig(entry, {
|
|
693
849
|
telegram: { botUsername: "example_bot" },
|
|
694
850
|
});
|
|
695
|
-
process.argv = [
|
|
851
|
+
process.argv = [
|
|
852
|
+
"bun",
|
|
853
|
+
"cadet",
|
|
854
|
+
"tunnel",
|
|
855
|
+
"--provider",
|
|
856
|
+
"tailscale",
|
|
857
|
+
"--foreground",
|
|
858
|
+
];
|
|
696
859
|
|
|
697
860
|
const warnings: string[] = [];
|
|
698
861
|
const warnSpy = spyOn(console, "warn").mockImplementation(
|
|
@@ -723,7 +886,14 @@ describe("tunnel edge targeting", () => {
|
|
|
723
886
|
const entry = makeLocalEntry();
|
|
724
887
|
writeLockfile(entry);
|
|
725
888
|
const workspaceDir = writeEntryWorkspaceConfig(entry, {});
|
|
726
|
-
process.argv = [
|
|
889
|
+
process.argv = [
|
|
890
|
+
"bun",
|
|
891
|
+
"cadet",
|
|
892
|
+
"tunnel",
|
|
893
|
+
"--provider",
|
|
894
|
+
"tailscale",
|
|
895
|
+
"--foreground",
|
|
896
|
+
];
|
|
727
897
|
|
|
728
898
|
await runTunnelCapturingLogs();
|
|
729
899
|
|
|
@@ -740,7 +910,14 @@ describe("tunnel edge targeting", () => {
|
|
|
740
910
|
const workspaceDir = writeEntryWorkspaceConfig(entry, {
|
|
741
911
|
telegram: { botUsername: "example_bot" },
|
|
742
912
|
});
|
|
743
|
-
process.argv = [
|
|
913
|
+
process.argv = [
|
|
914
|
+
"bun",
|
|
915
|
+
"cadet",
|
|
916
|
+
"tunnel",
|
|
917
|
+
"--provider",
|
|
918
|
+
"ngrok",
|
|
919
|
+
"--foreground",
|
|
920
|
+
];
|
|
744
921
|
|
|
745
922
|
await runTunnelCapturingLogs();
|
|
746
923
|
|
|
@@ -752,11 +929,18 @@ describe("tunnel edge targeting", () => {
|
|
|
752
929
|
});
|
|
753
930
|
|
|
754
931
|
test("missing nginx aborts before any provider spawn", async () => {
|
|
755
|
-
process.argv = [
|
|
932
|
+
process.argv = [
|
|
933
|
+
"bun",
|
|
934
|
+
"cadet",
|
|
935
|
+
"tunnel",
|
|
936
|
+
"--provider",
|
|
937
|
+
"ngrok",
|
|
938
|
+
"--foreground",
|
|
939
|
+
];
|
|
756
940
|
ensureTunnelEdgeMock.mockRejectedValue(
|
|
757
941
|
new Error(
|
|
758
942
|
"nginx is not installed, so the tunnel edge cannot start. " +
|
|
759
|
-
"
|
|
943
|
+
"See https://nginx.org/en/docs/install.html for how to install, " +
|
|
760
944
|
"or point NGINX_BIN at an existing binary.",
|
|
761
945
|
),
|
|
762
946
|
);
|
|
@@ -764,14 +948,23 @@ describe("tunnel edge targeting", () => {
|
|
|
764
948
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
765
949
|
|
|
766
950
|
expect(exited).toBe(true);
|
|
767
|
-
expect(errors).toContain(
|
|
951
|
+
expect(errors).toContain(
|
|
952
|
+
"See https://nginx.org/en/docs/install.html for how to install",
|
|
953
|
+
);
|
|
768
954
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
769
955
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
770
956
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
771
957
|
});
|
|
772
958
|
|
|
773
959
|
test("rejects an unknown --provider with a stale-CLI hint", async () => {
|
|
774
|
-
process.argv = [
|
|
960
|
+
process.argv = [
|
|
961
|
+
"bun",
|
|
962
|
+
"cadet",
|
|
963
|
+
"tunnel",
|
|
964
|
+
"--provider",
|
|
965
|
+
"bogus",
|
|
966
|
+
"--foreground",
|
|
967
|
+
];
|
|
775
968
|
|
|
776
969
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
777
970
|
|
|
@@ -788,7 +981,14 @@ describe("tunnel edge targeting", () => {
|
|
|
788
981
|
try {
|
|
789
982
|
for (const provider of TUNNEL_PROVIDERS) {
|
|
790
983
|
writeLockfile(makeLocalEntry());
|
|
791
|
-
process.argv = [
|
|
984
|
+
process.argv = [
|
|
985
|
+
"bun",
|
|
986
|
+
"cadet",
|
|
987
|
+
"tunnel",
|
|
988
|
+
"--provider",
|
|
989
|
+
provider,
|
|
990
|
+
"--foreground",
|
|
991
|
+
];
|
|
792
992
|
|
|
793
993
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
794
994
|
|
|
@@ -811,6 +1011,7 @@ describe("tunnel edge targeting", () => {
|
|
|
811
1011
|
"ngrok",
|
|
812
1012
|
"--domain",
|
|
813
1013
|
"foo.ngrok.app",
|
|
1014
|
+
"--foreground",
|
|
814
1015
|
];
|
|
815
1016
|
|
|
816
1017
|
await runTunnelCapturingLogs();
|
|
@@ -832,13 +1033,14 @@ describe("tunnel edge targeting", () => {
|
|
|
832
1033
|
"cloudflare",
|
|
833
1034
|
"--domain",
|
|
834
1035
|
"foo.ngrok.app",
|
|
1036
|
+
"--foreground",
|
|
835
1037
|
];
|
|
836
1038
|
|
|
837
1039
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
838
1040
|
|
|
839
1041
|
expect(exited).toBe(true);
|
|
840
1042
|
expect(errors).toContain(
|
|
841
|
-
"--domain is
|
|
1043
|
+
"--domain is not supported with --provider cloudflare",
|
|
842
1044
|
);
|
|
843
1045
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
844
1046
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -847,15 +1049,9 @@ describe("tunnel edge targeting", () => {
|
|
|
847
1049
|
test("--clear-domain drops the saved domain and runs domainless", async () => {
|
|
848
1050
|
const entry = makeLocalEntry();
|
|
849
1051
|
writeLockfile(entry);
|
|
850
|
-
const workspaceDir = join(
|
|
851
|
-
entry.resources!.instanceDir,
|
|
852
|
-
"workspace",
|
|
853
|
-
);
|
|
1052
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
854
1053
|
mkdirSync(workspaceDir, { recursive: true });
|
|
855
|
-
|
|
856
|
-
join(workspaceDir, "config.json"),
|
|
857
|
-
JSON.stringify({ ingress: { ngrok: { domain: "dead.ngrok.app" } } }),
|
|
858
|
-
);
|
|
1054
|
+
realNgrok.saveNgrokDomain("dead.ngrok.app");
|
|
859
1055
|
process.argv = [
|
|
860
1056
|
"bun",
|
|
861
1057
|
"cadet",
|
|
@@ -863,15 +1059,14 @@ describe("tunnel edge targeting", () => {
|
|
|
863
1059
|
"--provider",
|
|
864
1060
|
"ngrok",
|
|
865
1061
|
"--clear-domain",
|
|
1062
|
+
"--foreground",
|
|
866
1063
|
];
|
|
867
1064
|
|
|
868
1065
|
const logs = await runTunnelCapturingLogs();
|
|
869
1066
|
|
|
870
|
-
expect(logs).toContain("Cleared the saved
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
) as { ingress?: { ngrok?: { domain?: string } } };
|
|
874
|
-
expect(config.ingress?.ngrok).toBeUndefined();
|
|
1067
|
+
expect(logs).toContain("Cleared the saved reserved domain");
|
|
1068
|
+
// The saved domain lives in the XDG ngrok config, not the workspace config.
|
|
1069
|
+
expect(realNgrok.loadNgrokDomain()).toBeNull();
|
|
875
1070
|
// The run proceeds domainless: no `domain` key reaches runNgrokTunnel.
|
|
876
1071
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
877
1072
|
port: EDGE_PORT,
|
|
@@ -890,6 +1085,7 @@ describe("tunnel edge targeting", () => {
|
|
|
890
1085
|
"--domain",
|
|
891
1086
|
"foo.ngrok.app",
|
|
892
1087
|
"--clear-domain",
|
|
1088
|
+
"--foreground",
|
|
893
1089
|
];
|
|
894
1090
|
|
|
895
1091
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
@@ -907,13 +1103,14 @@ describe("tunnel edge targeting", () => {
|
|
|
907
1103
|
"--provider",
|
|
908
1104
|
"cloudflare",
|
|
909
1105
|
"--clear-domain",
|
|
1106
|
+
"--foreground",
|
|
910
1107
|
];
|
|
911
1108
|
|
|
912
1109
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
913
1110
|
|
|
914
1111
|
expect(exited).toBe(true);
|
|
915
1112
|
expect(errors).toContain(
|
|
916
|
-
"--clear-domain is
|
|
1113
|
+
"--clear-domain is not supported with --provider cloudflare",
|
|
917
1114
|
);
|
|
918
1115
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
919
1116
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -927,6 +1124,7 @@ describe("tunnel edge targeting", () => {
|
|
|
927
1124
|
"--provider",
|
|
928
1125
|
"ngrok",
|
|
929
1126
|
"--domain",
|
|
1127
|
+
"--foreground",
|
|
930
1128
|
];
|
|
931
1129
|
|
|
932
1130
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
@@ -939,13 +1137,20 @@ describe("tunnel edge targeting", () => {
|
|
|
939
1137
|
test("a not-yet-implemented provider error carries the stale-CLI hint", async () => {
|
|
940
1138
|
// `cadet` stays valid-but-unimplemented, so it hits this path rather
|
|
941
1139
|
// than the unknown-provider one.
|
|
942
|
-
process.argv = [
|
|
1140
|
+
process.argv = [
|
|
1141
|
+
"bun",
|
|
1142
|
+
"cadet",
|
|
1143
|
+
"tunnel",
|
|
1144
|
+
"--provider",
|
|
1145
|
+
"cadet",
|
|
1146
|
+
"--foreground",
|
|
1147
|
+
];
|
|
943
1148
|
|
|
944
1149
|
let err: Error | undefined;
|
|
945
1150
|
try {
|
|
946
1151
|
await tunnel();
|
|
947
1152
|
} catch (e) {
|
|
948
|
-
err = e
|
|
1153
|
+
err = e instanceof Error ? e : undefined;
|
|
949
1154
|
}
|
|
950
1155
|
|
|
951
1156
|
expect(err?.message).toContain("is not yet implemented");
|
|
@@ -959,20 +1164,26 @@ describe("tunnel edge targeting", () => {
|
|
|
959
1164
|
|
|
960
1165
|
describe("ngrok --domain spawn args", () => {
|
|
961
1166
|
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
|
-
|
|
1167
|
+
let lastChild: ChildProcess | null = null;
|
|
1168
|
+
|
|
1169
|
+
const spawnMock = mock(
|
|
1170
|
+
(
|
|
1171
|
+
_command: string,
|
|
1172
|
+
_args: readonly string[],
|
|
1173
|
+
_options?: SpawnOptions,
|
|
1174
|
+
): ChildProcess => {
|
|
1175
|
+
const child = new realChildProcess.ChildProcess();
|
|
1176
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
1177
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
1178
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
1179
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
1180
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
1181
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
1182
|
+
const fake = Object.assign(child, { pid: 4242, kill: () => true });
|
|
1183
|
+
lastChild = fake;
|
|
1184
|
+
return fake;
|
|
1185
|
+
},
|
|
1186
|
+
);
|
|
976
1187
|
const execFileSyncMock = mock(() => "ngrok version 3.9.0");
|
|
977
1188
|
|
|
978
1189
|
beforeAll(() => {
|
|
@@ -991,11 +1202,22 @@ describe("ngrok --domain spawn args", () => {
|
|
|
991
1202
|
spawnMock.mockClear();
|
|
992
1203
|
lastChild = null;
|
|
993
1204
|
delete process.env.IS_CONTAINERIZED;
|
|
1205
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
1206
|
+
// test keeps the saved-domain state hermetic.
|
|
1207
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-ngrok-xdg-"));
|
|
1208
|
+
tempDirs.push(configHome);
|
|
1209
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
994
1210
|
mockNgrokApiFetch([{ tunnels: [] }]);
|
|
995
1211
|
});
|
|
996
1212
|
|
|
997
1213
|
afterEach(() => {
|
|
998
1214
|
globalThis.fetch = originalFetch;
|
|
1215
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
1216
|
+
if (originalConfigHome === undefined) {
|
|
1217
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
1218
|
+
} else {
|
|
1219
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
1220
|
+
}
|
|
999
1221
|
if (originalContainerized === undefined) {
|
|
1000
1222
|
delete process.env.IS_CONTAINERIZED;
|
|
1001
1223
|
} else {
|
|
@@ -1021,22 +1243,20 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1021
1243
|
/** ngrok local API stub replaying one response per call, last one repeated. */
|
|
1022
1244
|
function mockNgrokApiFetch(responses: { tunnels: StubTunnel[] }[]): void {
|
|
1023
1245
|
let call = 0;
|
|
1024
|
-
globalThis.fetch = (async () => {
|
|
1246
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
1025
1247
|
const body = responses[Math.min(call, responses.length - 1)];
|
|
1026
1248
|
call++;
|
|
1027
1249
|
return new Response(JSON.stringify(body), {
|
|
1028
1250
|
status: 200,
|
|
1029
1251
|
headers: { "Content-Type": "application/json" },
|
|
1030
1252
|
});
|
|
1031
|
-
})
|
|
1253
|
+
});
|
|
1032
1254
|
}
|
|
1033
1255
|
|
|
1034
1256
|
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
|
-
};
|
|
1257
|
+
const config = IngressTestConfigSchema.parse(
|
|
1258
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1259
|
+
);
|
|
1040
1260
|
return config.ingress ?? {};
|
|
1041
1261
|
}
|
|
1042
1262
|
|
|
@@ -1048,7 +1268,7 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1048
1268
|
const lockfileDir = mkdtempSync(join(tmpdir(), "cadet-ngrok-lockfile-"));
|
|
1049
1269
|
tempDirs.push(lockfileDir);
|
|
1050
1270
|
writeFileSync(
|
|
1051
|
-
join(lockfileDir, ".
|
|
1271
|
+
join(lockfileDir, "lockfile.json"),
|
|
1052
1272
|
JSON.stringify({
|
|
1053
1273
|
activeAssistant: assistantId,
|
|
1054
1274
|
assistants: [
|
|
@@ -1104,33 +1324,34 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1104
1324
|
}
|
|
1105
1325
|
|
|
1106
1326
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1107
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1108
|
-
string,
|
|
1109
|
-
string[],
|
|
1110
|
-
];
|
|
1327
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1111
1328
|
expect(cmd).toBe("ngrok");
|
|
1329
|
+
// Plain `ngrok http` so the user's default config — where their
|
|
1330
|
+
// authtoken lives — is what the agent loads.
|
|
1112
1331
|
expect(args).toEqual([
|
|
1113
1332
|
"http",
|
|
1114
1333
|
"127.0.0.1:7831",
|
|
1115
1334
|
"--log=stdout",
|
|
1116
1335
|
"--domain=foo.ngrok.app",
|
|
1117
1336
|
]);
|
|
1337
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1118
1338
|
|
|
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");
|
|
1339
|
+
const config = IngressTestConfigSchema.parse(
|
|
1340
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1341
|
+
);
|
|
1342
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1124
1343
|
});
|
|
1125
1344
|
|
|
1126
1345
|
function mockTunnelListFetch(publicUrl: string, addr: string): void {
|
|
1127
|
-
globalThis.fetch = (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1346
|
+
globalThis.fetch = withPreconnect(
|
|
1347
|
+
async () =>
|
|
1348
|
+
new Response(
|
|
1349
|
+
JSON.stringify({
|
|
1350
|
+
tunnels: [{ public_url: publicUrl, config: { addr } }],
|
|
1351
|
+
}),
|
|
1352
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
1353
|
+
),
|
|
1354
|
+
);
|
|
1134
1355
|
}
|
|
1135
1356
|
|
|
1136
1357
|
test("runNgrokTunnel rejects an existing tunnel that mismatches the requested domain", async () => {
|
|
@@ -1143,11 +1364,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1143
1364
|
errors.push(a.join(" "));
|
|
1144
1365
|
},
|
|
1145
1366
|
);
|
|
1146
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1147
|
-
code?: number
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1367
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1368
|
+
(code?: number | string | undefined): never => {
|
|
1369
|
+
throw new Error(`exit:${code}`);
|
|
1370
|
+
},
|
|
1371
|
+
);
|
|
1151
1372
|
|
|
1152
1373
|
try {
|
|
1153
1374
|
await expect(
|
|
@@ -1170,9 +1391,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1170
1391
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1171
1392
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1172
1393
|
|
|
1173
|
-
const config =
|
|
1174
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1175
|
-
)
|
|
1394
|
+
const config = IngressTestConfigSchema.parse(
|
|
1395
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1396
|
+
);
|
|
1176
1397
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1177
1398
|
expect(config.ingress?.ngrok).toBeUndefined();
|
|
1178
1399
|
});
|
|
@@ -1189,6 +1410,8 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1189
1410
|
// The adopt path blocks until SIGINT/SIGTERM; pump SIGINT until the
|
|
1190
1411
|
// listener is registered and the promise settles. Earlier tests leak
|
|
1191
1412
|
// SIGINT handlers that call process.exit, so no-op it while pumping.
|
|
1413
|
+
// LOAD-BEARING cast: a no-op exit can't honestly be typed `: never` — it
|
|
1414
|
+
// must return so leaked SIGINT handlers can't crash the pump below.
|
|
1192
1415
|
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1193
1416
|
(() => undefined) as never,
|
|
1194
1417
|
);
|
|
@@ -1201,18 +1424,16 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1201
1424
|
}
|
|
1202
1425
|
|
|
1203
1426
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1204
|
-
const config =
|
|
1205
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1206
|
-
)
|
|
1207
|
-
expect(config.ingress
|
|
1208
|
-
expect(
|
|
1427
|
+
const config = IngressTestConfigSchema.parse(
|
|
1428
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1429
|
+
);
|
|
1430
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1431
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1209
1432
|
});
|
|
1210
1433
|
|
|
1211
1434
|
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
|
-
});
|
|
1435
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1436
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1216
1437
|
mockTunnelListFetch("https://other.ngrok-free.app", "localhost:7830");
|
|
1217
1438
|
|
|
1218
1439
|
const warnings: string[] = [];
|
|
@@ -1240,13 +1461,13 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1240
1461
|
"cadet tunnel --provider ngrok --domain foo.ngrok.app",
|
|
1241
1462
|
);
|
|
1242
1463
|
|
|
1243
|
-
const config =
|
|
1244
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1245
|
-
)
|
|
1464
|
+
const config = IngressTestConfigSchema.parse(
|
|
1465
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1466
|
+
);
|
|
1246
1467
|
// The mismatched tunnel is not blessed in config…
|
|
1247
|
-
expect(config.ingress
|
|
1468
|
+
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1248
1469
|
// …and the reserved domain stays saved as standing intent.
|
|
1249
|
-
expect(
|
|
1470
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1250
1471
|
});
|
|
1251
1472
|
|
|
1252
1473
|
test("an adopted automatic tunnel records the assistant it fronts", async () => {
|
|
@@ -1329,9 +1550,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1329
1550
|
expect(combined).toContain("not 18080");
|
|
1330
1551
|
expect(combined).toContain("cadet tunnel --provider ngrok");
|
|
1331
1552
|
|
|
1332
|
-
const config =
|
|
1333
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1334
|
-
)
|
|
1553
|
+
const config = IngressTestConfigSchema.parse(
|
|
1554
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1555
|
+
);
|
|
1335
1556
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1336
1557
|
});
|
|
1337
1558
|
|
|
@@ -1345,11 +1566,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1345
1566
|
errors.push(a.join(" "));
|
|
1346
1567
|
},
|
|
1347
1568
|
);
|
|
1348
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1349
|
-
code?: number
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1569
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1570
|
+
(code?: number | string | undefined): never => {
|
|
1571
|
+
throw new Error(`exit:${code}`);
|
|
1572
|
+
},
|
|
1573
|
+
);
|
|
1353
1574
|
|
|
1354
1575
|
try {
|
|
1355
1576
|
await expect(
|
|
@@ -1369,17 +1590,15 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1369
1590
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1370
1591
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1371
1592
|
|
|
1372
|
-
const config =
|
|
1373
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1374
|
-
)
|
|
1593
|
+
const config = IngressTestConfigSchema.parse(
|
|
1594
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1595
|
+
);
|
|
1375
1596
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1376
1597
|
});
|
|
1377
1598
|
|
|
1378
1599
|
test("maybeStartNgrokTunnel passes the saved domain to the spawn args", async () => {
|
|
1379
|
-
const ws = makeWorkspace({
|
|
1380
|
-
|
|
1381
|
-
ingress: { ngrok: { domain: "foo.ngrok.app" } },
|
|
1382
|
-
});
|
|
1600
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1601
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1383
1602
|
mockNgrokApiFetch([
|
|
1384
1603
|
{ tunnels: [] },
|
|
1385
1604
|
{
|
|
@@ -1397,11 +1616,10 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1397
1616
|
|
|
1398
1617
|
expect(child).not.toBeNull();
|
|
1399
1618
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1400
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1401
|
-
string,
|
|
1402
|
-
string[],
|
|
1403
|
-
];
|
|
1619
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1404
1620
|
expect(cmd).toBe("ngrok");
|
|
1621
|
+
// Plain `ngrok http` so the user's default config — where their
|
|
1622
|
+
// authtoken lives — is what the agent loads.
|
|
1405
1623
|
expect(args).toEqual([
|
|
1406
1624
|
"http",
|
|
1407
1625
|
"127.0.0.1:7830",
|
|
@@ -1409,16 +1627,15 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1409
1627
|
"--domain=foo.ngrok.app",
|
|
1410
1628
|
]);
|
|
1411
1629
|
|
|
1412
|
-
const config =
|
|
1413
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1414
|
-
)
|
|
1415
|
-
expect(config.ingress
|
|
1630
|
+
const config = IngressTestConfigSchema.parse(
|
|
1631
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1632
|
+
);
|
|
1633
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1416
1634
|
});
|
|
1417
1635
|
|
|
1418
1636
|
test("runNgrokTunnel without --domain reuses the saved domain and does not delete it", async () => {
|
|
1419
|
-
const ws = makeWorkspace({
|
|
1420
|
-
|
|
1421
|
-
});
|
|
1637
|
+
const ws = makeWorkspace({});
|
|
1638
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1422
1639
|
mockNgrokApiFetch([
|
|
1423
1640
|
{ tunnels: [] },
|
|
1424
1641
|
{
|
|
@@ -1440,7 +1657,7 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1440
1657
|
}
|
|
1441
1658
|
|
|
1442
1659
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1443
|
-
const [, args] = spawnMock.mock.calls[0]
|
|
1660
|
+
const [, args] = spawnMock.mock.calls[0];
|
|
1444
1661
|
expect(args).toEqual([
|
|
1445
1662
|
"http",
|
|
1446
1663
|
"127.0.0.1:7831",
|
|
@@ -1448,38 +1665,52 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1448
1665
|
"--domain=foo.ngrok.app",
|
|
1449
1666
|
]);
|
|
1450
1667
|
|
|
1451
|
-
const config =
|
|
1452
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1453
|
-
)
|
|
1454
|
-
expect(config.ingress
|
|
1455
|
-
expect(
|
|
1668
|
+
const config = IngressTestConfigSchema.parse(
|
|
1669
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1670
|
+
);
|
|
1671
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1672
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1456
1673
|
});
|
|
1457
1674
|
});
|
|
1458
1675
|
|
|
1459
|
-
describe("tunnel
|
|
1676
|
+
describe("tunnel background mode", () => {
|
|
1460
1677
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
1461
1678
|
|
|
1462
1679
|
function makeFakeChild(pid: number): ChildProcess {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}) as unknown as ChildProcess;
|
|
1680
|
+
const child = new realChildProcess.ChildProcess();
|
|
1681
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
1682
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
1683
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
1684
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
1685
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
1686
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
1687
|
+
return Object.assign(child, { pid, kill: () => true });
|
|
1472
1688
|
}
|
|
1473
1689
|
|
|
1474
|
-
const spawnMock = mock(
|
|
1690
|
+
const spawnMock = mock(
|
|
1691
|
+
(
|
|
1692
|
+
_command: string,
|
|
1693
|
+
_args: readonly string[],
|
|
1694
|
+
_options?: SpawnOptions,
|
|
1695
|
+
): ChildProcess => makeFakeChild(9999),
|
|
1696
|
+
);
|
|
1475
1697
|
|
|
1476
|
-
// spawnDetachedTunnel() scopes the log filename to its own (parent) pid,
|
|
1477
|
-
// which in-process is this test runner's pid.
|
|
1478
1698
|
function detachedLogPath(): string {
|
|
1479
|
-
return join(getLogDir(),
|
|
1699
|
+
return join(getLogDir(), "tunnel.log");
|
|
1480
1700
|
}
|
|
1481
1701
|
|
|
1482
1702
|
beforeAll(() => {
|
|
1703
|
+
// The edge-targeting describe's afterAll restores the REAL provider
|
|
1704
|
+
// modules; re-establish the fakes the foreground test here needs.
|
|
1705
|
+
mock.module("../lib/tunnels/ngrok", () => ({
|
|
1706
|
+
...realNgrok,
|
|
1707
|
+
runNgrokTunnel: runNgrokTunnelMock,
|
|
1708
|
+
}));
|
|
1709
|
+
mock.module("../lib/nginx-ingress.js", () => ({
|
|
1710
|
+
...realNginxIngress,
|
|
1711
|
+
ensureTunnelEdge: ensureTunnelEdgeMock,
|
|
1712
|
+
getNginxVersion: getNginxVersionMock,
|
|
1713
|
+
}));
|
|
1483
1714
|
mock.module("node:child_process", () => ({
|
|
1484
1715
|
...realChildProcess,
|
|
1485
1716
|
spawn: spawnMock,
|
|
@@ -1488,10 +1719,22 @@ describe("tunnel --detach", () => {
|
|
|
1488
1719
|
|
|
1489
1720
|
afterAll(() => {
|
|
1490
1721
|
mock.module("node:child_process", () => realChildProcess);
|
|
1722
|
+
mock.module("../lib/tunnels/ngrok", () => realNgrok);
|
|
1723
|
+
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
1491
1724
|
});
|
|
1492
1725
|
|
|
1493
1726
|
beforeEach(() => {
|
|
1494
1727
|
spawnMock.mockClear();
|
|
1728
|
+
runNgrokTunnelMock.mockReset();
|
|
1729
|
+
runNgrokTunnelMock.mockResolvedValue(
|
|
1730
|
+
fakeTunnelResult("https://test.example.ngrok.dev"),
|
|
1731
|
+
);
|
|
1732
|
+
ensureTunnelEdgeMock.mockReset();
|
|
1733
|
+
ensureTunnelEdgeMock.mockResolvedValue({
|
|
1734
|
+
port: EDGE_PORT,
|
|
1735
|
+
started: true,
|
|
1736
|
+
includesWebApp: true,
|
|
1737
|
+
});
|
|
1495
1738
|
const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
|
|
1496
1739
|
tempDirs.push(configHome);
|
|
1497
1740
|
process.env.XDG_CONFIG_HOME = configHome;
|
|
@@ -1509,15 +1752,8 @@ describe("tunnel --detach", () => {
|
|
|
1509
1752
|
}
|
|
1510
1753
|
});
|
|
1511
1754
|
|
|
1512
|
-
test("
|
|
1513
|
-
process.argv = [
|
|
1514
|
-
"bun",
|
|
1515
|
-
"cadet",
|
|
1516
|
-
"tunnel",
|
|
1517
|
-
"--provider",
|
|
1518
|
-
"ngrok",
|
|
1519
|
-
"--detach",
|
|
1520
|
-
];
|
|
1755
|
+
test("runs detached by default: re-invokes itself as a detached child and reports readiness", async () => {
|
|
1756
|
+
process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
|
|
1521
1757
|
|
|
1522
1758
|
// Simulate the detached child writing its readiness line to the log file
|
|
1523
1759
|
// the moment it is spawned.
|
|
@@ -1532,17 +1768,15 @@ describe("tunnel --detach", () => {
|
|
|
1532
1768
|
const logs = await runTunnelCapturingLogs();
|
|
1533
1769
|
|
|
1534
1770
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1535
|
-
const [, args, opts] = spawnMock.mock.calls[0]
|
|
1536
|
-
string,
|
|
1537
|
-
string[],
|
|
1538
|
-
Record<string, unknown>,
|
|
1539
|
-
];
|
|
1771
|
+
const [, args, opts] = spawnMock.mock.calls[0];
|
|
1540
1772
|
expect(args).toContain("tunnel");
|
|
1541
1773
|
expect(args).toContain("--provider");
|
|
1542
1774
|
expect(args).toContain("ngrok");
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1775
|
+
// The child runs the foreground path (it IS the background daemon); the
|
|
1776
|
+
// parent must not hand the default through or the child would daemonize
|
|
1777
|
+
// again.
|
|
1778
|
+
expect(args).toContain("--foreground");
|
|
1779
|
+
expect(opts?.detached).toBe(true);
|
|
1546
1780
|
|
|
1547
1781
|
expect(logs).toContain("Tunnel established: https://foo.ngrok.app");
|
|
1548
1782
|
expect(logs).toContain("Running in background (pid 9999)");
|
|
@@ -1554,37 +1788,30 @@ describe("tunnel --detach", () => {
|
|
|
1554
1788
|
expect(ensureTunnelEdgeMock).not.toHaveBeenCalled();
|
|
1555
1789
|
});
|
|
1556
1790
|
|
|
1557
|
-
test("
|
|
1558
|
-
process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale", "-d"];
|
|
1559
|
-
spawnMock.mockImplementationOnce(() => {
|
|
1560
|
-
appendFileSync(
|
|
1561
|
-
detachedLogPath(),
|
|
1562
|
-
"Tunnel established: https://tailnet.example.ts.net\n",
|
|
1563
|
-
);
|
|
1564
|
-
return makeFakeChild(1234);
|
|
1565
|
-
});
|
|
1566
|
-
|
|
1567
|
-
await runTunnelCapturingLogs();
|
|
1568
|
-
|
|
1569
|
-
const [, args] = spawnMock.mock.calls[0] as unknown as [string, string[]];
|
|
1570
|
-
expect(args).not.toContain("-d");
|
|
1571
|
-
expect(args).not.toContain("--detach");
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
|
-
test("reports an early child exit instead of hanging", async () => {
|
|
1791
|
+
test("--foreground runs the tunnel in this process instead of a detached child", async () => {
|
|
1575
1792
|
process.argv = [
|
|
1576
1793
|
"bun",
|
|
1577
1794
|
"cadet",
|
|
1578
1795
|
"tunnel",
|
|
1579
1796
|
"--provider",
|
|
1580
1797
|
"ngrok",
|
|
1581
|
-
"--
|
|
1798
|
+
"--foreground",
|
|
1582
1799
|
];
|
|
1800
|
+
|
|
1801
|
+
await runTunnelCapturingLogs();
|
|
1802
|
+
|
|
1803
|
+
expect(spawnMock).not.toHaveBeenCalled();
|
|
1804
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
1805
|
+
expect(runNgrokTunnelMock).toHaveBeenCalled();
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
test("reports an early child exit instead of hanging", async () => {
|
|
1809
|
+
process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
|
|
1583
1810
|
spawnMock.mockImplementationOnce(() => {
|
|
1584
1811
|
const child = makeFakeChild(4242);
|
|
1585
1812
|
// Listeners attach synchronously right after spawn() returns; defer the
|
|
1586
1813
|
// exit so they're in place before it fires.
|
|
1587
|
-
setTimeout(() =>
|
|
1814
|
+
setTimeout(() => child.emit("exit", 1), 0);
|
|
1588
1815
|
return child;
|
|
1589
1816
|
});
|
|
1590
1817
|
|
|
@@ -1596,14 +1823,7 @@ describe("tunnel --detach", () => {
|
|
|
1596
1823
|
});
|
|
1597
1824
|
|
|
1598
1825
|
test("an adopted ngrok tunnel gets an accurate stop message, not a false kill claim", async () => {
|
|
1599
|
-
process.argv = [
|
|
1600
|
-
"bun",
|
|
1601
|
-
"cadet",
|
|
1602
|
-
"tunnel",
|
|
1603
|
-
"--provider",
|
|
1604
|
-
"ngrok",
|
|
1605
|
-
"--detach",
|
|
1606
|
-
];
|
|
1826
|
+
process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
|
|
1607
1827
|
spawnMock.mockImplementationOnce(() => {
|
|
1608
1828
|
appendFileSync(
|
|
1609
1829
|
detachedLogPath(),
|