@vellumai/cli 0.10.6-dev.202607071937.200b337 → 0.10.6-dev.202607072040.fb971ce
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
CHANGED
|
@@ -30,12 +30,14 @@ const realLocal = {
|
|
|
30
30
|
generateLocalSigningKey: localModule.generateLocalSigningKey,
|
|
31
31
|
startLocalDaemon: localModule.startLocalDaemon,
|
|
32
32
|
startGateway: localModule.startGateway,
|
|
33
|
+
startCes: localModule.startCes,
|
|
33
34
|
};
|
|
34
35
|
const realExec = stepRunnerModule.exec;
|
|
35
36
|
|
|
36
|
-
// Prevent real daemon / gateway from starting
|
|
37
|
+
// Prevent real daemon / gateway / CES from starting
|
|
37
38
|
const startLocalDaemonMock = mock(async () => {});
|
|
38
39
|
const startGatewayMock = mock(async () => {});
|
|
40
|
+
const startCesMock = mock(async () => {});
|
|
39
41
|
|
|
40
42
|
// Capture exec calls without running real tar
|
|
41
43
|
const execMock = mock(async (_cmd: string, _args: string[]) => {});
|
|
@@ -45,6 +47,7 @@ beforeAll(() => {
|
|
|
45
47
|
generateLocalSigningKey: () => "deadbeefdeadbeefdeadbeefdeadbeef",
|
|
46
48
|
startLocalDaemon: startLocalDaemonMock,
|
|
47
49
|
startGateway: startGatewayMock,
|
|
50
|
+
startCes: startCesMock,
|
|
48
51
|
}));
|
|
49
52
|
mock.module("../lib/step-runner.js", () => ({ exec: execMock }));
|
|
50
53
|
});
|
|
@@ -128,6 +131,7 @@ beforeEach(() => {
|
|
|
128
131
|
execMock.mockClear();
|
|
129
132
|
startLocalDaemonMock.mockClear();
|
|
130
133
|
startGatewayMock.mockClear();
|
|
134
|
+
startCesMock.mockClear();
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
afterEach(() => {
|
|
@@ -119,6 +119,7 @@ const startGatewayMock = mock<typeof local.startGateway>(
|
|
|
119
119
|
const stopLocalProcessesMock = mock<typeof local.stopLocalProcesses>(
|
|
120
120
|
async () => {},
|
|
121
121
|
);
|
|
122
|
+
const startCesMock = mock<typeof local.startCes>(async () => {});
|
|
122
123
|
|
|
123
124
|
mock.module("../lib/local.js", () => ({
|
|
124
125
|
...realLocal,
|
|
@@ -127,6 +128,7 @@ mock.module("../lib/local.js", () => ({
|
|
|
127
128
|
startLocalDaemon: startLocalDaemonMock,
|
|
128
129
|
startGateway: startGatewayMock,
|
|
129
130
|
stopLocalProcesses: stopLocalProcessesMock,
|
|
131
|
+
startCes: startCesMock,
|
|
130
132
|
}));
|
|
131
133
|
|
|
132
134
|
const loopbackSafeFetchMock = mock<typeof loopbackFetch.loopbackSafeFetch>(
|
|
@@ -256,6 +258,8 @@ beforeEach(() => {
|
|
|
256
258
|
startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
|
|
257
259
|
stopLocalProcessesMock.mockReset();
|
|
258
260
|
stopLocalProcessesMock.mockResolvedValue(undefined);
|
|
261
|
+
startCesMock.mockReset();
|
|
262
|
+
startCesMock.mockResolvedValue(undefined);
|
|
259
263
|
loopbackSafeFetchMock.mockReset();
|
|
260
264
|
loopbackSafeFetchMock.mockResolvedValue({
|
|
261
265
|
ok: true,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import { startCes } from "../local.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mocks
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
// Capture spawn calls so we can assert on cmd/env.
|
|
13
|
+
let lastSpawnCall: {
|
|
14
|
+
cmd: string[];
|
|
15
|
+
options: { detached?: boolean; env?: Record<string, string | undefined>; cwd?: string };
|
|
16
|
+
} | null = null;
|
|
17
|
+
|
|
18
|
+
mock.module("node:child_process", () => ({
|
|
19
|
+
spawn: mock((cmd: string, args: string[], options: object) => {
|
|
20
|
+
lastSpawnCall = { cmd: [cmd, ...args], options };
|
|
21
|
+
// Return a fake subprocess with a pid and no-op methods.
|
|
22
|
+
return {
|
|
23
|
+
pid: 42,
|
|
24
|
+
unref: () => {},
|
|
25
|
+
stdout: { on: () => {} },
|
|
26
|
+
stderr: { on: () => {} },
|
|
27
|
+
on: () => {},
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
execSync: () => "",
|
|
31
|
+
execFileSync: () => "",
|
|
32
|
+
spawnSync: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Mock xdg-log so we don't open real log files.
|
|
36
|
+
mock.module("../xdg-log.js", () => ({
|
|
37
|
+
openLogFile: mock(() => 42),
|
|
38
|
+
pipeToLogFile: mock(() => {}),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock process helpers so stopProcessByPidFile is a no-op.
|
|
42
|
+
mock.module("../process.js", () => ({
|
|
43
|
+
stopProcessByPidFile: mock(async () => {}),
|
|
44
|
+
isProcessAlive: mock(() => false),
|
|
45
|
+
stopProcessGracefully: mock(async () => {}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Tests
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe("startCes", () => {
|
|
53
|
+
let tempDir: string;
|
|
54
|
+
|
|
55
|
+
beforeAll(() => {
|
|
56
|
+
tempDir = mkdtempSync(join(tmpdir(), "ces-test-"));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("spawns CES with correct env vars and writes PID file", async () => {
|
|
64
|
+
// Create the socket path before calling startCes so the wait loop exits.
|
|
65
|
+
// startCes unlinks it first, then waits for it to reappear. We create it
|
|
66
|
+
// after a short delay to simulate CES binding the socket.
|
|
67
|
+
const resources = {
|
|
68
|
+
instanceDir: tempDir,
|
|
69
|
+
name: "test-assistant",
|
|
70
|
+
} as unknown as Parameters<typeof startCes>[1];
|
|
71
|
+
|
|
72
|
+
// Create the socket file asynchronously after startCes unlinks it.
|
|
73
|
+
// We use a small setTimeout to create it during the wait loop.
|
|
74
|
+
const vellumDir = join(tempDir, ".vellum");
|
|
75
|
+
mkdirSync(vellumDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Pre-create the socket so it exists when startCes checks after unlinking.
|
|
78
|
+
// startCes unlinks the stale socket, then polls for it. We create it with
|
|
79
|
+
// a slight delay so the poll catches it.
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
const socketDir = join(vellumDir, "workspace");
|
|
82
|
+
mkdirSync(socketDir, { recursive: true });
|
|
83
|
+
writeFileSync(join(socketDir, "ces.sock"), "");
|
|
84
|
+
}, 50);
|
|
85
|
+
|
|
86
|
+
lastSpawnCall = null;
|
|
87
|
+
await startCes(false, resources);
|
|
88
|
+
|
|
89
|
+
// Verify spawn was called
|
|
90
|
+
expect(lastSpawnCall).not.toBeNull();
|
|
91
|
+
expect(lastSpawnCall!.options.detached).toBe(true);
|
|
92
|
+
|
|
93
|
+
// Verify env vars
|
|
94
|
+
const env = lastSpawnCall!.options.env!;
|
|
95
|
+
expect(env["CES_STANDALONE"]).toBe("1");
|
|
96
|
+
expect(env["CES_LOCAL_SOCKET"]).toBeDefined();
|
|
97
|
+
expect(env["CREDENTIAL_SECURITY_DIR"]).toBeDefined();
|
|
98
|
+
expect(env["VELLUM_WORKSPACE_DIR"]).toBeDefined();
|
|
99
|
+
|
|
100
|
+
// Verify PID file was written
|
|
101
|
+
const cesPidFile = join(vellumDir, "ces.pid");
|
|
102
|
+
expect(existsSync(cesPidFile)).toBe(true);
|
|
103
|
+
}, 15_000);
|
|
104
|
+
});
|
package/src/lib/local.ts
CHANGED
|
@@ -573,14 +573,12 @@ function applyDaemonEnvOverrides(
|
|
|
573
573
|
env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
574
574
|
options.defaultWorkspaceConfigPath;
|
|
575
575
|
}
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
|
|
583
|
-
}
|
|
576
|
+
// Pin the daemon to the exact socket the sibling binds so the two agree
|
|
577
|
+
// regardless of any stale CES_LOCAL_SOCKET inherited from the parent
|
|
578
|
+
// environment. The assistant connects to the sibling instead of spawning
|
|
579
|
+
// its own CES.
|
|
580
|
+
env.CES_STANDALONE = "1";
|
|
581
|
+
env.CES_LOCAL_SOCKET = resolveCesSocketPath(resources);
|
|
584
582
|
applyIpcSocketDirOverride(env);
|
|
585
583
|
}
|
|
586
584
|
|
|
@@ -850,19 +848,9 @@ function resolveCesSocketPath(resources?: LocalInstanceResources): string {
|
|
|
850
848
|
}
|
|
851
849
|
|
|
852
850
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* containerized homes already use.
|
|
857
|
-
*/
|
|
858
|
-
function isCesSiblingOptIn(): boolean {
|
|
859
|
-
return process.env.CES_STANDALONE === "1";
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Launch the local CES sibling over a Unix socket (opted into via
|
|
864
|
-
* `CES_STANDALONE=1`). No-op unless the opt-in is set, in which case the
|
|
865
|
-
* assistant continues to spawn CES itself as today.
|
|
851
|
+
* Launch the local CES sibling over a Unix socket. The sibling model is now
|
|
852
|
+
* the default topology for local (non-containerized) instances, matching how
|
|
853
|
+
* containerized homes already run CES.
|
|
866
854
|
*
|
|
867
855
|
* The sibling runs with `CES_STANDALONE=1` so its lifecycle is anchored to
|
|
868
856
|
* SIGTERM rather than stdin EOF, mirroring the gateway: a CLI-owned process
|
|
@@ -873,8 +861,6 @@ export async function startCes(
|
|
|
873
861
|
watch: boolean = false,
|
|
874
862
|
resources?: LocalInstanceResources,
|
|
875
863
|
): Promise<void> {
|
|
876
|
-
if (!isCesSiblingOptIn()) return;
|
|
877
|
-
|
|
878
864
|
const vellumDir = resources
|
|
879
865
|
? join(resources.instanceDir, ".vellum")
|
|
880
866
|
: join(homedir(), ".vellum");
|