@hellcoder/companion 0.96.0
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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ─── Hoisted mocks ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const mockExecSync = vi.hoisted(() => vi.fn());
|
|
6
|
+
const mockExistsSync = vi.hoisted(() => vi.fn((_path: string) => false));
|
|
7
|
+
const mockReaddirSync = vi.hoisted(() => vi.fn((_path: string) => [] as string[]));
|
|
8
|
+
const mockHomedir = vi.hoisted(() => vi.fn(() => "/home/testuser"));
|
|
9
|
+
|
|
10
|
+
vi.mock("node:child_process", () => ({ execSync: mockExecSync }));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:fs", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("node:fs")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
existsSync: mockExistsSync,
|
|
17
|
+
readdirSync: mockReaddirSync,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock("node:os", async (importOriginal) => {
|
|
22
|
+
const actual = await importOriginal<typeof import("node:os")>();
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
homedir: mockHomedir,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ─── Import after mocks ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
captureUserShellPath,
|
|
33
|
+
buildFallbackPath,
|
|
34
|
+
getEnrichedPath,
|
|
35
|
+
resolveBinary,
|
|
36
|
+
getServicePath,
|
|
37
|
+
_resetPathCache,
|
|
38
|
+
} from "./path-resolver.js";
|
|
39
|
+
|
|
40
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const originalEnv = { ...process.env };
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
_resetPathCache();
|
|
47
|
+
process.env = { ...originalEnv };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
process.env = originalEnv;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─── captureUserShellPath ───────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe("captureUserShellPath", () => {
|
|
57
|
+
it("extracts PATH from login shell output using sentinel markers", () => {
|
|
58
|
+
mockExecSync.mockReturnValueOnce(
|
|
59
|
+
"___PATH_START___/usr/bin:/home/testuser/.nvm/versions/node/v20/bin:/home/testuser/.cargo/bin___PATH_END___\n",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const result = captureUserShellPath();
|
|
63
|
+
expect(result).toBe(
|
|
64
|
+
"/usr/bin:/home/testuser/.nvm/versions/node/v20/bin:/home/testuser/.cargo/bin",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles noisy shell output (MOTD, warnings) before and after PATH", () => {
|
|
69
|
+
mockExecSync.mockReturnValueOnce(
|
|
70
|
+
"Last login: Mon Jan 1\nWelcome!\n___PATH_START___/usr/local/bin:/usr/bin___PATH_END___\nbye\n",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = captureUserShellPath();
|
|
74
|
+
expect(result).toBe("/usr/local/bin:/usr/bin");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("falls back to buildFallbackPath when shell sourcing fails", () => {
|
|
78
|
+
mockExecSync.mockImplementationOnce(() => {
|
|
79
|
+
throw new Error("shell failed");
|
|
80
|
+
});
|
|
81
|
+
// buildFallbackPath needs existsSync to return true for some dirs
|
|
82
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
83
|
+
p === "/usr/bin" || p === "/bin",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const result = captureUserShellPath();
|
|
87
|
+
expect(result).toContain("/usr/bin");
|
|
88
|
+
expect(result).toContain("/bin");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("falls back when shell output contains no sentinel markers", () => {
|
|
92
|
+
mockExecSync.mockReturnValueOnce("some garbage output\n");
|
|
93
|
+
mockExistsSync.mockImplementation((p: string) => p === "/usr/bin");
|
|
94
|
+
|
|
95
|
+
const result = captureUserShellPath();
|
|
96
|
+
// Should fall back to buildFallbackPath
|
|
97
|
+
expect(result).toContain("/usr/bin");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("uses $SHELL env var for the shell command", () => {
|
|
101
|
+
process.env.SHELL = "/bin/zsh";
|
|
102
|
+
mockExecSync.mockReturnValueOnce(
|
|
103
|
+
"___PATH_START___/usr/bin___PATH_END___\n",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
captureUserShellPath();
|
|
107
|
+
|
|
108
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
109
|
+
expect.stringContaining("/bin/zsh"),
|
|
110
|
+
expect.any(Object),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("defaults to /bin/bash when $SHELL is not set", () => {
|
|
115
|
+
delete process.env.SHELL;
|
|
116
|
+
mockExecSync.mockReturnValueOnce(
|
|
117
|
+
"___PATH_START___/usr/bin___PATH_END___\n",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
captureUserShellPath();
|
|
121
|
+
|
|
122
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining("/bin/bash"),
|
|
124
|
+
expect.any(Object),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ─── buildFallbackPath ──────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
describe("buildFallbackPath", () => {
|
|
132
|
+
it("includes standard system paths when they exist", () => {
|
|
133
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
134
|
+
["/usr/local/bin", "/usr/bin", "/bin"].includes(p as string),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result = buildFallbackPath();
|
|
138
|
+
expect(result).toContain("/usr/local/bin");
|
|
139
|
+
expect(result).toContain("/usr/bin");
|
|
140
|
+
expect(result).toContain("/bin");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("includes ~/.local/bin for claude CLI", () => {
|
|
144
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
145
|
+
p === "/home/testuser/.local/bin" || p === "/usr/bin",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = buildFallbackPath();
|
|
149
|
+
expect(result).toContain("/home/testuser/.local/bin");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("includes ~/.bun/bin", () => {
|
|
153
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
154
|
+
p === "/home/testuser/.bun/bin" || p === "/usr/bin",
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const result = buildFallbackPath();
|
|
158
|
+
expect(result).toContain("/home/testuser/.bun/bin");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("includes ~/.cargo/bin for Rust tools", () => {
|
|
162
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
163
|
+
p === "/home/testuser/.cargo/bin" || p === "/usr/bin",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const result = buildFallbackPath();
|
|
167
|
+
expect(result).toContain("/home/testuser/.cargo/bin");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("probes nvm versions directory and includes all version bins", () => {
|
|
171
|
+
// Ensure NVM_DIR is not set so the code falls back to ~/.nvm
|
|
172
|
+
delete process.env.NVM_DIR;
|
|
173
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
174
|
+
if (p === "/home/testuser/.nvm/versions/node") return true;
|
|
175
|
+
if (p.includes(".nvm/versions/node/v") && p.endsWith("/bin")) return true;
|
|
176
|
+
if (p === "/usr/bin") return true;
|
|
177
|
+
return false;
|
|
178
|
+
});
|
|
179
|
+
mockReaddirSync.mockReturnValue(["v18.20.0", "v22.17.0"] as any);
|
|
180
|
+
|
|
181
|
+
const result = buildFallbackPath();
|
|
182
|
+
expect(result).toContain("/home/testuser/.nvm/versions/node/v18.20.0/bin");
|
|
183
|
+
expect(result).toContain("/home/testuser/.nvm/versions/node/v22.17.0/bin");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("uses NVM_DIR env var when set", () => {
|
|
187
|
+
process.env.NVM_DIR = "/custom/nvm";
|
|
188
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
189
|
+
if (p === "/custom/nvm/versions/node") return true;
|
|
190
|
+
if (p.includes("/custom/nvm/versions/node/v") && p.endsWith("/bin"))
|
|
191
|
+
return true;
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
mockReaddirSync.mockReturnValue(["v20.0.0"] as any);
|
|
195
|
+
|
|
196
|
+
const result = buildFallbackPath();
|
|
197
|
+
expect(result).toContain("/custom/nvm/versions/node/v20.0.0/bin");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("excludes directories that don't exist", () => {
|
|
201
|
+
mockExistsSync.mockReturnValue(false);
|
|
202
|
+
|
|
203
|
+
const result = buildFallbackPath();
|
|
204
|
+
expect(result).toBe("");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("deduplicates PATH entries", () => {
|
|
208
|
+
mockExistsSync.mockReturnValue(true);
|
|
209
|
+
mockReaddirSync.mockReturnValue([] as any);
|
|
210
|
+
|
|
211
|
+
const result = buildFallbackPath();
|
|
212
|
+
const dirs = result.split(":");
|
|
213
|
+
expect(dirs.length).toBe(new Set(dirs).size);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("Windows support", () => {
|
|
217
|
+
const originalPlatform = process.platform;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("uses semicolon as PATH separator on win32", () => {
|
|
228
|
+
mockExistsSync.mockImplementation((p: string) =>
|
|
229
|
+
["/usr/local/bin", "/usr/bin"].includes(p as string),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const result = buildFallbackPath();
|
|
233
|
+
// Should use ; not : on Windows
|
|
234
|
+
expect(result).toContain(";");
|
|
235
|
+
expect(result).not.toContain(":");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ─── getEnrichedPath ────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
describe("getEnrichedPath", () => {
|
|
243
|
+
it("merges user shell PATH with current process PATH", () => {
|
|
244
|
+
process.env.PATH = "/usr/bin:/bin";
|
|
245
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
246
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
247
|
+
return "___PATH_START___/usr/bin:/home/testuser/.cargo/bin___PATH_END___\n";
|
|
248
|
+
}
|
|
249
|
+
return "";
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const result = getEnrichedPath();
|
|
253
|
+
expect(result).toContain("/home/testuser/.cargo/bin");
|
|
254
|
+
expect(result).toContain("/usr/bin");
|
|
255
|
+
expect(result).toContain("/bin");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("deduplicates entries from both PATHs", () => {
|
|
259
|
+
process.env.PATH = "/usr/bin:/bin:/usr/local/bin";
|
|
260
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
261
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
262
|
+
return "___PATH_START___/usr/bin:/usr/local/bin:/home/testuser/.volta/bin___PATH_END___\n";
|
|
263
|
+
}
|
|
264
|
+
return "";
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = getEnrichedPath();
|
|
268
|
+
const dirs = result.split(":");
|
|
269
|
+
expect(dirs.length).toBe(new Set(dirs).size);
|
|
270
|
+
// /usr/bin should appear exactly once
|
|
271
|
+
expect(dirs.filter((d) => d === "/usr/bin").length).toBe(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("caches the result after first call", () => {
|
|
275
|
+
process.env.PATH = "/usr/bin";
|
|
276
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
277
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
278
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
279
|
+
}
|
|
280
|
+
return "";
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const first = getEnrichedPath();
|
|
284
|
+
mockExecSync.mockClear();
|
|
285
|
+
const second = getEnrichedPath();
|
|
286
|
+
|
|
287
|
+
expect(first).toBe(second);
|
|
288
|
+
// execSync should NOT be called again (result was cached)
|
|
289
|
+
expect(mockExecSync).not.toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("gives user shell PATH precedence over process PATH", () => {
|
|
293
|
+
// User's shell has /opt/homebrew/bin first, process PATH has /usr/bin first
|
|
294
|
+
process.env.PATH = "/usr/bin:/bin";
|
|
295
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
296
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
297
|
+
return "___PATH_START___/opt/homebrew/bin:/usr/bin___PATH_END___\n";
|
|
298
|
+
}
|
|
299
|
+
return "";
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const result = getEnrichedPath();
|
|
303
|
+
const dirs = result.split(":");
|
|
304
|
+
expect(dirs.indexOf("/opt/homebrew/bin")).toBeLessThan(
|
|
305
|
+
dirs.indexOf("/bin"),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("Windows support", () => {
|
|
310
|
+
const originalPlatform = process.platform;
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
_resetPathCache(); // ensure no cross-contamination from non-Windows tests
|
|
314
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
afterEach(() => {
|
|
318
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("splits and joins PATH with semicolons on win32", () => {
|
|
322
|
+
process.env.PATH = "C:\\Windows\\System32;C:\\Windows";
|
|
323
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
324
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
325
|
+
return "___PATH_START___C:\\Users\\me\\AppData\\Roaming\\npm;C:\\Windows\\System32___PATH_END___\n";
|
|
326
|
+
}
|
|
327
|
+
return "";
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = getEnrichedPath();
|
|
331
|
+
// Should use ; as separator and contain all directories
|
|
332
|
+
expect(result).toContain("C:\\Users\\me\\AppData\\Roaming\\npm");
|
|
333
|
+
expect(result).toContain("C:\\Windows\\System32");
|
|
334
|
+
expect(result).toContain("C:\\Windows");
|
|
335
|
+
// Should be semicolon-separated
|
|
336
|
+
const dirs = result.split(";");
|
|
337
|
+
expect(dirs.length).toBeGreaterThanOrEqual(3);
|
|
338
|
+
// C:\Windows\System32 should appear exactly once (deduplication)
|
|
339
|
+
expect(dirs.filter((d) => d === "C:\\Windows\\System32").length).toBe(1);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ─── resolveBinary ──────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
describe("resolveBinary", () => {
|
|
347
|
+
beforeEach(() => {
|
|
348
|
+
// Seed getEnrichedPath cache to avoid shell-sourcing side effects
|
|
349
|
+
process.env.PATH = "/usr/bin:/bin";
|
|
350
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
351
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
352
|
+
return "___PATH_START___/usr/bin:/usr/local/bin___PATH_END___\n";
|
|
353
|
+
}
|
|
354
|
+
throw new Error("not found");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("returns absolute path when binary is found via which", () => {
|
|
359
|
+
_resetPathCache();
|
|
360
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
361
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
362
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
363
|
+
}
|
|
364
|
+
if (typeof cmd === "string" && cmd.startsWith("which claude")) {
|
|
365
|
+
return "/home/testuser/.local/bin/claude\n";
|
|
366
|
+
}
|
|
367
|
+
throw new Error("not found");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(resolveBinary("claude")).toBe("/home/testuser/.local/bin/claude");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns null when binary is not found anywhere", () => {
|
|
374
|
+
_resetPathCache();
|
|
375
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
376
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
377
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
378
|
+
}
|
|
379
|
+
throw new Error("not found");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(resolveBinary("nonexistent")).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("passes enriched PATH to which command", () => {
|
|
386
|
+
_resetPathCache();
|
|
387
|
+
mockExecSync.mockImplementation((cmd: string, opts?: any) => {
|
|
388
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
389
|
+
return "___PATH_START___/usr/bin:/home/testuser/.special/bin___PATH_END___\n";
|
|
390
|
+
}
|
|
391
|
+
if (typeof cmd === "string" && cmd.startsWith("which")) {
|
|
392
|
+
// Verify enriched PATH is passed in env
|
|
393
|
+
expect(opts?.env?.PATH).toContain("/home/testuser/.special/bin");
|
|
394
|
+
return "/home/testuser/.special/bin/mytool\n";
|
|
395
|
+
}
|
|
396
|
+
throw new Error("not found");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
resolveBinary("mytool");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("returns the path directly when given an absolute path that exists", () => {
|
|
403
|
+
mockExistsSync.mockReturnValue(true);
|
|
404
|
+
expect(resolveBinary("/opt/bin/claude")).toBe("/opt/bin/claude");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("returns null when given an absolute path that does not exist", () => {
|
|
408
|
+
mockExistsSync.mockReturnValue(false);
|
|
409
|
+
expect(resolveBinary("/nonexistent/claude")).toBeNull();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("Windows support", () => {
|
|
413
|
+
const originalPlatform = process.platform;
|
|
414
|
+
|
|
415
|
+
beforeEach(() => {
|
|
416
|
+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
afterEach(() => {
|
|
420
|
+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("accepts Windows absolute paths like C:\\... on win32", () => {
|
|
424
|
+
mockExistsSync.mockReturnValue(true);
|
|
425
|
+
expect(resolveBinary("C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd")).toBe(
|
|
426
|
+
"C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd",
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("returns null for a non-existent Windows absolute path", () => {
|
|
431
|
+
mockExistsSync.mockReturnValue(false);
|
|
432
|
+
expect(resolveBinary("D:\\nonexistent\\claude.cmd")).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("prefers 'where' over 'which' on Windows when both succeed", () => {
|
|
436
|
+
_resetPathCache();
|
|
437
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
438
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
439
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
440
|
+
}
|
|
441
|
+
// 'where' succeeds with a native Win32 path
|
|
442
|
+
if (typeof cmd === "string" && cmd.startsWith("where")) {
|
|
443
|
+
return "C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd\r\nC:\\Users\\me\\AppData\\Roaming\\npm\\claude\r\n";
|
|
444
|
+
}
|
|
445
|
+
// 'which' also succeeds but returns a POSIX-style path (Git Bash)
|
|
446
|
+
if (typeof cmd === "string" && cmd.startsWith("which")) {
|
|
447
|
+
return "/c/Users/me/AppData/Roaming/npm/claude";
|
|
448
|
+
}
|
|
449
|
+
throw new Error("not found");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Should return the 'where' result (native Win32 path), not the 'which' POSIX path
|
|
453
|
+
expect(resolveBinary("claude")).toBe("C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("falls back to 'which' when 'where' fails on Windows", () => {
|
|
457
|
+
_resetPathCache();
|
|
458
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
459
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
460
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
461
|
+
}
|
|
462
|
+
// 'where' fails
|
|
463
|
+
if (typeof cmd === "string" && cmd.startsWith("where")) {
|
|
464
|
+
throw new Error("not found");
|
|
465
|
+
}
|
|
466
|
+
// 'which' succeeds (Git Bash fallback)
|
|
467
|
+
if (typeof cmd === "string" && cmd.startsWith("which")) {
|
|
468
|
+
return "/c/Users/me/AppData/Roaming/npm/claude";
|
|
469
|
+
}
|
|
470
|
+
throw new Error("not found");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(resolveBinary("claude")).toBe("/c/Users/me/AppData/Roaming/npm/claude");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("prefers .cmd result from 'where' output with multiple lines", () => {
|
|
477
|
+
_resetPathCache();
|
|
478
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
479
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
480
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
481
|
+
}
|
|
482
|
+
if (typeof cmd === "string" && cmd.startsWith("which")) {
|
|
483
|
+
throw new Error("not found");
|
|
484
|
+
}
|
|
485
|
+
if (typeof cmd === "string" && cmd.startsWith("where")) {
|
|
486
|
+
return "C:\\Program Files\\nodejs\\node\r\nC:\\Users\\me\\AppData\\Roaming\\npm\\node.cmd\r\n";
|
|
487
|
+
}
|
|
488
|
+
throw new Error("not found");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(resolveBinary("node")).toBe("C:\\Users\\me\\AppData\\Roaming\\npm\\node.cmd");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("returns first line from 'where' when no .cmd match exists", () => {
|
|
495
|
+
_resetPathCache();
|
|
496
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
497
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
498
|
+
return "___PATH_START___/usr/bin___PATH_END___\n";
|
|
499
|
+
}
|
|
500
|
+
if (typeof cmd === "string" && cmd.startsWith("which")) {
|
|
501
|
+
throw new Error("not found");
|
|
502
|
+
}
|
|
503
|
+
if (typeof cmd === "string" && cmd.startsWith("where")) {
|
|
504
|
+
return "C:\\Program Files\\nodejs\\node.exe\r\n";
|
|
505
|
+
}
|
|
506
|
+
throw new Error("not found");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(resolveBinary("node")).toBe("C:\\Program Files\\nodejs\\node.exe");
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// ─── getServicePath ─────────────────────────────────────────────────────────
|
|
515
|
+
|
|
516
|
+
describe("getServicePath", () => {
|
|
517
|
+
it("returns the same value as getEnrichedPath", () => {
|
|
518
|
+
process.env.PATH = "/usr/bin";
|
|
519
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
520
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
521
|
+
return "___PATH_START___/usr/bin:/opt/homebrew/bin___PATH_END___\n";
|
|
522
|
+
}
|
|
523
|
+
return "";
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(getServicePath()).toBe(getEnrichedPath());
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// ─── _resetPathCache ────────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
describe("_resetPathCache", () => {
|
|
533
|
+
it("clears the cached PATH so next call re-computes", () => {
|
|
534
|
+
process.env.PATH = "/usr/bin";
|
|
535
|
+
let callCount = 0;
|
|
536
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
537
|
+
if (typeof cmd === "string" && cmd.includes("-lic")) {
|
|
538
|
+
callCount++;
|
|
539
|
+
return `___PATH_START___/usr/bin:/call-${callCount}___PATH_END___\n`;
|
|
540
|
+
}
|
|
541
|
+
return "";
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const first = getEnrichedPath();
|
|
545
|
+
_resetPathCache();
|
|
546
|
+
const second = getEnrichedPath();
|
|
547
|
+
|
|
548
|
+
expect(first).not.toBe(second);
|
|
549
|
+
expect(first).toContain("/call-1");
|
|
550
|
+
expect(second).toContain("/call-2");
|
|
551
|
+
});
|
|
552
|
+
});
|