@somewhatabstract/x 0.0.1 → 0.1.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.
@@ -0,0 +1,344 @@
1
+ import path from "node:path";
2
+ import {describe, expect, it} from "vitest";
3
+ import type {PackageInfo} from "../discover-packages";
4
+ import {resolveBinPath} from "../resolve-bin-path";
5
+
6
+ describe("resolveBinPath", () => {
7
+ const mockPackage: PackageInfo = {
8
+ name: "test-package",
9
+ path: "/workspace/packages/test-package",
10
+ version: "1.0.0",
11
+ };
12
+
13
+ describe("basic functionality", () => {
14
+ it("should resolve string-style bin when binName matches package name", () => {
15
+ // Arrange
16
+ const bin = "bin/cli.js";
17
+ const binName = "test-package";
18
+
19
+ // Act
20
+ const result = resolveBinPath(mockPackage, bin, binName);
21
+
22
+ // Assert
23
+ expect(result).toBe(
24
+ path.resolve("/workspace/packages/test-package", "bin/cli.js"),
25
+ );
26
+ });
27
+
28
+ it("should return null for string-style bin when binName doesn't match package name", () => {
29
+ // Arrange
30
+ const bin = "bin/cli.js";
31
+ const binName = "different-name";
32
+
33
+ // Act
34
+ const result = resolveBinPath(mockPackage, bin, binName);
35
+
36
+ // Assert
37
+ expect(result).toBeNull();
38
+ });
39
+
40
+ it("should resolve object-style bin when binName exists in bin object", () => {
41
+ // Arrange
42
+ const bin = {
43
+ cli: "bin/cli.js",
44
+ server: "bin/server.js",
45
+ };
46
+ const binName = "cli";
47
+
48
+ // Act
49
+ const result = resolveBinPath(mockPackage, bin, binName);
50
+
51
+ // Assert
52
+ expect(result).toBe(
53
+ path.resolve("/workspace/packages/test-package", "bin/cli.js"),
54
+ );
55
+ });
56
+
57
+ it("should return null for object-style bin when binName doesn't exist", () => {
58
+ // Arrange
59
+ const bin = {
60
+ cli: "bin/cli.js",
61
+ };
62
+ const binName = "nonexistent";
63
+
64
+ // Act
65
+ const result = resolveBinPath(mockPackage, bin, binName);
66
+
67
+ // Assert
68
+ expect(result).toBeNull();
69
+ });
70
+
71
+ it("should return null when bin is null", () => {
72
+ // Arrange
73
+ const bin = null;
74
+ const binName = "test-package";
75
+
76
+ // Act
77
+ const result = resolveBinPath(mockPackage, bin, binName);
78
+
79
+ // Assert
80
+ expect(result).toBeNull();
81
+ });
82
+
83
+ it("should return null when bin is undefined", () => {
84
+ // Arrange
85
+ const bin = undefined;
86
+ const binName = "test-package";
87
+
88
+ // Act
89
+ const result = resolveBinPath(mockPackage, bin, binName);
90
+
91
+ // Assert
92
+ expect(result).toBeNull();
93
+ });
94
+ });
95
+
96
+ describe("security - path traversal protection", () => {
97
+ it("should reject parent directory traversal in string-style bin", () => {
98
+ // Arrange
99
+ const bin = "../../../etc/passwd";
100
+ const binName = "test-package";
101
+
102
+ // Act
103
+ const result = resolveBinPath(mockPackage, bin, binName);
104
+
105
+ // Assert
106
+ expect(result).toBeNull();
107
+ });
108
+
109
+ it("should reject parent directory traversal in object-style bin", () => {
110
+ // Arrange
111
+ const bin = {
112
+ malicious: "../../../usr/bin/malicious",
113
+ };
114
+ const binName = "malicious";
115
+
116
+ // Act
117
+ const result = resolveBinPath(mockPackage, bin, binName);
118
+
119
+ // Assert
120
+ expect(result).toBeNull();
121
+ });
122
+
123
+ it("should reject absolute paths in string-style bin", () => {
124
+ // Arrange
125
+ const bin = "/usr/bin/malicious";
126
+ const binName = "test-package";
127
+
128
+ // Act
129
+ const result = resolveBinPath(mockPackage, bin, binName);
130
+
131
+ // Assert
132
+ expect(result).toBeNull();
133
+ });
134
+
135
+ it("should reject absolute paths in object-style bin", () => {
136
+ // Arrange
137
+ const bin = {
138
+ malicious: "/usr/bin/malicious",
139
+ };
140
+ const binName = "malicious";
141
+
142
+ // Act
143
+ const result = resolveBinPath(mockPackage, bin, binName);
144
+
145
+ // Assert
146
+ expect(result).toBeNull();
147
+ });
148
+
149
+ it("should accept valid subdirectory paths", () => {
150
+ // Arrange
151
+ const bin = "bin/nested/script.js";
152
+ const binName = "test-package";
153
+
154
+ // Act
155
+ const result = resolveBinPath(mockPackage, bin, binName);
156
+
157
+ // Assert
158
+ expect(result).toBe(
159
+ path.resolve(
160
+ "/workspace/packages/test-package",
161
+ "bin/nested/script.js",
162
+ ),
163
+ );
164
+ });
165
+
166
+ it("should accept valid paths at package root", () => {
167
+ // Arrange
168
+ const bin = "index.js";
169
+ const binName = "test-package";
170
+
171
+ // Act
172
+ const result = resolveBinPath(mockPackage, bin, binName);
173
+
174
+ // Assert
175
+ expect(result).toBe(
176
+ path.resolve("/workspace/packages/test-package", "index.js"),
177
+ );
178
+ });
179
+
180
+ it("should handle edge case where bin path equals package directory", () => {
181
+ // Arrange
182
+ const bin = ".";
183
+ const binName = "test-package";
184
+
185
+ // Act
186
+ const result = resolveBinPath(mockPackage, bin, binName);
187
+
188
+ // Assert
189
+ expect(result).toBe(
190
+ path.resolve("/workspace/packages/test-package"),
191
+ );
192
+ });
193
+
194
+ it("should reject path that tries to escape with multiple parent references", () => {
195
+ // Arrange
196
+ const bin = {
197
+ escape: "../../../../../../etc/shadow",
198
+ };
199
+ const binName = "escape";
200
+
201
+ // Act
202
+ const result = resolveBinPath(mockPackage, bin, binName);
203
+
204
+ // Assert
205
+ expect(result).toBeNull();
206
+ });
207
+
208
+ it("should handle path with backslashes (valid filename chars on Unix)", () => {
209
+ // Arrange
210
+ // On Unix, backslashes are valid filename characters, not path separators
211
+ // On Windows, Node.js normalizes these to forward slashes before our check
212
+ const bin = "bin\\script.js";
213
+ const binName = "test-package";
214
+
215
+ // Act
216
+ const result = resolveBinPath(mockPackage, bin, binName);
217
+
218
+ // Assert
219
+ // Should resolve successfully since backslashes are just filename chars on Unix
220
+ expect(result).toBe(
221
+ path.resolve(
222
+ "/workspace/packages/test-package",
223
+ "bin\\script.js",
224
+ ),
225
+ );
226
+ });
227
+ });
228
+
229
+ describe("type handling", () => {
230
+ it("should return null when bin is a number", () => {
231
+ // Arrange
232
+ const bin: any = 123;
233
+ const binName = "test-package";
234
+
235
+ // Act
236
+ const result = resolveBinPath(mockPackage, bin, binName);
237
+
238
+ // Assert
239
+ expect(result).toBeNull();
240
+ });
241
+
242
+ it("should return null when bin is a boolean", () => {
243
+ // Arrange
244
+ const bin: any = true;
245
+ const binName = "test-package";
246
+
247
+ // Act
248
+ const result = resolveBinPath(mockPackage, bin, binName);
249
+
250
+ // Assert
251
+ expect(result).toBeNull();
252
+ });
253
+
254
+ it("should return null when bin is an empty object", () => {
255
+ // Arrange
256
+ const bin = {};
257
+ const binName = "test-package";
258
+
259
+ // Act
260
+ const result = resolveBinPath(mockPackage, bin, binName);
261
+
262
+ // Assert
263
+ expect(result).toBeNull();
264
+ });
265
+
266
+ it("should return null when bin is an array", () => {
267
+ // Arrange
268
+ const bin: any = ["bin/cli.js"];
269
+ const binName = "test-package";
270
+
271
+ // Act
272
+ const result = resolveBinPath(mockPackage, bin, binName);
273
+
274
+ // Assert
275
+ expect(result).toBeNull();
276
+ });
277
+ });
278
+
279
+ describe("edge cases", () => {
280
+ it("should handle package path with trailing slash", () => {
281
+ // Arrange
282
+ const packageWithTrailingSlash: PackageInfo = {
283
+ name: "test-package",
284
+ path: "/workspace/packages/test-package/",
285
+ version: "1.0.0",
286
+ };
287
+ const bin = "bin/cli.js";
288
+ const binName = "test-package";
289
+
290
+ // Act
291
+ const result = resolveBinPath(
292
+ packageWithTrailingSlash,
293
+ bin,
294
+ binName,
295
+ );
296
+
297
+ // Assert
298
+ expect(result).toBe(
299
+ path.resolve("/workspace/packages/test-package/", "bin/cli.js"),
300
+ );
301
+ });
302
+
303
+ it("should handle bin path with leading slash", () => {
304
+ // Arrange
305
+ const bin = "/bin/cli.js";
306
+ const binName = "test-package";
307
+
308
+ // Act
309
+ const result = resolveBinPath(mockPackage, bin, binName);
310
+
311
+ // Assert
312
+ expect(result).toBeNull();
313
+ });
314
+
315
+ it("should handle bin path with only dots", () => {
316
+ // Arrange
317
+ const bin = "..";
318
+ const binName = "test-package";
319
+
320
+ // Act
321
+ const result = resolveBinPath(mockPackage, bin, binName);
322
+
323
+ // Assert
324
+ expect(result).toBeNull();
325
+ });
326
+
327
+ it("should resolve relative path with current directory reference", () => {
328
+ // Arrange
329
+ const bin = "./bin/cli.js";
330
+ const binName = "test-package";
331
+
332
+ // Act
333
+ const result = resolveBinPath(mockPackage, bin, binName);
334
+
335
+ // Assert
336
+ expect(result).toBe(
337
+ path.resolve(
338
+ "/workspace/packages/test-package",
339
+ "./bin/cli.js",
340
+ ),
341
+ );
342
+ });
343
+ });
344
+ });
@@ -1,15 +1,314 @@
1
- import {describe, expect, it, vi} from "vitest";
2
- import { xImpl } from "../x-impl";
1
+ import {beforeEach, describe, expect, it, vi} from "vitest";
2
+ import * as discoverPackagesModule from "../discover-packages";
3
+ import {HandledError} from "../errors";
4
+ import * as executeScriptModule from "../execute-script";
5
+ import * as findMatchingBinsModule from "../find-matching-bins";
6
+ import * as findWorkspaceRootModule from "../find-workspace-root";
7
+ import {xImpl} from "../x-impl";
8
+
9
+ // Mock the modules
10
+ vi.mock("../find-workspace-root");
11
+ vi.mock("../discover-packages");
12
+ vi.mock("../find-matching-bins");
13
+ vi.mock("../execute-script");
3
14
 
4
15
  describe("xImpl", () => {
5
- it("should log 'Hello, world!'", () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ // Suppress console output in tests
19
+ vi.spyOn(console, "error").mockImplementation(() => {});
20
+ vi.spyOn(console, "log").mockImplementation(() => {});
21
+ });
22
+
23
+ it("should return exit code 1 when script not found", async () => {
24
+ // Arrange
25
+ const scriptName = "nonexistent-script";
26
+
27
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
28
+ "/test/workspace",
29
+ );
30
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
31
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
32
+ ]);
33
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue(
34
+ [],
35
+ );
36
+
37
+ // Act
38
+ const result = await xImpl(scriptName);
39
+
40
+ // Assert
41
+ expect(result.exitCode).toBe(1);
42
+ });
43
+
44
+ it("should return exit code 1 when no packages found", async () => {
45
+ // Arrange
46
+ const scriptName = "test-script";
47
+
48
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
49
+ "/test/workspace",
50
+ );
51
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue(
52
+ [],
53
+ );
54
+
55
+ // Act
56
+ const result = await xImpl(scriptName);
57
+
58
+ // Assert
59
+ expect(result.exitCode).toBe(1);
60
+ });
61
+
62
+ it("should return exit code 1 when multiple bins match", async () => {
63
+ // Arrange
64
+ const scriptName = "test-script";
65
+
66
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
67
+ "/test/workspace",
68
+ );
69
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
70
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
71
+ {name: "pkg2", path: "/test/pkg2", version: "1.0.0"},
72
+ ]);
73
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
74
+ {
75
+ packageName: "pkg1",
76
+ packagePath: "/test/pkg1",
77
+ binName: "test-script",
78
+ binPath: "/test/pkg1/bin/test",
79
+ },
80
+ {
81
+ packageName: "pkg2",
82
+ packagePath: "/test/pkg2",
83
+ binName: "test-script",
84
+ binPath: "/test/pkg2/bin/test",
85
+ },
86
+ ]);
87
+
88
+ // Act
89
+ const result = await xImpl(scriptName);
90
+
91
+ // Assert
92
+ expect(result.exitCode).toBe(1);
93
+ });
94
+
95
+ it("should execute script in dry-run mode", async () => {
96
+ // Arrange
97
+ const scriptName = "test-script";
98
+ const options = {dryRun: true};
99
+
100
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
101
+ "/test/workspace",
102
+ );
103
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
104
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
105
+ ]);
106
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
107
+ {
108
+ packageName: "pkg1",
109
+ packagePath: "/test/pkg1",
110
+ binName: "test-script",
111
+ binPath: "/test/pkg1/bin/test",
112
+ },
113
+ ]);
114
+
115
+ // Act
116
+ const result = await xImpl(scriptName, [], options);
117
+
118
+ // Assert
119
+ expect(result.exitCode).toBe(0);
120
+ });
121
+
122
+ it("should not execute script when dry-run is true", async () => {
123
+ // Arrange
124
+ const scriptName = "test-script";
125
+ const options = {dryRun: true};
126
+
127
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
128
+ "/test/workspace",
129
+ );
130
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
131
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
132
+ ]);
133
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
134
+ {
135
+ packageName: "pkg1",
136
+ packagePath: "/test/pkg1",
137
+ binName: "test-script",
138
+ binPath: "/test/pkg1/bin/test",
139
+ },
140
+ ]);
141
+
142
+ // Act
143
+ await xImpl(scriptName, [], options);
144
+
145
+ // Assert
146
+ expect(executeScriptModule.executeScript).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("should execute script and return its exit code", async () => {
150
+ // Arrange
151
+ const scriptName = "test-script";
152
+
153
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
154
+ "/test/workspace",
155
+ );
156
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
157
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
158
+ ]);
159
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
160
+ {
161
+ packageName: "pkg1",
162
+ packagePath: "/test/pkg1",
163
+ binName: "test-script",
164
+ binPath: "/test/pkg1/bin/test",
165
+ },
166
+ ]);
167
+ vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
168
+
169
+ // Act
170
+ const result = await xImpl(scriptName);
171
+
172
+ // Assert
173
+ expect(result.exitCode).toBe(0);
174
+ });
175
+
176
+ it("should pass workspace root to executeScript", async () => {
177
+ // Arrange
178
+ const scriptName = "test-script";
179
+ const workspaceRoot = "/test/workspace";
180
+
181
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
182
+ workspaceRoot,
183
+ );
184
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
185
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
186
+ ]);
187
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
188
+ {
189
+ packageName: "pkg1",
190
+ packagePath: "/test/pkg1",
191
+ binName: "test-script",
192
+ binPath: "/test/pkg1/bin/test",
193
+ },
194
+ ]);
195
+ vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
196
+
197
+ // Act
198
+ await xImpl(scriptName);
199
+
200
+ // Assert
201
+ expect(executeScriptModule.executeScript).toHaveBeenCalledWith(
202
+ expect.any(Object),
203
+ expect.any(Array),
204
+ workspaceRoot,
205
+ );
206
+ });
207
+
208
+ it("should pass args to executeScript", async () => {
209
+ // Arrange
210
+ const scriptName = "test-script";
211
+ const args = ["--flag", "value"];
212
+
213
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockResolvedValue(
214
+ "/test/workspace",
215
+ );
216
+ vi.mocked(discoverPackagesModule.discoverPackages).mockResolvedValue([
217
+ {name: "pkg1", path: "/test/pkg1", version: "1.0.0"},
218
+ ]);
219
+ vi.mocked(findMatchingBinsModule.findMatchingBins).mockResolvedValue([
220
+ {
221
+ packageName: "pkg1",
222
+ packagePath: "/test/pkg1",
223
+ binName: "test-script",
224
+ binPath: "/test/pkg1/bin/test",
225
+ },
226
+ ]);
227
+ vi.mocked(executeScriptModule.executeScript).mockResolvedValue(0);
228
+
229
+ // Act
230
+ await xImpl(scriptName, args);
231
+
232
+ // Assert
233
+ expect(executeScriptModule.executeScript).toHaveBeenCalledWith(
234
+ expect.any(Object),
235
+ args,
236
+ expect.any(String),
237
+ );
238
+ });
239
+
240
+ it("should return exit code 1 on unhandled error", async () => {
241
+ // Arrange
242
+ const scriptName = "test-script";
243
+
244
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockRejectedValue(
245
+ new HandledError("Test error"),
246
+ );
247
+
248
+ // Act
249
+ const result = await xImpl(scriptName);
250
+
251
+ // Assert
252
+ expect(result.exitCode).toBe(1);
253
+ });
254
+
255
+ it("should rethrow non-HandledError errors", async () => {
256
+ // Arrange
257
+ const scriptName = "test-script";
258
+ const unexpectedError = new Error("Unexpected error");
259
+
260
+ vi.mocked(findWorkspaceRootModule.findWorkspaceRoot).mockRejectedValue(
261
+ unexpectedError,
262
+ );
263
+
264
+ // Act
265
+ const underTest = () => xImpl(scriptName);
266
+
267
+ // Assert
268
+ await expect(underTest).rejects.toThrow("Unexpected error");
269
+ });
270
+
271
+ it("should return exit code 1 when script name is empty", async () => {
272
+ // Arrange
273
+ const scriptName = "";
274
+
275
+ // Act
276
+ const result = await xImpl(scriptName);
277
+
278
+ // Assert
279
+ expect(result.exitCode).toBe(1);
280
+ });
281
+
282
+ it("should return exit code 1 when script name is only whitespace", async () => {
283
+ // Arrange
284
+ const scriptName = " ";
285
+
286
+ // Act
287
+ const result = await xImpl(scriptName);
288
+
289
+ // Assert
290
+ expect(result.exitCode).toBe(1);
291
+ });
292
+
293
+ it("should return exit code 1 when script name contains forward slash", async () => {
294
+ // Arrange
295
+ const scriptName = "bin/script";
296
+
297
+ // Act
298
+ const result = await xImpl(scriptName);
299
+
300
+ // Assert
301
+ expect(result.exitCode).toBe(1);
302
+ });
303
+
304
+ it("should return exit code 1 when script name contains backslash", async () => {
6
305
  // Arrange
7
- const consoleSpy = vi.spyOn(console, "log");
306
+ const scriptName = "bin\\script";
8
307
 
9
308
  // Act
10
- xImpl();
309
+ const result = await xImpl(scriptName);
11
310
 
12
311
  // Assert
13
- expect(consoleSpy).toHaveBeenCalledWith("Hello, world!");
312
+ expect(result.exitCode).toBe(1);
14
313
  });
15
- });
314
+ });