Skip to content

Commit d1ff640

Browse files
committed
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
1 parent 91a2a2b commit d1ff640

File tree

4 files changed

+320
-1
lines changed

4 files changed

+320
-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+
package-lock.json
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+
});

package.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
"ccr-dashscope": "./bin/claude-code-router-config.js"
1616
},
1717
"scripts": {
18-
"lint": "echo lint"
18+
"lint": "echo lint",
19+
"test": "jest",
20+
"test:watch": "jest --watch",
21+
"test:coverage": "jest --coverage",
22+
"ci": "npm run test"
1923
},
2024
"files": [
2125
"bin",
@@ -34,7 +38,25 @@
3438
"dependencies": {
3539
"fs-extra": "^11.1.1"
3640
},
41+
"devDependencies": {
42+
"jest": "^29.7.0"
43+
},
3744
"engines": {
3845
"node": ">=14.0.0"
46+
},
47+
"jest": {
48+
"testEnvironment": "node",
49+
"collectCoverageFrom": [
50+
"bin/**/*.js",
51+
"!**/node_modules/**"
52+
],
53+
"coverageThreshold": {
54+
"global": {
55+
"branches": 80,
56+
"functions": 80,
57+
"lines": 80,
58+
"statements": 80
59+
}
60+
}
3961
}
4062
}

0 commit comments

Comments
 (0)