Skip to content

Commit a59046b

Browse files
committed
feat: add input validation with descriptive errors to public API functions
Signed-off-by: leocavalcante <[email protected]>
1 parent d2e6fdb commit a59046b

File tree

2 files changed

+226
-2
lines changed

2 files changed

+226
-2
lines changed

src/paths.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ export const REQUIRED_FRONTMATTER_FIELDS = ["version", "requires"]
2222
* Get the package root directory from a module's import.meta.url
2323
* @param {string} importMetaUrl - The import.meta.url of the calling module
2424
* @returns {string} The package root directory path
25+
* @throws {TypeError} If importMetaUrl is not a non-empty string
2526
*/
2627
export function getPackageRoot(importMetaUrl) {
28+
if (typeof importMetaUrl !== "string") {
29+
throw new TypeError(
30+
`getPackageRoot: importMetaUrl must be a string, got ${importMetaUrl === null ? "null" : typeof importMetaUrl}`,
31+
)
32+
}
33+
if (importMetaUrl.trim() === "") {
34+
throw new TypeError("getPackageRoot: importMetaUrl must not be empty")
35+
}
2736
const __filename = fileURLToPath(importMetaUrl)
2837
const __dirname = dirname(__filename)
2938
// Both postinstall.mjs and preuninstall.mjs are in the package root
@@ -34,8 +43,17 @@ export function getPackageRoot(importMetaUrl) {
3443
* Get the source directory containing agent markdown files.
3544
* @param {string} packageRoot - The package root directory
3645
* @returns {string} Path to the agents source directory
46+
* @throws {TypeError} If packageRoot is not a non-empty string
3747
*/
3848
export function getAgentsSourceDir(packageRoot) {
49+
if (typeof packageRoot !== "string") {
50+
throw new TypeError(
51+
`getAgentsSourceDir: packageRoot must be a string, got ${packageRoot === null ? "null" : typeof packageRoot}`,
52+
)
53+
}
54+
if (packageRoot.trim() === "") {
55+
throw new TypeError("getAgentsSourceDir: packageRoot must not be empty")
56+
}
3957
return join(packageRoot, "agents")
4058
}
4159

@@ -337,8 +355,25 @@ function compareVersions(a, b) {
337355
* checkVersionCompatibility("^1.0.0", "2.0.0") // false
338356
* checkVersionCompatibility("~1.2.0", "1.2.5") // true
339357
* checkVersionCompatibility("~1.2.0", "1.3.0") // false
358+
* @throws {TypeError} If required or current is not a non-empty string
340359
*/
341360
export function checkVersionCompatibility(required, current) {
361+
if (typeof required !== "string") {
362+
throw new TypeError(
363+
`checkVersionCompatibility: required must be a string, got ${required === null ? "null" : typeof required}`,
364+
)
365+
}
366+
if (required.trim() === "") {
367+
throw new TypeError("checkVersionCompatibility: required must not be empty")
368+
}
369+
if (typeof current !== "string") {
370+
throw new TypeError(
371+
`checkVersionCompatibility: current must be a string, got ${current === null ? "null" : typeof current}`,
372+
)
373+
}
374+
if (current.trim() === "") {
375+
throw new TypeError("checkVersionCompatibility: current must not be empty")
376+
}
342377
const currentVersion = parseVersion(current)
343378
if (!currentVersion) return false
344379

@@ -431,8 +466,14 @@ export function checkVersionCompatibility(required, current) {
431466
* // Parse custom arguments
432467
* const flags = parseCliFlags(["node", "script.js", "--verbose", "--dry-run"])
433468
* // flags = { dryRun: true, verbose: true, help: false }
469+
* @throws {TypeError} If argv is not an array
434470
*/
435471
export function parseCliFlags(argv) {
472+
if (!Array.isArray(argv)) {
473+
throw new TypeError(
474+
`parseCliFlags: argv must be an array, got ${argv === null ? "null" : typeof argv}`,
475+
)
476+
}
436477
return {
437478
dryRun: argv.includes("--dry-run"),
438479
verbose: argv.includes("--verbose"),

tests/paths.test.ts

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,41 @@ describe("paths.mjs exports", () => {
4848
const result2 = getPackageRoot(import.meta.url)
4949
expect(result1).toBe(result2)
5050
})
51+
52+
it("should throw TypeError for null input", () => {
53+
expect(() => getPackageRoot(null as unknown as string)).toThrow(TypeError)
54+
expect(() => getPackageRoot(null as unknown as string)).toThrow(
55+
"getPackageRoot: importMetaUrl must be a string, got null",
56+
)
57+
})
58+
59+
it("should throw TypeError for undefined input", () => {
60+
expect(() => getPackageRoot(undefined as unknown as string)).toThrow(TypeError)
61+
expect(() => getPackageRoot(undefined as unknown as string)).toThrow(
62+
"getPackageRoot: importMetaUrl must be a string, got undefined",
63+
)
64+
})
65+
66+
it("should throw TypeError for non-string input", () => {
67+
expect(() => getPackageRoot(123 as unknown as string)).toThrow(TypeError)
68+
expect(() => getPackageRoot(123 as unknown as string)).toThrow(
69+
"getPackageRoot: importMetaUrl must be a string, got number",
70+
)
71+
expect(() => getPackageRoot({} as unknown as string)).toThrow(TypeError)
72+
expect(() => getPackageRoot({} as unknown as string)).toThrow(
73+
"getPackageRoot: importMetaUrl must be a string, got object",
74+
)
75+
})
76+
77+
it("should throw TypeError for empty string input", () => {
78+
expect(() => getPackageRoot("")).toThrow(TypeError)
79+
expect(() => getPackageRoot("")).toThrow("getPackageRoot: importMetaUrl must not be empty")
80+
})
81+
82+
it("should throw TypeError for whitespace-only string input", () => {
83+
expect(() => getPackageRoot(" ")).toThrow(TypeError)
84+
expect(() => getPackageRoot(" ")).toThrow("getPackageRoot: importMetaUrl must not be empty")
85+
})
5186
})
5287

5388
describe("getAgentsSourceDir", () => {
@@ -82,6 +117,45 @@ describe("paths.mjs exports", () => {
82117
const result = getAgentsSourceDir(packageRoot)
83118
expect(result).toBe(join(packageRoot, "agents"))
84119
})
120+
121+
it("should throw TypeError for null input", () => {
122+
expect(() => getAgentsSourceDir(null as unknown as string)).toThrow(TypeError)
123+
expect(() => getAgentsSourceDir(null as unknown as string)).toThrow(
124+
"getAgentsSourceDir: packageRoot must be a string, got null",
125+
)
126+
})
127+
128+
it("should throw TypeError for undefined input", () => {
129+
expect(() => getAgentsSourceDir(undefined as unknown as string)).toThrow(TypeError)
130+
expect(() => getAgentsSourceDir(undefined as unknown as string)).toThrow(
131+
"getAgentsSourceDir: packageRoot must be a string, got undefined",
132+
)
133+
})
134+
135+
it("should throw TypeError for non-string input", () => {
136+
expect(() => getAgentsSourceDir(123 as unknown as string)).toThrow(TypeError)
137+
expect(() => getAgentsSourceDir(123 as unknown as string)).toThrow(
138+
"getAgentsSourceDir: packageRoot must be a string, got number",
139+
)
140+
expect(() => getAgentsSourceDir({} as unknown as string)).toThrow(TypeError)
141+
expect(() => getAgentsSourceDir({} as unknown as string)).toThrow(
142+
"getAgentsSourceDir: packageRoot must be a string, got object",
143+
)
144+
})
145+
146+
it("should throw TypeError for empty string input", () => {
147+
expect(() => getAgentsSourceDir("")).toThrow(TypeError)
148+
expect(() => getAgentsSourceDir("")).toThrow(
149+
"getAgentsSourceDir: packageRoot must not be empty",
150+
)
151+
})
152+
153+
it("should throw TypeError for whitespace-only string input", () => {
154+
expect(() => getAgentsSourceDir(" ")).toThrow(TypeError)
155+
expect(() => getAgentsSourceDir(" ")).toThrow(
156+
"getAgentsSourceDir: packageRoot must not be empty",
157+
)
158+
})
85159
})
86160

87161
describe("AGENTS_TARGET_DIR", () => {
@@ -790,14 +864,94 @@ This is a test agent that handles various tasks.
790864
expect(checkVersionCompatibility(">=1.0.0", "invalid")).toBe(false)
791865
expect(checkVersionCompatibility(">=1.0.0", "1.0")).toBe(false)
792866
expect(checkVersionCompatibility(">=1.0.0", "1")).toBe(false)
793-
expect(checkVersionCompatibility(">=1.0.0", "")).toBe(false)
794867
})
795868

796869
it("should return false for invalid required version", () => {
797870
expect(checkVersionCompatibility("invalid", "1.0.0")).toBe(false)
798871
expect(checkVersionCompatibility(">=invalid", "1.0.0")).toBe(false)
799872
expect(checkVersionCompatibility("^", "1.0.0")).toBe(false)
800-
expect(checkVersionCompatibility("", "1.0.0")).toBe(false)
873+
})
874+
875+
it("should throw TypeError for null required", () => {
876+
expect(() => checkVersionCompatibility(null as unknown as string, "1.0.0")).toThrow(
877+
TypeError,
878+
)
879+
expect(() => checkVersionCompatibility(null as unknown as string, "1.0.0")).toThrow(
880+
"checkVersionCompatibility: required must be a string, got null",
881+
)
882+
})
883+
884+
it("should throw TypeError for undefined required", () => {
885+
expect(() => checkVersionCompatibility(undefined as unknown as string, "1.0.0")).toThrow(
886+
TypeError,
887+
)
888+
expect(() => checkVersionCompatibility(undefined as unknown as string, "1.0.0")).toThrow(
889+
"checkVersionCompatibility: required must be a string, got undefined",
890+
)
891+
})
892+
893+
it("should throw TypeError for non-string required", () => {
894+
expect(() => checkVersionCompatibility(123 as unknown as string, "1.0.0")).toThrow(
895+
TypeError,
896+
)
897+
expect(() => checkVersionCompatibility(123 as unknown as string, "1.0.0")).toThrow(
898+
"checkVersionCompatibility: required must be a string, got number",
899+
)
900+
})
901+
902+
it("should throw TypeError for empty string required", () => {
903+
expect(() => checkVersionCompatibility("", "1.0.0")).toThrow(TypeError)
904+
expect(() => checkVersionCompatibility("", "1.0.0")).toThrow(
905+
"checkVersionCompatibility: required must not be empty",
906+
)
907+
})
908+
909+
it("should throw TypeError for whitespace-only required", () => {
910+
expect(() => checkVersionCompatibility(" ", "1.0.0")).toThrow(TypeError)
911+
expect(() => checkVersionCompatibility(" ", "1.0.0")).toThrow(
912+
"checkVersionCompatibility: required must not be empty",
913+
)
914+
})
915+
916+
it("should throw TypeError for null current", () => {
917+
expect(() => checkVersionCompatibility(">=1.0.0", null as unknown as string)).toThrow(
918+
TypeError,
919+
)
920+
expect(() => checkVersionCompatibility(">=1.0.0", null as unknown as string)).toThrow(
921+
"checkVersionCompatibility: current must be a string, got null",
922+
)
923+
})
924+
925+
it("should throw TypeError for undefined current", () => {
926+
expect(() => checkVersionCompatibility(">=1.0.0", undefined as unknown as string)).toThrow(
927+
TypeError,
928+
)
929+
expect(() => checkVersionCompatibility(">=1.0.0", undefined as unknown as string)).toThrow(
930+
"checkVersionCompatibility: current must be a string, got undefined",
931+
)
932+
})
933+
934+
it("should throw TypeError for non-string current", () => {
935+
expect(() => checkVersionCompatibility(">=1.0.0", 100 as unknown as string)).toThrow(
936+
TypeError,
937+
)
938+
expect(() => checkVersionCompatibility(">=1.0.0", 100 as unknown as string)).toThrow(
939+
"checkVersionCompatibility: current must be a string, got number",
940+
)
941+
})
942+
943+
it("should throw TypeError for empty string current", () => {
944+
expect(() => checkVersionCompatibility(">=1.0.0", "")).toThrow(TypeError)
945+
expect(() => checkVersionCompatibility(">=1.0.0", "")).toThrow(
946+
"checkVersionCompatibility: current must not be empty",
947+
)
948+
})
949+
950+
it("should throw TypeError for whitespace-only current", () => {
951+
expect(() => checkVersionCompatibility(">=1.0.0", " ")).toThrow(TypeError)
952+
expect(() => checkVersionCompatibility(">=1.0.0", " ")).toThrow(
953+
"checkVersionCompatibility: current must not be empty",
954+
)
801955
})
802956
})
803957
})
@@ -861,6 +1015,35 @@ This is a test agent that handles various tasks.
8611015
expect(result.dryRun).toBe(false)
8621016
expect(result.verbose).toBe(false)
8631017
})
1018+
1019+
it("should throw TypeError for null input", () => {
1020+
expect(() => parseCliFlags(null as unknown as string[])).toThrow(TypeError)
1021+
expect(() => parseCliFlags(null as unknown as string[])).toThrow(
1022+
"parseCliFlags: argv must be an array, got null",
1023+
)
1024+
})
1025+
1026+
it("should throw TypeError for undefined input", () => {
1027+
expect(() => parseCliFlags(undefined as unknown as string[])).toThrow(TypeError)
1028+
expect(() => parseCliFlags(undefined as unknown as string[])).toThrow(
1029+
"parseCliFlags: argv must be an array, got undefined",
1030+
)
1031+
})
1032+
1033+
it("should throw TypeError for non-array input", () => {
1034+
expect(() => parseCliFlags("string" as unknown as string[])).toThrow(TypeError)
1035+
expect(() => parseCliFlags("string" as unknown as string[])).toThrow(
1036+
"parseCliFlags: argv must be an array, got string",
1037+
)
1038+
expect(() => parseCliFlags(123 as unknown as string[])).toThrow(TypeError)
1039+
expect(() => parseCliFlags(123 as unknown as string[])).toThrow(
1040+
"parseCliFlags: argv must be an array, got number",
1041+
)
1042+
expect(() => parseCliFlags({} as unknown as string[])).toThrow(TypeError)
1043+
expect(() => parseCliFlags({} as unknown as string[])).toThrow(
1044+
"parseCliFlags: argv must be an array, got object",
1045+
)
1046+
})
8641047
})
8651048

8661049
describe("createLogger", () => {

0 commit comments

Comments
 (0)