agent-relay 2.1.0 → 2.1.1

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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Comprehensive tests for relay-pty binary path resolution.
3
+ *
4
+ * Tests all installation scenarios by verifying the search paths are correct:
5
+ * 1. npx (from @agent-relay/* scoped package)
6
+ * 2. npx (from agent-relay directly)
7
+ * 3. npm install -g (nvm)
8
+ * 4. npm install -g (Homebrew macOS)
9
+ * 5. npm install -g (Homebrew macOS arm64)
10
+ * 6. npm install (local project)
11
+ * 7. pnpm global
12
+ * 8. Development (monorepo)
13
+ * 9. Docker container
14
+ * 10. Environment variable override
15
+ */
16
+
17
+ import { describe, it, expect, beforeEach } from 'vitest';
18
+ import {
19
+ findRelayPtyBinary,
20
+ getLastSearchPaths,
21
+ clearBinaryCache,
22
+ isPlatformSupported,
23
+ getSupportedPlatforms,
24
+ } from './relay-pty-path.js';
25
+
26
+ describe('findRelayPtyBinary - search path verification', () => {
27
+ beforeEach(() => {
28
+ clearBinaryCache();
29
+ delete process.env.RELAY_PTY_BINARY;
30
+ });
31
+
32
+ describe('npx installation (scoped @agent-relay/* package)', () => {
33
+ // When running via npx, the code runs from:
34
+ // ~/.npm/_npx/{hash}/node_modules/@agent-relay/bridge/dist/
35
+ // Binary should be searched at:
36
+ // ~/.npm/_npx/{hash}/node_modules/agent-relay/bin/relay-pty-darwin-arm64
37
+
38
+ it('should include correct npx cache path for scoped package', () => {
39
+ const callerDirname = '/Users/testuser/.npm/_npx/abc123/node_modules/@agent-relay/bridge/dist';
40
+
41
+ findRelayPtyBinary(callerDirname);
42
+ const paths = getLastSearchPaths();
43
+
44
+ // Should include the sibling agent-relay package path
45
+ const expectedPath = '/Users/testuser/.npm/_npx/abc123/node_modules/agent-relay/bin';
46
+ expect(paths.some((p) => p.startsWith(expectedPath))).toBe(true);
47
+ });
48
+
49
+ it('should check platform-specific binary BEFORE generic binary', () => {
50
+ const callerDirname = '/Users/testuser/.npm/_npx/abc123/node_modules/@agent-relay/bridge/dist';
51
+
52
+ findRelayPtyBinary(callerDirname);
53
+ const paths = getLastSearchPaths();
54
+
55
+ // Find the first occurrence of the npx cache path
56
+ const npxBasePath = '/Users/testuser/.npm/_npx/abc123/node_modules/agent-relay/bin';
57
+ const platformIdx = paths.findIndex(
58
+ (p) => p.startsWith(npxBasePath) && p.includes('relay-pty-')
59
+ );
60
+ const genericIdx = paths.findIndex((p) => p === `${npxBasePath}/relay-pty`);
61
+
62
+ expect(platformIdx).toBeGreaterThanOrEqual(0);
63
+ expect(genericIdx).toBeGreaterThan(platformIdx);
64
+ });
65
+ });
66
+
67
+ describe('npx installation (direct agent-relay package)', () => {
68
+ // When CLI code itself calls findRelayPtyBinary:
69
+ // ~/.npm/_npx/{hash}/node_modules/agent-relay/dist/src/cli/
70
+
71
+ it('should include correct path for direct agent-relay in npx cache', () => {
72
+ const callerDirname = '/Users/testuser/.npm/_npx/xyz789/node_modules/agent-relay/dist/src/cli';
73
+
74
+ findRelayPtyBinary(callerDirname);
75
+ const paths = getLastSearchPaths();
76
+
77
+ const expectedPath = '/Users/testuser/.npm/_npx/xyz789/node_modules/agent-relay/bin';
78
+ expect(paths.some((p) => p.startsWith(expectedPath))).toBe(true);
79
+ });
80
+ });
81
+
82
+ describe('Global npm install (nvm)', () => {
83
+ // ~/.nvm/versions/node/v20.0.0/lib/node_modules/agent-relay/
84
+
85
+ it('should include nvm global install path', () => {
86
+ const callerDirname =
87
+ '/Users/testuser/.nvm/versions/node/v20.0.0/lib/node_modules/@agent-relay/bridge/dist';
88
+
89
+ findRelayPtyBinary(callerDirname);
90
+ const paths = getLastSearchPaths();
91
+
92
+ const expectedPath =
93
+ '/Users/testuser/.nvm/versions/node/v20.0.0/lib/node_modules/agent-relay/bin';
94
+ expect(paths.some((p) => p.startsWith(expectedPath))).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe('Global npm install (Homebrew macOS)', () => {
99
+ // /usr/local/lib/node_modules/agent-relay/ (Intel)
100
+ // /opt/homebrew/lib/node_modules/agent-relay/ (Apple Silicon)
101
+
102
+ it('should include Homebrew Intel location', () => {
103
+ const callerDirname = '/usr/local/lib/node_modules/@agent-relay/bridge/dist';
104
+
105
+ findRelayPtyBinary(callerDirname);
106
+ const paths = getLastSearchPaths();
107
+
108
+ expect(paths.some((p) => p.startsWith('/usr/local/lib/node_modules/agent-relay/bin'))).toBe(
109
+ true
110
+ );
111
+ });
112
+
113
+ it('should include Homebrew Apple Silicon location', () => {
114
+ const callerDirname = '/opt/homebrew/lib/node_modules/@agent-relay/bridge/dist';
115
+
116
+ findRelayPtyBinary(callerDirname);
117
+ const paths = getLastSearchPaths();
118
+
119
+ expect(
120
+ paths.some((p) => p.startsWith('/opt/homebrew/lib/node_modules/agent-relay/bin'))
121
+ ).toBe(true);
122
+ });
123
+ });
124
+
125
+ describe('Local project install (npm install agent-relay)', () => {
126
+ // /path/to/project/node_modules/agent-relay/
127
+
128
+ it('should include local node_modules path from scoped package', () => {
129
+ const callerDirname = '/path/to/myproject/node_modules/@agent-relay/bridge/dist';
130
+
131
+ findRelayPtyBinary(callerDirname);
132
+ const paths = getLastSearchPaths();
133
+
134
+ const expectedPath = '/path/to/myproject/node_modules/agent-relay/bin';
135
+ expect(paths.some((p) => p.startsWith(expectedPath))).toBe(true);
136
+ });
137
+
138
+ it('should include cwd-based node_modules path', () => {
139
+ const callerDirname = '/some/other/location';
140
+
141
+ findRelayPtyBinary(callerDirname);
142
+ const paths = getLastSearchPaths();
143
+
144
+ const cwdPath = `${process.cwd()}/node_modules/agent-relay/bin`;
145
+ expect(paths.some((p) => p.startsWith(cwdPath))).toBe(true);
146
+ });
147
+ });
148
+
149
+ describe('pnpm global install', () => {
150
+ // ~/.local/share/pnpm/global/node_modules/agent-relay/
151
+
152
+ it('should include pnpm global location', () => {
153
+ const callerDirname =
154
+ '/Users/testuser/.local/share/pnpm/global/node_modules/@agent-relay/bridge/dist';
155
+
156
+ findRelayPtyBinary(callerDirname);
157
+ const paths = getLastSearchPaths();
158
+
159
+ expect(
160
+ paths.some((p) =>
161
+ p.startsWith('/Users/testuser/.local/share/pnpm/global/node_modules/agent-relay/bin')
162
+ )
163
+ ).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe('Development (monorepo)', () => {
168
+ // /path/to/relay/packages/bridge/dist/
169
+
170
+ it('should include project root bin/ path', () => {
171
+ const callerDirname = '/path/to/relay/packages/bridge/dist';
172
+
173
+ findRelayPtyBinary(callerDirname);
174
+ const paths = getLastSearchPaths();
175
+
176
+ // Development path: go up 3 levels to project root
177
+ expect(paths.some((p) => p.startsWith('/path/to/relay/bin'))).toBe(true);
178
+ });
179
+
180
+ it('should include Rust target/release path', () => {
181
+ const callerDirname = '/path/to/relay/packages/bridge/dist';
182
+
183
+ findRelayPtyBinary(callerDirname);
184
+ const paths = getLastSearchPaths();
185
+
186
+ expect(paths.some((p) => p.includes('relay-pty/target/release/relay-pty'))).toBe(true);
187
+ });
188
+
189
+ it('should include Rust target/debug path', () => {
190
+ const callerDirname = '/path/to/relay/packages/bridge/dist';
191
+
192
+ findRelayPtyBinary(callerDirname);
193
+ const paths = getLastSearchPaths();
194
+
195
+ expect(paths.some((p) => p.includes('relay-pty/target/debug/relay-pty'))).toBe(true);
196
+ });
197
+ });
198
+
199
+ describe('Docker container', () => {
200
+ it('should include /app/bin/relay-pty path', () => {
201
+ const callerDirname = '/app/node_modules/@agent-relay/bridge/dist';
202
+
203
+ findRelayPtyBinary(callerDirname);
204
+ const paths = getLastSearchPaths();
205
+
206
+ expect(paths).toContain('/app/bin/relay-pty');
207
+ });
208
+ });
209
+
210
+ describe('System-wide install', () => {
211
+ it('should include /usr/local/bin/relay-pty path', () => {
212
+ const callerDirname = '/some/path';
213
+
214
+ findRelayPtyBinary(callerDirname);
215
+ const paths = getLastSearchPaths();
216
+
217
+ expect(paths).toContain('/usr/local/bin/relay-pty');
218
+ });
219
+ });
220
+
221
+ describe('Platform-specific binary naming', () => {
222
+ it('should include platform-specific binary name in search paths', () => {
223
+ const callerDirname = '/path/node_modules/@agent-relay/bridge/dist';
224
+
225
+ findRelayPtyBinary(callerDirname);
226
+ const paths = getLastSearchPaths();
227
+
228
+ // Should have platform-specific binary names based on current platform
229
+ const platform = process.platform;
230
+ const arch = process.arch;
231
+ let expectedBinaryName: string | null = null;
232
+
233
+ if (platform === 'darwin' && arch === 'arm64') {
234
+ expectedBinaryName = 'relay-pty-darwin-arm64';
235
+ } else if (platform === 'darwin' && arch === 'x64') {
236
+ expectedBinaryName = 'relay-pty-darwin-x64';
237
+ } else if (platform === 'linux' && arch === 'arm64') {
238
+ expectedBinaryName = 'relay-pty-linux-arm64';
239
+ } else if (platform === 'linux' && arch === 'x64') {
240
+ expectedBinaryName = 'relay-pty-linux-x64';
241
+ }
242
+
243
+ if (expectedBinaryName) {
244
+ expect(paths.some((p) => p.includes(expectedBinaryName!))).toBe(true);
245
+ }
246
+ });
247
+ });
248
+
249
+ describe('Search path order priority', () => {
250
+ it('should have platform-specific binaries before generic binaries for same location', () => {
251
+ const callerDirname = '/Users/testuser/.npm/_npx/abc123/node_modules/@agent-relay/bridge/dist';
252
+
253
+ findRelayPtyBinary(callerDirname);
254
+ const paths = getLastSearchPaths();
255
+
256
+ // For each unique bin directory, platform-specific should come before generic
257
+ const binDirs = new Set<string>();
258
+ for (const p of paths) {
259
+ const binDir = p.replace(/\/[^/]+$/, ''); // Remove filename
260
+ if (binDir.endsWith('/bin')) {
261
+ binDirs.add(binDir);
262
+ }
263
+ }
264
+
265
+ for (const binDir of binDirs) {
266
+ const platformIdx = paths.findIndex(
267
+ (p) => p.startsWith(binDir) && p.includes('relay-pty-')
268
+ );
269
+ const genericIdx = paths.findIndex((p) => p === `${binDir}/relay-pty`);
270
+
271
+ // If both exist, platform should come first
272
+ if (platformIdx >= 0 && genericIdx >= 0) {
273
+ expect(platformIdx).toBeLessThan(genericIdx);
274
+ }
275
+ }
276
+ });
277
+ });
278
+
279
+ describe('Environment variable override', () => {
280
+ it('should use RELAY_PTY_BINARY when file is executable', () => {
281
+ // Use an actual executable that exists on all Unix systems
282
+ // /bin/ls is guaranteed to exist and be executable
283
+ const executableFile = '/bin/ls';
284
+ process.env.RELAY_PTY_BINARY = executableFile;
285
+
286
+ const result = findRelayPtyBinary('/any/path');
287
+
288
+ // Should return the env var path since the file is executable
289
+ expect(result).toBe(executableFile);
290
+
291
+ delete process.env.RELAY_PTY_BINARY;
292
+ });
293
+
294
+ it('should fall back to normal search when RELAY_PTY_BINARY file does not exist', () => {
295
+ process.env.RELAY_PTY_BINARY = '/nonexistent/path/relay-pty';
296
+
297
+ findRelayPtyBinary('/any/path');
298
+ const paths = getLastSearchPaths();
299
+
300
+ // Should have searched multiple paths (not just the env var)
301
+ expect(paths.length).toBeGreaterThan(1);
302
+
303
+ delete process.env.RELAY_PTY_BINARY;
304
+ });
305
+ });
306
+
307
+ describe('Real binary resolution', () => {
308
+ it('should find actual relay-pty binary in development', () => {
309
+ // This test uses the real file system to verify the function works
310
+ // in development context
311
+ const devPath = `${process.cwd()}/packages/utils/dist`;
312
+
313
+ const result = findRelayPtyBinary(devPath);
314
+
315
+ // In development, we should find the binary in bin/
316
+ // This test only passes when run from the monorepo root
317
+ if (process.cwd().includes('relay')) {
318
+ expect(result).not.toBeNull();
319
+ expect(result).toMatch(/relay-pty/);
320
+ }
321
+ });
322
+ });
323
+ });
324
+
325
+ describe('isPlatformSupported', () => {
326
+ it('should return true for current platform (darwin/linux arm64/x64)', () => {
327
+ const platform = process.platform;
328
+ const arch = process.arch;
329
+
330
+ // This test runs on macOS/Linux CI, so current platform should be supported
331
+ if ((platform === 'darwin' || platform === 'linux') && (arch === 'arm64' || arch === 'x64')) {
332
+ expect(isPlatformSupported()).toBe(true);
333
+ }
334
+ });
335
+
336
+ it('should be consistent with platform binary availability', () => {
337
+ // If platform is supported, we should have a binary name for it
338
+ const supported = isPlatformSupported();
339
+ const platforms = getSupportedPlatforms();
340
+
341
+ if (supported) {
342
+ const currentPlatformArch = `${process.platform}-${process.arch}`;
343
+ expect(platforms).toContain(currentPlatformArch);
344
+ }
345
+ });
346
+ });
347
+
348
+ describe('getSupportedPlatforms', () => {
349
+ it('should return all supported platform-arch combinations', () => {
350
+ const platforms = getSupportedPlatforms();
351
+
352
+ // Should include all 4 supported combinations
353
+ expect(platforms).toContain('darwin-arm64');
354
+ expect(platforms).toContain('darwin-x64');
355
+ expect(platforms).toContain('linux-arm64');
356
+ expect(platforms).toContain('linux-x64');
357
+ });
358
+
359
+ it('should not include Windows', () => {
360
+ const platforms = getSupportedPlatforms();
361
+
362
+ expect(platforms).not.toContain('win32-x64');
363
+ expect(platforms).not.toContain('win32-arm64');
364
+ expect(platforms).not.toMatch(/win32/);
365
+ });
366
+
367
+ it('should return a comma-separated string', () => {
368
+ const platforms = getSupportedPlatforms();
369
+
370
+ expect(typeof platforms).toBe('string');
371
+ expect(platforms).toMatch(/^[\w-]+(, [\w-]+)*$/);
372
+ });
373
+ });