Skip to content

Commit 8658d82

Browse files
authored
feat: add comprehensive test suite and CI workflow (#3)
* feat: add comprehensive test suite and CI workflow - Add Jest test framework with coverage reporting - Add comprehensive test suite for ClaudeCodeRouterConfig class - Add GitHub Actions CI workflow for automated testing - Update package.json with test scripts and Jest configuration - Add package-lock.json to .gitignore - Set coverage thresholds at 80% for all metrics * feat: add lock file for ci
1 parent 91a2a2b commit 8658d82

File tree

5 files changed

+6999
-1
lines changed

5 files changed

+6999
-1
lines changed

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ['*']
6+
pull_request:
7+
branches: ['*']
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Use Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: "20.x"
21+
cache: "npm"
22+
23+
- name: Install dependencies
24+
run: npm install
25+
26+
- name: Run tests
27+
run: npm run test
28+
29+
- name: Run tests with coverage
30+
run: npm run test:coverage

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ yarn-error.log*
3434
# typescript
3535
*.tsbuildinfo
3636
next-env.d.ts
37+
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
const fs = require("fs-extra");
2+
const os = require("os");
3+
4+
// Mock fs-extra
5+
jest.mock("fs-extra");
6+
7+
// Mock process.env
8+
const originalEnv = process.env;
9+
10+
// Mock process.exit
11+
const originalExit = process.exit;
12+
13+
describe("ClaudeCodeRouterConfig", () => {
14+
let ClaudeCodeRouterConfig;
15+
let config;
16+
17+
beforeEach(() => {
18+
// Reset mocks
19+
jest.clearAllMocks();
20+
21+
// Mock os.homedir()
22+
jest.spyOn(os, "homedir").mockReturnValue("/mock/home");
23+
24+
// Reset process.env
25+
process.env = { ...originalEnv };
26+
27+
// Mock process.exit
28+
process.exit = jest.fn();
29+
30+
// Import the class
31+
ClaudeCodeRouterConfig = require("../bin/claude-code-router-config");
32+
});
33+
34+
afterEach(() => {
35+
// Restore process.env
36+
process.env = originalEnv;
37+
// Restore process.exit
38+
process.exit = originalExit;
39+
});
40+
41+
describe("constructor", () => {
42+
it("should correctly initialize configuration paths", () => {
43+
config = new ClaudeCodeRouterConfig();
44+
45+
expect(config.homeDir).toBe("/mock/home");
46+
expect(config.configDir).toBe("/mock/home/.claude-code-router");
47+
expect(config.configFile).toBe(
48+
"/mock/home/.claude-code-router/config.json"
49+
);
50+
expect(config.pluginsDir).toBe("/mock/home/.claude-code-router/plugins");
51+
expect(config.transformerFile).toBe(
52+
"/mock/home/.claude-code-router/plugins/dashscope-transformer.js"
53+
);
54+
});
55+
56+
it("should detect Chinese language environment", () => {
57+
process.env.LANG = "zh_CN.UTF-8";
58+
config = new ClaudeCodeRouterConfig();
59+
expect(config.language).toBe("zh");
60+
});
61+
62+
it("should detect English language environment", () => {
63+
process.env.LANG = "en_US.UTF-8";
64+
config = new ClaudeCodeRouterConfig();
65+
expect(config.language).toBe("en");
66+
});
67+
68+
it("should default to English", () => {
69+
delete process.env.LANG;
70+
delete process.env.LANGUAGE;
71+
delete process.env.LC_ALL;
72+
config = new ClaudeCodeRouterConfig();
73+
expect(config.language).toBe("en");
74+
});
75+
});
76+
77+
describe("getMessages", () => {
78+
beforeEach(() => {
79+
config = new ClaudeCodeRouterConfig();
80+
});
81+
82+
it("should return Chinese messages", () => {
83+
config.language = "zh";
84+
const messages = config.getMessages();
85+
86+
expect(messages.configuring).toBe("🚀 正在配置 claude-code-router...");
87+
expect(messages.configComplete).toBe("✅ claude-code-router 配置完成!");
88+
});
89+
90+
it("should return English messages", () => {
91+
config.language = "en";
92+
const messages = config.getMessages();
93+
94+
expect(messages.configuring).toBe("🚀 Configuring claude-code-router...");
95+
expect(messages.configComplete).toBe(
96+
"✅ claude-code-router configuration completed!"
97+
);
98+
});
99+
});
100+
101+
describe("createDirectories", () => {
102+
beforeEach(() => {
103+
config = new ClaudeCodeRouterConfig();
104+
});
105+
106+
it("should create configuration and plugins directories", async () => {
107+
await config.createDirectories();
108+
109+
expect(fs.ensureDir).toHaveBeenCalledWith(config.configDir);
110+
expect(fs.ensureDir).toHaveBeenCalledWith(config.pluginsDir);
111+
});
112+
113+
it("should handle directory creation errors", async () => {
114+
const error = new Error("Permission denied");
115+
fs.ensureDir.mockRejectedValue(error);
116+
117+
await expect(config.createDirectories()).rejects.toThrow(
118+
"Permission denied"
119+
);
120+
});
121+
});
122+
123+
describe("createConfigFile", () => {
124+
beforeEach(() => {
125+
config = new ClaudeCodeRouterConfig();
126+
});
127+
128+
it("should create config file with API Key from environment variable", async () => {
129+
process.env.DASHSCOPE_API_KEY = "test-api-key";
130+
131+
await config.createConfigFile();
132+
133+
expect(fs.writeJson).toHaveBeenCalledWith(
134+
config.configFile,
135+
expect.objectContaining({
136+
Providers: expect.arrayContaining([
137+
expect.objectContaining({
138+
api_key: "test-api-key",
139+
}),
140+
]),
141+
}),
142+
{ spaces: 2 }
143+
);
144+
});
145+
146+
it("should use undefined API Key when environment variable is not present", async () => {
147+
delete process.env.DASHSCOPE_API_KEY;
148+
149+
await config.createConfigFile();
150+
151+
expect(fs.writeJson).toHaveBeenCalledWith(
152+
config.configFile,
153+
expect.objectContaining({
154+
Providers: expect.arrayContaining([
155+
expect.objectContaining({
156+
api_key: undefined,
157+
}),
158+
]),
159+
}),
160+
{ spaces: 2 }
161+
);
162+
});
163+
164+
it("should contain correct configuration structure", async () => {
165+
await config.createConfigFile();
166+
167+
const writeJsonCall = fs.writeJson.mock.calls[0];
168+
const configContent = writeJsonCall[1];
169+
170+
expect(configContent).toHaveProperty("LOG", true);
171+
expect(configContent).toHaveProperty("transformers");
172+
expect(configContent).toHaveProperty("Providers");
173+
expect(configContent).toHaveProperty("Router");
174+
expect(configContent.transformers).toHaveLength(1);
175+
expect(configContent.Providers).toHaveLength(1);
176+
});
177+
});
178+
179+
describe("createTransformerFile", () => {
180+
beforeEach(() => {
181+
config = new ClaudeCodeRouterConfig();
182+
});
183+
184+
it("should create transformer file", async () => {
185+
await config.createTransformerFile();
186+
187+
expect(fs.writeFile).toHaveBeenCalledWith(
188+
config.transformerFile,
189+
expect.stringContaining("class DashScopeTransformer")
190+
);
191+
});
192+
193+
it("should contain correct transformer code", async () => {
194+
await config.createTransformerFile();
195+
196+
const writeFileCall = fs.writeFile.mock.calls[0];
197+
const content = writeFileCall[1];
198+
199+
expect(content).toContain("class DashScopeTransformer");
200+
expect(content).toContain('name = "dashscope"');
201+
expect(content).toContain("transformRequestIn");
202+
expect(content).toContain("module.exports = DashScopeTransformer");
203+
});
204+
});
205+
206+
describe("setup", () => {
207+
beforeEach(() => {
208+
config = new ClaudeCodeRouterConfig();
209+
210+
// Mock console.log
211+
jest.spyOn(console, "log").mockImplementation();
212+
jest.spyOn(console, "error").mockImplementation();
213+
214+
// Mock the methods to avoid actual execution
215+
jest.spyOn(config, "createDirectories").mockResolvedValue();
216+
jest.spyOn(config, "createConfigFile").mockResolvedValue();
217+
jest.spyOn(config, "createTransformerFile").mockResolvedValue();
218+
});
219+
220+
afterEach(() => {
221+
console.log.mockRestore();
222+
console.error.mockRestore();
223+
});
224+
225+
it("should successfully complete setup process", async () => {
226+
await config.setup();
227+
228+
expect(config.createDirectories).toHaveBeenCalled();
229+
expect(config.createConfigFile).toHaveBeenCalled();
230+
expect(config.createTransformerFile).toHaveBeenCalled();
231+
});
232+
233+
it("should detect API Key in environment variable", async () => {
234+
process.env.DASHSCOPE_API_KEY = "test-key";
235+
236+
await config.setup();
237+
238+
expect(console.log).toHaveBeenCalledWith(
239+
expect.stringContaining(
240+
"DASHSCOPE_API_KEY environment variable detected"
241+
)
242+
);
243+
});
244+
245+
it("should show warning when environment variable is not present", async () => {
246+
delete process.env.DASHSCOPE_API_KEY;
247+
248+
await config.setup();
249+
250+
expect(console.log).toHaveBeenCalledWith(
251+
expect.stringContaining(
252+
"DASHSCOPE_API_KEY environment variable not found"
253+
)
254+
);
255+
});
256+
257+
it("should handle errors during setup process", async () => {
258+
const error = new Error("Setup failed");
259+
config.createDirectories.mockRejectedValue(error);
260+
261+
await config.setup();
262+
263+
expect(process.exit).toHaveBeenCalledWith(1);
264+
});
265+
});
266+
});

0 commit comments

Comments
 (0)