Skip to content

Commit f02ecea

Browse files
jiunbaeclaude
andcommitted
test: add unit tests for repo, discovery, execution, completion, cli
- packages/repo: resolver (18) + metadata-cache (15) = 33 tests - packages/discovery: ranker (26) + scanner (21) = 47 tests - packages/execution: engine (12) + worker (8) = 20 tests - packages/completion: diff-validator (14) + handler (10) + issue-linker (7) = 31 tests - packages/cli: cli (5) + doctor (13) = 18 tests - Fix dashboard test script to pass with no tests Total: 265 tests across 17 files, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d82625 commit f02ecea

12 files changed

Lines changed: 3393 additions & 1 deletion

File tree

‎packages/cli/tests/cli.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
3+
import { createCliProgram } from "../src/cli.js";
4+
5+
beforeEach(() => {
6+
vi.restoreAllMocks();
7+
});
8+
9+
describe("createCliProgram", () => {
10+
it("returns a Commander program with expected commands", async () => {
11+
const program = await createCliProgram();
12+
const commandNames = program.commands.map((cmd) => cmd.name());
13+
14+
expect(commandNames).toContain("init");
15+
expect(commandNames).toContain("doctor");
16+
expect(commandNames).toContain("scan");
17+
expect(commandNames).toContain("plan");
18+
expect(commandNames).toContain("run");
19+
});
20+
21+
it("has global options --config, --verbose, --json, --no-color", async () => {
22+
const program = await createCliProgram();
23+
const optionNames = program.options.map((option) => option.long);
24+
25+
expect(optionNames).toContain("--config");
26+
expect(optionNames).toContain("--verbose");
27+
expect(optionNames).toContain("--json");
28+
expect(optionNames).toContain("--no-color");
29+
});
30+
31+
it("has name 'oac'", async () => {
32+
const program = await createCliProgram();
33+
expect(program.name()).toBe("oac");
34+
});
35+
36+
it("has a version string", async () => {
37+
const program = await createCliProgram();
38+
expect(program.version()).toBeDefined();
39+
expect(typeof program.version()).toBe("string");
40+
});
41+
42+
it("has 5 subcommands", async () => {
43+
const program = await createCliProgram();
44+
expect(program.commands).toHaveLength(5);
45+
});
46+
});

‎packages/cli/tests/doctor.test.ts‎

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { spawn } from "node:child_process";
2+
import { EventEmitter } from "node:events";
3+
4+
import { Command } from "commander";
5+
import { describe, it, expect, vi, beforeEach } from "vitest";
6+
7+
import { createDoctorCommand } from "../src/commands/doctor.js";
8+
9+
vi.mock("node:child_process", () => ({
10+
spawn: vi.fn(),
11+
}));
12+
13+
interface DoctorCheck {
14+
id: string;
15+
status: "pass" | "fail";
16+
value: string;
17+
}
18+
19+
interface DoctorPayload {
20+
checks: DoctorCheck[];
21+
allPassed: boolean;
22+
}
23+
24+
interface MockSpawnResult {
25+
stdout?: string;
26+
stderr?: string;
27+
exitCode?: number | null;
28+
error?: NodeJS.ErrnoException;
29+
}
30+
31+
class MockStream extends EventEmitter {
32+
setEncoding(_encoding: BufferEncoding): this {
33+
return this;
34+
}
35+
}
36+
37+
type MockChildProcess = EventEmitter & {
38+
stdout: MockStream;
39+
stderr: MockStream;
40+
};
41+
42+
const mockedSpawn = vi.mocked(spawn);
43+
const originalNodeVersion = process.versions.node;
44+
const originalGithubToken = process.env.GITHUB_TOKEN;
45+
46+
function setNodeVersion(version: string): void {
47+
Object.defineProperty(process.versions, "node", {
48+
configurable: true,
49+
enumerable: true,
50+
value: version,
51+
});
52+
}
53+
54+
function createMockChildProcess(result: MockSpawnResult): ReturnType<typeof spawn> {
55+
const child = new EventEmitter() as MockChildProcess;
56+
child.stdout = new MockStream();
57+
child.stderr = new MockStream();
58+
59+
queueMicrotask(() => {
60+
if (typeof result.stdout === "string" && result.stdout.length > 0) {
61+
child.stdout.emit("data", result.stdout);
62+
}
63+
64+
if (typeof result.stderr === "string" && result.stderr.length > 0) {
65+
child.stderr.emit("data", result.stderr);
66+
}
67+
68+
if (result.error) {
69+
child.emit("error", result.error);
70+
return;
71+
}
72+
73+
child.emit("close", result.exitCode ?? 0);
74+
});
75+
76+
return child as unknown as ReturnType<typeof spawn>;
77+
}
78+
79+
function mockCommandResults(results: Record<string, MockSpawnResult>): void {
80+
mockedSpawn.mockImplementation((command: string) => {
81+
const result = results[command];
82+
if (!result) {
83+
throw new Error(`Unexpected command call: ${command}`);
84+
}
85+
86+
return createMockChildProcess(result);
87+
});
88+
}
89+
90+
function passingCommandResults(
91+
overrides: Partial<Record<string, MockSpawnResult>> = {},
92+
): Record<string, MockSpawnResult> {
93+
return {
94+
git: { stdout: "git version 2.43.0\n", exitCode: 0 },
95+
gh: { stdout: "Logged in to github.com", exitCode: 0 },
96+
claude: { stdout: "Claude Code v1.0.16\n", exitCode: 0 },
97+
codex: { stdout: "v0.2.0\n", exitCode: 0 },
98+
...overrides,
99+
};
100+
}
101+
102+
async function runDoctorJsonCommand(): Promise<DoctorPayload> {
103+
const root = new Command()
104+
.option("--config <path>", "Config file path", "oac.config.ts")
105+
.option("--verbose", "Enable verbose logging", false)
106+
.option("--json", "Output machine-readable JSON", false)
107+
.option("--no-color", "Disable ANSI colors");
108+
109+
root.addCommand(createDoctorCommand());
110+
111+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
112+
await root.parseAsync(["node", "oac", "--json", "doctor"]);
113+
114+
const output = logSpy.mock.calls.at(-1)?.[0];
115+
if (typeof output !== "string") {
116+
throw new Error("Doctor command did not emit JSON output.");
117+
}
118+
119+
return JSON.parse(output) as DoctorPayload;
120+
}
121+
122+
function getCheck(payload: DoctorPayload, id: string): DoctorCheck {
123+
const check = payload.checks.find((entry) => entry.id === id);
124+
expect(check).toBeDefined();
125+
return check as DoctorCheck;
126+
}
127+
128+
beforeEach(() => {
129+
vi.restoreAllMocks();
130+
mockedSpawn.mockReset();
131+
setNodeVersion(originalNodeVersion);
132+
133+
if (originalGithubToken === undefined) {
134+
delete process.env.GITHUB_TOKEN;
135+
} else {
136+
process.env.GITHUB_TOKEN = originalGithubToken;
137+
}
138+
139+
process.exitCode = 0;
140+
});
141+
142+
describe("createDoctorCommand", () => {
143+
it("returns a Commander Command instance", () => {
144+
const cmd = createDoctorCommand();
145+
expect(cmd.name()).toBe("doctor");
146+
});
147+
});
148+
149+
describe("isVersionAtLeast behavior through node check status", () => {
150+
const cases = [
151+
{ version: "24.0.0", expected: true },
152+
{ version: "24.1.0", expected: true },
153+
{ version: "23.9.9", expected: false },
154+
{ version: "24.0.1", expected: true },
155+
{ version: "22.0.0", expected: false },
156+
] as const;
157+
158+
for (const testCase of cases) {
159+
it(`marks node ${testCase.version} against minimum 24.0.0 as ${testCase.expected ? "pass" : "fail"}`, async () => {
160+
setNodeVersion(testCase.version);
161+
mockCommandResults(passingCommandResults());
162+
163+
const payload = await runDoctorJsonCommand();
164+
const nodeCheck = getCheck(payload, "node");
165+
166+
expect(nodeCheck.status).toBe(testCase.expected ? "pass" : "fail");
167+
expect(nodeCheck.value).toBe(`v${testCase.version}`);
168+
expect(payload.allPassed).toBe(testCase.expected);
169+
expect(process.exitCode).toBe(testCase.expected ? 0 : 1);
170+
});
171+
}
172+
});
173+
174+
describe("extractVersion behavior through command output", () => {
175+
it('extracts "v24.0.0" from direct version output', async () => {
176+
setNodeVersion("24.1.0");
177+
mockCommandResults(
178+
passingCommandResults({
179+
git: { stdout: "v24.0.0", exitCode: 0 },
180+
}),
181+
);
182+
183+
const payload = await runDoctorJsonCommand();
184+
expect(getCheck(payload, "git").value).toBe("v24.0.0");
185+
});
186+
187+
it('extracts "v2.43.0" from "git version 2.43.0"', async () => {
188+
setNodeVersion("24.1.0");
189+
mockCommandResults(
190+
passingCommandResults({
191+
git: { stdout: "git version 2.43.0", exitCode: 0 },
192+
}),
193+
);
194+
195+
const payload = await runDoctorJsonCommand();
196+
expect(getCheck(payload, "git").value).toBe("v2.43.0");
197+
});
198+
199+
it('extracts "v1.0.16" from "Claude Code v1.0.16"', async () => {
200+
setNodeVersion("24.1.0");
201+
mockCommandResults(
202+
passingCommandResults({
203+
claude: { stdout: "Claude Code v1.0.16", exitCode: 0 },
204+
}),
205+
);
206+
207+
const payload = await runDoctorJsonCommand();
208+
expect(getCheck(payload, "claude-cli").value).toBe("v1.0.16");
209+
});
210+
211+
it('returns "--" when output has no version token', async () => {
212+
setNodeVersion("24.1.0");
213+
mockCommandResults(
214+
passingCommandResults({
215+
git: { stdout: "no version here", exitCode: 0 },
216+
}),
217+
);
218+
219+
const payload = await runDoctorJsonCommand();
220+
expect(getCheck(payload, "git").value).toBe("--");
221+
});
222+
223+
it('returns "--" when output is empty', async () => {
224+
setNodeVersion("24.1.0");
225+
mockCommandResults(
226+
passingCommandResults({
227+
git: { stdout: "", exitCode: 0 },
228+
}),
229+
);
230+
231+
const payload = await runDoctorJsonCommand();
232+
expect(getCheck(payload, "git").value).toBe("--");
233+
});
234+
});
235+
236+
describe("maskToken behavior through github-auth check", () => {
237+
it('masks short token "abc" as "ab****"', async () => {
238+
setNodeVersion("24.1.0");
239+
process.env.GITHUB_TOKEN = "abc";
240+
mockCommandResults(passingCommandResults());
241+
242+
const payload = await runDoctorJsonCommand();
243+
const githubAuthCheck = getCheck(payload, "github-auth");
244+
245+
expect(githubAuthCheck.value).toBe("env:ab****");
246+
expect(mockedSpawn.mock.calls.map((call) => call[0])).not.toContain("gh");
247+
});
248+
249+
it('masks long token "ghp_abcd1234xy" as "ghp_****xy"', async () => {
250+
setNodeVersion("24.1.0");
251+
process.env.GITHUB_TOKEN = "ghp_abcd1234xy";
252+
mockCommandResults(passingCommandResults());
253+
254+
const payload = await runDoctorJsonCommand();
255+
const githubAuthCheck = getCheck(payload, "github-auth");
256+
257+
expect(githubAuthCheck.value).toBe("env:ghp_****xy");
258+
expect(mockedSpawn.mock.calls.map((call) => call[0])).not.toContain("gh");
259+
});
260+
});

0 commit comments

Comments
 (0)