@vellumai/cli 0.8.6 → 0.8.7-dev.202606052118.34cd356
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/bun.lock +8 -0
- package/knip.json +5 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +22 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +511 -11
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +89 -17
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +16 -0
- package/src/commands/retire.ts +20 -47
- package/src/commands/sleep.ts +7 -0
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/wake.ts +7 -0
- package/src/components/DefaultMainScreen.tsx +100 -14
- package/src/index.ts +16 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +15 -3
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +25 -1
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +11 -35
- package/src/lib/guardian-token.ts +132 -9
- package/src/lib/hatch-local.ts +73 -33
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +20 -6
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `vellum client --token <jwt> --url <gateway>`: an ephemeral session
|
|
3
|
+
* that authenticates with a handed-over token and needs no lockfile entry.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
test,
|
|
12
|
+
} from "bun:test";
|
|
13
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
// NB: do NOT mock.module("../lib/guardian-token.js") here — that mock is
|
|
18
|
+
// process-global in Bun and leaks into other test files (it dropped the
|
|
19
|
+
// module's other exports and broke guardian-token-paths / setup suites in CI).
|
|
20
|
+
// The "--token skips the credential lookup" behavior is enforced structurally
|
|
21
|
+
// in parseArgs (the lookup is gated on the override) and reviewed there.
|
|
22
|
+
|
|
23
|
+
// An EMPTY temp dir so there is no lockfile entry — the ephemeral path must
|
|
24
|
+
// work without one. Env mutation is scoped to each test and restored after, so
|
|
25
|
+
// it can't leak into other test files in the same Bun run.
|
|
26
|
+
const testDir = mkdtempSync(join(tmpdir(), "client-token-test-"));
|
|
27
|
+
const ORIGINAL_LOCKFILE_DIR = process.env.VELLUM_LOCKFILE_DIR;
|
|
28
|
+
const ORIGINAL_ARGV = [...process.argv];
|
|
29
|
+
|
|
30
|
+
import { parseArgs } from "../commands/client.js";
|
|
31
|
+
|
|
32
|
+
// A clearly non-local URL so maybeSwapToLocalhost won't rewrite it to 127.0.0.1.
|
|
33
|
+
const REMOTE_URL = "http://192.0.2.50:7830";
|
|
34
|
+
|
|
35
|
+
describe("client --token (ephemeral)", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
process.argv = [...ORIGINAL_ARGV];
|
|
42
|
+
if (ORIGINAL_LOCKFILE_DIR === undefined) {
|
|
43
|
+
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
44
|
+
} else {
|
|
45
|
+
process.env.VELLUM_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("--url + --token resolves to a bearer session with no lockfile entry", () => {
|
|
54
|
+
process.argv = [
|
|
55
|
+
"bun",
|
|
56
|
+
"vellum",
|
|
57
|
+
"client",
|
|
58
|
+
"--url",
|
|
59
|
+
REMOTE_URL,
|
|
60
|
+
"--token",
|
|
61
|
+
"test-jwt-token",
|
|
62
|
+
];
|
|
63
|
+
const parsed = parseArgs();
|
|
64
|
+
|
|
65
|
+
expect(parsed.runtimeUrl).toBe(REMOTE_URL);
|
|
66
|
+
expect(parsed.assistantId).toBe("self"); // DAEMON_INTERNAL_ASSISTANT_ID
|
|
67
|
+
expect(parsed.bearerToken).toBe("test-jwt-token");
|
|
68
|
+
expect(parsed.platformToken).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("--assistant-id overrides the default 'self' segment", () => {
|
|
72
|
+
process.argv = [
|
|
73
|
+
"bun",
|
|
74
|
+
"vellum",
|
|
75
|
+
"client",
|
|
76
|
+
"--url",
|
|
77
|
+
REMOTE_URL,
|
|
78
|
+
"--token",
|
|
79
|
+
"tok",
|
|
80
|
+
"--assistant-id",
|
|
81
|
+
"remote-xyz",
|
|
82
|
+
];
|
|
83
|
+
const parsed = parseArgs();
|
|
84
|
+
expect(parsed.assistantId).toBe("remote-xyz");
|
|
85
|
+
expect(parsed.bearerToken).toBe("tok");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for resolveFreshBearerToken: the `vellum client` TUI proactively
|
|
3
|
+
* refreshes a stale STORED guardian token at startup, while leaving platform
|
|
4
|
+
* session auth, ephemeral --token overrides, and still-fresh tokens untouched.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const ORIGINAL_XDG = process.env.XDG_CONFIG_HOME;
|
|
12
|
+
const ORIGINAL_ENV = process.env.VELLUM_ENVIRONMENT;
|
|
13
|
+
const ORIGINAL_FETCH = globalThis.fetch;
|
|
14
|
+
|
|
15
|
+
import { resolveFreshBearerToken } from "../commands/client.js";
|
|
16
|
+
import { saveGuardianToken } from "../lib/guardian-token.js";
|
|
17
|
+
|
|
18
|
+
const RUNTIME = "http://10.0.0.9:7830";
|
|
19
|
+
const past = () => new Date(Date.now() - 60_000).toISOString();
|
|
20
|
+
const future = () => new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
21
|
+
|
|
22
|
+
function seed(opts: {
|
|
23
|
+
accessToken: string;
|
|
24
|
+
refreshToken: string;
|
|
25
|
+
refreshAfter: string;
|
|
26
|
+
}): void {
|
|
27
|
+
saveGuardianToken("px", {
|
|
28
|
+
guardianPrincipalId: "imported",
|
|
29
|
+
accessToken: opts.accessToken,
|
|
30
|
+
accessTokenExpiresAt: future(),
|
|
31
|
+
refreshToken: opts.refreshToken,
|
|
32
|
+
refreshTokenExpiresAt: future(),
|
|
33
|
+
refreshAfter: opts.refreshAfter,
|
|
34
|
+
isNew: false,
|
|
35
|
+
deviceId: "dev",
|
|
36
|
+
leasedAt: new Date().toISOString(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Stub global fetch; returns whether the refresh endpoint was hit. */
|
|
41
|
+
function stubRefresh(ok: boolean): { hit: () => boolean } {
|
|
42
|
+
let called = false;
|
|
43
|
+
globalThis.fetch = (async (url: unknown, _init?: RequestInit) => {
|
|
44
|
+
if (String(url).includes("/v1/guardian/refresh")) {
|
|
45
|
+
called = true;
|
|
46
|
+
return new Response(
|
|
47
|
+
ok ? JSON.stringify({ accessToken: "new-acc" }) : "nope",
|
|
48
|
+
{
|
|
49
|
+
status: ok ? 200 : 401,
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return new Response("", { status: 200 });
|
|
55
|
+
}) as typeof fetch;
|
|
56
|
+
return { hit: () => called };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("resolveFreshBearerToken", () => {
|
|
60
|
+
let tempHome: string;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tempHome = mkdtempSync(join(tmpdir(), "client-tui-refresh-test-"));
|
|
64
|
+
process.env.XDG_CONFIG_HOME = tempHome;
|
|
65
|
+
delete process.env.VELLUM_ENVIRONMENT; // prod config dir
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
globalThis.fetch = ORIGINAL_FETCH;
|
|
70
|
+
if (ORIGINAL_XDG === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
71
|
+
else process.env.XDG_CONFIG_HOME = ORIGINAL_XDG;
|
|
72
|
+
if (ORIGINAL_ENV === undefined) delete process.env.VELLUM_ENVIRONMENT;
|
|
73
|
+
else process.env.VELLUM_ENVIRONMENT = ORIGINAL_ENV;
|
|
74
|
+
rmSync(tempHome, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("refreshes a stale stored token and returns the new access token", async () => {
|
|
78
|
+
seed({ accessToken: "old-acc", refreshToken: "ref", refreshAfter: past() });
|
|
79
|
+
const refresh = stubRefresh(true);
|
|
80
|
+
|
|
81
|
+
const token = await resolveFreshBearerToken(
|
|
82
|
+
RUNTIME,
|
|
83
|
+
"px",
|
|
84
|
+
"old-acc",
|
|
85
|
+
"paired",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(token).toBe("new-acc");
|
|
89
|
+
expect(refresh.hit()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("leaves a still-fresh stored token unchanged (no refresh)", async () => {
|
|
93
|
+
seed({
|
|
94
|
+
accessToken: "old-acc",
|
|
95
|
+
refreshToken: "ref",
|
|
96
|
+
refreshAfter: future(),
|
|
97
|
+
});
|
|
98
|
+
const refresh = stubRefresh(true);
|
|
99
|
+
|
|
100
|
+
const token = await resolveFreshBearerToken(
|
|
101
|
+
RUNTIME,
|
|
102
|
+
"px",
|
|
103
|
+
"old-acc",
|
|
104
|
+
"paired",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(token).toBe("old-acc");
|
|
108
|
+
expect(refresh.hit()).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("does not refresh an ephemeral --token (mismatches the store)", async () => {
|
|
112
|
+
seed({ accessToken: "old-acc", refreshToken: "ref", refreshAfter: past() });
|
|
113
|
+
const refresh = stubRefresh(true);
|
|
114
|
+
|
|
115
|
+
// bearerToken differs from the stored accessToken => ephemeral override.
|
|
116
|
+
const token = await resolveFreshBearerToken(
|
|
117
|
+
RUNTIME,
|
|
118
|
+
"px",
|
|
119
|
+
"ephemeral-tok",
|
|
120
|
+
"paired",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(token).toBe("ephemeral-tok");
|
|
124
|
+
expect(refresh.hit()).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("never refreshes on the platform session-auth path", async () => {
|
|
128
|
+
seed({ accessToken: "old-acc", refreshToken: "ref", refreshAfter: past() });
|
|
129
|
+
const refresh = stubRefresh(true);
|
|
130
|
+
|
|
131
|
+
const token = await resolveFreshBearerToken(
|
|
132
|
+
RUNTIME,
|
|
133
|
+
"px",
|
|
134
|
+
"old-acc",
|
|
135
|
+
"vellum",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(token).toBe("old-acc");
|
|
139
|
+
expect(refresh.hit()).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("falls back to the existing token when refresh fails", async () => {
|
|
143
|
+
seed({ accessToken: "old-acc", refreshToken: "ref", refreshAfter: past() });
|
|
144
|
+
stubRefresh(false); // refresh endpoint returns non-ok
|
|
145
|
+
|
|
146
|
+
const token = await resolveFreshBearerToken(
|
|
147
|
+
RUNTIME,
|
|
148
|
+
"px",
|
|
149
|
+
"old-acc",
|
|
150
|
+
"paired",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(token).toBe("old-acc");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("does not refresh an access-only stored token (no refresh credential)", async () => {
|
|
157
|
+
seed({ accessToken: "old-acc", refreshToken: "", refreshAfter: past() });
|
|
158
|
+
const refresh = stubRefresh(true);
|
|
159
|
+
|
|
160
|
+
const token = await resolveFreshBearerToken(
|
|
161
|
+
RUNTIME,
|
|
162
|
+
"px",
|
|
163
|
+
"old-acc",
|
|
164
|
+
"paired",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(token).toBe("old-acc");
|
|
168
|
+
expect(refresh.hit()).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import type { ChildProcess } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
import { waitForCloudflareTunnelUrl } from "../lib/cloudflare-tunnel.js";
|
|
6
|
+
|
|
7
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a minimal fake ChildProcess whose stdout and stderr are plain
|
|
11
|
+
* EventEmitters. Tests control what data is emitted and when.
|
|
12
|
+
*/
|
|
13
|
+
function makeChild(): {
|
|
14
|
+
child: ChildProcess;
|
|
15
|
+
stdout: EventEmitter;
|
|
16
|
+
stderr: EventEmitter;
|
|
17
|
+
emitExit: (code: number | null) => void;
|
|
18
|
+
} {
|
|
19
|
+
const stdout = new EventEmitter();
|
|
20
|
+
const stderr = new EventEmitter();
|
|
21
|
+
const childEmitter = new EventEmitter();
|
|
22
|
+
|
|
23
|
+
const child = Object.assign(childEmitter, {
|
|
24
|
+
stdout,
|
|
25
|
+
stderr,
|
|
26
|
+
killed: false,
|
|
27
|
+
kill: () => false,
|
|
28
|
+
pid: 99999,
|
|
29
|
+
}) as unknown as ChildProcess;
|
|
30
|
+
|
|
31
|
+
const emitExit = (code: number | null) => childEmitter.emit("exit", code);
|
|
32
|
+
|
|
33
|
+
return { child, stdout, stderr, emitExit };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("waitForCloudflareTunnelUrl", () => {
|
|
39
|
+
test("resolves when the URL appears on stderr", async () => {
|
|
40
|
+
const { child, stderr } = makeChild();
|
|
41
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
42
|
+
|
|
43
|
+
stderr.emit(
|
|
44
|
+
"data",
|
|
45
|
+
Buffer.from(
|
|
46
|
+
"2024-01-01T00:00:00Z INF | https://quick-slug-test.trycloudflare.com |\n",
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await expect(promise).resolves.toBe(
|
|
51
|
+
"https://quick-slug-test.trycloudflare.com",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("resolves when the URL appears on stdout", async () => {
|
|
56
|
+
const { child, stdout } = makeChild();
|
|
57
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
58
|
+
|
|
59
|
+
stdout.emit(
|
|
60
|
+
"data",
|
|
61
|
+
Buffer.from("https://another-slug.trycloudflare.com\n"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await expect(promise).resolves.toBe(
|
|
65
|
+
"https://another-slug.trycloudflare.com",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("resolves with the first URL when multiple lines contain one", async () => {
|
|
70
|
+
const { child, stderr } = makeChild();
|
|
71
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
72
|
+
|
|
73
|
+
stderr.emit(
|
|
74
|
+
"data",
|
|
75
|
+
Buffer.from(
|
|
76
|
+
"INFO https://first-slug.trycloudflare.com\nINFO https://second-slug.trycloudflare.com\n",
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await expect(promise).resolves.toBe("https://first-slug.trycloudflare.com");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("handles a URL split across two data chunks", async () => {
|
|
84
|
+
const { child, stderr } = makeChild();
|
|
85
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
86
|
+
|
|
87
|
+
// First chunk ends mid-line before the URL
|
|
88
|
+
stderr.emit("data", Buffer.from("INFO Visit: "));
|
|
89
|
+
// Second chunk completes the line
|
|
90
|
+
stderr.emit(
|
|
91
|
+
"data",
|
|
92
|
+
Buffer.from("https://chunked-slug.trycloudflare.com\n"),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await expect(promise).resolves.toBe(
|
|
96
|
+
"https://chunked-slug.trycloudflare.com",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("rejects when the process exits before a URL is found", async () => {
|
|
101
|
+
const { child, emitExit } = makeChild();
|
|
102
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
103
|
+
|
|
104
|
+
emitExit(1);
|
|
105
|
+
|
|
106
|
+
await expect(promise).rejects.toThrow("exited with code 1");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("rejects on null exit code (killed by signal)", async () => {
|
|
110
|
+
const { child, emitExit } = makeChild();
|
|
111
|
+
const promise = waitForCloudflareTunnelUrl(child);
|
|
112
|
+
|
|
113
|
+
emitExit(null);
|
|
114
|
+
|
|
115
|
+
await expect(promise).rejects.toThrow("exited with code unknown");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("rejects after the timeout when no URL appears", async () => {
|
|
119
|
+
const { child } = makeChild();
|
|
120
|
+
// Use a very short timeout so the test runs fast
|
|
121
|
+
const promise = waitForCloudflareTunnelUrl(child, 50);
|
|
122
|
+
|
|
123
|
+
await expect(promise).rejects.toThrow("did not appear within");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("does not resolve for non-trycloudflare.com hostnames", async () => {
|
|
127
|
+
const { child, stderr, emitExit } = makeChild();
|
|
128
|
+
const promise = waitForCloudflareTunnelUrl(child, 50);
|
|
129
|
+
|
|
130
|
+
// Emit a line with a URL that is not a Cloudflare quick-tunnel URL
|
|
131
|
+
stderr.emit("data", Buffer.from("Connecting to https://example.com/api\n"));
|
|
132
|
+
|
|
133
|
+
// Should still time out — the fake URL does not match the pattern
|
|
134
|
+
emitExit(null);
|
|
135
|
+
await expect(promise).rejects.toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|