@aigne/doc-smith 0.8.6 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aigne/doc-smith/output/structure-plan.json +1 -5
- package/CHANGELOG.md +14 -0
- package/README.md +3 -3
- package/agents/{chat.yaml → chat/index.yaml} +7 -7
- package/agents/generate/check-document-structure.yaml +30 -0
- package/agents/{check-structure-plan.mjs → generate/check-need-generate-structure.mjs} +20 -20
- package/agents/{structure-planning.yaml → generate/generate-structure.yaml} +6 -6
- package/agents/{docs-generator.yaml → generate/index.yaml} +11 -12
- package/agents/generate/refine-document-structure.yaml +12 -0
- package/agents/{input-generator.mjs → init/index.mjs} +12 -5
- package/agents/{manage-prefs.mjs → prefs/index.mjs} +1 -1
- package/agents/{team-publish-docs.yaml → publish/index.yaml} +1 -2
- package/agents/{publish-docs.mjs → publish/publish-docs.mjs} +6 -5
- package/agents/schema/{structure-plan-result.yaml → document-execution-structure.yaml} +3 -3
- package/agents/schema/document-structure.yaml +26 -0
- package/agents/{language-selector.mjs → translate/choose-language.mjs} +5 -5
- package/agents/{retranslate.yaml → translate/index.yaml} +11 -12
- package/agents/{translate.yaml → translate/translate-document.yaml} +3 -2
- package/agents/{batch-translate.yaml → translate/translate-multilingual.yaml} +5 -5
- package/agents/update/batch-generate-document.yaml +19 -0
- package/agents/{check-detail.mjs → update/check-document.mjs} +16 -16
- package/agents/{detail-generator-and-translate.yaml → update/generate-and-translate-document.yaml} +23 -23
- package/agents/update/generate-document.yaml +50 -0
- package/agents/{detail-regenerator.yaml → update/index.yaml} +10 -11
- package/agents/{action-success.mjs → utils/action-success.mjs} +1 -1
- package/agents/{check-detail-result.mjs → utils/check-detail-result.mjs} +3 -3
- package/agents/{check-feedback-refiner.mjs → utils/check-feedback-refiner.mjs} +6 -6
- package/agents/{find-items-by-paths.mjs → utils/choose-docs.mjs} +25 -13
- package/agents/{docs-fs.yaml → utils/docs-fs-actor.yaml} +3 -1
- package/agents/{feedback-refiner.yaml → utils/feedback-refiner.yaml} +2 -4
- package/agents/{find-item-by-path.mjs → utils/find-item-by-path.mjs} +17 -11
- package/agents/{find-user-preferences-by-path.mjs → utils/find-user-preferences-by-path.mjs} +1 -1
- package/agents/utils/format-document-structure.mjs +25 -0
- package/agents/{load-sources.mjs → utils/load-sources.mjs} +41 -28
- package/agents/{save-docs.mjs → utils/save-docs.mjs} +16 -16
- package/agents/{save-single-doc.mjs → utils/save-single-doc.mjs} +2 -2
- package/agents/{transform-detail-datasources.mjs → utils/transform-detail-datasources.mjs} +1 -1
- package/aigne.yaml +35 -35
- package/docs-mcp/analyze-docs-relevance.yaml +10 -10
- package/docs-mcp/docs-search.yaml +5 -3
- package/package.json +10 -8
- package/prompts/{document → detail/custom}/custom-code-block.md +6 -6
- package/prompts/detail/custom/custom-components.md +172 -0
- package/prompts/{document → detail}/d2-chart/rules.md +95 -1
- package/prompts/{document → detail}/detail-example.md +80 -61
- package/prompts/{document/detail-generator.md → detail/document-rules.md} +4 -8
- package/prompts/{content-detail-generator.md → detail/generate-document.md} +48 -25
- package/prompts/{check-structure-planning-result.md → structure/check-document-structure.md} +23 -17
- package/prompts/{document/structure-planning.md → structure/document-rules.md} +0 -2
- package/prompts/{structure-planning.md → structure/generate-structure.md} +51 -30
- package/prompts/{document → structure}/structure-example.md +2 -2
- package/prompts/{document → structure}/structure-getting-started.md +2 -2
- package/prompts/translate/glossary.md +6 -0
- package/prompts/{translator.md → translate/translate-document.md} +29 -10
- package/prompts/{feedback-refiner.md → utils/feedback-refiner.md} +8 -8
- package/tests/agents/chat/chat.test.mjs +46 -0
- package/tests/agents/generate/check-document-structure.test.mjs +51 -0
- package/tests/agents/generate/check-need-generate-structure.test.mjs +292 -0
- package/tests/agents/generate/generate-structure.test.mjs +51 -0
- package/tests/{input-generator.test.mjs → agents/init/init.test.mjs} +13 -13
- package/tests/agents/prefs/prefs.test.mjs +431 -0
- package/tests/agents/publish/publish-docs.test.mjs +642 -0
- package/tests/agents/translate/choose-language.test.mjs +311 -0
- package/tests/agents/translate/translate-document.test.mjs +51 -0
- package/tests/agents/update/check-document.test.mjs +523 -0
- package/tests/agents/update/generate-document.test.mjs +51 -0
- package/tests/agents/utils/action-success.test.mjs +54 -0
- package/tests/{check-detail-result.test.mjs → agents/utils/check-detail-result.test.mjs} +98 -98
- package/tests/agents/utils/check-feedback-refiner.test.mjs +478 -0
- package/tests/agents/utils/choose-docs.test.mjs +413 -0
- package/tests/agents/utils/exit.test.mjs +70 -0
- package/tests/agents/utils/feedback-refiner.test.mjs +51 -0
- package/tests/agents/utils/find-item-by-path.test.mjs +517 -0
- package/tests/agents/utils/find-user-preferences-by-path.test.mjs +382 -0
- package/tests/agents/utils/format-document-structure.test.mjs +264 -0
- package/tests/agents/utils/fs.test.mjs +267 -0
- package/tests/{load-sources.test.mjs → agents/utils/load-sources.test.mjs} +153 -25
- package/tests/{save-docs.test.mjs → agents/utils/save-docs.test.mjs} +11 -5
- package/tests/agents/utils/save-output.test.mjs +315 -0
- package/tests/agents/utils/save-single-doc.test.mjs +364 -0
- package/tests/agents/utils/transform-detail-datasources.test.mjs +363 -0
- package/tests/utils/auth-utils.test.mjs +358 -0
- package/tests/utils/blocklet.test.mjs +334 -0
- package/tests/{conflict-resolution.test.mjs → utils/conflict-detector.test.mjs} +3 -3
- package/tests/utils/constants.test.mjs +295 -0
- package/tests/utils/d2-utils.test.mjs +423 -0
- package/tests/{deploy.test.mjs → utils/deploy.test.mjs} +25 -36
- package/tests/utils/docs-finder-utils.test.mjs +625 -0
- package/tests/utils/file-utils.test.mjs +213 -0
- package/tests/{kroki-utils.test.mjs → utils/kroki-utils.test.mjs} +2 -2
- package/tests/utils/load-config.test.mjs +141 -0
- package/tests/{mermaid-validation.test.mjs → utils/mermaid-validator.test.mjs} +2 -2
- package/tests/utils/mock-chat-model.mjs +12 -0
- package/tests/{preferences-utils.test.mjs → utils/preferences-utils.test.mjs} +1 -1
- package/tests/{save-value-to-config.test.mjs → utils/save-value-to-config.test.mjs} +61 -4
- package/tests/utils/utils.test.mjs +939 -0
- package/utils/conflict-detector.mjs +1 -1
- package/utils/constants.mjs +5 -3
- package/utils/d2-utils.mjs +194 -0
- package/utils/docs-finder-utils.mjs +26 -26
- package/utils/icon-map.mjs +26 -0
- package/{agents → utils}/load-config.mjs +2 -18
- package/utils/markdown-checker.mjs +5 -5
- package/agents/batch-docs-detail-generator.yaml +0 -19
- package/agents/check-structure-planning-result.yaml +0 -30
- package/agents/content-detail-generator.yaml +0 -50
- package/agents/format-structure-plan.mjs +0 -25
- package/agents/reflective-structure-planner.yaml +0 -12
- package/agents/schema/structure-plan.yaml +0 -26
- package/prompts/document/custom-components.md +0 -104
- package/tests/README.md +0 -93
- package/tests/utils.test.mjs +0 -2067
- /package/agents/{exit.mjs → utils/exit.mjs} +0 -0
- /package/agents/{fs.mjs → utils/fs.mjs} +0 -0
- /package/agents/{save-output.mjs → utils/save-output.mjs} +0 -0
- /package/prompts/{document → detail}/d2-chart/official-examples.md +0 -0
- /package/prompts/{document → detail}/jsx/rules.md +0 -0
package/tests/utils.test.mjs
DELETED
|
@@ -1,2067 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
detectSystemLanguage,
|
|
8
|
-
getAvailablePaths,
|
|
9
|
-
getContentHash,
|
|
10
|
-
getCurrentGitHead,
|
|
11
|
-
getGitHubRepoInfo,
|
|
12
|
-
getGithubRepoUrl,
|
|
13
|
-
getModifiedFilesBetweenCommits,
|
|
14
|
-
getProjectInfo,
|
|
15
|
-
hasFileChangesBetweenCommits,
|
|
16
|
-
hasSourceFilesChanged,
|
|
17
|
-
isGlobPattern,
|
|
18
|
-
loadConfigFromFile,
|
|
19
|
-
normalizePath,
|
|
20
|
-
processConfigFields,
|
|
21
|
-
processContent,
|
|
22
|
-
resolveFileReferences,
|
|
23
|
-
saveDocWithTranslations,
|
|
24
|
-
saveGitHeadToConfig,
|
|
25
|
-
saveValueToConfig,
|
|
26
|
-
toRelativePath,
|
|
27
|
-
validatePath,
|
|
28
|
-
validatePaths,
|
|
29
|
-
} from "../utils/utils.mjs";
|
|
30
|
-
|
|
31
|
-
describe("utils", () => {
|
|
32
|
-
let tempDir;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
tempDir = path.join(os.tmpdir(), `utils-test-${Date.now()}`);
|
|
36
|
-
if (!existsSync(tempDir)) {
|
|
37
|
-
mkdirSync(tempDir, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
if (existsSync(tempDir)) {
|
|
43
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("normalizePath", () => {
|
|
48
|
-
test("should return absolute path when given absolute path", () => {
|
|
49
|
-
const absolutePath = "/usr/local/bin";
|
|
50
|
-
expect(normalizePath(absolutePath)).toBe(absolutePath);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("should resolve relative path to absolute path", () => {
|
|
54
|
-
const relativePath = "./test";
|
|
55
|
-
const result = normalizePath(relativePath);
|
|
56
|
-
expect(path.isAbsolute(result)).toBe(true);
|
|
57
|
-
expect(result).toContain("test");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("should handle empty string", () => {
|
|
61
|
-
const result = normalizePath("");
|
|
62
|
-
expect(path.isAbsolute(result)).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("toRelativePath", () => {
|
|
67
|
-
test("should convert absolute path to relative", () => {
|
|
68
|
-
const absolutePath = path.join(process.cwd(), "test", "file.js");
|
|
69
|
-
const result = toRelativePath(absolutePath);
|
|
70
|
-
expect(result).toBe(path.join("test", "file.js"));
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("should return relative path unchanged", () => {
|
|
74
|
-
const relativePath = "./test/file.js";
|
|
75
|
-
expect(toRelativePath(relativePath)).toBe(relativePath);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("should handle current working directory", () => {
|
|
79
|
-
const result = toRelativePath(process.cwd());
|
|
80
|
-
expect(result).toBe("");
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("processContent", () => {
|
|
85
|
-
test("should process markdown links correctly", () => {
|
|
86
|
-
const content = "Check out [this link](./docs/readme) for more info.";
|
|
87
|
-
const result = processContent({ content });
|
|
88
|
-
expect(result).toBe("Check out [this link](./docs-readme.md) for more info.");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("should preserve external links", () => {
|
|
92
|
-
const content = "Visit [Google](https://google.com) for search.";
|
|
93
|
-
const result = processContent({ content });
|
|
94
|
-
expect(result).toBe(content);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("should preserve mailto links", () => {
|
|
98
|
-
const content = "Contact [us](mailto:test@example.com).";
|
|
99
|
-
const result = processContent({ content });
|
|
100
|
-
expect(result).toBe(content);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("should handle links with anchors", () => {
|
|
104
|
-
const content = "See [section](./guide#installation) for details.";
|
|
105
|
-
const result = processContent({ content });
|
|
106
|
-
expect(result).toBe("See [section](./guide.md#installation) for details.");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("should not process image links", () => {
|
|
110
|
-
const content = "Here's an image: ";
|
|
111
|
-
const result = processContent({ content });
|
|
112
|
-
expect(result).toBe(content);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("should handle links with existing extensions", () => {
|
|
116
|
-
const content = "Download [file](./docs/readme.pdf) here.";
|
|
117
|
-
const result = processContent({ content });
|
|
118
|
-
expect(result).toBe(content);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("should handle root-relative paths", () => {
|
|
122
|
-
const content = "Check [root link](/docs/api) here.";
|
|
123
|
-
const result = processContent({ content });
|
|
124
|
-
expect(result).toBe("Check [root link](./docs-api.md) here.");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("should handle paths starting with dot", () => {
|
|
128
|
-
const content = "See [dotted path](./src/utils) for more.";
|
|
129
|
-
const result = processContent({ content });
|
|
130
|
-
expect(result).toBe("See [dotted path](./src-utils.md) for more.");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("getContentHash", () => {
|
|
135
|
-
test("should return consistent hash for same content", () => {
|
|
136
|
-
const content = "test content";
|
|
137
|
-
const hash1 = getContentHash(content);
|
|
138
|
-
const hash2 = getContentHash(content);
|
|
139
|
-
expect(hash1).toBe(hash2);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("should return different hashes for different content", () => {
|
|
143
|
-
const content1 = "test content 1";
|
|
144
|
-
const content2 = "test content 2";
|
|
145
|
-
const hash1 = getContentHash(content1);
|
|
146
|
-
const hash2 = getContentHash(content2);
|
|
147
|
-
expect(hash1).not.toBe(hash2);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("should return 64-character hex string", () => {
|
|
151
|
-
const content = "test";
|
|
152
|
-
const hash = getContentHash(content);
|
|
153
|
-
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("should handle empty string", () => {
|
|
157
|
-
const hash = getContentHash("");
|
|
158
|
-
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("validatePath", () => {
|
|
163
|
-
test("should validate existing file", () => {
|
|
164
|
-
const testFile = path.join(tempDir, "test.txt");
|
|
165
|
-
writeFileSync(testFile, "test content");
|
|
166
|
-
|
|
167
|
-
const result = validatePath(testFile);
|
|
168
|
-
expect(result.isValid).toBe(true);
|
|
169
|
-
expect(result.error).toBe(null);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("should validate existing directory", () => {
|
|
173
|
-
const result = validatePath(tempDir);
|
|
174
|
-
expect(result.isValid).toBe(true);
|
|
175
|
-
expect(result.error).toBe(null);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("should invalidate non-existent path", () => {
|
|
179
|
-
const nonExistentPath = path.join(tempDir, "non-existent");
|
|
180
|
-
const result = validatePath(nonExistentPath);
|
|
181
|
-
expect(result.isValid).toBe(false);
|
|
182
|
-
expect(result.error).toContain("does not exist");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("should handle relative paths", () => {
|
|
186
|
-
const testFile = path.join(tempDir, "relative-test.txt");
|
|
187
|
-
writeFileSync(testFile, "content");
|
|
188
|
-
|
|
189
|
-
const relativePath = path.relative(process.cwd(), testFile);
|
|
190
|
-
const result = validatePath(relativePath);
|
|
191
|
-
expect(result.isValid).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("validatePaths", () => {
|
|
196
|
-
test("should validate multiple valid paths", () => {
|
|
197
|
-
const testFile1 = path.join(tempDir, "test1.txt");
|
|
198
|
-
const testFile2 = path.join(tempDir, "test2.txt");
|
|
199
|
-
writeFileSync(testFile1, "content1");
|
|
200
|
-
writeFileSync(testFile2, "content2");
|
|
201
|
-
|
|
202
|
-
const result = validatePaths([testFile1, testFile2]);
|
|
203
|
-
expect(result.validPaths).toHaveLength(2);
|
|
204
|
-
expect(result.errors).toHaveLength(0);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("should separate valid and invalid paths", () => {
|
|
208
|
-
const testFile = path.join(tempDir, "valid.txt");
|
|
209
|
-
const invalidFile = path.join(tempDir, "invalid.txt");
|
|
210
|
-
writeFileSync(testFile, "content");
|
|
211
|
-
|
|
212
|
-
const result = validatePaths([testFile, invalidFile]);
|
|
213
|
-
expect(result.validPaths).toHaveLength(1);
|
|
214
|
-
expect(result.errors).toHaveLength(1);
|
|
215
|
-
expect(result.validPaths[0]).toBe(testFile);
|
|
216
|
-
expect(result.errors[0].path).toBe(invalidFile);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("should handle empty array", () => {
|
|
220
|
-
const result = validatePaths([]);
|
|
221
|
-
expect(result.validPaths).toHaveLength(0);
|
|
222
|
-
expect(result.errors).toHaveLength(0);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe("detectSystemLanguage", () => {
|
|
227
|
-
test("should return a valid language code", () => {
|
|
228
|
-
const result = detectSystemLanguage();
|
|
229
|
-
expect(typeof result).toBe("string");
|
|
230
|
-
expect(result.length).toBeGreaterThan(0);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("should return 'en' as default when no env vars", () => {
|
|
234
|
-
const originalEnv = {
|
|
235
|
-
LANG: process.env.LANG,
|
|
236
|
-
LANGUAGE: process.env.LANGUAGE,
|
|
237
|
-
LC_ALL: process.env.LC_ALL,
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
delete process.env.LANG;
|
|
241
|
-
delete process.env.LANGUAGE;
|
|
242
|
-
delete process.env.LC_ALL;
|
|
243
|
-
|
|
244
|
-
const result = detectSystemLanguage();
|
|
245
|
-
|
|
246
|
-
// Restore original env vars
|
|
247
|
-
if (originalEnv.LANG) process.env.LANG = originalEnv.LANG;
|
|
248
|
-
if (originalEnv.LANGUAGE) process.env.LANGUAGE = originalEnv.LANGUAGE;
|
|
249
|
-
if (originalEnv.LC_ALL) process.env.LC_ALL = originalEnv.LC_ALL;
|
|
250
|
-
|
|
251
|
-
expect(result).toBe("en");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("should handle Chinese locale variants", () => {
|
|
255
|
-
const originalLang = process.env.LANG;
|
|
256
|
-
|
|
257
|
-
process.env.LANG = "zh_TW.UTF-8";
|
|
258
|
-
let result = detectSystemLanguage();
|
|
259
|
-
expect(result).toBe("zh"); // "zh" is found first in SUPPORTED_LANGUAGES
|
|
260
|
-
|
|
261
|
-
process.env.LANG = "zh_CN.UTF-8";
|
|
262
|
-
result = detectSystemLanguage();
|
|
263
|
-
expect(result).toBe("zh");
|
|
264
|
-
|
|
265
|
-
if (originalLang) {
|
|
266
|
-
process.env.LANG = originalLang;
|
|
267
|
-
} else {
|
|
268
|
-
delete process.env.LANG;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("detectSystemLanguage should handle edge cases", () => {
|
|
273
|
-
const originalEnv = {
|
|
274
|
-
LANG: process.env.LANG,
|
|
275
|
-
LANGUAGE: process.env.LANGUAGE,
|
|
276
|
-
LC_ALL: process.env.LC_ALL,
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Test case 1: No system locale at all
|
|
280
|
-
delete process.env.LANG;
|
|
281
|
-
delete process.env.LANGUAGE;
|
|
282
|
-
delete process.env.LC_ALL;
|
|
283
|
-
|
|
284
|
-
// Mock Intl to also fail
|
|
285
|
-
const originalDateTimeFormat = Intl.DateTimeFormat;
|
|
286
|
-
Intl.DateTimeFormat = () => {
|
|
287
|
-
throw new Error("Intl not available");
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
const result = detectSystemLanguage();
|
|
292
|
-
expect(result).toBe("en"); // Should fall back to default
|
|
293
|
-
} finally {
|
|
294
|
-
Intl.DateTimeFormat = originalDateTimeFormat;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Test case 2: Handle special Chinese locale variants
|
|
298
|
-
process.env.LANG = "zh_TW";
|
|
299
|
-
let result = detectSystemLanguage();
|
|
300
|
-
expect(["zh", "zh-TW"].includes(result)).toBe(true);
|
|
301
|
-
|
|
302
|
-
process.env.LANG = "zh_HK.Big5";
|
|
303
|
-
result = detectSystemLanguage();
|
|
304
|
-
expect(["zh", "zh-TW"].includes(result)).toBe(true);
|
|
305
|
-
|
|
306
|
-
// Test case 3: Unsupported language
|
|
307
|
-
process.env.LANG = "xx_XX.UTF-8"; // Non-existent language
|
|
308
|
-
result = detectSystemLanguage();
|
|
309
|
-
expect(result).toBe("en"); // Should fall back to default
|
|
310
|
-
|
|
311
|
-
// Restore original environment
|
|
312
|
-
if (originalEnv.LANG) process.env.LANG = originalEnv.LANG;
|
|
313
|
-
else delete process.env.LANG;
|
|
314
|
-
if (originalEnv.LANGUAGE) process.env.LANGUAGE = originalEnv.LANGUAGE;
|
|
315
|
-
else delete process.env.LANGUAGE;
|
|
316
|
-
if (originalEnv.LC_ALL) process.env.LC_ALL = originalEnv.LC_ALL;
|
|
317
|
-
else delete process.env.LC_ALL;
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
describe("isGlobPattern", () => {
|
|
322
|
-
test("should return true for patterns with asterisk", () => {
|
|
323
|
-
expect(isGlobPattern("*.js")).toBe(true);
|
|
324
|
-
expect(isGlobPattern("src/*.js")).toBe(true);
|
|
325
|
-
expect(isGlobPattern("**/*.js")).toBe(true);
|
|
326
|
-
expect(isGlobPattern("src/**/*.js")).toBe(true);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("should return true for patterns with question mark", () => {
|
|
330
|
-
expect(isGlobPattern("file?.js")).toBe(true);
|
|
331
|
-
expect(isGlobPattern("src/?odal.js")).toBe(true);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("should return true for patterns with character classes", () => {
|
|
335
|
-
expect(isGlobPattern("file[abc].js")).toBe(true);
|
|
336
|
-
expect(isGlobPattern("src/[Bb]utton.js")).toBe(true);
|
|
337
|
-
expect(isGlobPattern("file[0-9].js")).toBe(true);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("should return true for patterns with double asterisk", () => {
|
|
341
|
-
expect(isGlobPattern("**/file.js")).toBe(true);
|
|
342
|
-
expect(isGlobPattern("src/**/file.js")).toBe(true);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test("should return false for regular paths", () => {
|
|
346
|
-
expect(isGlobPattern("src/file.js")).toBe(false);
|
|
347
|
-
expect(isGlobPattern("./src")).toBe(false);
|
|
348
|
-
expect(isGlobPattern("/absolute/path")).toBe(false);
|
|
349
|
-
expect(isGlobPattern("regular-file.js")).toBe(false);
|
|
350
|
-
expect(isGlobPattern("package.json")).toBe(false);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("should return false for empty or undefined input", () => {
|
|
354
|
-
expect(isGlobPattern("")).toBe(false);
|
|
355
|
-
expect(isGlobPattern(undefined)).toBe(false);
|
|
356
|
-
expect(isGlobPattern(null)).toBe(false);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
test("should handle complex patterns", () => {
|
|
360
|
-
expect(isGlobPattern("src/**/*.{js,ts,jsx,tsx}")).toBe(true);
|
|
361
|
-
expect(isGlobPattern("components/**/[A-Z]*.js")).toBe(true);
|
|
362
|
-
expect(isGlobPattern("test/**/*test.js")).toBe(true);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe("saveDocWithTranslations", () => {
|
|
367
|
-
test("should save document without translations", async () => {
|
|
368
|
-
const docsDir = path.join(tempDir, "docs");
|
|
369
|
-
const result = await saveDocWithTranslations({
|
|
370
|
-
path: "test-doc",
|
|
371
|
-
content: "# Test Document\n\nContent here.",
|
|
372
|
-
docsDir,
|
|
373
|
-
locale: "en",
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
expect(result).toHaveLength(1);
|
|
377
|
-
expect(result[0].success).toBe(true);
|
|
378
|
-
expect(existsSync(path.join(docsDir, "test-doc.md"))).toBe(true);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("should save document with translations", async () => {
|
|
382
|
-
const docsDir = path.join(tempDir, "docs");
|
|
383
|
-
const result = await saveDocWithTranslations({
|
|
384
|
-
path: "test-doc",
|
|
385
|
-
content: "# Test Document",
|
|
386
|
-
docsDir,
|
|
387
|
-
locale: "en",
|
|
388
|
-
translates: [{ language: "zh", translation: "# 测试文档" }],
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
expect(result).toHaveLength(2);
|
|
392
|
-
expect(result.every((r) => r.success)).toBe(true);
|
|
393
|
-
expect(existsSync(path.join(docsDir, "test-doc.md"))).toBe(true);
|
|
394
|
-
expect(existsSync(path.join(docsDir, "test-doc.zh.md"))).toBe(true);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test("should handle path with slashes", async () => {
|
|
398
|
-
const docsDir = path.join(tempDir, "docs");
|
|
399
|
-
const result = await saveDocWithTranslations({
|
|
400
|
-
path: "/api/user",
|
|
401
|
-
content: "# API Documentation",
|
|
402
|
-
docsDir,
|
|
403
|
-
locale: "en",
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
expect(result[0].success).toBe(true);
|
|
407
|
-
expect(existsSync(path.join(docsDir, "api-user.md"))).toBe(true);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test("should add labels to front matter", async () => {
|
|
411
|
-
const docsDir = path.join(tempDir, "docs");
|
|
412
|
-
await saveDocWithTranslations({
|
|
413
|
-
path: "labeled-doc",
|
|
414
|
-
content: "# Test",
|
|
415
|
-
docsDir,
|
|
416
|
-
locale: "en",
|
|
417
|
-
labels: ["api", "guide"],
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const content = readFileSync(path.join(docsDir, "labeled-doc.md"), "utf8");
|
|
421
|
-
expect(content).toContain('labels: ["api","guide"]');
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test("should skip main content when isTranslate is true", async () => {
|
|
425
|
-
const docsDir = path.join(tempDir, "docs");
|
|
426
|
-
const result = await saveDocWithTranslations({
|
|
427
|
-
path: "translate-only",
|
|
428
|
-
content: "# Main",
|
|
429
|
-
docsDir,
|
|
430
|
-
locale: "en",
|
|
431
|
-
translates: [{ language: "zh", translation: "# 中文" }],
|
|
432
|
-
isTranslate: true,
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
expect(result).toHaveLength(1);
|
|
436
|
-
expect(existsSync(path.join(docsDir, "translate-only.md"))).toBe(false);
|
|
437
|
-
expect(existsSync(path.join(docsDir, "translate-only.zh.md"))).toBe(true);
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
describe("getCurrentGitHead", () => {
|
|
442
|
-
test("should return current git HEAD hash in real git repository", () => {
|
|
443
|
-
const result = getCurrentGitHead();
|
|
444
|
-
// In our real git repository, should return a valid hash
|
|
445
|
-
expect(typeof result).toBe("string");
|
|
446
|
-
expect(result.length).toBe(40); // Git SHA-1 hash length
|
|
447
|
-
expect(result).toMatch(/^[a-f0-9]{40}$/); // Valid hex hash
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test("should handle git command errors gracefully", () => {
|
|
451
|
-
// Mock console.warn to capture warning messages
|
|
452
|
-
const originalWarn = console.warn;
|
|
453
|
-
const warnMessages = [];
|
|
454
|
-
console.warn = (message) => warnMessages.push(message);
|
|
455
|
-
|
|
456
|
-
// Change to a non-git directory temporarily
|
|
457
|
-
const originalCwd = process.cwd();
|
|
458
|
-
const nonGitDir = path.join(tempDir, "non-git");
|
|
459
|
-
mkdirSync(nonGitDir, { recursive: true });
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
process.chdir(nonGitDir);
|
|
463
|
-
const result = getCurrentGitHead();
|
|
464
|
-
expect(result).toBe(null);
|
|
465
|
-
// Should have logged a warning
|
|
466
|
-
expect(warnMessages.some((msg) => msg.includes("Failed to get git HEAD:"))).toBe(true);
|
|
467
|
-
} finally {
|
|
468
|
-
process.chdir(originalCwd);
|
|
469
|
-
console.warn = originalWarn;
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
describe("getModifiedFilesBetweenCommits", () => {
|
|
475
|
-
test("should return modified files between recent commits", () => {
|
|
476
|
-
// Dynamically get the last few commits to avoid hardcoded commit issues
|
|
477
|
-
const currentHead = getCurrentGitHead();
|
|
478
|
-
if (!currentHead) {
|
|
479
|
-
// Skip test if not in a git repository
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Try to get the previous commit (HEAD~1)
|
|
484
|
-
let previousCommit;
|
|
485
|
-
try {
|
|
486
|
-
previousCommit = execSync("git rev-parse HEAD~1", {
|
|
487
|
-
encoding: "utf8",
|
|
488
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
489
|
-
}).trim();
|
|
490
|
-
} catch {
|
|
491
|
-
// If there's no previous commit, skip this test
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const result = getModifiedFilesBetweenCommits(previousCommit, currentHead);
|
|
496
|
-
expect(Array.isArray(result)).toBe(true);
|
|
497
|
-
|
|
498
|
-
// Validate file format if any files are returned
|
|
499
|
-
result.forEach((file) => {
|
|
500
|
-
expect(typeof file).toBe("string");
|
|
501
|
-
expect(file.length).toBeGreaterThan(0);
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
test("should detect changes between commits with more history", () => {
|
|
506
|
-
// Try to find commits that are further apart by checking if we have enough history
|
|
507
|
-
let olderCommit;
|
|
508
|
-
try {
|
|
509
|
-
olderCommit = execSync("git rev-parse HEAD~3", {
|
|
510
|
-
encoding: "utf8",
|
|
511
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
512
|
-
}).trim();
|
|
513
|
-
} catch {
|
|
514
|
-
// If we don't have enough history, skip this test
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const result = getModifiedFilesBetweenCommits(olderCommit, "HEAD");
|
|
519
|
-
expect(Array.isArray(result)).toBe(true);
|
|
520
|
-
// With 3+ commits difference, there should usually be some changes
|
|
521
|
-
// But we won't enforce this since it depends on the actual history
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
test("should filter by provided file paths when files exist in changes", () => {
|
|
525
|
-
// First try to get some modified files
|
|
526
|
-
let olderCommit;
|
|
527
|
-
try {
|
|
528
|
-
olderCommit = execSync("git rev-parse HEAD~2", {
|
|
529
|
-
encoding: "utf8",
|
|
530
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
531
|
-
}).trim();
|
|
532
|
-
} catch {
|
|
533
|
-
return; // Skip if not enough history
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const allModified = getModifiedFilesBetweenCommits(olderCommit, "HEAD");
|
|
537
|
-
|
|
538
|
-
if (allModified.length > 0) {
|
|
539
|
-
// Test filtering with actual modified file
|
|
540
|
-
const testFile = allModified[0];
|
|
541
|
-
const result = getModifiedFilesBetweenCommits(olderCommit, "HEAD", [testFile]);
|
|
542
|
-
expect(Array.isArray(result)).toBe(true);
|
|
543
|
-
expect(result).toContain(testFile);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
test("should return empty array for same commit", () => {
|
|
548
|
-
const result = getModifiedFilesBetweenCommits("HEAD", "HEAD");
|
|
549
|
-
expect(Array.isArray(result)).toBe(true);
|
|
550
|
-
expect(result.length).toBe(0);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
test("should handle invalid commits gracefully", () => {
|
|
554
|
-
const result = getModifiedFilesBetweenCommits("invalid-commit1", "invalid-commit2");
|
|
555
|
-
expect(Array.isArray(result)).toBe(true);
|
|
556
|
-
expect(result.length).toBe(0); // Should return empty array for invalid commits
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
describe("hasSourceFilesChanged", () => {
|
|
561
|
-
test("should return false for empty inputs", () => {
|
|
562
|
-
expect(hasSourceFilesChanged([], [])).toBe(false);
|
|
563
|
-
expect(hasSourceFilesChanged(null, [])).toBe(false);
|
|
564
|
-
expect(hasSourceFilesChanged(["test.js"], null)).toBe(false);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
test("should detect changes when files match", () => {
|
|
568
|
-
const sourceIds = ["/path/to/file.js"];
|
|
569
|
-
const modifiedFiles = ["/path/to/file.js"];
|
|
570
|
-
expect(hasSourceFilesChanged(sourceIds, modifiedFiles)).toBe(true);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
test("should return false when no files match", () => {
|
|
574
|
-
const sourceIds = ["/path/to/file1.js"];
|
|
575
|
-
const modifiedFiles = ["/path/to/file2.js"];
|
|
576
|
-
expect(hasSourceFilesChanged(sourceIds, modifiedFiles)).toBe(false);
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
describe("hasFileChangesBetweenCommits", () => {
|
|
581
|
-
test("should detect file additions/deletions with dynamic commits", () => {
|
|
582
|
-
// hasFileChangesBetweenCommits only checks for added (A) and deleted (D) files, not modified (M) files
|
|
583
|
-
// It also excludes test files by default since they don't affect documentation structure
|
|
584
|
-
|
|
585
|
-
// Try to get commits dynamically
|
|
586
|
-
let olderCommit;
|
|
587
|
-
try {
|
|
588
|
-
olderCommit = execSync("git rev-parse HEAD~3", {
|
|
589
|
-
encoding: "utf8",
|
|
590
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
591
|
-
}).trim();
|
|
592
|
-
} catch {
|
|
593
|
-
// If we don't have enough history, skip this test
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const result = hasFileChangesBetweenCommits(olderCommit, "HEAD");
|
|
598
|
-
expect(typeof result).toBe("boolean");
|
|
599
|
-
|
|
600
|
-
// The result depends on actual git history, so we just verify it's a boolean
|
|
601
|
-
// In most cases with test files being excluded, it might be false
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("should detect changes when exclude patterns are empty", () => {
|
|
605
|
-
// Test with empty exclude patterns to verify detection mechanism works
|
|
606
|
-
let olderCommit;
|
|
607
|
-
try {
|
|
608
|
-
olderCommit = execSync("git rev-parse HEAD~2", {
|
|
609
|
-
encoding: "utf8",
|
|
610
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
611
|
-
}).trim();
|
|
612
|
-
} catch {
|
|
613
|
-
return; // Skip if not enough history
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const result = hasFileChangesBetweenCommits(olderCommit, "HEAD", ["*.mjs", "*.js"], []);
|
|
617
|
-
expect(typeof result).toBe("boolean");
|
|
618
|
-
|
|
619
|
-
// With no exclusions and broad include patterns, more likely to detect changes
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
test("should return false for same commit", () => {
|
|
623
|
-
const result = hasFileChangesBetweenCommits("HEAD", "HEAD");
|
|
624
|
-
expect(result).toBe(false);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
test("should respect include patterns for JavaScript files", () => {
|
|
628
|
-
// Try with recent commits
|
|
629
|
-
let olderCommit;
|
|
630
|
-
try {
|
|
631
|
-
olderCommit = execSync("git rev-parse HEAD~1", {
|
|
632
|
-
encoding: "utf8",
|
|
633
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
634
|
-
}).trim();
|
|
635
|
-
} catch {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const result = hasFileChangesBetweenCommits(
|
|
640
|
-
olderCommit,
|
|
641
|
-
"HEAD",
|
|
642
|
-
["*.js", "*.mjs", "*.ts"], // Include JS-related files
|
|
643
|
-
[],
|
|
644
|
-
);
|
|
645
|
-
expect(typeof result).toBe("boolean");
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
test("should respect exclude patterns", () => {
|
|
649
|
-
// Test excluding test files but including other JS files
|
|
650
|
-
let olderCommit;
|
|
651
|
-
try {
|
|
652
|
-
olderCommit = execSync("git rev-parse HEAD~2", {
|
|
653
|
-
encoding: "utf8",
|
|
654
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
655
|
-
}).trim();
|
|
656
|
-
} catch {
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const result = hasFileChangesBetweenCommits(
|
|
661
|
-
olderCommit,
|
|
662
|
-
"HEAD",
|
|
663
|
-
["*.mjs"], // Include mjs files
|
|
664
|
-
["tests/**"], // But exclude test directory
|
|
665
|
-
);
|
|
666
|
-
expect(typeof result).toBe("boolean");
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
test("should handle complex include/exclude pattern combinations", () => {
|
|
670
|
-
// Test with a broader range if available
|
|
671
|
-
let olderCommit;
|
|
672
|
-
try {
|
|
673
|
-
olderCommit = execSync("git rev-parse HEAD~4", {
|
|
674
|
-
encoding: "utf8",
|
|
675
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
676
|
-
}).trim();
|
|
677
|
-
} catch {
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const result = hasFileChangesBetweenCommits(
|
|
682
|
-
olderCommit,
|
|
683
|
-
"HEAD",
|
|
684
|
-
["*.mjs", "*.js"], // Include JS files
|
|
685
|
-
["node_modules/**", "dist/**"], // Exclude build artifacts
|
|
686
|
-
);
|
|
687
|
-
expect(typeof result).toBe("boolean");
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
test("should return false for invalid commits", () => {
|
|
691
|
-
const result = hasFileChangesBetweenCommits("invalid-commit1", "invalid-commit2");
|
|
692
|
-
expect(result).toBe(false);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
test("should handle commits with no matching file patterns", () => {
|
|
696
|
-
let olderCommit;
|
|
697
|
-
try {
|
|
698
|
-
olderCommit = execSync("git rev-parse HEAD~1", {
|
|
699
|
-
encoding: "utf8",
|
|
700
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
701
|
-
}).trim();
|
|
702
|
-
} catch {
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const result = hasFileChangesBetweenCommits(
|
|
707
|
-
olderCommit,
|
|
708
|
-
"HEAD",
|
|
709
|
-
["*.nonexistent"], // Pattern that won't match any files
|
|
710
|
-
[],
|
|
711
|
-
);
|
|
712
|
-
expect(result).toBe(false);
|
|
713
|
-
});
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
describe("getAvailablePaths", () => {
|
|
717
|
-
beforeEach(() => {
|
|
718
|
-
// Create a complex directory structure for testing
|
|
719
|
-
const testStructure = {
|
|
720
|
-
src: {
|
|
721
|
-
components: {
|
|
722
|
-
"Button.js": "export default Button",
|
|
723
|
-
"Modal.js": "export default Modal",
|
|
724
|
-
},
|
|
725
|
-
utils: {
|
|
726
|
-
"helpers.js": "export const help = () => {}",
|
|
727
|
-
"constants.js": "export const API_URL = 'test'",
|
|
728
|
-
},
|
|
729
|
-
"index.js": "export * from './components'",
|
|
730
|
-
},
|
|
731
|
-
tests: {
|
|
732
|
-
unit: {
|
|
733
|
-
"button.test.js": "test('button', () => {})",
|
|
734
|
-
},
|
|
735
|
-
integration: {
|
|
736
|
-
"app.test.js": "test('app', () => {})",
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
docs: {
|
|
740
|
-
"README.md": "# Documentation",
|
|
741
|
-
"api.md": "# API Reference",
|
|
742
|
-
},
|
|
743
|
-
"package.json": '{"name": "test-project"}',
|
|
744
|
-
"config.yaml": "test: true",
|
|
745
|
-
".gitignore": "node_modules/",
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
createDirectoryStructure(tempDir, testStructure);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
test("should return current directory contents when no input", () => {
|
|
752
|
-
// Mock process.cwd to return tempDir for this test
|
|
753
|
-
const originalCwd = process.cwd;
|
|
754
|
-
process.cwd = () => tempDir;
|
|
755
|
-
|
|
756
|
-
const result = getAvailablePaths();
|
|
757
|
-
|
|
758
|
-
process.cwd = originalCwd;
|
|
759
|
-
|
|
760
|
-
expect(Array.isArray(result)).toBe(true);
|
|
761
|
-
|
|
762
|
-
// We created 5 items: src, tests, docs, package.json, config.yaml, .gitignore
|
|
763
|
-
// But .gitignore should be filtered out (hidden file)
|
|
764
|
-
expect(result.length).toBe(5);
|
|
765
|
-
|
|
766
|
-
// Should have required properties
|
|
767
|
-
result.forEach((item) => {
|
|
768
|
-
expect(item).toHaveProperty("name");
|
|
769
|
-
expect(item).toHaveProperty("value");
|
|
770
|
-
expect(item).toHaveProperty("description");
|
|
771
|
-
expect(typeof item.name).toBe("string");
|
|
772
|
-
expect(typeof item.value).toBe("string");
|
|
773
|
-
expect(typeof item.description).toBe("string");
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
// Should find our test directories and files (with ./ prefix)
|
|
777
|
-
const names = result.map((r) => r.name);
|
|
778
|
-
expect(names).toContain("./src");
|
|
779
|
-
expect(names).toContain("./tests");
|
|
780
|
-
expect(names).toContain("./docs");
|
|
781
|
-
expect(names).toContain("./package.json");
|
|
782
|
-
expect(names).toContain("./config.yaml");
|
|
783
|
-
|
|
784
|
-
// Should not contain hidden files
|
|
785
|
-
expect(names.find((name) => name.includes(".gitignore"))).toBeUndefined();
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
test("should filter by search term correctly", () => {
|
|
789
|
-
const originalCwd = process.cwd;
|
|
790
|
-
process.cwd = () => tempDir;
|
|
791
|
-
|
|
792
|
-
// Search for items containing "src"
|
|
793
|
-
const result = getAvailablePaths("src");
|
|
794
|
-
|
|
795
|
-
process.cwd = originalCwd;
|
|
796
|
-
|
|
797
|
-
expect(Array.isArray(result)).toBe(true);
|
|
798
|
-
|
|
799
|
-
expect(result.length).toBe(1);
|
|
800
|
-
expect(result[0].description).toBe("📁 Directory");
|
|
801
|
-
|
|
802
|
-
expect(result[0].name).toBe("src");
|
|
803
|
-
|
|
804
|
-
process.cwd = () => tempDir;
|
|
805
|
-
const packResult = getAvailablePaths("pack");
|
|
806
|
-
process.cwd = originalCwd;
|
|
807
|
-
|
|
808
|
-
expect(packResult.length).toBe(1);
|
|
809
|
-
expect(packResult[0].name).toBe("./package.json");
|
|
810
|
-
expect(packResult[0].description).toBe("📄 File");
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
test("Should not return duplicate paths for the same file/directory", () => {
|
|
814
|
-
const originalCwd = process.cwd;
|
|
815
|
-
process.cwd = () => tempDir;
|
|
816
|
-
|
|
817
|
-
// This test demonstrates the duplication bug
|
|
818
|
-
const result = getAvailablePaths("src");
|
|
819
|
-
|
|
820
|
-
process.cwd = originalCwd;
|
|
821
|
-
|
|
822
|
-
// Extract all unique actual paths (normalize both "src" and "./src" to the same absolute path)
|
|
823
|
-
const absolutePaths = result.map((r) => path.resolve(tempDir, r.name));
|
|
824
|
-
const uniqueAbsolutePaths = [...new Set(absolutePaths)];
|
|
825
|
-
|
|
826
|
-
expect(result.length).toBe(uniqueAbsolutePaths.length); // Should be equal (no duplicates)
|
|
827
|
-
|
|
828
|
-
// Additional check: No two results should point to the same physical path
|
|
829
|
-
expect(absolutePaths.length).toBe(uniqueAbsolutePaths.length);
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
test("should handle absolute path navigation", () => {
|
|
833
|
-
const srcPath = path.join(tempDir, "src");
|
|
834
|
-
const result = getAvailablePaths(srcPath);
|
|
835
|
-
|
|
836
|
-
expect(Array.isArray(result)).toBe(true);
|
|
837
|
-
// Should find exactly the src directory itself (as it's navigating TO the src path)
|
|
838
|
-
expect(result.length).toBe(1);
|
|
839
|
-
expect(result[0].name).toBe(srcPath);
|
|
840
|
-
expect(result[0].value).toBe(srcPath);
|
|
841
|
-
expect(result[0].description).toBe("📁 Directory");
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
test("should handle relative path with ./ prefix", () => {
|
|
845
|
-
const originalCwd = process.cwd;
|
|
846
|
-
process.cwd = () => tempDir;
|
|
847
|
-
|
|
848
|
-
const result = getAvailablePaths("./src");
|
|
849
|
-
|
|
850
|
-
process.cwd = originalCwd;
|
|
851
|
-
|
|
852
|
-
expect(Array.isArray(result)).toBe(true);
|
|
853
|
-
// Should find exactly the src directory (matching exact path)
|
|
854
|
-
expect(result.length).toBe(1);
|
|
855
|
-
expect(result[0].name).toBe("./src");
|
|
856
|
-
expect(result[0].description).toBe("📁 Directory");
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
test("should handle nested path navigation", () => {
|
|
860
|
-
const originalCwd = process.cwd;
|
|
861
|
-
process.cwd = () => tempDir;
|
|
862
|
-
|
|
863
|
-
const result = getAvailablePaths("./src/comp");
|
|
864
|
-
|
|
865
|
-
process.cwd = originalCwd;
|
|
866
|
-
|
|
867
|
-
expect(Array.isArray(result)).toBe(true);
|
|
868
|
-
// Should find exactly the components directory that matches "comp"
|
|
869
|
-
expect(result.length).toBe(1);
|
|
870
|
-
expect(result[0].name).toBe("./src/components");
|
|
871
|
-
expect(result[0].description).toBe("📁 Directory");
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
test("should distinguish between files and directories", () => {
|
|
875
|
-
const originalCwd = process.cwd;
|
|
876
|
-
process.cwd = () => tempDir;
|
|
877
|
-
|
|
878
|
-
const result = getAvailablePaths();
|
|
879
|
-
|
|
880
|
-
process.cwd = originalCwd;
|
|
881
|
-
|
|
882
|
-
expect(Array.isArray(result)).toBe(true);
|
|
883
|
-
expect(result.length).toBe(5);
|
|
884
|
-
|
|
885
|
-
// Should have exactly 3 directories and 2 files
|
|
886
|
-
const directories = result.filter((r) => r.description === "📁 Directory");
|
|
887
|
-
const files = result.filter((r) => r.description === "📄 File");
|
|
888
|
-
|
|
889
|
-
expect(directories.length).toBe(3); // src, tests, docs
|
|
890
|
-
expect(files.length).toBe(2); // package.json, config.yaml
|
|
891
|
-
|
|
892
|
-
// Verify specific items
|
|
893
|
-
const srcItem = result.find((r) => r.name === "./src");
|
|
894
|
-
expect(srcItem.description).toBe("📁 Directory");
|
|
895
|
-
|
|
896
|
-
const packageItem = result.find((r) => r.name === "./package.json");
|
|
897
|
-
expect(packageItem.description).toBe("📄 File");
|
|
898
|
-
|
|
899
|
-
const configItem = result.find((r) => r.name === "./config.yaml");
|
|
900
|
-
expect(configItem.description).toBe("📄 File");
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
test("should handle non-existent directory gracefully", () => {
|
|
904
|
-
const result = getAvailablePaths("/non/existent/path");
|
|
905
|
-
|
|
906
|
-
expect(Array.isArray(result)).toBe(true);
|
|
907
|
-
// Should return exactly 1 error item
|
|
908
|
-
expect(result.length).toBe(1);
|
|
909
|
-
expect(result[0]).toHaveProperty("description");
|
|
910
|
-
expect(result[0].description).toContain("does not exist");
|
|
911
|
-
expect(result[0].name).toBe("/non/existent");
|
|
912
|
-
expect(result[0].value).toBe("/non/existent");
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
test("should exclude common ignore patterns", () => {
|
|
916
|
-
// Create additional test structure with ignored items
|
|
917
|
-
const ignoredStructure = {
|
|
918
|
-
node_modules: {
|
|
919
|
-
package: "ignored",
|
|
920
|
-
},
|
|
921
|
-
".git": {
|
|
922
|
-
config: "ignored",
|
|
923
|
-
},
|
|
924
|
-
dist: {
|
|
925
|
-
"bundle.js": "ignored",
|
|
926
|
-
},
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
createDirectoryStructure(tempDir, ignoredStructure);
|
|
930
|
-
|
|
931
|
-
const originalCwd = process.cwd;
|
|
932
|
-
process.cwd = () => tempDir;
|
|
933
|
-
|
|
934
|
-
const result = getAvailablePaths();
|
|
935
|
-
|
|
936
|
-
process.cwd = originalCwd;
|
|
937
|
-
|
|
938
|
-
expect(Array.isArray(result)).toBe(true);
|
|
939
|
-
// Should still be 5 items (original ones), ignored items should be filtered out
|
|
940
|
-
expect(result.length).toBe(5);
|
|
941
|
-
|
|
942
|
-
// Should not include ignored patterns
|
|
943
|
-
const names = result.map((r) => r.name);
|
|
944
|
-
expect(names).not.toContain("./node_modules");
|
|
945
|
-
expect(names).not.toContain("./.git");
|
|
946
|
-
expect(names).not.toContain("./dist");
|
|
947
|
-
expect(names).not.toContain("./build");
|
|
948
|
-
|
|
949
|
-
// Should still contain the original items
|
|
950
|
-
expect(names).toContain("./src");
|
|
951
|
-
expect(names).toContain("./tests");
|
|
952
|
-
expect(names).toContain("./docs");
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
test("should sort results alphabetically with directories first", () => {
|
|
956
|
-
const originalCwd = process.cwd;
|
|
957
|
-
process.cwd = () => tempDir;
|
|
958
|
-
|
|
959
|
-
const result = getAvailablePaths();
|
|
960
|
-
|
|
961
|
-
process.cwd = originalCwd;
|
|
962
|
-
|
|
963
|
-
expect(Array.isArray(result)).toBe(true);
|
|
964
|
-
expect(result.length).toBe(5);
|
|
965
|
-
|
|
966
|
-
// Expected order: directories first (docs, src, tests), then files (config.yaml, package.json)
|
|
967
|
-
expect(result[0].name).toBe("./docs");
|
|
968
|
-
expect(result[0].description).toBe("📁 Directory");
|
|
969
|
-
|
|
970
|
-
expect(result[1].name).toBe("./src");
|
|
971
|
-
expect(result[1].description).toBe("📁 Directory");
|
|
972
|
-
|
|
973
|
-
expect(result[2].name).toBe("./tests");
|
|
974
|
-
expect(result[2].description).toBe("📁 Directory");
|
|
975
|
-
|
|
976
|
-
expect(result[3].name).toBe("./config.yaml");
|
|
977
|
-
expect(result[3].description).toBe("📄 File");
|
|
978
|
-
|
|
979
|
-
expect(result[4].name).toBe("./package.json");
|
|
980
|
-
expect(result[4].description).toBe("📄 File");
|
|
981
|
-
|
|
982
|
-
// Verify directories come before files
|
|
983
|
-
const directories = result.filter((r) => r.description === "📁 Directory");
|
|
984
|
-
const files = result.filter((r) => r.description === "📄 File");
|
|
985
|
-
expect(directories.length).toBe(3);
|
|
986
|
-
expect(files.length).toBe(2);
|
|
987
|
-
|
|
988
|
-
// All directories should appear before all files
|
|
989
|
-
const lastDirIndex = result.lastIndexOf(directories[directories.length - 1]);
|
|
990
|
-
const firstFileIndex = result.indexOf(files[0]);
|
|
991
|
-
expect(lastDirIndex).toBeLessThan(firstFileIndex);
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
test("getAvailablePaths should handle relative path validation errors", () => {
|
|
995
|
-
const originalCwd = process.cwd;
|
|
996
|
-
process.cwd = () => tempDir;
|
|
997
|
-
|
|
998
|
-
// Test path with invalid directory
|
|
999
|
-
const result = getAvailablePaths("./nonexistent/file");
|
|
1000
|
-
|
|
1001
|
-
process.cwd = originalCwd;
|
|
1002
|
-
|
|
1003
|
-
expect(Array.isArray(result)).toBe(true);
|
|
1004
|
-
expect(result.length).toBe(1);
|
|
1005
|
-
expect(result[0].name).toBe("./nonexistent/");
|
|
1006
|
-
expect(result[0].description).toContain("does not exist");
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
test("getAvailablePaths should handle relative paths without slash", () => {
|
|
1010
|
-
const originalCwd = process.cwd;
|
|
1011
|
-
process.cwd = () => tempDir;
|
|
1012
|
-
|
|
1013
|
-
// Test case where lastSlashIndex === -1 for relative path
|
|
1014
|
-
const result = getAvailablePaths("./noslashthingy");
|
|
1015
|
-
|
|
1016
|
-
process.cwd = originalCwd;
|
|
1017
|
-
|
|
1018
|
-
expect(Array.isArray(result)).toBe(true);
|
|
1019
|
-
// Should search current directory for the term
|
|
1020
|
-
});
|
|
1021
|
-
});
|
|
1022
|
-
|
|
1023
|
-
// Helper function to create directory structure
|
|
1024
|
-
function createDirectoryStructure(basePath, structure) {
|
|
1025
|
-
for (const [name, content] of Object.entries(structure)) {
|
|
1026
|
-
const itemPath = path.join(basePath, name);
|
|
1027
|
-
|
|
1028
|
-
if (typeof content === "string") {
|
|
1029
|
-
// It's a file
|
|
1030
|
-
writeFileSync(itemPath, content);
|
|
1031
|
-
} else {
|
|
1032
|
-
// It's a directory
|
|
1033
|
-
mkdirSync(itemPath, { recursive: true });
|
|
1034
|
-
createDirectoryStructure(itemPath, content);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
describe("getGithubRepoUrl", () => {
|
|
1040
|
-
test("should return string", () => {
|
|
1041
|
-
const result = getGithubRepoUrl();
|
|
1042
|
-
expect(typeof result).toBe("string");
|
|
1043
|
-
});
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
describe("processConfigFields", () => {
|
|
1047
|
-
test("should apply default values for missing fields", () => {
|
|
1048
|
-
const config = {};
|
|
1049
|
-
const result = processConfigFields(config);
|
|
1050
|
-
|
|
1051
|
-
expect(result.nodeName).toBe("Section");
|
|
1052
|
-
expect(result.locale).toBe("en");
|
|
1053
|
-
expect(result.sourcesPath).toEqual(["./"]);
|
|
1054
|
-
expect(result.docsDir).toBe("./.aigne/doc-smith/docs");
|
|
1055
|
-
expect(result.outputDir).toBe("./.aigne/doc-smith/output");
|
|
1056
|
-
expect(result.translateLanguages).toEqual([]);
|
|
1057
|
-
expect(result.rules).toBe("");
|
|
1058
|
-
expect(result.targetAudience).toBe("");
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
test("should only set defaults, not preserve other values", () => {
|
|
1062
|
-
const config = {
|
|
1063
|
-
locale: "zh",
|
|
1064
|
-
sourcesPath: ["./src"],
|
|
1065
|
-
};
|
|
1066
|
-
const result = processConfigFields(config);
|
|
1067
|
-
|
|
1068
|
-
// Function only sets defaults when values are missing/empty, doesn't copy existing non-default values
|
|
1069
|
-
expect(result.nodeName).toBe("Section"); // Default applied
|
|
1070
|
-
expect(result.locale).toBeUndefined(); // Not copied because zh is not empty/missing per logic
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
test("should process document purpose array", () => {
|
|
1074
|
-
const config = {
|
|
1075
|
-
documentPurpose: ["getStarted"],
|
|
1076
|
-
};
|
|
1077
|
-
const result = processConfigFields(config);
|
|
1078
|
-
|
|
1079
|
-
expect(result.rules).toContain("Document Purpose");
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
test("should process target audience types", () => {
|
|
1083
|
-
const config = {
|
|
1084
|
-
targetAudienceTypes: ["developers"],
|
|
1085
|
-
};
|
|
1086
|
-
const result = processConfigFields(config);
|
|
1087
|
-
|
|
1088
|
-
expect(result.rules).toContain("Target Audience");
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
test("should handle string rules only (array rules cause TypeError)", () => {
|
|
1092
|
-
const config = {
|
|
1093
|
-
rules: "Custom rule content",
|
|
1094
|
-
};
|
|
1095
|
-
const result = processConfigFields(config);
|
|
1096
|
-
|
|
1097
|
-
// The function should process string rules
|
|
1098
|
-
expect(typeof result.rules).toBe("string");
|
|
1099
|
-
expect(result.rules).toContain("Custom rule");
|
|
1100
|
-
});
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
describe("resolveFileReferences", () => {
|
|
1104
|
-
test("should return non-string values unchanged", async () => {
|
|
1105
|
-
const config = { number: 123, boolean: true };
|
|
1106
|
-
const result = await resolveFileReferences(config);
|
|
1107
|
-
expect(result).toEqual(config);
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
test("should return strings without @ prefix unchanged", async () => {
|
|
1111
|
-
const config = { text: "normal string" };
|
|
1112
|
-
const result = await resolveFileReferences(config);
|
|
1113
|
-
expect(result).toEqual(config);
|
|
1114
|
-
});
|
|
1115
|
-
|
|
1116
|
-
test("should handle arrays recursively", async () => {
|
|
1117
|
-
const config = ["normal", "@nonexistent.txt"];
|
|
1118
|
-
const result = await resolveFileReferences(config);
|
|
1119
|
-
expect(result[0]).toBe("normal");
|
|
1120
|
-
expect(result[1]).toBe("@nonexistent.txt"); // File doesn't exist, returns original
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
test("should handle nested objects", async () => {
|
|
1124
|
-
const config = {
|
|
1125
|
-
nested: {
|
|
1126
|
-
value: "@nonexistent.txt",
|
|
1127
|
-
},
|
|
1128
|
-
};
|
|
1129
|
-
const result = await resolveFileReferences(config);
|
|
1130
|
-
expect(result.nested.value).toBe("@nonexistent.txt");
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
test("should load existing file content", async () => {
|
|
1134
|
-
const testFile = path.join(tempDir, "test.txt");
|
|
1135
|
-
writeFileSync(testFile, "file content");
|
|
1136
|
-
|
|
1137
|
-
const config = { file: `@${testFile}` };
|
|
1138
|
-
const result = await resolveFileReferences(config);
|
|
1139
|
-
expect(result.file).toBe("file content");
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
test("should handle JSON files", async () => {
|
|
1143
|
-
const jsonFile = path.join(tempDir, "test.json");
|
|
1144
|
-
writeFileSync(jsonFile, JSON.stringify({ key: "value" }));
|
|
1145
|
-
|
|
1146
|
-
const config = { data: `@${jsonFile}` };
|
|
1147
|
-
const result = await resolveFileReferences(config);
|
|
1148
|
-
expect(result.data).toEqual({ key: "value" });
|
|
1149
|
-
});
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
describe("saveGitHeadToConfig", () => {
|
|
1153
|
-
test("should handle no git HEAD", async () => {
|
|
1154
|
-
// Should not throw error and should return without action
|
|
1155
|
-
await saveGitHeadToConfig(null);
|
|
1156
|
-
// No assertion needed, just verify it doesn't crash
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
test("should skip in test environment", async () => {
|
|
1160
|
-
// Should skip because BUN_TEST env var is set
|
|
1161
|
-
await saveGitHeadToConfig("abcd1234");
|
|
1162
|
-
// No assertion needed, just verify it doesn't crash
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
test("should handle test environment correctly", async () => {
|
|
1166
|
-
// This function should skip in test environment (BUN_TEST is set)
|
|
1167
|
-
// We don't need to test the actual file creation since that would affect real config
|
|
1168
|
-
await saveGitHeadToConfig("test-hash");
|
|
1169
|
-
// Should complete without error due to test environment skip
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
describe("loadConfigFromFile", () => {
|
|
1174
|
-
test("should return null or config object", async () => {
|
|
1175
|
-
const result = await loadConfigFromFile();
|
|
1176
|
-
// Function either returns null (if no config) or a config object
|
|
1177
|
-
expect(result === null || typeof result === "object").toBe(true);
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
test("should handle malformed config file gracefully", async () => {
|
|
1181
|
-
// Create invalid config file in temp directory with process.cwd() override
|
|
1182
|
-
const originalCwd = process.cwd;
|
|
1183
|
-
process.cwd = () => tempDir;
|
|
1184
|
-
|
|
1185
|
-
const configDir = path.join(tempDir, ".aigne", "doc-smith");
|
|
1186
|
-
mkdirSync(configDir, { recursive: true });
|
|
1187
|
-
writeFileSync(path.join(configDir, "config.yaml"), "invalid: yaml: [");
|
|
1188
|
-
|
|
1189
|
-
const result = await loadConfigFromFile();
|
|
1190
|
-
expect(result).toBe(null); // Should handle parse error gracefully
|
|
1191
|
-
|
|
1192
|
-
process.cwd = originalCwd;
|
|
1193
|
-
});
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
describe("saveValueToConfig", () => {
|
|
1197
|
-
test("should skip undefined values", async () => {
|
|
1198
|
-
await saveValueToConfig("testKey", undefined);
|
|
1199
|
-
// Should not crash and should skip the operation
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
test("should handle string values", async () => {
|
|
1203
|
-
const originalCwd = process.cwd;
|
|
1204
|
-
process.cwd = () => tempDir;
|
|
1205
|
-
|
|
1206
|
-
await saveValueToConfig("testKey", "testValue");
|
|
1207
|
-
|
|
1208
|
-
process.cwd = originalCwd;
|
|
1209
|
-
// Should not crash
|
|
1210
|
-
});
|
|
1211
|
-
|
|
1212
|
-
test("should handle array values", async () => {
|
|
1213
|
-
const originalCwd = process.cwd;
|
|
1214
|
-
process.cwd = () => tempDir;
|
|
1215
|
-
|
|
1216
|
-
await saveValueToConfig("testArray", ["item1", "item2"]);
|
|
1217
|
-
|
|
1218
|
-
process.cwd = originalCwd;
|
|
1219
|
-
// Should not crash
|
|
1220
|
-
});
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
describe("getProjectInfo", () => {
|
|
1224
|
-
test("should return project info object", async () => {
|
|
1225
|
-
const result = await getProjectInfo();
|
|
1226
|
-
|
|
1227
|
-
expect(typeof result).toBe("object");
|
|
1228
|
-
expect(result).toHaveProperty("name");
|
|
1229
|
-
expect(result).toHaveProperty("description");
|
|
1230
|
-
expect(result).toHaveProperty("icon");
|
|
1231
|
-
expect(result).toHaveProperty("fromGitHub");
|
|
1232
|
-
expect(typeof result.fromGitHub).toBe("boolean");
|
|
1233
|
-
});
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
describe("getGitHubRepoInfo", () => {
|
|
1237
|
-
test("should return null for invalid URL", async () => {
|
|
1238
|
-
const result = await getGitHubRepoInfo("invalid-url");
|
|
1239
|
-
expect(result).toBe(null);
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
test("should return null for non-GitHub URL", async () => {
|
|
1243
|
-
const result = await getGitHubRepoInfo("https://gitlab.com/user/repo");
|
|
1244
|
-
expect(result).toBe(null);
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
test("should handle network errors gracefully", async () => {
|
|
1248
|
-
const result = await getGitHubRepoInfo("https://github.com/nonexistent/nonexistent");
|
|
1249
|
-
// Should not crash, may return null depending on network
|
|
1250
|
-
expect(result === null || typeof result === "object").toBe(true);
|
|
1251
|
-
});
|
|
1252
|
-
|
|
1253
|
-
test("should fetch real GitHub repository info - aigne-doc-smith", async () => {
|
|
1254
|
-
const result = await getGitHubRepoInfo("https://github.com/AIGNE-io/aigne-doc-smith.git");
|
|
1255
|
-
|
|
1256
|
-
if (result !== null) {
|
|
1257
|
-
// If successful, should have expected repository structure
|
|
1258
|
-
expect(typeof result).toBe("object");
|
|
1259
|
-
expect(result).toHaveProperty("name");
|
|
1260
|
-
expect(result).toHaveProperty("description");
|
|
1261
|
-
expect(result.name).toBe("aigne-doc-smith");
|
|
1262
|
-
} else {
|
|
1263
|
-
// Network might be unavailable or API rate limited - that's acceptable
|
|
1264
|
-
expect(result).toBe(null);
|
|
1265
|
-
}
|
|
1266
|
-
}, 10000); // 10 second timeout for network request
|
|
1267
|
-
|
|
1268
|
-
test("should fetch real GitHub repository info - FastAPI", async () => {
|
|
1269
|
-
// Test with SSH URL format converted to HTTPS
|
|
1270
|
-
const result = await getGitHubRepoInfo("git@github.com:fastapi/fastapi.git");
|
|
1271
|
-
|
|
1272
|
-
if (result !== null) {
|
|
1273
|
-
// If successful, should have expected repository structure
|
|
1274
|
-
expect(typeof result).toBe("object");
|
|
1275
|
-
expect(result).toHaveProperty("name");
|
|
1276
|
-
expect(result).toHaveProperty("description");
|
|
1277
|
-
expect(result.name).toBe("fastapi");
|
|
1278
|
-
expect(typeof result.description).toBe("string");
|
|
1279
|
-
expect(result.description.length).toBeGreaterThan(0);
|
|
1280
|
-
} else {
|
|
1281
|
-
// Network might be unavailable or API rate limited - that's acceptable
|
|
1282
|
-
expect(result).toBe(null);
|
|
1283
|
-
}
|
|
1284
|
-
}, 10000); // 10 second timeout for network request
|
|
1285
|
-
|
|
1286
|
-
test("should handle SSH URL format correctly", async () => {
|
|
1287
|
-
// Test that SSH URLs are properly converted to GitHub API URLs
|
|
1288
|
-
const sshUrl = "git@github.com:fastapi/fastapi.git";
|
|
1289
|
-
const result = await getGitHubRepoInfo(sshUrl);
|
|
1290
|
-
|
|
1291
|
-
// Should either return repository info or null (network issues)
|
|
1292
|
-
expect(result === null || typeof result === "object").toBe(true);
|
|
1293
|
-
|
|
1294
|
-
if (result !== null) {
|
|
1295
|
-
expect(result.name).toBe("fastapi");
|
|
1296
|
-
}
|
|
1297
|
-
}, 10000);
|
|
1298
|
-
|
|
1299
|
-
test("should handle HTTPS URL with .git suffix", async () => {
|
|
1300
|
-
const httpsUrl = "https://github.com/AIGNE-io/aigne-doc-smith.git";
|
|
1301
|
-
const result = await getGitHubRepoInfo(httpsUrl);
|
|
1302
|
-
|
|
1303
|
-
// Should either return repository info or null (network issues)
|
|
1304
|
-
expect(result === null || typeof result === "object").toBe(true);
|
|
1305
|
-
|
|
1306
|
-
if (result !== null) {
|
|
1307
|
-
expect(result.name).toBe("aigne-doc-smith");
|
|
1308
|
-
}
|
|
1309
|
-
}, 10000);
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
describe("error handling edge cases", () => {
|
|
1313
|
-
test("processContent should handle malformed links", () => {
|
|
1314
|
-
const content = "Malformed [link](incomplete";
|
|
1315
|
-
const result = processContent({ content });
|
|
1316
|
-
expect(result).toBe(content); // Should not crash
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
test("validatePath should handle extremely long paths", () => {
|
|
1320
|
-
const longPath = "a".repeat(1000);
|
|
1321
|
-
const result = validatePath(longPath);
|
|
1322
|
-
expect(result).toHaveProperty("isValid");
|
|
1323
|
-
expect(result).toHaveProperty("error");
|
|
1324
|
-
});
|
|
1325
|
-
|
|
1326
|
-
test("getAvailablePaths should handle permission errors", () => {
|
|
1327
|
-
const result = getAvailablePaths("/root/restricted");
|
|
1328
|
-
expect(Array.isArray(result)).toBe(true);
|
|
1329
|
-
});
|
|
1330
|
-
|
|
1331
|
-
test("normalizePath should handle special characters", () => {
|
|
1332
|
-
const result = normalizePath("./test with spaces/file.txt");
|
|
1333
|
-
expect(path.isAbsolute(result)).toBe(true);
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
test("toRelativePath should handle root path", () => {
|
|
1337
|
-
const result = toRelativePath("/");
|
|
1338
|
-
expect(typeof result).toBe("string");
|
|
1339
|
-
});
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
|
-
// Additional tests for uncovered lines
|
|
1343
|
-
describe("additional coverage tests", () => {
|
|
1344
|
-
test("saveDocWithTranslations should add labels to front matter", async () => {
|
|
1345
|
-
const testDocsDir = path.join(tempDir, "docs");
|
|
1346
|
-
const content = "# Test content";
|
|
1347
|
-
const labels = ["test", "example"];
|
|
1348
|
-
|
|
1349
|
-
const results = await saveDocWithTranslations({
|
|
1350
|
-
path: "test-with-labels.md",
|
|
1351
|
-
content,
|
|
1352
|
-
docsDir: testDocsDir,
|
|
1353
|
-
locale: "en",
|
|
1354
|
-
labels,
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
expect(results.length).toBe(1);
|
|
1358
|
-
|
|
1359
|
-
if (!results[0].success) {
|
|
1360
|
-
console.log("Error:", results[0].error);
|
|
1361
|
-
}
|
|
1362
|
-
expect(results[0].success).toBe(true);
|
|
1363
|
-
|
|
1364
|
-
// The actual path should be what's returned in results
|
|
1365
|
-
const actualPath = results[0].path;
|
|
1366
|
-
expect(existsSync(actualPath)).toBe(true);
|
|
1367
|
-
|
|
1368
|
-
const savedContent = readFileSync(actualPath, "utf8");
|
|
1369
|
-
expect(savedContent).toContain('labels: ["test","example"]');
|
|
1370
|
-
expect(savedContent).toContain("# Test content");
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
test("saveDocWithTranslations should handle error cases", async () => {
|
|
1374
|
-
// Test with invalid directory - use read-only directory
|
|
1375
|
-
const results = await saveDocWithTranslations({
|
|
1376
|
-
path: "test.md",
|
|
1377
|
-
content: "# Test content",
|
|
1378
|
-
docsDir: "/root/invalid", // This should fail
|
|
1379
|
-
locale: "en",
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
expect(results.length).toBe(1);
|
|
1383
|
-
expect(results[0].success).toBe(false);
|
|
1384
|
-
expect(results[0].error).toBeDefined();
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
test("saveGitHeadToConfig should create directory and handle file operations", async () => {
|
|
1388
|
-
// Test in non-test environment by temporarily unsetting test env
|
|
1389
|
-
const originalBunTest = process.env.BUN_TEST;
|
|
1390
|
-
const originalNodeEnv = process.env.NODE_ENV;
|
|
1391
|
-
delete process.env.BUN_TEST;
|
|
1392
|
-
delete process.env.NODE_ENV;
|
|
1393
|
-
|
|
1394
|
-
const originalCwd = process.cwd;
|
|
1395
|
-
const testCwd = path.join(tempDir, "git-test");
|
|
1396
|
-
mkdirSync(testCwd, { recursive: true });
|
|
1397
|
-
process.cwd = () => testCwd;
|
|
1398
|
-
|
|
1399
|
-
try {
|
|
1400
|
-
await saveGitHeadToConfig("abc123456");
|
|
1401
|
-
|
|
1402
|
-
const configPath = path.join(testCwd, ".aigne", "doc-smith", "config.yaml");
|
|
1403
|
-
if (existsSync(configPath)) {
|
|
1404
|
-
const configContent = readFileSync(configPath, "utf8");
|
|
1405
|
-
expect(configContent).toContain("lastGitHead:");
|
|
1406
|
-
}
|
|
1407
|
-
} finally {
|
|
1408
|
-
// Restore environment
|
|
1409
|
-
process.cwd = originalCwd;
|
|
1410
|
-
if (originalBunTest) process.env.BUN_TEST = originalBunTest;
|
|
1411
|
-
if (originalNodeEnv) process.env.NODE_ENV = originalNodeEnv;
|
|
1412
|
-
}
|
|
1413
|
-
});
|
|
1414
|
-
|
|
1415
|
-
test("loadConfigFromFile should handle existing config file", async () => {
|
|
1416
|
-
const originalCwd = process.cwd;
|
|
1417
|
-
process.cwd = () => tempDir;
|
|
1418
|
-
|
|
1419
|
-
try {
|
|
1420
|
-
const configDir = path.join(tempDir, ".aigne", "doc-smith");
|
|
1421
|
-
mkdirSync(configDir, { recursive: true });
|
|
1422
|
-
|
|
1423
|
-
const validConfig = `
|
|
1424
|
-
projectName: test-project
|
|
1425
|
-
locale: en
|
|
1426
|
-
sourcesPath:
|
|
1427
|
-
- ./src
|
|
1428
|
-
`;
|
|
1429
|
-
writeFileSync(path.join(configDir, "config.yaml"), validConfig);
|
|
1430
|
-
|
|
1431
|
-
const result = await loadConfigFromFile();
|
|
1432
|
-
expect(result).toBeDefined();
|
|
1433
|
-
expect(result.projectName).toBe("test-project");
|
|
1434
|
-
} finally {
|
|
1435
|
-
process.cwd = originalCwd;
|
|
1436
|
-
}
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
test("saveValueToConfig should handle different value types", async () => {
|
|
1440
|
-
const configDir = path.join(tempDir, "save-config-test");
|
|
1441
|
-
mkdirSync(configDir, { recursive: true });
|
|
1442
|
-
|
|
1443
|
-
const originalCwd = process.cwd;
|
|
1444
|
-
process.cwd = () => configDir;
|
|
1445
|
-
|
|
1446
|
-
try {
|
|
1447
|
-
// Test with various data types
|
|
1448
|
-
await saveValueToConfig("testKey", "string value");
|
|
1449
|
-
await saveValueToConfig("testArray", ["item1", "item2"]);
|
|
1450
|
-
await saveValueToConfig("testNumber", 42);
|
|
1451
|
-
await saveValueToConfig("testBoolean", true);
|
|
1452
|
-
|
|
1453
|
-
const configPath = path.join(configDir, ".aigne", "doc-smith", "config.yaml");
|
|
1454
|
-
if (existsSync(configPath)) {
|
|
1455
|
-
const configContent = readFileSync(configPath, "utf8");
|
|
1456
|
-
expect(configContent).toContain("testKey:");
|
|
1457
|
-
expect(configContent).toContain("testArray:");
|
|
1458
|
-
}
|
|
1459
|
-
} finally {
|
|
1460
|
-
process.cwd = originalCwd;
|
|
1461
|
-
}
|
|
1462
|
-
});
|
|
1463
|
-
|
|
1464
|
-
test("resolveFileReferences should handle file read errors", async () => {
|
|
1465
|
-
const config = {
|
|
1466
|
-
file: "@/nonexistent/path/file.txt",
|
|
1467
|
-
};
|
|
1468
|
-
const result = await resolveFileReferences(config);
|
|
1469
|
-
// Should return original reference when file doesn't exist
|
|
1470
|
-
expect(result.file).toBe("@/nonexistent/path/file.txt");
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
test("processConfigFields should handle empty config", () => {
|
|
1474
|
-
const config = {};
|
|
1475
|
-
const result = processConfigFields(config);
|
|
1476
|
-
|
|
1477
|
-
expect(result.locale).toBe("en");
|
|
1478
|
-
expect(Array.isArray(result.sourcesPath)).toBe(true);
|
|
1479
|
-
expect(result.sourcesPath.length).toBe(1);
|
|
1480
|
-
expect(result.sourcesPath[0]).toBe("./");
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
|
-
test("saveDocWithTranslations should handle translations correctly", async () => {
|
|
1484
|
-
const testDocsDir = path.join(tempDir, "docs-translate");
|
|
1485
|
-
const content = "# Test";
|
|
1486
|
-
|
|
1487
|
-
// Create translations with valid structure
|
|
1488
|
-
const translations = [{ language: "zh", translation: "# 测试" }];
|
|
1489
|
-
|
|
1490
|
-
const results = await saveDocWithTranslations({
|
|
1491
|
-
path: "translation-test.md",
|
|
1492
|
-
content,
|
|
1493
|
-
docsDir: testDocsDir,
|
|
1494
|
-
locale: "en",
|
|
1495
|
-
translates: translations,
|
|
1496
|
-
});
|
|
1497
|
-
|
|
1498
|
-
expect(Array.isArray(results)).toBe(true);
|
|
1499
|
-
expect(results.length).toBe(2); // Original + 1 translation
|
|
1500
|
-
expect(results.every((r) => r.success)).toBe(true);
|
|
1501
|
-
});
|
|
1502
|
-
|
|
1503
|
-
test("saveDocWithTranslations should add labels to translations", async () => {
|
|
1504
|
-
const testDocsDir = path.join(tempDir, "docs-translation-labels");
|
|
1505
|
-
const content = "# Main Content";
|
|
1506
|
-
const labels = ["test", "translation"];
|
|
1507
|
-
const translations = [{ language: "zh", translation: "# 主要内容" }];
|
|
1508
|
-
|
|
1509
|
-
const results = await saveDocWithTranslations({
|
|
1510
|
-
path: "labeled-translation",
|
|
1511
|
-
content,
|
|
1512
|
-
docsDir: testDocsDir,
|
|
1513
|
-
locale: "en",
|
|
1514
|
-
translates: translations,
|
|
1515
|
-
labels,
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
expect(results.length).toBe(2); // Main + translation
|
|
1519
|
-
expect(results.every((r) => r.success)).toBe(true);
|
|
1520
|
-
|
|
1521
|
-
// Check that translation file has labels
|
|
1522
|
-
const translationPath = results[1].path;
|
|
1523
|
-
expect(existsSync(translationPath)).toBe(true);
|
|
1524
|
-
const translationContent = readFileSync(translationPath, "utf8");
|
|
1525
|
-
expect(translationContent).toContain('labels: ["test","translation"]');
|
|
1526
|
-
expect(translationContent).toContain("# 主要内容");
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
test("saveGitHeadToConfig should handle file replacement scenario", async () => {
|
|
1530
|
-
const originalBunTest = process.env.BUN_TEST;
|
|
1531
|
-
const originalNodeEnv = process.env.NODE_ENV;
|
|
1532
|
-
delete process.env.BUN_TEST;
|
|
1533
|
-
delete process.env.NODE_ENV;
|
|
1534
|
-
|
|
1535
|
-
const originalCwd = process.cwd;
|
|
1536
|
-
const testCwd = path.join(tempDir, "replace-git-test");
|
|
1537
|
-
mkdirSync(testCwd, { recursive: true });
|
|
1538
|
-
process.cwd = () => testCwd;
|
|
1539
|
-
|
|
1540
|
-
try {
|
|
1541
|
-
// First call creates the file
|
|
1542
|
-
await saveGitHeadToConfig("first-hash");
|
|
1543
|
-
|
|
1544
|
-
// Second call should replace existing lastGitHead
|
|
1545
|
-
await saveGitHeadToConfig("second-hash");
|
|
1546
|
-
|
|
1547
|
-
const configPath = path.join(testCwd, ".aigne", "doc-smith", "config.yaml");
|
|
1548
|
-
if (existsSync(configPath)) {
|
|
1549
|
-
const configContent = readFileSync(configPath, "utf8");
|
|
1550
|
-
expect(configContent).toContain("second-hash");
|
|
1551
|
-
expect(configContent).not.toContain("first-hash");
|
|
1552
|
-
}
|
|
1553
|
-
} finally {
|
|
1554
|
-
// Restore environment
|
|
1555
|
-
process.cwd = originalCwd;
|
|
1556
|
-
if (originalBunTest) process.env.BUN_TEST = originalBunTest;
|
|
1557
|
-
if (originalNodeEnv) process.env.NODE_ENV = originalNodeEnv;
|
|
1558
|
-
}
|
|
1559
|
-
});
|
|
1560
|
-
|
|
1561
|
-
test("saveGitHeadToConfig should handle file write errors gracefully", async () => {
|
|
1562
|
-
const originalBunTest = process.env.BUN_TEST;
|
|
1563
|
-
const originalNodeEnv = process.env.NODE_ENV;
|
|
1564
|
-
const originalWarn = console.warn;
|
|
1565
|
-
delete process.env.BUN_TEST;
|
|
1566
|
-
delete process.env.NODE_ENV;
|
|
1567
|
-
|
|
1568
|
-
const warnMessages = [];
|
|
1569
|
-
console.warn = (message) => warnMessages.push(message);
|
|
1570
|
-
|
|
1571
|
-
const originalCwd = process.cwd;
|
|
1572
|
-
process.cwd = () => "/root"; // Read-only directory
|
|
1573
|
-
|
|
1574
|
-
try {
|
|
1575
|
-
await saveGitHeadToConfig("test-hash");
|
|
1576
|
-
// Should handle error gracefully and log warning
|
|
1577
|
-
expect(
|
|
1578
|
-
warnMessages.some((msg) => msg.includes("Failed to save git HEAD to config.yaml:")),
|
|
1579
|
-
).toBe(true);
|
|
1580
|
-
} finally {
|
|
1581
|
-
// Restore environment
|
|
1582
|
-
process.cwd = originalCwd;
|
|
1583
|
-
console.warn = originalWarn;
|
|
1584
|
-
if (originalBunTest) process.env.BUN_TEST = originalBunTest;
|
|
1585
|
-
if (originalNodeEnv) process.env.NODE_ENV = originalNodeEnv;
|
|
1586
|
-
}
|
|
1587
|
-
});
|
|
1588
|
-
|
|
1589
|
-
test("saveGitHeadToConfig should append to file without ending newline", async () => {
|
|
1590
|
-
const originalBunTest = process.env.BUN_TEST;
|
|
1591
|
-
const originalNodeEnv = process.env.NODE_ENV;
|
|
1592
|
-
delete process.env.BUN_TEST;
|
|
1593
|
-
delete process.env.NODE_ENV;
|
|
1594
|
-
|
|
1595
|
-
const originalCwd = process.cwd;
|
|
1596
|
-
const testCwd = path.join(tempDir, "append-git-test");
|
|
1597
|
-
mkdirSync(testCwd, { recursive: true });
|
|
1598
|
-
process.cwd = () => testCwd;
|
|
1599
|
-
|
|
1600
|
-
try {
|
|
1601
|
-
// Create config directory and file without ending newline
|
|
1602
|
-
const configDir = path.join(testCwd, ".aigne", "doc-smith");
|
|
1603
|
-
mkdirSync(configDir, { recursive: true });
|
|
1604
|
-
const configPath = path.join(configDir, "config.yaml");
|
|
1605
|
-
writeFileSync(configPath, "existingKey: value"); // No ending newline
|
|
1606
|
-
|
|
1607
|
-
await saveGitHeadToConfig("test-hash");
|
|
1608
|
-
|
|
1609
|
-
const configContent = readFileSync(configPath, "utf8");
|
|
1610
|
-
expect(configContent).toContain("existingKey: value");
|
|
1611
|
-
expect(configContent).toContain("lastGitHead: test-hash");
|
|
1612
|
-
// Should properly handle newline addition
|
|
1613
|
-
expect(configContent.endsWith("\n")).toBe(true);
|
|
1614
|
-
} finally {
|
|
1615
|
-
// Restore environment
|
|
1616
|
-
process.cwd = originalCwd;
|
|
1617
|
-
if (originalBunTest) process.env.BUN_TEST = originalBunTest;
|
|
1618
|
-
if (originalNodeEnv) process.env.NODE_ENV = originalNodeEnv;
|
|
1619
|
-
}
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
test("loadConfigFromFile should handle non-existent config directory", async () => {
|
|
1623
|
-
const originalCwd = process.cwd;
|
|
1624
|
-
process.cwd = () => path.join(tempDir, "no-config-dir");
|
|
1625
|
-
|
|
1626
|
-
try {
|
|
1627
|
-
const result = await loadConfigFromFile();
|
|
1628
|
-
expect(result).toBe(null);
|
|
1629
|
-
} finally {
|
|
1630
|
-
process.cwd = originalCwd;
|
|
1631
|
-
}
|
|
1632
|
-
});
|
|
1633
|
-
|
|
1634
|
-
test("resolveFileReferences should handle various file types", async () => {
|
|
1635
|
-
// Test with YAML file
|
|
1636
|
-
const yamlFile = path.join(tempDir, "test.yaml");
|
|
1637
|
-
writeFileSync(yamlFile, "key: value\narray:\n - item1\n - item2");
|
|
1638
|
-
|
|
1639
|
-
const config = {
|
|
1640
|
-
yaml: `@${yamlFile}`,
|
|
1641
|
-
nonexistent: "@nonexistent.txt",
|
|
1642
|
-
normal: "normal value",
|
|
1643
|
-
};
|
|
1644
|
-
|
|
1645
|
-
const result = await resolveFileReferences(config);
|
|
1646
|
-
expect(result.yaml).toBeDefined();
|
|
1647
|
-
expect(typeof result.yaml).toBe("object");
|
|
1648
|
-
expect(result.nonexistent).toBe("@nonexistent.txt");
|
|
1649
|
-
expect(result.normal).toBe("normal value");
|
|
1650
|
-
});
|
|
1651
|
-
|
|
1652
|
-
test("saveValueToConfig should handle file append scenario", async () => {
|
|
1653
|
-
const configDir = path.join(tempDir, "append-config-test");
|
|
1654
|
-
mkdirSync(configDir, { recursive: true });
|
|
1655
|
-
|
|
1656
|
-
const originalCwd = process.cwd;
|
|
1657
|
-
process.cwd = () => configDir;
|
|
1658
|
-
|
|
1659
|
-
try {
|
|
1660
|
-
// Create initial config without newline ending
|
|
1661
|
-
const aigneDir = path.join(configDir, ".aigne", "doc-smith");
|
|
1662
|
-
mkdirSync(aigneDir, { recursive: true });
|
|
1663
|
-
writeFileSync(path.join(aigneDir, "config.yaml"), "existingKey: value");
|
|
1664
|
-
|
|
1665
|
-
// This should append with proper newline handling
|
|
1666
|
-
await saveValueToConfig("newKey", "newValue");
|
|
1667
|
-
|
|
1668
|
-
const configPath = path.join(aigneDir, "config.yaml");
|
|
1669
|
-
const configContent = readFileSync(configPath, "utf8");
|
|
1670
|
-
expect(configContent).toContain("existingKey: value");
|
|
1671
|
-
expect(configContent).toContain("newKey: newValue");
|
|
1672
|
-
} finally {
|
|
1673
|
-
process.cwd = originalCwd;
|
|
1674
|
-
}
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
test("saveValueToConfig should handle complex array update scenarios", async () => {
|
|
1678
|
-
const configDir = path.join(tempDir, "array-update-test");
|
|
1679
|
-
mkdirSync(configDir, { recursive: true });
|
|
1680
|
-
|
|
1681
|
-
const originalCwd = process.cwd;
|
|
1682
|
-
process.cwd = () => configDir;
|
|
1683
|
-
|
|
1684
|
-
try {
|
|
1685
|
-
const aigneDir = path.join(configDir, ".aigne", "doc-smith");
|
|
1686
|
-
mkdirSync(aigneDir, { recursive: true });
|
|
1687
|
-
const configPath = path.join(aigneDir, "config.yaml");
|
|
1688
|
-
|
|
1689
|
-
// Test case 1: Array with inline format
|
|
1690
|
-
writeFileSync(configPath, "testArray: [item1, item2]");
|
|
1691
|
-
await saveValueToConfig("testArray", ["newItem1", "newItem2"]);
|
|
1692
|
-
let configContent = readFileSync(configPath, "utf8");
|
|
1693
|
-
expect(configContent).toContain("testArray:");
|
|
1694
|
-
expect(configContent).toContain("newItem1");
|
|
1695
|
-
|
|
1696
|
-
// Test case 2: Array with mixed content
|
|
1697
|
-
writeFileSync(
|
|
1698
|
-
configPath,
|
|
1699
|
-
`# Initial comment
|
|
1700
|
-
testArray:
|
|
1701
|
-
- oldItem
|
|
1702
|
-
# Another comment
|
|
1703
|
-
otherKey: value`,
|
|
1704
|
-
);
|
|
1705
|
-
await saveValueToConfig("testArray", ["replacedItem"]);
|
|
1706
|
-
configContent = readFileSync(configPath, "utf8");
|
|
1707
|
-
expect(configContent).toContain("replacedItem");
|
|
1708
|
-
expect(configContent).toContain("otherKey: value");
|
|
1709
|
-
|
|
1710
|
-
// Test case 3: Array at end of file
|
|
1711
|
-
writeFileSync(
|
|
1712
|
-
configPath,
|
|
1713
|
-
`someKey: value
|
|
1714
|
-
testArray:
|
|
1715
|
-
- item1
|
|
1716
|
-
- item2`,
|
|
1717
|
-
);
|
|
1718
|
-
await saveValueToConfig("testArray", ["endItem"]);
|
|
1719
|
-
configContent = readFileSync(configPath, "utf8");
|
|
1720
|
-
expect(configContent).toContain("endItem");
|
|
1721
|
-
expect(configContent).toContain("someKey: value");
|
|
1722
|
-
|
|
1723
|
-
// Test case 4: Add new array to end without newline
|
|
1724
|
-
writeFileSync(configPath, "existingKey: value");
|
|
1725
|
-
await saveValueToConfig("newArray", ["newArrayItem"], "Array comment");
|
|
1726
|
-
configContent = readFileSync(configPath, "utf8");
|
|
1727
|
-
expect(configContent).toContain("existingKey: value");
|
|
1728
|
-
expect(configContent).toContain("# Array comment");
|
|
1729
|
-
expect(configContent).toContain("newArray:");
|
|
1730
|
-
expect(configContent).toContain("newArrayItem");
|
|
1731
|
-
} finally {
|
|
1732
|
-
process.cwd = originalCwd;
|
|
1733
|
-
}
|
|
1734
|
-
});
|
|
1735
|
-
|
|
1736
|
-
test("saveDocWithTranslations should skip main content when isTranslate is true", async () => {
|
|
1737
|
-
const testDocsDir = path.join(tempDir, "skip-main-test");
|
|
1738
|
-
|
|
1739
|
-
const results = await saveDocWithTranslations({
|
|
1740
|
-
path: "skip-test.md",
|
|
1741
|
-
content: "# Should be skipped",
|
|
1742
|
-
docsDir: testDocsDir,
|
|
1743
|
-
locale: "en",
|
|
1744
|
-
isTranslate: true,
|
|
1745
|
-
translates: [{ language: "zh", translation: "# 翻译内容" }],
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
expect(results.length).toBe(1); // Only translation, main content skipped
|
|
1749
|
-
expect(results[0].path).toContain(".zh.md");
|
|
1750
|
-
});
|
|
1751
|
-
|
|
1752
|
-
test("processConfigFields should handle complex configurations", () => {
|
|
1753
|
-
const config = {
|
|
1754
|
-
documentPurpose: ["getStarted", "findAnswers"], // Already an array
|
|
1755
|
-
targetAudienceTypes: ["developers", "devops"],
|
|
1756
|
-
rules: "string rule", // Keep as string to avoid error
|
|
1757
|
-
locale: "zh-CN",
|
|
1758
|
-
sourcesPath: [], // Empty array should get default
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
const result = processConfigFields(config);
|
|
1762
|
-
|
|
1763
|
-
// Function processes arrays if constants are defined
|
|
1764
|
-
expect(typeof result.rules).toBe("string");
|
|
1765
|
-
expect(result.sourcesPath).toContain("./");
|
|
1766
|
-
|
|
1767
|
-
// Target audience should be processed
|
|
1768
|
-
if (result.targetAudience) {
|
|
1769
|
-
expect(typeof result.targetAudience).toBe("string");
|
|
1770
|
-
}
|
|
1771
|
-
});
|
|
1772
|
-
|
|
1773
|
-
test("resolveFileReferences should handle unsupported file extensions", async () => {
|
|
1774
|
-
const unsupportedFile = path.join(tempDir, "test.exe");
|
|
1775
|
-
writeFileSync(unsupportedFile, "binary content");
|
|
1776
|
-
|
|
1777
|
-
const config = { file: `@${unsupportedFile}` };
|
|
1778
|
-
const result = await resolveFileReferences(config);
|
|
1779
|
-
|
|
1780
|
-
// Should return original reference for unsupported file type
|
|
1781
|
-
expect(result.file).toBe(`@${unsupportedFile}`);
|
|
1782
|
-
});
|
|
1783
|
-
|
|
1784
|
-
test("resolveFileReferences should handle JSON parsing errors", async () => {
|
|
1785
|
-
const malformedJsonFile = path.join(tempDir, "malformed.json");
|
|
1786
|
-
writeFileSync(malformedJsonFile, '{"key": value without quotes}');
|
|
1787
|
-
|
|
1788
|
-
const config = { file: `@${malformedJsonFile}` };
|
|
1789
|
-
const result = await resolveFileReferences(config);
|
|
1790
|
-
|
|
1791
|
-
// Should return raw content when JSON parsing fails
|
|
1792
|
-
expect(result.file).toBe('{"key": value without quotes}');
|
|
1793
|
-
});
|
|
1794
|
-
|
|
1795
|
-
test("resolveFileReferences should handle YAML parsing errors", async () => {
|
|
1796
|
-
const malformedYamlFile = path.join(tempDir, "malformed.yaml");
|
|
1797
|
-
writeFileSync(malformedYamlFile, "key: value\n invalid: indentation: error");
|
|
1798
|
-
|
|
1799
|
-
const config = { file: `@${malformedYamlFile}` };
|
|
1800
|
-
const result = await resolveFileReferences(config);
|
|
1801
|
-
|
|
1802
|
-
// Should return raw content when YAML parsing fails
|
|
1803
|
-
expect(result.file).toBe("key: value\n invalid: indentation: error");
|
|
1804
|
-
});
|
|
1805
|
-
|
|
1806
|
-
test("resolveFileReferences should handle absolute file paths", async () => {
|
|
1807
|
-
const absoluteFile = path.join(tempDir, "absolute.txt");
|
|
1808
|
-
writeFileSync(absoluteFile, "absolute path content");
|
|
1809
|
-
|
|
1810
|
-
const config = { file: `@${absoluteFile}` };
|
|
1811
|
-
const result = await resolveFileReferences(config, "/different/base/path");
|
|
1812
|
-
|
|
1813
|
-
// Should work with absolute path regardless of basePath
|
|
1814
|
-
expect(result.file).toBe("absolute path content");
|
|
1815
|
-
});
|
|
1816
|
-
|
|
1817
|
-
test("resolveFileReferences should handle file read errors gracefully", async () => {
|
|
1818
|
-
const config = { file: "@/root/protected/file.txt" };
|
|
1819
|
-
const result = await resolveFileReferences(config);
|
|
1820
|
-
|
|
1821
|
-
// Should return original reference when file read fails
|
|
1822
|
-
expect(result.file).toBe("@/root/protected/file.txt");
|
|
1823
|
-
});
|
|
1824
|
-
|
|
1825
|
-
test("processConfigFields should handle existing target audience with new audience types", () => {
|
|
1826
|
-
const config = {
|
|
1827
|
-
targetAudience: "Existing audience description",
|
|
1828
|
-
targetAudienceTypes: ["developers"],
|
|
1829
|
-
};
|
|
1830
|
-
|
|
1831
|
-
const result = processConfigFields(config);
|
|
1832
|
-
|
|
1833
|
-
if (result.targetAudience) {
|
|
1834
|
-
expect(result.targetAudience).toContain("Existing audience description");
|
|
1835
|
-
}
|
|
1836
|
-
});
|
|
1837
|
-
|
|
1838
|
-
test("getAvailablePaths should handle permission errors gracefully", () => {
|
|
1839
|
-
const originalWarn = console.warn;
|
|
1840
|
-
const warnMessages = [];
|
|
1841
|
-
console.warn = (message) => warnMessages.push(message);
|
|
1842
|
-
|
|
1843
|
-
try {
|
|
1844
|
-
const result = getAvailablePaths("/root/protected");
|
|
1845
|
-
expect(Array.isArray(result)).toBe(true);
|
|
1846
|
-
// Should handle permission errors gracefully
|
|
1847
|
-
} finally {
|
|
1848
|
-
console.warn = originalWarn;
|
|
1849
|
-
}
|
|
1850
|
-
});
|
|
1851
|
-
|
|
1852
|
-
test("processConfigFields should handle reader knowledge level content", () => {
|
|
1853
|
-
const config = {
|
|
1854
|
-
readerKnowledgeLevel: "domainFamiliar",
|
|
1855
|
-
};
|
|
1856
|
-
|
|
1857
|
-
const result = processConfigFields(config);
|
|
1858
|
-
|
|
1859
|
-
if (result.readerKnowledgeContent) {
|
|
1860
|
-
expect(typeof result.readerKnowledgeContent).toBe("string");
|
|
1861
|
-
}
|
|
1862
|
-
});
|
|
1863
|
-
|
|
1864
|
-
test("processConfigFields should handle documentation depth content", () => {
|
|
1865
|
-
const config = {
|
|
1866
|
-
documentationDepth: "comprehensive",
|
|
1867
|
-
};
|
|
1868
|
-
|
|
1869
|
-
const result = processConfigFields(config);
|
|
1870
|
-
|
|
1871
|
-
if (result.documentationDepthContent) {
|
|
1872
|
-
expect(typeof result.documentationDepthContent).toBe("string");
|
|
1873
|
-
}
|
|
1874
|
-
});
|
|
1875
|
-
|
|
1876
|
-
test("getProjectInfo should handle git repository without GitHub", async () => {
|
|
1877
|
-
// Mock execSync to return non-GitHub remote
|
|
1878
|
-
const originalWarn = console.warn;
|
|
1879
|
-
console.warn = () => {}; // Suppress warnings
|
|
1880
|
-
|
|
1881
|
-
try {
|
|
1882
|
-
const result = await getProjectInfo();
|
|
1883
|
-
expect(typeof result).toBe("object");
|
|
1884
|
-
expect(result).toHaveProperty("fromGitHub");
|
|
1885
|
-
expect(typeof result.fromGitHub).toBe("boolean");
|
|
1886
|
-
} finally {
|
|
1887
|
-
console.warn = originalWarn;
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
test("getProjectInfo should handle no git repository", async () => {
|
|
1892
|
-
const originalWarn = console.warn;
|
|
1893
|
-
const warnMessages = [];
|
|
1894
|
-
console.warn = (message) => warnMessages.push(message);
|
|
1895
|
-
|
|
1896
|
-
const originalCwd = process.cwd();
|
|
1897
|
-
const nonGitDir = path.join(tempDir, "no-git");
|
|
1898
|
-
mkdirSync(nonGitDir, { recursive: true });
|
|
1899
|
-
|
|
1900
|
-
try {
|
|
1901
|
-
process.chdir(nonGitDir);
|
|
1902
|
-
const result = await getProjectInfo();
|
|
1903
|
-
|
|
1904
|
-
expect(typeof result).toBe("object");
|
|
1905
|
-
expect(result.fromGitHub).toBe(false);
|
|
1906
|
-
expect(warnMessages.some((msg) => msg.includes("No git repository found"))).toBe(true);
|
|
1907
|
-
} finally {
|
|
1908
|
-
process.chdir(originalCwd);
|
|
1909
|
-
console.warn = originalWarn;
|
|
1910
|
-
}
|
|
1911
|
-
});
|
|
1912
|
-
|
|
1913
|
-
test("saveValueToConfig should handle write errors gracefully", async () => {
|
|
1914
|
-
const originalWarn = console.warn;
|
|
1915
|
-
const warnMessages = [];
|
|
1916
|
-
console.warn = (message) => warnMessages.push(message);
|
|
1917
|
-
|
|
1918
|
-
const originalCwd = process.cwd;
|
|
1919
|
-
process.cwd = () => "/root"; // Read-only directory
|
|
1920
|
-
|
|
1921
|
-
try {
|
|
1922
|
-
await saveValueToConfig("testKey", "testValue");
|
|
1923
|
-
// Should handle error gracefully and log warning
|
|
1924
|
-
expect(
|
|
1925
|
-
warnMessages.some((msg) => msg.includes("Failed to save testKey to config.yaml:")),
|
|
1926
|
-
).toBe(true);
|
|
1927
|
-
} finally {
|
|
1928
|
-
process.cwd = originalCwd;
|
|
1929
|
-
console.warn = originalWarn;
|
|
1930
|
-
}
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
test("validatePath should handle access permission errors", () => {
|
|
1934
|
-
// Test with a path that exists but might not be accessible
|
|
1935
|
-
const result = validatePath("/root");
|
|
1936
|
-
// Should handle gracefully regardless of access permissions
|
|
1937
|
-
expect(result).toHaveProperty("isValid");
|
|
1938
|
-
expect(result).toHaveProperty("error");
|
|
1939
|
-
});
|
|
1940
|
-
|
|
1941
|
-
test("getAvailablePaths should handle directory read errors", () => {
|
|
1942
|
-
const originalWarn = console.warn;
|
|
1943
|
-
const warnMessages = [];
|
|
1944
|
-
console.warn = (message) => warnMessages.push(message);
|
|
1945
|
-
|
|
1946
|
-
try {
|
|
1947
|
-
// Test with a problematic path that might cause read errors
|
|
1948
|
-
const result = getAvailablePaths("/proc/nonexistent");
|
|
1949
|
-
expect(Array.isArray(result)).toBe(true);
|
|
1950
|
-
// May or may not log warnings depending on system
|
|
1951
|
-
} finally {
|
|
1952
|
-
console.warn = originalWarn;
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
1955
|
-
|
|
1956
|
-
test("saveValueToConfig should handle array end detection edge cases", async () => {
|
|
1957
|
-
const testDir = path.join(tempDir, "array-edge-test");
|
|
1958
|
-
mkdirSync(testDir, { recursive: true });
|
|
1959
|
-
|
|
1960
|
-
const originalCwd = process.cwd;
|
|
1961
|
-
process.cwd = () => testDir;
|
|
1962
|
-
|
|
1963
|
-
try {
|
|
1964
|
-
// Create config with array that has inline start and complex structure
|
|
1965
|
-
const aigneDir = path.join(testDir, ".aigne", "doc-smith");
|
|
1966
|
-
mkdirSync(aigneDir, { recursive: true });
|
|
1967
|
-
|
|
1968
|
-
// Test case 1: Array with inline start
|
|
1969
|
-
writeFileSync(
|
|
1970
|
-
path.join(aigneDir, "config.yaml"),
|
|
1971
|
-
"testArray: [item1, item2]\notherKey: value\n",
|
|
1972
|
-
);
|
|
1973
|
-
|
|
1974
|
-
await saveValueToConfig("testArray", ["new1", "new2"]);
|
|
1975
|
-
|
|
1976
|
-
let configContent = readFileSync(path.join(aigneDir, "config.yaml"), "utf8");
|
|
1977
|
-
expect(configContent).toContain("- new1");
|
|
1978
|
-
expect(configContent).toContain("- new2");
|
|
1979
|
-
|
|
1980
|
-
// Test case 2: Array at end of file without trailing newline
|
|
1981
|
-
writeFileSync(
|
|
1982
|
-
path.join(aigneDir, "config.yaml"),
|
|
1983
|
-
"otherKey: value\ntestArray:\n - item1\n - item2",
|
|
1984
|
-
);
|
|
1985
|
-
|
|
1986
|
-
await saveValueToConfig("testArray", ["final1", "final2"]);
|
|
1987
|
-
|
|
1988
|
-
configContent = readFileSync(path.join(aigneDir, "config.yaml"), "utf8");
|
|
1989
|
-
expect(configContent).toContain("- final1");
|
|
1990
|
-
expect(configContent).toContain("- final2");
|
|
1991
|
-
} finally {
|
|
1992
|
-
process.cwd = originalCwd;
|
|
1993
|
-
}
|
|
1994
|
-
});
|
|
1995
|
-
|
|
1996
|
-
test("getDirectoryContents should handle read errors", () => {
|
|
1997
|
-
const originalWarn = console.warn;
|
|
1998
|
-
const warnMessages = [];
|
|
1999
|
-
console.warn = (message) => warnMessages.push(message);
|
|
2000
|
-
|
|
2001
|
-
try {
|
|
2002
|
-
// Import the internal function - this may not work due to module structure
|
|
2003
|
-
// So we'll test via getAvailablePaths which calls it
|
|
2004
|
-
const result = getAvailablePaths("/root/nonexistent/path");
|
|
2005
|
-
expect(Array.isArray(result)).toBe(true);
|
|
2006
|
-
// May log warnings for directory read errors
|
|
2007
|
-
} finally {
|
|
2008
|
-
console.warn = originalWarn;
|
|
2009
|
-
}
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
test("getGitHubRepoInfo should handle API response errors", async () => {
|
|
2013
|
-
// Mock fetch to return error response
|
|
2014
|
-
const originalFetch = global.fetch;
|
|
2015
|
-
global.fetch = () =>
|
|
2016
|
-
Promise.resolve({
|
|
2017
|
-
ok: false,
|
|
2018
|
-
statusText: "Not Found",
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
const originalWarn = console.warn;
|
|
2022
|
-
const warnMessages = [];
|
|
2023
|
-
console.warn = (message) => warnMessages.push(message);
|
|
2024
|
-
|
|
2025
|
-
try {
|
|
2026
|
-
const result = await getGitHubRepoInfo("https://github.com/user/repo");
|
|
2027
|
-
expect(result).toBe(null);
|
|
2028
|
-
expect(
|
|
2029
|
-
warnMessages.some((msg) => msg.includes("Failed to fetch GitHub repository info:")),
|
|
2030
|
-
).toBe(true);
|
|
2031
|
-
} finally {
|
|
2032
|
-
global.fetch = originalFetch;
|
|
2033
|
-
console.warn = originalWarn;
|
|
2034
|
-
}
|
|
2035
|
-
});
|
|
2036
|
-
|
|
2037
|
-
test("getGitHubRepoInfo should handle fetch errors", async () => {
|
|
2038
|
-
// Mock fetch to throw error
|
|
2039
|
-
const originalFetch = global.fetch;
|
|
2040
|
-
global.fetch = () => Promise.reject(new Error("Network error"));
|
|
2041
|
-
|
|
2042
|
-
const originalWarn = console.warn;
|
|
2043
|
-
const warnMessages = [];
|
|
2044
|
-
console.warn = (message) => warnMessages.push(message);
|
|
2045
|
-
|
|
2046
|
-
try {
|
|
2047
|
-
const result = await getGitHubRepoInfo("https://github.com/user/repo");
|
|
2048
|
-
expect(result).toBe(null);
|
|
2049
|
-
expect(
|
|
2050
|
-
warnMessages.some((msg) => msg.includes("Failed to fetch GitHub repository info:")),
|
|
2051
|
-
).toBe(true);
|
|
2052
|
-
} finally {
|
|
2053
|
-
global.fetch = originalFetch;
|
|
2054
|
-
console.warn = originalWarn;
|
|
2055
|
-
}
|
|
2056
|
-
});
|
|
2057
|
-
|
|
2058
|
-
test("resolveFileReferences should handle file read errors", async () => {
|
|
2059
|
-
// Test with file that doesn't exist
|
|
2060
|
-
const config = { file: "@/nonexistent/file.txt" };
|
|
2061
|
-
const result = await resolveFileReferences(config);
|
|
2062
|
-
|
|
2063
|
-
// Should return original reference when file read fails
|
|
2064
|
-
expect(result.file).toBe("@/nonexistent/file.txt");
|
|
2065
|
-
});
|
|
2066
|
-
});
|
|
2067
|
-
});
|