@vellumai/cli 0.8.7 → 0.8.8-dev.202606060043.60454ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@vellumai/local-mode/package.json +2 -1
- 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 +15 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +15 -8
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +42 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +22 -4
- package/node_modules/@vellumai/local-mode/src/index.ts +26 -4
- 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 +9 -7
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/package.json +1 -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__/guardian-token.test.ts +126 -2
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +115 -26
- 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/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 +84 -13
- package/src/index.ts +16 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +12 -0
- 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/resolve.ts +9 -30
- package/src/lib/guardian-token.ts +120 -4
- package/src/lib/local.ts +20 -6
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `vellum connect import <blob>`: decode a `vellum pair` bundle and
|
|
3
|
+
* persist a lockfile entry + guardian token under a unique local id.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
spyOn,
|
|
12
|
+
test,
|
|
13
|
+
} from "bun:test";
|
|
14
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const testDir = mkdtempSync(join(tmpdir(), "connect-import-test-"));
|
|
19
|
+
const ORIGINAL_LOCKFILE_DIR = process.env.VELLUM_LOCKFILE_DIR;
|
|
20
|
+
const ORIGINAL_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
|
|
21
|
+
const ORIGINAL_ARGV = [...process.argv];
|
|
22
|
+
|
|
23
|
+
import { connectImport } from "../commands/connect/import.js";
|
|
24
|
+
import {
|
|
25
|
+
findAssistantByName,
|
|
26
|
+
saveAssistantEntry,
|
|
27
|
+
} from "../lib/assistant-config.js";
|
|
28
|
+
import { loadGuardianToken } from "../lib/guardian-token.js";
|
|
29
|
+
|
|
30
|
+
function bundleFor(overrides: Record<string, unknown> = {}): string {
|
|
31
|
+
const obj = {
|
|
32
|
+
gatewayUrl: "http://10.0.0.5:7830",
|
|
33
|
+
assistantId: "self",
|
|
34
|
+
token: "test-token",
|
|
35
|
+
deviceId: "dev-aaa",
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
return Buffer.from(JSON.stringify(obj)).toString("base64");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("connect import", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
44
|
+
process.env.XDG_CONFIG_HOME = testDir;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
process.argv = [...ORIGINAL_ARGV];
|
|
49
|
+
if (ORIGINAL_LOCKFILE_DIR === undefined)
|
|
50
|
+
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
51
|
+
else process.env.VELLUM_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
|
|
52
|
+
if (ORIGINAL_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
53
|
+
else process.env.XDG_CONFIG_HOME = ORIGINAL_CONFIG_HOME;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("writes a lockfile entry + guardian token from a valid bundle", async () => {
|
|
61
|
+
process.argv = ["bun", "vellum", "connect", "import", bundleFor()];
|
|
62
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
63
|
+
try {
|
|
64
|
+
await connectImport();
|
|
65
|
+
} finally {
|
|
66
|
+
logSpy.mockRestore();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const entry = findAssistantByName("paired-dev-aaa");
|
|
70
|
+
expect(entry).not.toBeNull();
|
|
71
|
+
expect(entry!.runtimeUrl).toBe("http://10.0.0.5:7830");
|
|
72
|
+
expect(entry!.cloud).toBe("paired");
|
|
73
|
+
expect(loadGuardianToken("paired-dev-aaa")?.accessToken).toBe("test-token");
|
|
74
|
+
// Back-compat: a bundle without refresh fields imports access-only.
|
|
75
|
+
expect(loadGuardianToken("paired-dev-aaa")?.refreshToken).toBe("");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("persists the refresh credential when the bundle carries one", async () => {
|
|
79
|
+
process.argv = [
|
|
80
|
+
"bun",
|
|
81
|
+
"vellum",
|
|
82
|
+
"connect",
|
|
83
|
+
"import",
|
|
84
|
+
bundleFor({
|
|
85
|
+
deviceId: "dev-refresh",
|
|
86
|
+
token: "acc-tok",
|
|
87
|
+
refreshToken: "refresh-tok",
|
|
88
|
+
refreshTokenExpiresAt: "2027-01-01T00:00:00.000Z",
|
|
89
|
+
refreshAfter: "2026-07-01T00:00:00.000Z",
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
93
|
+
try {
|
|
94
|
+
await connectImport();
|
|
95
|
+
} finally {
|
|
96
|
+
logSpy.mockRestore();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tok = loadGuardianToken("paired-dev-refresh");
|
|
100
|
+
expect(tok?.accessToken).toBe("acc-tok");
|
|
101
|
+
expect(tok?.refreshToken).toBe("refresh-tok");
|
|
102
|
+
expect(tok?.refreshTokenExpiresAt).toBe("2027-01-01T00:00:00.000Z");
|
|
103
|
+
expect(tok?.refreshAfter).toBe("2026-07-01T00:00:00.000Z");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("preserves a numeric (epoch-ms) refreshTokenExpiresAt", async () => {
|
|
107
|
+
// GuardianTokenData allows refreshTokenExpiresAt to be an epoch-ms number;
|
|
108
|
+
// a numeric value in the bundle must round-trip, not be dropped to 0.
|
|
109
|
+
const expiresMs = 1893456000000; // 2030-01-01
|
|
110
|
+
process.argv = [
|
|
111
|
+
"bun",
|
|
112
|
+
"vellum",
|
|
113
|
+
"connect",
|
|
114
|
+
"import",
|
|
115
|
+
bundleFor({
|
|
116
|
+
deviceId: "dev-num",
|
|
117
|
+
refreshToken: "refresh-tok",
|
|
118
|
+
refreshTokenExpiresAt: expiresMs,
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
121
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
122
|
+
try {
|
|
123
|
+
await connectImport();
|
|
124
|
+
} finally {
|
|
125
|
+
logSpy.mockRestore();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
expect(loadGuardianToken("paired-dev-num")?.refreshTokenExpiresAt).toBe(
|
|
129
|
+
expiresMs,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("two different bundles (both assistantId 'self') do not collide", async () => {
|
|
134
|
+
process.argv = [
|
|
135
|
+
"bun",
|
|
136
|
+
"vellum",
|
|
137
|
+
"connect",
|
|
138
|
+
"import",
|
|
139
|
+
bundleFor({ deviceId: "dev-one", token: "tok1" }),
|
|
140
|
+
];
|
|
141
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
142
|
+
try {
|
|
143
|
+
await connectImport();
|
|
144
|
+
process.argv = [
|
|
145
|
+
"bun",
|
|
146
|
+
"vellum",
|
|
147
|
+
"connect",
|
|
148
|
+
"import",
|
|
149
|
+
bundleFor({ deviceId: "dev-two", token: "tok2" }),
|
|
150
|
+
];
|
|
151
|
+
await connectImport();
|
|
152
|
+
} finally {
|
|
153
|
+
logSpy.mockRestore();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
expect(findAssistantByName("paired-dev-one")).not.toBeNull();
|
|
157
|
+
expect(findAssistantByName("paired-dev-two")).not.toBeNull();
|
|
158
|
+
expect(loadGuardianToken("paired-dev-one")?.accessToken).toBe("tok1");
|
|
159
|
+
expect(loadGuardianToken("paired-dev-two")?.accessToken).toBe("tok2");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("--name registers the entry under that name", async () => {
|
|
163
|
+
process.argv = [
|
|
164
|
+
"bun",
|
|
165
|
+
"vellum",
|
|
166
|
+
"connect",
|
|
167
|
+
"import",
|
|
168
|
+
bundleFor({ deviceId: "dev-named" }),
|
|
169
|
+
"--name",
|
|
170
|
+
"Desk Box",
|
|
171
|
+
];
|
|
172
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
173
|
+
try {
|
|
174
|
+
await connectImport();
|
|
175
|
+
} finally {
|
|
176
|
+
logSpy.mockRestore();
|
|
177
|
+
}
|
|
178
|
+
// Slugified to a stable id.
|
|
179
|
+
expect(findAssistantByName("desk-box")).not.toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("sanitizes a malicious bundle deviceId (no path traversal in the local id)", async () => {
|
|
183
|
+
process.argv = [
|
|
184
|
+
"bun",
|
|
185
|
+
"vellum",
|
|
186
|
+
"connect",
|
|
187
|
+
"import",
|
|
188
|
+
bundleFor({ deviceId: "-/../../tmp/x", token: "tokX" }),
|
|
189
|
+
];
|
|
190
|
+
const logs: string[] = [];
|
|
191
|
+
const logSpy = spyOn(console, "log").mockImplementation(
|
|
192
|
+
(...a: unknown[]) => {
|
|
193
|
+
logs.push(a.join(" "));
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
try {
|
|
197
|
+
await connectImport();
|
|
198
|
+
} finally {
|
|
199
|
+
logSpy.mockRestore();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The registered id must contain no path separators or `..`.
|
|
203
|
+
const m = logs.join("\n").match(/paired assistant '([^']+)'/);
|
|
204
|
+
expect(m).not.toBeNull();
|
|
205
|
+
const id = m![1];
|
|
206
|
+
expect(id).not.toContain("/");
|
|
207
|
+
expect(id).not.toContain("..");
|
|
208
|
+
expect(loadGuardianToken(id)?.accessToken).toBe("tokX");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("does not overwrite an existing non-paired assistant", async () => {
|
|
212
|
+
saveAssistantEntry({
|
|
213
|
+
assistantId: "desk",
|
|
214
|
+
name: "Desk",
|
|
215
|
+
runtimeUrl: "http://127.0.0.1:7830",
|
|
216
|
+
cloud: "local",
|
|
217
|
+
species: "vellum",
|
|
218
|
+
});
|
|
219
|
+
process.argv = [
|
|
220
|
+
"bun",
|
|
221
|
+
"vellum",
|
|
222
|
+
"connect",
|
|
223
|
+
"import",
|
|
224
|
+
bundleFor({ deviceId: "dx" }),
|
|
225
|
+
"--name",
|
|
226
|
+
"desk",
|
|
227
|
+
];
|
|
228
|
+
const errSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
229
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
|
|
230
|
+
throw new Error(`exit:${c}`);
|
|
231
|
+
}) as never);
|
|
232
|
+
let exited = false;
|
|
233
|
+
try {
|
|
234
|
+
await connectImport();
|
|
235
|
+
} catch (e) {
|
|
236
|
+
exited = (e as Error).message === "exit:1";
|
|
237
|
+
} finally {
|
|
238
|
+
errSpy.mockRestore();
|
|
239
|
+
exitSpy.mockRestore();
|
|
240
|
+
}
|
|
241
|
+
expect(exited).toBe(true);
|
|
242
|
+
// The original local assistant is untouched (not overwritten).
|
|
243
|
+
const e = findAssistantByName("desk");
|
|
244
|
+
expect(e!.runtimeUrl).toBe("http://127.0.0.1:7830");
|
|
245
|
+
expect(e!.paired).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("re-importing the same pairing updates in place", async () => {
|
|
249
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
250
|
+
try {
|
|
251
|
+
process.argv = [
|
|
252
|
+
"bun",
|
|
253
|
+
"vellum",
|
|
254
|
+
"connect",
|
|
255
|
+
"import",
|
|
256
|
+
bundleFor({ deviceId: "dev-re", token: "t1" }),
|
|
257
|
+
];
|
|
258
|
+
await connectImport();
|
|
259
|
+
process.argv = [
|
|
260
|
+
"bun",
|
|
261
|
+
"vellum",
|
|
262
|
+
"connect",
|
|
263
|
+
"import",
|
|
264
|
+
bundleFor({ deviceId: "dev-re", token: "t2" }),
|
|
265
|
+
];
|
|
266
|
+
await connectImport();
|
|
267
|
+
} finally {
|
|
268
|
+
logSpy.mockRestore();
|
|
269
|
+
}
|
|
270
|
+
expect(loadGuardianToken("paired-dev-re")?.accessToken).toBe("t2");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("rejects a bundle whose gatewayUrl is not http(s)", async () => {
|
|
274
|
+
process.argv = [
|
|
275
|
+
"bun",
|
|
276
|
+
"vellum",
|
|
277
|
+
"connect",
|
|
278
|
+
"import",
|
|
279
|
+
bundleFor({ gatewayUrl: "ftp://nope", deviceId: "dz" }),
|
|
280
|
+
];
|
|
281
|
+
const errSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
282
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
|
|
283
|
+
throw new Error(`exit:${c}`);
|
|
284
|
+
}) as never);
|
|
285
|
+
let exited = false;
|
|
286
|
+
try {
|
|
287
|
+
await connectImport();
|
|
288
|
+
} catch (e) {
|
|
289
|
+
exited = (e as Error).message === "exit:1";
|
|
290
|
+
} finally {
|
|
291
|
+
errSpy.mockRestore();
|
|
292
|
+
exitSpy.mockRestore();
|
|
293
|
+
}
|
|
294
|
+
expect(exited).toBe(true);
|
|
295
|
+
expect(findAssistantByName("paired-dz")).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a malformed bundle exits 1 and registers nothing", async () => {
|
|
299
|
+
process.argv = ["bun", "vellum", "connect", "import", "not-valid-base64!!"];
|
|
300
|
+
const errSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
301
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
|
|
302
|
+
throw new Error(`exit:${c}`);
|
|
303
|
+
}) as never);
|
|
304
|
+
let exited = false;
|
|
305
|
+
try {
|
|
306
|
+
await connectImport();
|
|
307
|
+
} catch (e) {
|
|
308
|
+
exited = (e as Error).message === "exit:1";
|
|
309
|
+
} finally {
|
|
310
|
+
errSpy.mockRestore();
|
|
311
|
+
exitSpy.mockRestore();
|
|
312
|
+
}
|
|
313
|
+
expect(exited).toBe(true);
|
|
314
|
+
// A malformed bundle has no deviceId, so no `paired-*` entry is created.
|
|
315
|
+
expect(findAssistantByName("paired-")).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
});
|