@mycadet/cli 0.12.7 → 0.12.9
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 +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} +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 +21 -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 +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -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 +181 -197
- package/src/__tests__/tunnel.test.ts +257 -222
- 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 +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -76
- 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 +23 -23
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/ps.ts +244 -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 +109 -109
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +138 -157
- package/src/commands/uninstall.ts +57 -9
- 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-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} +11 -9
- 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 +260 -53
- package/src/lib/ensure-binary.ts +171 -0
- 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/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 +19 -23
- 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 +10 -31
- package/src/lib/nginx-ingress.ts +42 -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/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();
|
|
@@ -217,15 +247,27 @@ describe("tunnel edge targeting", () => {
|
|
|
217
247
|
beforeEach(() => {
|
|
218
248
|
process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
|
|
219
249
|
writeLockfile(makeLocalEntry());
|
|
220
|
-
globalThis.fetch = (async () => {
|
|
250
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
221
251
|
throw new Error("gateway unavailable");
|
|
222
|
-
})
|
|
252
|
+
});
|
|
253
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
254
|
+
// test keeps the saved-domain state hermetic.
|
|
255
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
|
|
256
|
+
tempDirs.push(configHome);
|
|
257
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
223
258
|
runCloudflareTunnelMock.mockReset();
|
|
224
|
-
runCloudflareTunnelMock.mockResolvedValue(
|
|
259
|
+
runCloudflareTunnelMock.mockResolvedValue(
|
|
260
|
+
fakeTunnelResult("https://test.example.cloudflare.dev"),
|
|
261
|
+
);
|
|
225
262
|
runNgrokTunnelMock.mockReset();
|
|
226
|
-
runNgrokTunnelMock.mockResolvedValue(
|
|
263
|
+
runNgrokTunnelMock.mockResolvedValue(
|
|
264
|
+
fakeTunnelResult("https://test.example.ngrok.dev"),
|
|
265
|
+
);
|
|
227
266
|
runTailscaleTunnelMock.mockReset();
|
|
228
|
-
runTailscaleTunnelMock.mockResolvedValue(
|
|
267
|
+
runTailscaleTunnelMock.mockResolvedValue(
|
|
268
|
+
fakeTunnelResult("https://test-machine.tailnet.ts.net"),
|
|
269
|
+
);
|
|
270
|
+
notifyGatewayOfPublicUrlMock.mockReset();
|
|
229
271
|
ensureTunnelEdgeMock.mockReset();
|
|
230
272
|
ensureTunnelEdgeMock.mockResolvedValue({
|
|
231
273
|
port: EDGE_PORT,
|
|
@@ -237,6 +279,12 @@ describe("tunnel edge targeting", () => {
|
|
|
237
279
|
afterEach(() => {
|
|
238
280
|
process.argv = originalArgv;
|
|
239
281
|
globalThis.fetch = originalFetch;
|
|
282
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
283
|
+
if (originalConfigHome === undefined) {
|
|
284
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
285
|
+
} else {
|
|
286
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
287
|
+
}
|
|
240
288
|
if (originalLockfileDir === undefined) {
|
|
241
289
|
delete process.env.CADET_LOCKFILE_DIR;
|
|
242
290
|
} else {
|
|
@@ -253,9 +301,9 @@ describe("tunnel edge targeting", () => {
|
|
|
253
301
|
});
|
|
254
302
|
|
|
255
303
|
afterAll(() => {
|
|
256
|
-
mock.module("../lib/cloudflare
|
|
257
|
-
mock.module("../lib/ngrok", () => realNgrok);
|
|
258
|
-
mock.module("../lib/tailscale
|
|
304
|
+
mock.module("../lib/tunnels/cloudflare.js", () => realCloudflareTunnel);
|
|
305
|
+
mock.module("../lib/tunnels/ngrok", () => realNgrok);
|
|
306
|
+
mock.module("../lib/tunnels/tailscale.js", () => realTailscaleTunnel);
|
|
259
307
|
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
260
308
|
});
|
|
261
309
|
|
|
@@ -267,10 +315,7 @@ describe("tunnel edge targeting", () => {
|
|
|
267
315
|
|
|
268
316
|
const logs = await runTunnelCapturingLogs();
|
|
269
317
|
|
|
270
|
-
const workspaceDir = join(
|
|
271
|
-
entry.resources!.instanceDir,
|
|
272
|
-
"workspace",
|
|
273
|
-
);
|
|
318
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
274
319
|
expect(ensureTunnelEdgeMock).toHaveBeenCalledWith({
|
|
275
320
|
assistantId: "assistant-1",
|
|
276
321
|
workspaceDir,
|
|
@@ -283,7 +328,20 @@ describe("tunnel edge targeting", () => {
|
|
|
283
328
|
});
|
|
284
329
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
285
330
|
expect(logs).toContain(`Started the nginx edge on 127.0.0.1:${EDGE_PORT}`);
|
|
286
|
-
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("the tunnel level notifies the gateway of the provider's public URL", async () => {
|
|
334
|
+
writeLockfile(makeLocalEntry());
|
|
335
|
+
process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
|
|
336
|
+
|
|
337
|
+
await runTunnelCapturingLogs();
|
|
338
|
+
|
|
339
|
+
// The provider hands its URL back; the notification is the tunnel
|
|
340
|
+
// level's job, so it holds no matter which provider ran.
|
|
341
|
+
expect(notifyGatewayOfPublicUrlMock).toHaveBeenCalledWith(
|
|
342
|
+
"https://test.example.ngrok.dev",
|
|
343
|
+
"assistant-1",
|
|
344
|
+
);
|
|
287
345
|
});
|
|
288
346
|
|
|
289
347
|
test("an active cloud assistant falls back to the sole local entry with a note", async () => {
|
|
@@ -294,10 +352,7 @@ describe("tunnel edge targeting", () => {
|
|
|
294
352
|
|
|
295
353
|
const logs = await runTunnelCapturingLogs();
|
|
296
354
|
|
|
297
|
-
const workspaceDir = join(
|
|
298
|
-
local.resources!.instanceDir,
|
|
299
|
-
"workspace",
|
|
300
|
-
);
|
|
355
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
301
356
|
expect(logs).toContain(
|
|
302
357
|
"Assistant 'cloud-1' runs on Cadet Cloud and needs no tunnel. " +
|
|
303
358
|
"Tunneling the local assistant 'assistant-1' instead.",
|
|
@@ -419,14 +474,7 @@ describe("tunnel edge targeting", () => {
|
|
|
419
474
|
test("a positional cloud assistant name errors without auto-fallback", async () => {
|
|
420
475
|
const local = makeLocalEntry();
|
|
421
476
|
writeLockfile([makeCloudEntry(), local], local.assistantId);
|
|
422
|
-
process.argv = [
|
|
423
|
-
"bun",
|
|
424
|
-
"cadet",
|
|
425
|
-
"tunnel",
|
|
426
|
-
"cloud-1",
|
|
427
|
-
"--provider",
|
|
428
|
-
"ngrok",
|
|
429
|
-
];
|
|
477
|
+
process.argv = ["bun", "cadet", "tunnel", "cloud-1", "--provider", "ngrok"];
|
|
430
478
|
|
|
431
479
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
432
480
|
|
|
@@ -449,10 +497,7 @@ describe("tunnel edge targeting", () => {
|
|
|
449
497
|
|
|
450
498
|
await runTunnelCapturingLogs();
|
|
451
499
|
|
|
452
|
-
const workspaceDir = join(
|
|
453
|
-
local.resources!.instanceDir,
|
|
454
|
-
"workspace",
|
|
455
|
-
);
|
|
500
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
456
501
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
457
502
|
port: EDGE_PORT,
|
|
458
503
|
assistantId: "assistant-1",
|
|
@@ -476,10 +521,7 @@ describe("tunnel edge targeting", () => {
|
|
|
476
521
|
|
|
477
522
|
await runTunnelCapturingLogs();
|
|
478
523
|
|
|
479
|
-
const workspaceDir = join(
|
|
480
|
-
local.resources!.instanceDir,
|
|
481
|
-
"workspace",
|
|
482
|
-
);
|
|
524
|
+
const workspaceDir = join(local.resources!.instanceDir, "workspace");
|
|
483
525
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
484
526
|
port: EDGE_PORT,
|
|
485
527
|
assistantId: "assistant-1",
|
|
@@ -491,14 +533,7 @@ describe("tunnel edge targeting", () => {
|
|
|
491
533
|
const workspaceDir = useTempDefaultWorkspaceDir();
|
|
492
534
|
const local = makeLocalEntry();
|
|
493
535
|
writeLockfile([local, makeAppleContainerEntry()], local.assistantId);
|
|
494
|
-
process.argv = [
|
|
495
|
-
"bun",
|
|
496
|
-
"cadet",
|
|
497
|
-
"tunnel",
|
|
498
|
-
"apple-1",
|
|
499
|
-
"--provider",
|
|
500
|
-
"ngrok",
|
|
501
|
-
];
|
|
536
|
+
process.argv = ["bun", "cadet", "tunnel", "apple-1", "--provider", "ngrok"];
|
|
502
537
|
|
|
503
538
|
await runTunnelCapturingLogs();
|
|
504
539
|
|
|
@@ -641,49 +676,42 @@ describe("tunnel edge targeting", () => {
|
|
|
641
676
|
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
642
677
|
});
|
|
643
678
|
expect(logs).toContain(`Reusing the nginx edge on 127.0.0.1:${EDGE_PORT}`);
|
|
644
|
-
expect(logs).toContain("serves webhooks only");
|
|
645
679
|
});
|
|
646
680
|
|
|
647
|
-
test("a bare invocation
|
|
648
|
-
|
|
681
|
+
test("a bare invocation tunnels the default provider", async () => {
|
|
682
|
+
const entry = makeLocalEntry();
|
|
683
|
+
writeLockfile(entry);
|
|
649
684
|
process.argv = ["bun", "cadet", "tunnel"];
|
|
650
685
|
|
|
651
|
-
const
|
|
686
|
+
const logs = await runTunnelCapturingLogs();
|
|
652
687
|
|
|
653
|
-
expect(
|
|
654
|
-
expect(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
expect(ensureTunnelEdgeMock).
|
|
688
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
689
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
690
|
+
port: EDGE_PORT,
|
|
691
|
+
assistantId: "assistant-1",
|
|
692
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
693
|
+
});
|
|
694
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
660
695
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
661
|
-
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
662
696
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
663
697
|
});
|
|
664
698
|
|
|
665
|
-
test("a named assistant without --provider
|
|
666
|
-
|
|
699
|
+
test("a named assistant without --provider tunnels it with the default provider", async () => {
|
|
700
|
+
const entry = makeLocalEntry();
|
|
701
|
+
entry.name = "Ada";
|
|
702
|
+
writeLockfile(entry);
|
|
667
703
|
process.argv = ["bun", "cadet", "tunnel", "Ada"];
|
|
668
704
|
|
|
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();
|
|
705
|
+
const logs = await runTunnelCapturingLogs();
|
|
685
706
|
|
|
686
|
-
expect(
|
|
707
|
+
expect(logs).toContain("Using the default provider: ngrok");
|
|
708
|
+
// The assistant that was asked for is the one tunneled.
|
|
709
|
+
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
710
|
+
port: EDGE_PORT,
|
|
711
|
+
assistantId: entry.assistantId,
|
|
712
|
+
workspaceDir: join(entry.resources!.instanceDir, "workspace"),
|
|
713
|
+
});
|
|
714
|
+
expect(ensureTunnelEdgeMock).toHaveBeenCalled();
|
|
687
715
|
});
|
|
688
716
|
|
|
689
717
|
test("--provider tailscale keeps the webhook callback base and says so", async () => {
|
|
@@ -756,7 +784,7 @@ describe("tunnel edge targeting", () => {
|
|
|
756
784
|
ensureTunnelEdgeMock.mockRejectedValue(
|
|
757
785
|
new Error(
|
|
758
786
|
"nginx is not installed, so the tunnel edge cannot start. " +
|
|
759
|
-
"
|
|
787
|
+
"See https://nginx.org/en/docs/install.html for how to install, " +
|
|
760
788
|
"or point NGINX_BIN at an existing binary.",
|
|
761
789
|
),
|
|
762
790
|
);
|
|
@@ -764,7 +792,9 @@ describe("tunnel edge targeting", () => {
|
|
|
764
792
|
const { exited, errors } = await runTunnelExpectingExit1();
|
|
765
793
|
|
|
766
794
|
expect(exited).toBe(true);
|
|
767
|
-
expect(errors).toContain(
|
|
795
|
+
expect(errors).toContain(
|
|
796
|
+
"See https://nginx.org/en/docs/install.html for how to install",
|
|
797
|
+
);
|
|
768
798
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
769
799
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
770
800
|
expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
|
|
@@ -838,7 +868,7 @@ describe("tunnel edge targeting", () => {
|
|
|
838
868
|
|
|
839
869
|
expect(exited).toBe(true);
|
|
840
870
|
expect(errors).toContain(
|
|
841
|
-
"--domain is
|
|
871
|
+
"--domain is not supported with --provider cloudflare",
|
|
842
872
|
);
|
|
843
873
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
844
874
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -847,15 +877,9 @@ describe("tunnel edge targeting", () => {
|
|
|
847
877
|
test("--clear-domain drops the saved domain and runs domainless", async () => {
|
|
848
878
|
const entry = makeLocalEntry();
|
|
849
879
|
writeLockfile(entry);
|
|
850
|
-
const workspaceDir = join(
|
|
851
|
-
entry.resources!.instanceDir,
|
|
852
|
-
"workspace",
|
|
853
|
-
);
|
|
880
|
+
const workspaceDir = join(entry.resources!.instanceDir, "workspace");
|
|
854
881
|
mkdirSync(workspaceDir, { recursive: true });
|
|
855
|
-
|
|
856
|
-
join(workspaceDir, "config.json"),
|
|
857
|
-
JSON.stringify({ ingress: { ngrok: { domain: "dead.ngrok.app" } } }),
|
|
858
|
-
);
|
|
882
|
+
realNgrok.saveNgrokDomain("dead.ngrok.app");
|
|
859
883
|
process.argv = [
|
|
860
884
|
"bun",
|
|
861
885
|
"cadet",
|
|
@@ -867,11 +891,9 @@ describe("tunnel edge targeting", () => {
|
|
|
867
891
|
|
|
868
892
|
const logs = await runTunnelCapturingLogs();
|
|
869
893
|
|
|
870
|
-
expect(logs).toContain("Cleared the saved
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
) as { ingress?: { ngrok?: { domain?: string } } };
|
|
874
|
-
expect(config.ingress?.ngrok).toBeUndefined();
|
|
894
|
+
expect(logs).toContain("Cleared the saved reserved domain");
|
|
895
|
+
// The saved domain lives in the XDG ngrok config, not the workspace config.
|
|
896
|
+
expect(realNgrok.loadNgrokDomain()).toBeNull();
|
|
875
897
|
// The run proceeds domainless: no `domain` key reaches runNgrokTunnel.
|
|
876
898
|
expect(runNgrokTunnelMock).toHaveBeenCalledWith({
|
|
877
899
|
port: EDGE_PORT,
|
|
@@ -913,7 +935,7 @@ describe("tunnel edge targeting", () => {
|
|
|
913
935
|
|
|
914
936
|
expect(exited).toBe(true);
|
|
915
937
|
expect(errors).toContain(
|
|
916
|
-
"--clear-domain is
|
|
938
|
+
"--clear-domain is not supported with --provider cloudflare",
|
|
917
939
|
);
|
|
918
940
|
expect(runNgrokTunnelMock).not.toHaveBeenCalled();
|
|
919
941
|
expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
|
|
@@ -945,7 +967,7 @@ describe("tunnel edge targeting", () => {
|
|
|
945
967
|
try {
|
|
946
968
|
await tunnel();
|
|
947
969
|
} catch (e) {
|
|
948
|
-
err = e
|
|
970
|
+
err = e instanceof Error ? e : undefined;
|
|
949
971
|
}
|
|
950
972
|
|
|
951
973
|
expect(err?.message).toContain("is not yet implemented");
|
|
@@ -959,20 +981,26 @@ describe("tunnel edge targeting", () => {
|
|
|
959
981
|
|
|
960
982
|
describe("ngrok --domain spawn args", () => {
|
|
961
983
|
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
|
-
|
|
984
|
+
let lastChild: ChildProcess | null = null;
|
|
985
|
+
|
|
986
|
+
const spawnMock = mock(
|
|
987
|
+
(
|
|
988
|
+
_command: string,
|
|
989
|
+
_args: readonly string[],
|
|
990
|
+
_options?: SpawnOptions,
|
|
991
|
+
): ChildProcess => {
|
|
992
|
+
const child = new realChildProcess.ChildProcess();
|
|
993
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
994
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
995
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
996
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
997
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
998
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
999
|
+
const fake = Object.assign(child, { pid: 4242, kill: () => true });
|
|
1000
|
+
lastChild = fake;
|
|
1001
|
+
return fake;
|
|
1002
|
+
},
|
|
1003
|
+
);
|
|
976
1004
|
const execFileSyncMock = mock(() => "ngrok version 3.9.0");
|
|
977
1005
|
|
|
978
1006
|
beforeAll(() => {
|
|
@@ -991,11 +1019,22 @@ describe("ngrok --domain spawn args", () => {
|
|
|
991
1019
|
spawnMock.mockClear();
|
|
992
1020
|
lastChild = null;
|
|
993
1021
|
delete process.env.IS_CONTAINERIZED;
|
|
1022
|
+
// The ngrok config file lives under the XDG config dir; a fresh one per
|
|
1023
|
+
// test keeps the saved-domain state hermetic.
|
|
1024
|
+
const configHome = mkdtempSync(join(tmpdir(), "cadet-ngrok-xdg-"));
|
|
1025
|
+
tempDirs.push(configHome);
|
|
1026
|
+
process.env.XDG_CONFIG_HOME = configHome;
|
|
994
1027
|
mockNgrokApiFetch([{ tunnels: [] }]);
|
|
995
1028
|
});
|
|
996
1029
|
|
|
997
1030
|
afterEach(() => {
|
|
998
1031
|
globalThis.fetch = originalFetch;
|
|
1032
|
+
rmSync(realNgrok.ngrokConfigPath(), { force: true });
|
|
1033
|
+
if (originalConfigHome === undefined) {
|
|
1034
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
1035
|
+
} else {
|
|
1036
|
+
process.env.XDG_CONFIG_HOME = originalConfigHome;
|
|
1037
|
+
}
|
|
999
1038
|
if (originalContainerized === undefined) {
|
|
1000
1039
|
delete process.env.IS_CONTAINERIZED;
|
|
1001
1040
|
} else {
|
|
@@ -1021,22 +1060,20 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1021
1060
|
/** ngrok local API stub replaying one response per call, last one repeated. */
|
|
1022
1061
|
function mockNgrokApiFetch(responses: { tunnels: StubTunnel[] }[]): void {
|
|
1023
1062
|
let call = 0;
|
|
1024
|
-
globalThis.fetch = (async () => {
|
|
1063
|
+
globalThis.fetch = withPreconnect(async () => {
|
|
1025
1064
|
const body = responses[Math.min(call, responses.length - 1)];
|
|
1026
1065
|
call++;
|
|
1027
1066
|
return new Response(JSON.stringify(body), {
|
|
1028
1067
|
status: 200,
|
|
1029
1068
|
headers: { "Content-Type": "application/json" },
|
|
1030
1069
|
});
|
|
1031
|
-
})
|
|
1070
|
+
});
|
|
1032
1071
|
}
|
|
1033
1072
|
|
|
1034
1073
|
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
|
-
};
|
|
1074
|
+
const config = IngressTestConfigSchema.parse(
|
|
1075
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1076
|
+
);
|
|
1040
1077
|
return config.ingress ?? {};
|
|
1041
1078
|
}
|
|
1042
1079
|
|
|
@@ -1048,7 +1085,7 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1048
1085
|
const lockfileDir = mkdtempSync(join(tmpdir(), "cadet-ngrok-lockfile-"));
|
|
1049
1086
|
tempDirs.push(lockfileDir);
|
|
1050
1087
|
writeFileSync(
|
|
1051
|
-
join(lockfileDir, ".
|
|
1088
|
+
join(lockfileDir, "lockfile.json"),
|
|
1052
1089
|
JSON.stringify({
|
|
1053
1090
|
activeAssistant: assistantId,
|
|
1054
1091
|
assistants: [
|
|
@@ -1104,33 +1141,34 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1104
1141
|
}
|
|
1105
1142
|
|
|
1106
1143
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1107
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1108
|
-
string,
|
|
1109
|
-
string[],
|
|
1110
|
-
];
|
|
1144
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1111
1145
|
expect(cmd).toBe("ngrok");
|
|
1146
|
+
// Plain `ngrok http` so the user's default config — where their
|
|
1147
|
+
// authtoken lives — is what the agent loads.
|
|
1112
1148
|
expect(args).toEqual([
|
|
1113
1149
|
"http",
|
|
1114
1150
|
"127.0.0.1:7831",
|
|
1115
1151
|
"--log=stdout",
|
|
1116
1152
|
"--domain=foo.ngrok.app",
|
|
1117
1153
|
]);
|
|
1154
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1118
1155
|
|
|
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");
|
|
1156
|
+
const config = IngressTestConfigSchema.parse(
|
|
1157
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1158
|
+
);
|
|
1159
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1124
1160
|
});
|
|
1125
1161
|
|
|
1126
1162
|
function mockTunnelListFetch(publicUrl: string, addr: string): void {
|
|
1127
|
-
globalThis.fetch = (
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1163
|
+
globalThis.fetch = withPreconnect(
|
|
1164
|
+
async () =>
|
|
1165
|
+
new Response(
|
|
1166
|
+
JSON.stringify({
|
|
1167
|
+
tunnels: [{ public_url: publicUrl, config: { addr } }],
|
|
1168
|
+
}),
|
|
1169
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
1170
|
+
),
|
|
1171
|
+
);
|
|
1134
1172
|
}
|
|
1135
1173
|
|
|
1136
1174
|
test("runNgrokTunnel rejects an existing tunnel that mismatches the requested domain", async () => {
|
|
@@ -1143,11 +1181,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1143
1181
|
errors.push(a.join(" "));
|
|
1144
1182
|
},
|
|
1145
1183
|
);
|
|
1146
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1147
|
-
code?: number
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1184
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1185
|
+
(code?: number | string | undefined): never => {
|
|
1186
|
+
throw new Error(`exit:${code}`);
|
|
1187
|
+
},
|
|
1188
|
+
);
|
|
1151
1189
|
|
|
1152
1190
|
try {
|
|
1153
1191
|
await expect(
|
|
@@ -1170,9 +1208,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1170
1208
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1171
1209
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1172
1210
|
|
|
1173
|
-
const config =
|
|
1174
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1175
|
-
)
|
|
1211
|
+
const config = IngressTestConfigSchema.parse(
|
|
1212
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1213
|
+
);
|
|
1176
1214
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1177
1215
|
expect(config.ingress?.ngrok).toBeUndefined();
|
|
1178
1216
|
});
|
|
@@ -1189,6 +1227,8 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1189
1227
|
// The adopt path blocks until SIGINT/SIGTERM; pump SIGINT until the
|
|
1190
1228
|
// listener is registered and the promise settles. Earlier tests leak
|
|
1191
1229
|
// SIGINT handlers that call process.exit, so no-op it while pumping.
|
|
1230
|
+
// LOAD-BEARING cast: a no-op exit can't honestly be typed `: never` — it
|
|
1231
|
+
// must return so leaked SIGINT handlers can't crash the pump below.
|
|
1192
1232
|
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1193
1233
|
(() => undefined) as never,
|
|
1194
1234
|
);
|
|
@@ -1201,18 +1241,16 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1201
1241
|
}
|
|
1202
1242
|
|
|
1203
1243
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1204
|
-
const config =
|
|
1205
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1206
|
-
)
|
|
1207
|
-
expect(config.ingress
|
|
1208
|
-
expect(
|
|
1244
|
+
const config = IngressTestConfigSchema.parse(
|
|
1245
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1246
|
+
);
|
|
1247
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1248
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1209
1249
|
});
|
|
1210
1250
|
|
|
1211
1251
|
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
|
-
});
|
|
1252
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1253
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1216
1254
|
mockTunnelListFetch("https://other.ngrok-free.app", "localhost:7830");
|
|
1217
1255
|
|
|
1218
1256
|
const warnings: string[] = [];
|
|
@@ -1240,13 +1278,13 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1240
1278
|
"cadet tunnel --provider ngrok --domain foo.ngrok.app",
|
|
1241
1279
|
);
|
|
1242
1280
|
|
|
1243
|
-
const config =
|
|
1244
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1245
|
-
)
|
|
1281
|
+
const config = IngressTestConfigSchema.parse(
|
|
1282
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1283
|
+
);
|
|
1246
1284
|
// The mismatched tunnel is not blessed in config…
|
|
1247
|
-
expect(config.ingress
|
|
1285
|
+
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1248
1286
|
// …and the reserved domain stays saved as standing intent.
|
|
1249
|
-
expect(
|
|
1287
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1250
1288
|
});
|
|
1251
1289
|
|
|
1252
1290
|
test("an adopted automatic tunnel records the assistant it fronts", async () => {
|
|
@@ -1329,9 +1367,9 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1329
1367
|
expect(combined).toContain("not 18080");
|
|
1330
1368
|
expect(combined).toContain("cadet tunnel --provider ngrok");
|
|
1331
1369
|
|
|
1332
|
-
const config =
|
|
1333
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1334
|
-
)
|
|
1370
|
+
const config = IngressTestConfigSchema.parse(
|
|
1371
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1372
|
+
);
|
|
1335
1373
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1336
1374
|
});
|
|
1337
1375
|
|
|
@@ -1345,11 +1383,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1345
1383
|
errors.push(a.join(" "));
|
|
1346
1384
|
},
|
|
1347
1385
|
);
|
|
1348
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1349
|
-
code?: number
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1386
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
1387
|
+
(code?: number | string | undefined): never => {
|
|
1388
|
+
throw new Error(`exit:${code}`);
|
|
1389
|
+
},
|
|
1390
|
+
);
|
|
1353
1391
|
|
|
1354
1392
|
try {
|
|
1355
1393
|
await expect(
|
|
@@ -1369,17 +1407,15 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1369
1407
|
expect(combined).toContain("Stop the existing ngrok agent");
|
|
1370
1408
|
expect(spawnMock).not.toHaveBeenCalled();
|
|
1371
1409
|
|
|
1372
|
-
const config =
|
|
1373
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1374
|
-
)
|
|
1410
|
+
const config = IngressTestConfigSchema.parse(
|
|
1411
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1412
|
+
);
|
|
1375
1413
|
expect(config.ingress?.publicBaseUrl).toBeUndefined();
|
|
1376
1414
|
});
|
|
1377
1415
|
|
|
1378
1416
|
test("maybeStartNgrokTunnel passes the saved domain to the spawn args", async () => {
|
|
1379
|
-
const ws = makeWorkspace({
|
|
1380
|
-
|
|
1381
|
-
ingress: { ngrok: { domain: "foo.ngrok.app" } },
|
|
1382
|
-
});
|
|
1417
|
+
const ws = makeWorkspace({ telegram: { botUsername: "example_bot" } });
|
|
1418
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1383
1419
|
mockNgrokApiFetch([
|
|
1384
1420
|
{ tunnels: [] },
|
|
1385
1421
|
{
|
|
@@ -1397,11 +1433,10 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1397
1433
|
|
|
1398
1434
|
expect(child).not.toBeNull();
|
|
1399
1435
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1400
|
-
const [cmd, args] = spawnMock.mock.calls[0]
|
|
1401
|
-
string,
|
|
1402
|
-
string[],
|
|
1403
|
-
];
|
|
1436
|
+
const [cmd, args] = spawnMock.mock.calls[0];
|
|
1404
1437
|
expect(cmd).toBe("ngrok");
|
|
1438
|
+
// Plain `ngrok http` so the user's default config — where their
|
|
1439
|
+
// authtoken lives — is what the agent loads.
|
|
1405
1440
|
expect(args).toEqual([
|
|
1406
1441
|
"http",
|
|
1407
1442
|
"127.0.0.1:7830",
|
|
@@ -1409,16 +1444,15 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1409
1444
|
"--domain=foo.ngrok.app",
|
|
1410
1445
|
]);
|
|
1411
1446
|
|
|
1412
|
-
const config =
|
|
1413
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1414
|
-
)
|
|
1415
|
-
expect(config.ingress
|
|
1447
|
+
const config = IngressTestConfigSchema.parse(
|
|
1448
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1449
|
+
);
|
|
1450
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1416
1451
|
});
|
|
1417
1452
|
|
|
1418
1453
|
test("runNgrokTunnel without --domain reuses the saved domain and does not delete it", async () => {
|
|
1419
|
-
const ws = makeWorkspace({
|
|
1420
|
-
|
|
1421
|
-
});
|
|
1454
|
+
const ws = makeWorkspace({});
|
|
1455
|
+
realNgrok.saveNgrokDomain("foo.ngrok.app");
|
|
1422
1456
|
mockNgrokApiFetch([
|
|
1423
1457
|
{ tunnels: [] },
|
|
1424
1458
|
{
|
|
@@ -1440,7 +1474,7 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1440
1474
|
}
|
|
1441
1475
|
|
|
1442
1476
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1443
|
-
const [, args] = spawnMock.mock.calls[0]
|
|
1477
|
+
const [, args] = spawnMock.mock.calls[0];
|
|
1444
1478
|
expect(args).toEqual([
|
|
1445
1479
|
"http",
|
|
1446
1480
|
"127.0.0.1:7831",
|
|
@@ -1448,11 +1482,11 @@ describe("ngrok --domain spawn args", () => {
|
|
|
1448
1482
|
"--domain=foo.ngrok.app",
|
|
1449
1483
|
]);
|
|
1450
1484
|
|
|
1451
|
-
const config =
|
|
1452
|
-
readFileSync(join(ws, "config.json"), "utf-8"),
|
|
1453
|
-
)
|
|
1454
|
-
expect(config.ingress
|
|
1455
|
-
expect(
|
|
1485
|
+
const config = IngressTestConfigSchema.parse(
|
|
1486
|
+
JSON.parse(readFileSync(join(ws, "config.json"), "utf-8")),
|
|
1487
|
+
);
|
|
1488
|
+
expect(config.ingress?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
1489
|
+
expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
|
|
1456
1490
|
});
|
|
1457
1491
|
});
|
|
1458
1492
|
|
|
@@ -1460,18 +1494,23 @@ describe("tunnel --detach", () => {
|
|
|
1460
1494
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
1461
1495
|
|
|
1462
1496
|
function makeFakeChild(pid: number): ChildProcess {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}) as unknown as ChildProcess;
|
|
1497
|
+
const child = new realChildProcess.ChildProcess();
|
|
1498
|
+
// kill() stays stubbed: the fake pid must never be signaled for real.
|
|
1499
|
+
// stdout/stderr: the real class resolves them through a getter over the
|
|
1500
|
+
// internal spawn handle, so null-shadow them via defineProperty — an
|
|
1501
|
+
// Object.assign [[Set]] through the getter-only accessor throws.
|
|
1502
|
+
Object.defineProperty(child, "stdout", { value: null });
|
|
1503
|
+
Object.defineProperty(child, "stderr", { value: null });
|
|
1504
|
+
return Object.assign(child, { pid, kill: () => true });
|
|
1472
1505
|
}
|
|
1473
1506
|
|
|
1474
|
-
const spawnMock = mock(
|
|
1507
|
+
const spawnMock = mock(
|
|
1508
|
+
(
|
|
1509
|
+
_command: string,
|
|
1510
|
+
_args: readonly string[],
|
|
1511
|
+
_options?: SpawnOptions,
|
|
1512
|
+
): ChildProcess => makeFakeChild(9999),
|
|
1513
|
+
);
|
|
1475
1514
|
|
|
1476
1515
|
// spawnDetachedTunnel() scopes the log filename to its own (parent) pid,
|
|
1477
1516
|
// which in-process is this test runner's pid.
|
|
@@ -1532,17 +1571,13 @@ describe("tunnel --detach", () => {
|
|
|
1532
1571
|
const logs = await runTunnelCapturingLogs();
|
|
1533
1572
|
|
|
1534
1573
|
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
1535
|
-
const [, args, opts] = spawnMock.mock.calls[0]
|
|
1536
|
-
string,
|
|
1537
|
-
string[],
|
|
1538
|
-
Record<string, unknown>,
|
|
1539
|
-
];
|
|
1574
|
+
const [, args, opts] = spawnMock.mock.calls[0];
|
|
1540
1575
|
expect(args).toContain("tunnel");
|
|
1541
1576
|
expect(args).toContain("--provider");
|
|
1542
1577
|
expect(args).toContain("ngrok");
|
|
1543
1578
|
expect(args).not.toContain("--detach");
|
|
1544
1579
|
expect(args).not.toContain("-d");
|
|
1545
|
-
expect(opts
|
|
1580
|
+
expect(opts?.detached).toBe(true);
|
|
1546
1581
|
|
|
1547
1582
|
expect(logs).toContain("Tunnel established: https://foo.ngrok.app");
|
|
1548
1583
|
expect(logs).toContain("Running in background (pid 9999)");
|
|
@@ -1566,7 +1601,7 @@ describe("tunnel --detach", () => {
|
|
|
1566
1601
|
|
|
1567
1602
|
await runTunnelCapturingLogs();
|
|
1568
1603
|
|
|
1569
|
-
const [, args] = spawnMock.mock.calls[0]
|
|
1604
|
+
const [, args] = spawnMock.mock.calls[0];
|
|
1570
1605
|
expect(args).not.toContain("-d");
|
|
1571
1606
|
expect(args).not.toContain("--detach");
|
|
1572
1607
|
});
|
|
@@ -1584,7 +1619,7 @@ describe("tunnel --detach", () => {
|
|
|
1584
1619
|
const child = makeFakeChild(4242);
|
|
1585
1620
|
// Listeners attach synchronously right after spawn() returns; defer the
|
|
1586
1621
|
// exit so they're in place before it fires.
|
|
1587
|
-
setTimeout(() =>
|
|
1622
|
+
setTimeout(() => child.emit("exit", 1), 0);
|
|
1588
1623
|
return child;
|
|
1589
1624
|
});
|
|
1590
1625
|
|