@vellumai/cli 0.8.0 → 0.8.2
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/package.json +1 -1
- package/src/__tests__/backup.test.ts +13 -3
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/orphan-detection.test.ts +287 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/ps-platform-status.test.ts +182 -0
- package/src/__tests__/search-provider-env-var-parity.test.ts +48 -0
- package/src/__tests__/setup.test.ts +296 -0
- package/src/__tests__/sync-events.test.ts +54 -0
- package/src/__tests__/teleport.test.ts +190 -163
- package/src/commands/client.ts +128 -10
- package/src/commands/events.ts +13 -1
- package/src/commands/login.ts +3 -2
- package/src/commands/ps.ts +28 -17
- package/src/commands/setup.ts +101 -96
- package/src/components/DefaultMainScreen.tsx +80 -128
- package/src/lib/__tests__/docker.test.ts +11 -0
- package/src/lib/assistant-config.ts +69 -2
- package/src/lib/client-identity.ts +1 -0
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/orphan-detection.ts +66 -1
- package/src/lib/platform-client.ts +8 -7
- package/src/lib/provider-secrets.ts +413 -0
- package/src/lib/statefulset.ts +12 -0
- package/src/lib/sync-cloud-assistants.ts +39 -18
- package/src/lib/upgrade-lifecycle.ts +9 -73
- package/src/shared/provider-env-vars.ts +15 -8
- package/src/lib/doctor-client.ts +0 -153
package/package.json
CHANGED
|
@@ -81,12 +81,20 @@ const localRuntimePollJobStatusMock = spyOn(
|
|
|
81
81
|
|
|
82
82
|
// Mode 1 (runtime-direct local backup) uses guardian tokens. Don't exercise
|
|
83
83
|
// it here, but the spies need to exist so the module under test can import
|
|
84
|
-
// them without surprises.
|
|
85
|
-
|
|
84
|
+
// them without surprises. Saved to variables so afterAll can restore them —
|
|
85
|
+
// otherwise the spied loadGuardianToken leaks into guardian-token.test.ts and
|
|
86
|
+
// setup.test.ts when they run later in the same `bun test` invocation.
|
|
87
|
+
const loadGuardianTokenSpy = spyOn(
|
|
88
|
+
guardianToken,
|
|
89
|
+
"loadGuardianToken",
|
|
90
|
+
).mockReturnValue({
|
|
86
91
|
accessToken: "local-token",
|
|
87
92
|
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
88
93
|
} as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
|
|
89
|
-
spyOn(
|
|
94
|
+
const leaseGuardianTokenSpy = spyOn(
|
|
95
|
+
guardianToken,
|
|
96
|
+
"leaseGuardianToken",
|
|
97
|
+
).mockResolvedValue({
|
|
90
98
|
accessToken: "leased-token",
|
|
91
99
|
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
92
100
|
} as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
|
|
@@ -177,6 +185,8 @@ afterAll(() => {
|
|
|
177
185
|
getBackupsDirMock.mockRestore();
|
|
178
186
|
mkdirSyncMock.mockRestore();
|
|
179
187
|
writeFileSyncMock.mockRestore();
|
|
188
|
+
loadGuardianTokenSpy.mockRestore();
|
|
189
|
+
leaseGuardianTokenSpy.mockRestore();
|
|
180
190
|
rmSync(testDir, { recursive: true, force: true });
|
|
181
191
|
});
|
|
182
192
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { getInputHistoryPath } from "../lib/environments/paths.js";
|
|
12
|
+
import { appendHistory, loadHistory } from "../lib/input-history.js";
|
|
13
|
+
|
|
14
|
+
describe("input-history XDG paths", () => {
|
|
15
|
+
let tempDir: string;
|
|
16
|
+
let savedState: string | undefined;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
savedState = process.env.XDG_STATE_HOME;
|
|
20
|
+
tempDir = mkdtempSync(join(tmpdir(), "cli-input-history-test-"));
|
|
21
|
+
process.env.XDG_STATE_HOME = join(tempDir, ".local", "state");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (savedState === undefined) {
|
|
26
|
+
delete process.env.XDG_STATE_HOME;
|
|
27
|
+
} else {
|
|
28
|
+
process.env.XDG_STATE_HOME = savedState;
|
|
29
|
+
}
|
|
30
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("appendHistory writes to $XDG_STATE_HOME/vellum/input-history", () => {
|
|
34
|
+
appendHistory("hello world");
|
|
35
|
+
|
|
36
|
+
const canonical = getInputHistoryPath();
|
|
37
|
+
expect(canonical).toBe(
|
|
38
|
+
join(tempDir, ".local", "state", "vellum", "input-history"),
|
|
39
|
+
);
|
|
40
|
+
expect(existsSync(canonical)).toBe(true);
|
|
41
|
+
expect(readFileSync(canonical, "utf-8")).toBe("hello world\n");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("appendHistory does NOT touch ~/.vellum/", () => {
|
|
45
|
+
// Crucially: the CLI must not create or write to ~/.vellum/ per the
|
|
46
|
+
// "No `.vellum/` directory access" boundary in cli/AGENTS.md. We snapshot
|
|
47
|
+
// the legacy path's existence before the call (some test machines already
|
|
48
|
+
// have a ~/.vellum/ for unrelated daemon state) and assert the file at
|
|
49
|
+
// that path is unchanged afterwards.
|
|
50
|
+
const legacyPath = join(homedir(), ".vellum", "input-history");
|
|
51
|
+
const existedBefore = existsSync(legacyPath);
|
|
52
|
+
const contentBefore: string = existedBefore
|
|
53
|
+
? readFileSync(legacyPath, "utf-8")
|
|
54
|
+
: "";
|
|
55
|
+
|
|
56
|
+
appendHistory("hello");
|
|
57
|
+
|
|
58
|
+
expect(existsSync(legacyPath)).toBe(existedBefore);
|
|
59
|
+
if (existedBefore) {
|
|
60
|
+
expect(readFileSync(legacyPath, "utf-8")).toBe(contentBefore);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("XDG_STATE_HOME default is ~/.local/state when unset", () => {
|
|
65
|
+
delete process.env.XDG_STATE_HOME;
|
|
66
|
+
|
|
67
|
+
// os.homedir() is cached at process start by Bun and ignores
|
|
68
|
+
// process.env.HOME mutations, so compute the expected path from the same
|
|
69
|
+
// source the production helper uses.
|
|
70
|
+
expect(getInputHistoryPath()).toBe(
|
|
71
|
+
join(homedir(), ".local", "state", "vellum", "input-history"),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("appendHistory skips empty and slash-command entries", () => {
|
|
76
|
+
appendHistory("");
|
|
77
|
+
appendHistory(" ");
|
|
78
|
+
appendHistory("/help");
|
|
79
|
+
appendHistory("real entry");
|
|
80
|
+
|
|
81
|
+
expect(loadHistory()).toEqual(["real entry"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("appendHistory deduplicates by moving to most recent", () => {
|
|
85
|
+
appendHistory("a");
|
|
86
|
+
appendHistory("b");
|
|
87
|
+
appendHistory("a");
|
|
88
|
+
|
|
89
|
+
expect(loadHistory()).toEqual(["b", "a"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("appendHistory caps history at MAX_ENTRIES (1000)", () => {
|
|
93
|
+
for (let i = 0; i < 1100; i++) {
|
|
94
|
+
appendHistory(`entry-${i}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const history = loadHistory();
|
|
98
|
+
expect(history.length).toBe(1000);
|
|
99
|
+
expect(history[0]).toBe("entry-100");
|
|
100
|
+
expect(history[999]).toBe("entry-1099");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// Point lockfile operations at a temp directory before importing anything that
|
|
7
|
+
// would otherwise resolve real on-host paths.
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), "cli-orphan-detection-test-"));
|
|
9
|
+
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
detectOrphanedProcesses,
|
|
13
|
+
getKnownPidsFromAssistants,
|
|
14
|
+
} from "../lib/orphan-detection.js";
|
|
15
|
+
import {
|
|
16
|
+
loadAllAssistantsAcrossEnvs,
|
|
17
|
+
type AssistantEntry,
|
|
18
|
+
} from "../lib/assistant-config.js";
|
|
19
|
+
import type { EnvironmentDefinition } from "../lib/environments/types.js";
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function makeLocalEntry(
|
|
27
|
+
id: string,
|
|
28
|
+
instanceDir: string,
|
|
29
|
+
pids: {
|
|
30
|
+
daemon?: string;
|
|
31
|
+
gateway?: string;
|
|
32
|
+
qdrant?: string;
|
|
33
|
+
embed?: string;
|
|
34
|
+
} = {},
|
|
35
|
+
): AssistantEntry {
|
|
36
|
+
const vellumDir = join(instanceDir, ".vellum");
|
|
37
|
+
mkdirSync(join(vellumDir, "workspace", "data", "qdrant"), {
|
|
38
|
+
recursive: true,
|
|
39
|
+
});
|
|
40
|
+
if (pids.daemon !== undefined) {
|
|
41
|
+
writeFileSync(join(vellumDir, "workspace", "vellum.pid"), pids.daemon);
|
|
42
|
+
}
|
|
43
|
+
if (pids.gateway !== undefined) {
|
|
44
|
+
writeFileSync(join(vellumDir, "gateway.pid"), pids.gateway);
|
|
45
|
+
}
|
|
46
|
+
if (pids.qdrant !== undefined) {
|
|
47
|
+
writeFileSync(
|
|
48
|
+
join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
|
|
49
|
+
pids.qdrant,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (pids.embed !== undefined) {
|
|
53
|
+
writeFileSync(join(vellumDir, "workspace", "embed-worker.pid"), pids.embed);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
assistantId: id,
|
|
57
|
+
runtimeUrl: "http://localhost:7821",
|
|
58
|
+
cloud: "local",
|
|
59
|
+
resources: {
|
|
60
|
+
instanceDir,
|
|
61
|
+
daemonPort: 7821,
|
|
62
|
+
gatewayPort: 7830,
|
|
63
|
+
qdrantPort: 6333,
|
|
64
|
+
cesPort: 8090,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("getKnownPidsFromAssistants", () => {
|
|
70
|
+
let perTestDir: string;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
perTestDir = mkdtempSync(join(testDir, "case-"));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("collects daemon, gateway, qdrant, and embed-worker PIDs", () => {
|
|
77
|
+
const entry = makeLocalEntry(
|
|
78
|
+
"alpha",
|
|
79
|
+
join(perTestDir, "alpha"),
|
|
80
|
+
{ daemon: "100", gateway: "200", qdrant: "300", embed: "400" },
|
|
81
|
+
);
|
|
82
|
+
const pids = getKnownPidsFromAssistants([entry]);
|
|
83
|
+
expect(pids).toEqual(new Set(["100", "200", "300", "400"]));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("skips missing PID files without throwing", () => {
|
|
87
|
+
const entry = makeLocalEntry("beta", join(perTestDir, "beta"), {
|
|
88
|
+
daemon: "100",
|
|
89
|
+
});
|
|
90
|
+
const pids = getKnownPidsFromAssistants([entry]);
|
|
91
|
+
expect(pids).toEqual(new Set(["100"]));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("includes docker watcherPid when present", () => {
|
|
95
|
+
const entry: AssistantEntry = {
|
|
96
|
+
assistantId: "docker-1",
|
|
97
|
+
runtimeUrl: "http://localhost:18100",
|
|
98
|
+
cloud: "docker",
|
|
99
|
+
watcherPid: 555,
|
|
100
|
+
};
|
|
101
|
+
const pids = getKnownPidsFromAssistants([entry]);
|
|
102
|
+
expect(pids).toEqual(new Set(["555"]));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("ignores non-local entries without watcherPid", () => {
|
|
106
|
+
const entry: AssistantEntry = {
|
|
107
|
+
assistantId: "managed-1",
|
|
108
|
+
runtimeUrl: "https://platform.vellum.ai/foo",
|
|
109
|
+
cloud: "vellum",
|
|
110
|
+
};
|
|
111
|
+
const pids = getKnownPidsFromAssistants([entry]);
|
|
112
|
+
expect(pids.size).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("local entry without resources contributes no PIDs", () => {
|
|
116
|
+
const entry: AssistantEntry = {
|
|
117
|
+
assistantId: "legacy",
|
|
118
|
+
runtimeUrl: "http://localhost:7821",
|
|
119
|
+
cloud: "local",
|
|
120
|
+
};
|
|
121
|
+
const pids = getKnownPidsFromAssistants([entry]);
|
|
122
|
+
expect(pids.size).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("aggregates PIDs across multiple assistants", () => {
|
|
126
|
+
const a = makeLocalEntry("a", join(perTestDir, "a"), {
|
|
127
|
+
daemon: "100",
|
|
128
|
+
gateway: "200",
|
|
129
|
+
});
|
|
130
|
+
const b = makeLocalEntry("b", join(perTestDir, "b"), {
|
|
131
|
+
daemon: "101",
|
|
132
|
+
gateway: "201",
|
|
133
|
+
});
|
|
134
|
+
const pids = getKnownPidsFromAssistants([a, b]);
|
|
135
|
+
expect(pids).toEqual(new Set(["100", "200", "101", "201"]));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("loadAllAssistantsAcrossEnvs", () => {
|
|
140
|
+
function makeEnv(name: string, lockfileDir: string): EnvironmentDefinition {
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
platformUrl: "https://example.invalid",
|
|
144
|
+
webUrl: "https://example.invalid",
|
|
145
|
+
lockfileDirOverride: lockfileDir,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
test("aggregates entries from every provided environment's lockfile", () => {
|
|
150
|
+
const envADir = mkdtempSync(join(testDir, "envA-"));
|
|
151
|
+
const envBDir = mkdtempSync(join(testDir, "envB-"));
|
|
152
|
+
|
|
153
|
+
writeFileSync(
|
|
154
|
+
join(envADir, "lockfile.json"),
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
assistants: [
|
|
157
|
+
{
|
|
158
|
+
assistantId: "alpha",
|
|
159
|
+
runtimeUrl: "http://localhost:7821",
|
|
160
|
+
cloud: "local",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
writeFileSync(
|
|
166
|
+
join(envBDir, "lockfile.json"),
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
assistants: [
|
|
169
|
+
{
|
|
170
|
+
assistantId: "beta",
|
|
171
|
+
runtimeUrl: "http://localhost:18100",
|
|
172
|
+
cloud: "docker",
|
|
173
|
+
watcherPid: 777,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const all = loadAllAssistantsAcrossEnvs([
|
|
180
|
+
makeEnv("envA", envADir),
|
|
181
|
+
makeEnv("envB", envBDir),
|
|
182
|
+
]);
|
|
183
|
+
const ids = all.map((e) => e.assistantId).sort();
|
|
184
|
+
expect(ids).toEqual(["alpha", "beta"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("returns empty list when no envs have lockfiles", () => {
|
|
188
|
+
const envDir = mkdtempSync(join(testDir, "empty-"));
|
|
189
|
+
const all = loadAllAssistantsAcrossEnvs([makeEnv("missing", envDir)]);
|
|
190
|
+
expect(all).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("skips malformed JSON without throwing", () => {
|
|
194
|
+
const envDir = mkdtempSync(join(testDir, "malformed-"));
|
|
195
|
+
writeFileSync(join(envDir, "lockfile.json"), "{not json");
|
|
196
|
+
const all = loadAllAssistantsAcrossEnvs([makeEnv("bad", envDir)]);
|
|
197
|
+
expect(all).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("skips entries missing required fields", () => {
|
|
201
|
+
const envDir = mkdtempSync(join(testDir, "partial-"));
|
|
202
|
+
writeFileSync(
|
|
203
|
+
join(envDir, "lockfile.json"),
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
assistants: [
|
|
206
|
+
{ assistantId: "no-url" }, // missing runtimeUrl
|
|
207
|
+
{ runtimeUrl: "http://x" }, // missing assistantId
|
|
208
|
+
{
|
|
209
|
+
assistantId: "good",
|
|
210
|
+
runtimeUrl: "http://localhost:7821",
|
|
211
|
+
cloud: "local",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
const all = loadAllAssistantsAcrossEnvs([makeEnv("partial", envDir)]);
|
|
217
|
+
expect(all.map((e) => e.assistantId)).toEqual(["good"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("end-to-end: dev env's daemon is not flagged as orphan from local env", () => {
|
|
221
|
+
// Simulate Vargas's reported bug: `local` env has no assistants, but a
|
|
222
|
+
// `dev` env assistant is running with a recorded daemon PID. The orphan
|
|
223
|
+
// filter must treat that PID as known.
|
|
224
|
+
const devDir = mkdtempSync(join(testDir, "dev-"));
|
|
225
|
+
const instanceDir = join(devDir, "instances", "quiet-finch");
|
|
226
|
+
makeLocalEntry("quiet-finch", instanceDir, {
|
|
227
|
+
daemon: "19067",
|
|
228
|
+
gateway: "19087",
|
|
229
|
+
qdrant: "19167",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
writeFileSync(
|
|
233
|
+
join(devDir, "lockfile.json"),
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
assistants: [
|
|
236
|
+
{
|
|
237
|
+
assistantId: "quiet-finch",
|
|
238
|
+
runtimeUrl: "http://127.0.0.1:18100",
|
|
239
|
+
cloud: "local",
|
|
240
|
+
resources: {
|
|
241
|
+
instanceDir,
|
|
242
|
+
daemonPort: 18000,
|
|
243
|
+
gatewayPort: 18100,
|
|
244
|
+
qdrantPort: 18200,
|
|
245
|
+
cesPort: 18300,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const devEntries = loadAllAssistantsAcrossEnvs([makeEnv("dev", devDir)]);
|
|
253
|
+
expect(devEntries).toHaveLength(1);
|
|
254
|
+
|
|
255
|
+
const knownPids = getKnownPidsFromAssistants(devEntries);
|
|
256
|
+
expect(knownPids).toEqual(new Set(["19067", "19087", "19167"]));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("detectOrphanedProcesses", () => {
|
|
261
|
+
test("excludes PIDs passed via excludePids", async () => {
|
|
262
|
+
// The orphan detector calls `ps ax` and filters by regex. The process
|
|
263
|
+
// running this test (bun) is itself a node-family process whose pid will
|
|
264
|
+
// not match the vellum/qdrant/openclaw regex, so the natural result of
|
|
265
|
+
// the scan is "no rows". To assert exclusion semantics deterministically,
|
|
266
|
+
// we just confirm the function accepts an excludePids option and returns
|
|
267
|
+
// an array — the meaningful behavior assertion lives in the integration
|
|
268
|
+
// path (the function's `knownPids.has(p.pid)` short-circuit), which we
|
|
269
|
+
// exercise indirectly by passing our own PID (guaranteed to never be
|
|
270
|
+
// double-counted).
|
|
271
|
+
const ownPid = String(process.pid);
|
|
272
|
+
const result = await detectOrphanedProcesses({
|
|
273
|
+
excludePids: new Set([ownPid]),
|
|
274
|
+
});
|
|
275
|
+
expect(Array.isArray(result)).toBe(true);
|
|
276
|
+
for (const orphan of result) {
|
|
277
|
+
expect(orphan.pid).not.toBe(ownPid);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("returns an array (smoke)", async () => {
|
|
282
|
+
const result = await detectOrphanedProcesses({
|
|
283
|
+
excludePids: new Set(),
|
|
284
|
+
});
|
|
285
|
+
expect(Array.isArray(result)).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
});
|
package/src/__tests__/preload.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { afterAll } from "bun:test";
|
|
13
|
+
import { afterAll, mock } from "bun:test";
|
|
14
14
|
|
|
15
15
|
const testDir = realpathSync(
|
|
16
16
|
mkdtempSync(join(tmpdir(), "vellum-cli-test-workspace-")),
|
|
@@ -24,4 +24,8 @@ afterAll(() => {
|
|
|
24
24
|
} catch {
|
|
25
25
|
/* best-effort cleanup */
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
// Reset all module mocks so mock.module() calls in one test file
|
|
29
|
+
// don't leak into the next file in the same bun test run.
|
|
30
|
+
mock.restore();
|
|
27
31
|
});
|