Skip to content

Commit d422f12

Browse files
authored
Merge pull request #265 from PaulJPhilp/ep-cli/test-coverage
test(ep-cli): add 83 tests for pre-release coverage
2 parents 6f16daf + 6e9262d commit d422f12

7 files changed

Lines changed: 810 additions & 6 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Tests for colorize utility
3+
*/
4+
5+
import { afterEach, describe, expect, it } from "vitest";
6+
import { colorize } from "../utils.js";
7+
import { ANSI_COLORS } from "../constants.js";
8+
9+
describe("colorize", () => {
10+
const savedEnv = { ...process.env };
11+
const savedIsTTY = process.stdout.isTTY;
12+
13+
afterEach(() => {
14+
process.env = { ...savedEnv };
15+
Object.defineProperty(process.stdout, "isTTY", { value: savedIsTTY, writable: true });
16+
});
17+
18+
it("returns plain text when NO_COLOR is set", () => {
19+
process.env.NO_COLOR = "1";
20+
delete process.env.CI;
21+
delete process.env.TERM;
22+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
23+
expect(colorize("hello", "RED")).toBe("hello");
24+
});
25+
26+
it("returns plain text when CI is set", () => {
27+
delete process.env.NO_COLOR;
28+
process.env.CI = "true";
29+
delete process.env.TERM;
30+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
31+
expect(colorize("hello", "RED")).toBe("hello");
32+
});
33+
34+
it("returns plain text when TERM=dumb", () => {
35+
delete process.env.NO_COLOR;
36+
delete process.env.CI;
37+
process.env.TERM = "dumb";
38+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
39+
expect(colorize("hello", "RED")).toBe("hello");
40+
});
41+
42+
it("returns plain text when stdout is not a TTY", () => {
43+
delete process.env.NO_COLOR;
44+
delete process.env.CI;
45+
delete process.env.TERM;
46+
Object.defineProperty(process.stdout, "isTTY", { value: false, writable: true });
47+
expect(colorize("hello", "RED")).toBe("hello");
48+
});
49+
50+
it("returns colored text in TTY environment", () => {
51+
delete process.env.NO_COLOR;
52+
delete process.env.CI;
53+
delete process.env.TERM;
54+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
55+
56+
const result = colorize("hello", "RED");
57+
expect(result).toBe(`${ANSI_COLORS.RED}hello${ANSI_COLORS.RESET}`);
58+
});
59+
60+
it("works with all color keys", () => {
61+
delete process.env.NO_COLOR;
62+
delete process.env.CI;
63+
delete process.env.TERM;
64+
Object.defineProperty(process.stdout, "isTTY", { value: true, writable: true });
65+
66+
for (const key of Object.keys(ANSI_COLORS) as (keyof typeof ANSI_COLORS)[]) {
67+
if (key === "RESET") continue;
68+
const result = colorize("text", key);
69+
expect(result).toContain(ANSI_COLORS[key]);
70+
expect(result).toContain(ANSI_COLORS.RESET);
71+
}
72+
});
73+
});
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* Tests for pure helper functions exported from index.ts
3+
*/
4+
5+
import { afterEach, describe, expect, it } from "vitest";
6+
import {
7+
extractErrorMessage,
8+
findClosest,
9+
getCommandSuggestion,
10+
levenshtein,
11+
mapCliLogLevel,
12+
resolveLoggerConfig,
13+
} from "../index.js";
14+
15+
describe("levenshtein", () => {
16+
it("returns 0 for identical strings", () => {
17+
expect(levenshtein("abc", "abc")).toBe(0);
18+
});
19+
20+
it("returns length of other string when one is empty", () => {
21+
expect(levenshtein("", "abc")).toBe(3);
22+
expect(levenshtein("abc", "")).toBe(3);
23+
});
24+
25+
it("returns 0 for two empty strings", () => {
26+
expect(levenshtein("", "")).toBe(0);
27+
});
28+
29+
it("computes single-char edits", () => {
30+
expect(levenshtein("cat", "bat")).toBe(1);
31+
expect(levenshtein("cat", "cats")).toBe(1);
32+
expect(levenshtein("cat", "at")).toBe(1);
33+
});
34+
35+
it("computes multi-char edits", () => {
36+
expect(levenshtein("kitten", "sitting")).toBe(3);
37+
});
38+
});
39+
40+
describe("findClosest", () => {
41+
it("returns null for empty candidates", () => {
42+
expect(findClosest("search", [])).toBeNull();
43+
});
44+
45+
it("finds exact match", () => {
46+
expect(findClosest("search", ["search", "list", "show"])).toBe("search");
47+
});
48+
49+
it("finds close match within threshold", () => {
50+
expect(findClosest("serch", ["search", "list", "show"])).toBe("search");
51+
});
52+
53+
it("returns null when no candidate is close enough", () => {
54+
expect(findClosest("zzzzzzz", ["search", "list", "show"])).toBeNull();
55+
});
56+
57+
it("is case-insensitive", () => {
58+
expect(findClosest("SEARCH", ["search", "list"])).toBe("search");
59+
});
60+
});
61+
62+
describe("getCommandSuggestion", () => {
63+
it("returns null when no args provided", () => {
64+
expect(getCommandSuggestion(["node", "ep"])).toBeNull();
65+
});
66+
67+
it("suggests close root command", () => {
68+
const result = getCommandSuggestion(["node", "ep", "serch"]);
69+
expect(result).toContain("search");
70+
});
71+
72+
it("returns null for valid root command", () => {
73+
expect(getCommandSuggestion(["node", "ep", "search"])).toBeNull();
74+
});
75+
76+
it("suggests close nested command", () => {
77+
const result = getCommandSuggestion(["node", "ep", "install", "ad"]);
78+
expect(result).toContain("add");
79+
});
80+
81+
it("returns null for valid nested command", () => {
82+
expect(getCommandSuggestion(["node", "ep", "install", "add"])).toBeNull();
83+
});
84+
85+
it("returns null for command without nested subcommands", () => {
86+
expect(getCommandSuggestion(["node", "ep", "search", "anything"])).toBeNull();
87+
});
88+
});
89+
90+
describe("extractErrorMessage", () => {
91+
const argv = ["node", "ep", "search", "foo"];
92+
93+
it("returns string errors as-is", () => {
94+
expect(extractErrorMessage("some error", argv)).toBe("some error");
95+
});
96+
97+
it("returns suggestion for CommandMismatch", () => {
98+
const err = new Error("CommandMismatch: blah");
99+
const result = extractErrorMessage(err, ["node", "ep", "serch"]);
100+
expect(result).toContain("search");
101+
});
102+
103+
it("returns help message for CommandMismatch without suggestion", () => {
104+
const err = new Error("CommandMismatch: blah");
105+
const result = extractErrorMessage(err, ["node", "ep", "zzzzzzz"]);
106+
expect(result).toContain("ep --help");
107+
});
108+
109+
it("handles network errors", () => {
110+
const err = new Error("Unable to connect. Is the computer able to access the url?");
111+
const result = extractErrorMessage(err, argv);
112+
expect(result).toContain("Effect Patterns API");
113+
});
114+
115+
it("handles 401 errors", () => {
116+
const err = new Error("Pattern API unauthorized (401)");
117+
const result = extractErrorMessage(err, argv);
118+
expect(result).toContain("PATTERN_API_KEY");
119+
});
120+
121+
it("handles SkillsDirectoryNotFoundError", () => {
122+
const err = new Error("SkillsDirectoryNotFoundError");
123+
const result = extractErrorMessage(err, argv);
124+
expect(result).toContain("Skills directory");
125+
});
126+
127+
it("returns null for DisabledFeatureError", () => {
128+
const err = new Error("DisabledFeatureError");
129+
expect(extractErrorMessage(err, argv)).toBeNull();
130+
});
131+
132+
it("returns null for ValidationFailedError", () => {
133+
const err = new Error("ValidationFailedError");
134+
expect(extractErrorMessage(err, argv)).toBeNull();
135+
});
136+
137+
it("returns null for UnsupportedToolError", () => {
138+
const err = new Error("UnsupportedToolError");
139+
expect(extractErrorMessage(err, argv)).toBeNull();
140+
});
141+
142+
it("returns null for generic 'An error has occurred'", () => {
143+
const err = new Error("An error has occurred");
144+
expect(extractErrorMessage(err, argv)).toBeNull();
145+
});
146+
147+
it("includes docs URL for generic errors", () => {
148+
const err = new Error("Something went wrong");
149+
const result = extractErrorMessage(err, argv);
150+
expect(result).toContain("Something went wrong");
151+
expect(result).toContain("Docs:");
152+
});
153+
154+
it("stringifies non-Error objects", () => {
155+
expect(extractErrorMessage(42, argv)).toBe("42");
156+
});
157+
});
158+
159+
describe("mapCliLogLevel", () => {
160+
it("maps 'all' to debug", () => {
161+
expect(mapCliLogLevel("all")).toBe("debug");
162+
});
163+
164+
it("maps 'trace' to debug", () => {
165+
expect(mapCliLogLevel("trace")).toBe("debug");
166+
});
167+
168+
it("maps 'debug' to debug", () => {
169+
expect(mapCliLogLevel("debug")).toBe("debug");
170+
});
171+
172+
it("maps 'info' to info", () => {
173+
expect(mapCliLogLevel("info")).toBe("info");
174+
});
175+
176+
it("maps 'warning' to warn", () => {
177+
expect(mapCliLogLevel("warning")).toBe("warn");
178+
});
179+
180+
it("maps 'error' to error", () => {
181+
expect(mapCliLogLevel("error")).toBe("error");
182+
});
183+
184+
it("maps 'fatal' to error", () => {
185+
expect(mapCliLogLevel("fatal")).toBe("error");
186+
});
187+
188+
it("maps 'none' to silent", () => {
189+
expect(mapCliLogLevel("none")).toBe("silent");
190+
});
191+
192+
it("returns undefined for unknown values", () => {
193+
expect(mapCliLogLevel("unknown")).toBeUndefined();
194+
});
195+
196+
it("trims and lowercases input", () => {
197+
expect(mapCliLogLevel(" DEBUG ")).toBe("debug");
198+
});
199+
});
200+
201+
describe("resolveLoggerConfig", () => {
202+
const savedEnv = { ...process.env };
203+
204+
const cleanEnv = () => {
205+
delete process.env.LOG_LEVEL;
206+
delete process.env.DEBUG;
207+
delete process.env.VERBOSE;
208+
};
209+
210+
afterEach(() => {
211+
process.env = { ...savedEnv };
212+
});
213+
214+
it("defaults to info", () => {
215+
cleanEnv();
216+
const config = resolveLoggerConfig(["node", "ep"]);
217+
expect(config.logLevel).toBe("info");
218+
expect(config.verbose).toBe(false);
219+
});
220+
221+
it("respects LOG_LEVEL env", () => {
222+
cleanEnv();
223+
process.env.LOG_LEVEL = "debug";
224+
const config = resolveLoggerConfig(["node", "ep"]);
225+
expect(config.logLevel).toBe("debug");
226+
});
227+
228+
it("respects DEBUG env", () => {
229+
cleanEnv();
230+
process.env.DEBUG = "1";
231+
const config = resolveLoggerConfig(["node", "ep"]);
232+
expect(config.logLevel).toBe("debug");
233+
expect(config.verbose).toBe(true);
234+
});
235+
236+
it("respects VERBOSE env", () => {
237+
cleanEnv();
238+
process.env.VERBOSE = "1";
239+
const config = resolveLoggerConfig(["node", "ep"]);
240+
expect(config.logLevel).toBe("debug");
241+
});
242+
243+
it("CLI --log-level overrides env", () => {
244+
cleanEnv();
245+
process.env.LOG_LEVEL = "error";
246+
const config = resolveLoggerConfig(["node", "ep", "--log-level", "debug"]);
247+
expect(config.logLevel).toBe("debug");
248+
});
249+
250+
it("CLI --log-level=value syntax works", () => {
251+
cleanEnv();
252+
const config = resolveLoggerConfig(["node", "ep", "--log-level=error"]);
253+
expect(config.logLevel).toBe("error");
254+
});
255+
});

0 commit comments

Comments
 (0)