@aigne/doc-smith 0.8.12-beta.8 → 0.8.12-beta.9

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.
Files changed (264) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/agents/publish/index.yaml +4 -0
  3. package/agents/publish/publish-docs.mjs +77 -5
  4. package/agents/publish/translate-meta.mjs +103 -0
  5. package/agents/update/generate-document.yaml +30 -28
  6. package/agents/update/update-document-detail.yaml +3 -1
  7. package/agents/utils/update-branding.mjs +69 -0
  8. package/package.json +16 -2
  9. package/prompts/common/document/role-and-personality.md +3 -1
  10. package/prompts/detail/d2-diagram/guide.md +7 -1
  11. package/prompts/detail/d2-diagram/user-prompt.md +3 -0
  12. package/prompts/detail/generate/system-prompt.md +6 -7
  13. package/prompts/detail/generate/user-prompt.md +12 -3
  14. package/prompts/detail/update/user-prompt.md +0 -2
  15. package/prompts/structure/update/user-prompt.md +0 -4
  16. package/utils/file-utils.mjs +69 -24
  17. package/utils/markdown-checker.mjs +0 -20
  18. package/utils/request.mjs +7 -0
  19. package/utils/upload-files.mjs +231 -0
  20. package/utils/utils.mjs +11 -1
  21. package/.aigne/doc-smith/config.yaml +0 -77
  22. package/.aigne/doc-smith/history.yaml +0 -37
  23. package/.aigne/doc-smith/media-description.yaml +0 -91
  24. package/.aigne/doc-smith/output/structure-plan.json +0 -162
  25. package/.aigne/doc-smith/preferences.yml +0 -97
  26. package/.aigne/doc-smith/upload-cache.yaml +0 -1830
  27. package/.github/PULL_REQUEST_TEMPLATE.md +0 -28
  28. package/.github/workflows/ci.yml +0 -54
  29. package/.github/workflows/create-release-pr.yaml +0 -21
  30. package/.github/workflows/publish-docs.yml +0 -65
  31. package/.github/workflows/release.yml +0 -49
  32. package/.github/workflows/reviewer.yml +0 -54
  33. package/.release-please-manifest.json +0 -3
  34. package/RELEASE.md +0 -9
  35. package/assets/screenshots/doc-complete-setup.png +0 -0
  36. package/assets/screenshots/doc-generate-docs.png +0 -0
  37. package/assets/screenshots/doc-generate.png +0 -0
  38. package/assets/screenshots/doc-generated-successfully.png +0 -0
  39. package/assets/screenshots/doc-publish.png +0 -0
  40. package/assets/screenshots/doc-regenerate.png +0 -0
  41. package/assets/screenshots/doc-translate-langs.png +0 -0
  42. package/assets/screenshots/doc-translate.png +0 -0
  43. package/assets/screenshots/doc-update.png +0 -0
  44. package/biome.json +0 -73
  45. package/codecov.yml +0 -15
  46. package/docs/_sidebar.md +0 -15
  47. package/docs/configuration-initial-setup.ja.md +0 -179
  48. package/docs/configuration-initial-setup.md +0 -198
  49. package/docs/configuration-initial-setup.zh-TW.md +0 -179
  50. package/docs/configuration-initial-setup.zh.md +0 -179
  51. package/docs/configuration-managing-preferences.ja.md +0 -100
  52. package/docs/configuration-managing-preferences.md +0 -100
  53. package/docs/configuration-managing-preferences.zh-TW.md +0 -100
  54. package/docs/configuration-managing-preferences.zh.md +0 -100
  55. package/docs/configuration.ja.md +0 -69
  56. package/docs/configuration.md +0 -69
  57. package/docs/configuration.zh-TW.md +0 -69
  58. package/docs/configuration.zh.md +0 -69
  59. package/docs/getting-started.ja.md +0 -107
  60. package/docs/getting-started.md +0 -107
  61. package/docs/getting-started.zh-TW.md +0 -107
  62. package/docs/getting-started.zh.md +0 -107
  63. package/docs/guides-cleaning-up.ja.md +0 -51
  64. package/docs/guides-cleaning-up.md +0 -52
  65. package/docs/guides-cleaning-up.zh-TW.md +0 -51
  66. package/docs/guides-cleaning-up.zh.md +0 -51
  67. package/docs/guides-evaluating-documents.ja.md +0 -66
  68. package/docs/guides-evaluating-documents.md +0 -107
  69. package/docs/guides-evaluating-documents.zh-TW.md +0 -66
  70. package/docs/guides-evaluating-documents.zh.md +0 -66
  71. package/docs/guides-generating-documentation.ja.md +0 -151
  72. package/docs/guides-generating-documentation.md +0 -89
  73. package/docs/guides-generating-documentation.zh-TW.md +0 -151
  74. package/docs/guides-generating-documentation.zh.md +0 -151
  75. package/docs/guides-interactive-chat.ja.md +0 -85
  76. package/docs/guides-interactive-chat.md +0 -93
  77. package/docs/guides-interactive-chat.zh-TW.md +0 -85
  78. package/docs/guides-interactive-chat.zh.md +0 -85
  79. package/docs/guides-managing-history.ja.md +0 -48
  80. package/docs/guides-managing-history.md +0 -53
  81. package/docs/guides-managing-history.zh-TW.md +0 -48
  82. package/docs/guides-managing-history.zh.md +0 -48
  83. package/docs/guides-publishing-your-docs.ja.md +0 -78
  84. package/docs/guides-publishing-your-docs.md +0 -83
  85. package/docs/guides-publishing-your-docs.zh-TW.md +0 -78
  86. package/docs/guides-publishing-your-docs.zh.md +0 -78
  87. package/docs/guides-translating-documentation.ja.md +0 -95
  88. package/docs/guides-translating-documentation.md +0 -100
  89. package/docs/guides-translating-documentation.zh-TW.md +0 -95
  90. package/docs/guides-translating-documentation.zh.md +0 -95
  91. package/docs/guides-updating-documentation.ja.md +0 -77
  92. package/docs/guides-updating-documentation.md +0 -79
  93. package/docs/guides-updating-documentation.zh-TW.md +0 -77
  94. package/docs/guides-updating-documentation.zh.md +0 -77
  95. package/docs/guides.ja.md +0 -32
  96. package/docs/guides.md +0 -32
  97. package/docs/guides.zh-TW.md +0 -32
  98. package/docs/guides.zh.md +0 -32
  99. package/docs/overview.ja.md +0 -61
  100. package/docs/overview.md +0 -61
  101. package/docs/overview.zh-TW.md +0 -61
  102. package/docs/overview.zh.md +0 -61
  103. package/docs/release-notes.ja.md +0 -255
  104. package/docs/release-notes.md +0 -288
  105. package/docs/release-notes.zh-TW.md +0 -255
  106. package/docs/release-notes.zh.md +0 -255
  107. package/prompts/common/afs/afs-tools-usage.md +0 -5
  108. package/prompts/common/afs/use-afs-instruction.md +0 -1
  109. package/release-please-config.json +0 -14
  110. package/tests/agents/chat/chat.test.mjs +0 -46
  111. package/tests/agents/clear/choose-contents.test.mjs +0 -284
  112. package/tests/agents/clear/clear-auth-tokens.test.mjs +0 -268
  113. package/tests/agents/clear/clear-document-config.test.mjs +0 -167
  114. package/tests/agents/clear/clear-document-structure.test.mjs +0 -380
  115. package/tests/agents/clear/clear-generated-docs.test.mjs +0 -222
  116. package/tests/agents/evaluate/code-snippet.test.mjs +0 -163
  117. package/tests/agents/evaluate/fixtures/api-services.md +0 -87
  118. package/tests/agents/evaluate/fixtures/js-sdk.md +0 -94
  119. package/tests/agents/evaluate/generate-report.test.mjs +0 -312
  120. package/tests/agents/generate/check-document-structure.test.mjs +0 -45
  121. package/tests/agents/generate/check-need-generate-structure.test.mjs +0 -279
  122. package/tests/agents/generate/document-structure-tools/add-document.test.mjs +0 -449
  123. package/tests/agents/generate/document-structure-tools/delete-document.test.mjs +0 -410
  124. package/tests/agents/generate/document-structure-tools/generate-sub-structure.test.mjs +0 -277
  125. package/tests/agents/generate/document-structure-tools/move-document.test.mjs +0 -476
  126. package/tests/agents/generate/document-structure-tools/update-document.test.mjs +0 -548
  127. package/tests/agents/generate/generate-structure.test.mjs +0 -45
  128. package/tests/agents/generate/user-review-document-structure.test.mjs +0 -319
  129. package/tests/agents/history/view.test.mjs +0 -97
  130. package/tests/agents/init/init.test.mjs +0 -1657
  131. package/tests/agents/prefs/prefs.test.mjs +0 -431
  132. package/tests/agents/publish/publish-docs.test.mjs +0 -787
  133. package/tests/agents/translate/choose-language.test.mjs +0 -311
  134. package/tests/agents/translate/translate-document.test.mjs +0 -51
  135. package/tests/agents/update/check-document.test.mjs +0 -463
  136. package/tests/agents/update/check-update-is-single.test.mjs +0 -300
  137. package/tests/agents/update/document-tools/update-document-content.test.mjs +0 -329
  138. package/tests/agents/update/generate-document.test.mjs +0 -51
  139. package/tests/agents/update/save-and-translate-document.test.mjs +0 -369
  140. package/tests/agents/update/user-review-document.test.mjs +0 -582
  141. package/tests/agents/utils/action-success.test.mjs +0 -54
  142. package/tests/agents/utils/check-detail-result.test.mjs +0 -743
  143. package/tests/agents/utils/check-feedback-refiner.test.mjs +0 -478
  144. package/tests/agents/utils/choose-docs.test.mjs +0 -406
  145. package/tests/agents/utils/exit.test.mjs +0 -70
  146. package/tests/agents/utils/feedback-refiner.test.mjs +0 -51
  147. package/tests/agents/utils/find-item-by-path.test.mjs +0 -517
  148. package/tests/agents/utils/find-user-preferences-by-path.test.mjs +0 -382
  149. package/tests/agents/utils/format-document-structure.test.mjs +0 -364
  150. package/tests/agents/utils/fs.test.mjs +0 -267
  151. package/tests/agents/utils/load-sources.test.mjs +0 -1470
  152. package/tests/agents/utils/save-docs.test.mjs +0 -109
  153. package/tests/agents/utils/save-output.test.mjs +0 -315
  154. package/tests/agents/utils/save-single-doc.test.mjs +0 -364
  155. package/tests/agents/utils/transform-detail-datasources.test.mjs +0 -320
  156. package/tests/utils/auth-utils.test.mjs +0 -596
  157. package/tests/utils/blocklet.test.mjs +0 -336
  158. package/tests/utils/conflict-detector.test.mjs +0 -355
  159. package/tests/utils/constants.test.mjs +0 -295
  160. package/tests/utils/d2-utils.test.mjs +0 -437
  161. package/tests/utils/deploy.test.mjs +0 -399
  162. package/tests/utils/docs-finder-utils.test.mjs +0 -650
  163. package/tests/utils/file-utils.test.mjs +0 -521
  164. package/tests/utils/history-utils.test.mjs +0 -206
  165. package/tests/utils/kroki-utils.test.mjs +0 -646
  166. package/tests/utils/linter/fixtures/css/keyword-error.css +0 -1
  167. package/tests/utils/linter/fixtures/css/missing-semicolon.css +0 -1
  168. package/tests/utils/linter/fixtures/css/syntax-error.css +0 -1
  169. package/tests/utils/linter/fixtures/css/undeclare-variable.css +0 -1
  170. package/tests/utils/linter/fixtures/css/unused-variable.css +0 -2
  171. package/tests/utils/linter/fixtures/css/valid-code.css +0 -1
  172. package/tests/utils/linter/fixtures/dockerfile/keyword-error.dockerfile +0 -1
  173. package/tests/utils/linter/fixtures/dockerfile/missing-semicolon.dockerfile +0 -2
  174. package/tests/utils/linter/fixtures/dockerfile/syntax-error.dockerfile +0 -2
  175. package/tests/utils/linter/fixtures/dockerfile/undeclare-variable.dockerfile +0 -1
  176. package/tests/utils/linter/fixtures/dockerfile/unused-variable.dockerfile +0 -1
  177. package/tests/utils/linter/fixtures/dockerfile/valid-code.dockerfile +0 -2
  178. package/tests/utils/linter/fixtures/go/keyword-error.go +0 -5
  179. package/tests/utils/linter/fixtures/go/missing-semicolon.go +0 -5
  180. package/tests/utils/linter/fixtures/go/syntax-error.go +0 -6
  181. package/tests/utils/linter/fixtures/go/undeclare-variable.go +0 -5
  182. package/tests/utils/linter/fixtures/go/unused-variable.go +0 -5
  183. package/tests/utils/linter/fixtures/go/valid-code.go +0 -7
  184. package/tests/utils/linter/fixtures/js/keyword-error.js +0 -3
  185. package/tests/utils/linter/fixtures/js/missing-semicolon.js +0 -6
  186. package/tests/utils/linter/fixtures/js/syntax-error.js +0 -4
  187. package/tests/utils/linter/fixtures/js/undeclare-variable.js +0 -3
  188. package/tests/utils/linter/fixtures/js/unused-variable.js +0 -7
  189. package/tests/utils/linter/fixtures/js/valid-code.js +0 -15
  190. package/tests/utils/linter/fixtures/json/keyword-error.json +0 -1
  191. package/tests/utils/linter/fixtures/json/missing-semicolon.json +0 -1
  192. package/tests/utils/linter/fixtures/json/syntax-error.json +0 -1
  193. package/tests/utils/linter/fixtures/json/undeclare-variable.json +0 -1
  194. package/tests/utils/linter/fixtures/json/unused-variable.json +0 -1
  195. package/tests/utils/linter/fixtures/json/valid-code.json +0 -1
  196. package/tests/utils/linter/fixtures/jsx/keyword-error.jsx +0 -5
  197. package/tests/utils/linter/fixtures/jsx/missing-semicolon.jsx +0 -5
  198. package/tests/utils/linter/fixtures/jsx/syntax-error.jsx +0 -5
  199. package/tests/utils/linter/fixtures/jsx/undeclare-variable.jsx +0 -5
  200. package/tests/utils/linter/fixtures/jsx/unused-variable.jsx +0 -4
  201. package/tests/utils/linter/fixtures/jsx/valid-code.jsx +0 -5
  202. package/tests/utils/linter/fixtures/python/keyword-error.py +0 -3
  203. package/tests/utils/linter/fixtures/python/missing-semicolon.py +0 -2
  204. package/tests/utils/linter/fixtures/python/syntax-error.py +0 -3
  205. package/tests/utils/linter/fixtures/python/undeclare-variable.py +0 -3
  206. package/tests/utils/linter/fixtures/python/unused-variable.py +0 -6
  207. package/tests/utils/linter/fixtures/python/valid-code.py +0 -12
  208. package/tests/utils/linter/fixtures/ruby/keyword-error.rb +0 -2
  209. package/tests/utils/linter/fixtures/ruby/missing-semicolon.rb +0 -1
  210. package/tests/utils/linter/fixtures/ruby/syntax-error.rb +0 -2
  211. package/tests/utils/linter/fixtures/ruby/undeclare-variable.rb +0 -1
  212. package/tests/utils/linter/fixtures/ruby/unused-variable.rb +0 -2
  213. package/tests/utils/linter/fixtures/ruby/valid-code.rb +0 -1
  214. package/tests/utils/linter/fixtures/sass/keyword-error.sass +0 -2
  215. package/tests/utils/linter/fixtures/sass/missing-semicolon.sass +0 -3
  216. package/tests/utils/linter/fixtures/sass/syntax-error.sass +0 -3
  217. package/tests/utils/linter/fixtures/sass/undeclare-variable.sass +0 -2
  218. package/tests/utils/linter/fixtures/sass/unused-variable.sass +0 -4
  219. package/tests/utils/linter/fixtures/sass/valid-code.sass +0 -2
  220. package/tests/utils/linter/fixtures/scss/keyword-error.scss +0 -1
  221. package/tests/utils/linter/fixtures/scss/missing-semicolon.scss +0 -1
  222. package/tests/utils/linter/fixtures/scss/syntax-error.scss +0 -1
  223. package/tests/utils/linter/fixtures/scss/undeclare-variable.scss +0 -1
  224. package/tests/utils/linter/fixtures/scss/unused-variable.scss +0 -2
  225. package/tests/utils/linter/fixtures/scss/valid-code.scss +0 -1
  226. package/tests/utils/linter/fixtures/shell/keyword-error.sh +0 -5
  227. package/tests/utils/linter/fixtures/shell/missing-semicolon.sh +0 -3
  228. package/tests/utils/linter/fixtures/shell/syntax-error.sh +0 -4
  229. package/tests/utils/linter/fixtures/shell/undeclare-variable.sh +0 -3
  230. package/tests/utils/linter/fixtures/shell/unused-variable.sh +0 -4
  231. package/tests/utils/linter/fixtures/shell/valid-code.sh +0 -3
  232. package/tests/utils/linter/fixtures/ts/keyword-error.ts +0 -1
  233. package/tests/utils/linter/fixtures/ts/missing-semicolon.ts +0 -1
  234. package/tests/utils/linter/fixtures/ts/syntax-error.ts +0 -1
  235. package/tests/utils/linter/fixtures/ts/undeclare-variable.ts +0 -1
  236. package/tests/utils/linter/fixtures/ts/unused-variable.ts +0 -3
  237. package/tests/utils/linter/fixtures/ts/valid-code.ts +0 -3
  238. package/tests/utils/linter/fixtures/tsx/keyword-error.tsx +0 -5
  239. package/tests/utils/linter/fixtures/tsx/missing-semicolon.tsx +0 -5
  240. package/tests/utils/linter/fixtures/tsx/syntax-error.tsx +0 -5
  241. package/tests/utils/linter/fixtures/tsx/undeclare-variable.tsx +0 -6
  242. package/tests/utils/linter/fixtures/tsx/unused-variable.tsx +0 -6
  243. package/tests/utils/linter/fixtures/tsx/valid-code.tsx +0 -5
  244. package/tests/utils/linter/fixtures/vue/keyword-error.vue +0 -6
  245. package/tests/utils/linter/fixtures/vue/missing-semicolon.vue +0 -6
  246. package/tests/utils/linter/fixtures/vue/syntax-error.vue +0 -6
  247. package/tests/utils/linter/fixtures/vue/undeclare-variable.vue +0 -6
  248. package/tests/utils/linter/fixtures/vue/unused-variable.vue +0 -7
  249. package/tests/utils/linter/fixtures/vue/valid-code.vue +0 -6
  250. package/tests/utils/linter/fixtures/yaml/keyword-error.yml +0 -1
  251. package/tests/utils/linter/fixtures/yaml/missing-semicolon.yml +0 -2
  252. package/tests/utils/linter/fixtures/yaml/syntax-error.yml +0 -1
  253. package/tests/utils/linter/fixtures/yaml/undeclare-variable.yml +0 -1
  254. package/tests/utils/linter/fixtures/yaml/unused-variable.yml +0 -2
  255. package/tests/utils/linter/fixtures/yaml/valid-code.yml +0 -3
  256. package/tests/utils/linter/index.test.mjs +0 -440
  257. package/tests/utils/linter/scan-results.mjs +0 -42
  258. package/tests/utils/load-config.test.mjs +0 -141
  259. package/tests/utils/markdown/index.test.mjs +0 -478
  260. package/tests/utils/mermaid-validator.test.mjs +0 -541
  261. package/tests/utils/mock-chat-model.mjs +0 -12
  262. package/tests/utils/preferences-utils.test.mjs +0 -465
  263. package/tests/utils/save-value-to-config.test.mjs +0 -483
  264. package/tests/utils/utils.test.mjs +0 -941
@@ -1,941 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
- import * as childProcess from "node:child_process";
3
- import * as fs from "node:fs";
4
- import fsPromisesDefault, * as fsPromises from "node:fs/promises";
5
- import {
6
- detectSystemLanguage,
7
- getAvailablePaths,
8
- getContentHash,
9
- getCurrentGitHead,
10
- getGitHubRepoInfo,
11
- getGithubRepoUrl,
12
- getModifiedFilesBetweenCommits,
13
- getProjectInfo,
14
- hasFileChangesBetweenCommits,
15
- hasSourceFilesChanged,
16
- isGlobPattern,
17
- loadConfigFromFile,
18
- normalizePath,
19
- processConfigFields,
20
- processContent,
21
- resolveFileReferences,
22
- saveDocWithTranslations,
23
- saveGitHeadToConfig,
24
- saveValueToConfig,
25
- toRelativePath,
26
- validatePath,
27
- validatePaths,
28
- } from "../../utils/utils.mjs";
29
-
30
- describe("utils", () => {
31
- let originalEnv;
32
- let execSyncSpy;
33
- let existsSyncSpy;
34
- let mkdirSyncSpy;
35
- let readdirSyncSpy;
36
- let statSyncSpy;
37
- let accessSyncSpy;
38
- let readFileSpy;
39
- let writeFileSpy;
40
- let mkdirSpy;
41
- let consoleSpy;
42
- let processSpies;
43
- let fetchSpy;
44
-
45
- beforeEach(() => {
46
- originalEnv = { ...process.env };
47
-
48
- // Mock child_process
49
- execSyncSpy = spyOn(childProcess, "execSync").mockReturnValue("mocked-hash");
50
-
51
- // Mock fs sync operations
52
- existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(true);
53
- mkdirSyncSpy = spyOn(fs, "mkdirSync").mockImplementation(() => {});
54
- readdirSyncSpy = spyOn(fs, "readdirSync").mockReturnValue([]);
55
- statSyncSpy = spyOn(fs, "statSync").mockReturnValue({
56
- isDirectory: () => false,
57
- });
58
- accessSyncSpy = spyOn(fs, "accessSync").mockImplementation(() => {});
59
-
60
- // Mock fs async operations
61
- readFileSpy = spyOn(fsPromises, "readFile").mockResolvedValue("test content");
62
- writeFileSpy = spyOn(fsPromises, "writeFile").mockResolvedValue();
63
- mkdirSpy = spyOn(fsPromises, "mkdir").mockResolvedValue();
64
-
65
- // Also mock the default import that utils.mjs uses
66
- spyOn(fsPromisesDefault, "writeFile").mockResolvedValue();
67
- spyOn(fsPromisesDefault, "mkdir").mockResolvedValue();
68
-
69
- // Mock console
70
- consoleSpy = spyOn(console, "warn").mockImplementation(() => {});
71
-
72
- // Mock process methods
73
- processSpies = {
74
- cwd: spyOn(process, "cwd").mockReturnValue("/mock/cwd"),
75
- };
76
-
77
- // Mock global fetch
78
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
79
- ok: true,
80
- json: () => Promise.resolve({ name: "test-repo", description: "Test repo" }),
81
- statusText: "OK",
82
- });
83
- });
84
-
85
- afterEach(() => {
86
- // Restore environment
87
- process.env = originalEnv;
88
-
89
- // Restore all spies
90
- execSyncSpy?.mockRestore();
91
- existsSyncSpy?.mockRestore();
92
- mkdirSyncSpy?.mockRestore();
93
- readdirSyncSpy?.mockRestore();
94
- statSyncSpy?.mockRestore();
95
- accessSyncSpy?.mockRestore();
96
- readFileSpy?.mockRestore();
97
- writeFileSpy?.mockRestore();
98
- mkdirSpy?.mockRestore();
99
- consoleSpy?.mockRestore();
100
- fetchSpy?.mockRestore();
101
-
102
- // Restore process spies - important for isolation between test files
103
- Object.values(processSpies).forEach((spy) => {
104
- spy?.mockRestore();
105
- });
106
-
107
- // Reset process spies object to ensure clean state
108
- processSpies = {};
109
- });
110
-
111
- // PATH FUNCTIONS TESTS
112
- describe("normalizePath", () => {
113
- test("should return absolute path if already absolute", () => {
114
- const absolutePath = "/home/user/project";
115
- const result = normalizePath(absolutePath);
116
- expect(result).toBe(absolutePath);
117
- });
118
-
119
- test("should convert relative path to absolute", () => {
120
- processSpies.cwd.mockReturnValue("/home/user");
121
- const relativePath = "./project";
122
- const result = normalizePath(relativePath);
123
- expect(result).toBe("/home/user/project");
124
- });
125
-
126
- test("should handle complex relative paths", () => {
127
- processSpies.cwd.mockReturnValue("/home/user");
128
- const complexPath = "../other/project";
129
- const result = normalizePath(complexPath);
130
- expect(result).toBe("/home/other/project");
131
- });
132
- });
133
-
134
- describe("toRelativePath", () => {
135
- test("should convert absolute path to relative", () => {
136
- processSpies.cwd.mockReturnValue("/home/user");
137
- const absolutePath = "/home/user/project/file.md";
138
- const result = toRelativePath(absolutePath);
139
- expect(result).toBe("project/file.md");
140
- });
141
-
142
- test("should return relative path unchanged", () => {
143
- const relativePath = "./project/file.md";
144
- const result = toRelativePath(relativePath);
145
- expect(result).toBe(relativePath);
146
- });
147
- });
148
-
149
- describe("isGlobPattern", () => {
150
- test("should detect glob patterns", () => {
151
- expect(isGlobPattern("*.js")).toBe(true);
152
- expect(isGlobPattern("**/*.md")).toBe(true);
153
- expect(isGlobPattern("file?.txt")).toBe(true);
154
- expect(isGlobPattern("files[0-9].txt")).toBe(true);
155
- });
156
-
157
- test("should return false for non-glob patterns", () => {
158
- expect(isGlobPattern("file.txt")).toBe(false);
159
- expect(isGlobPattern("path/to/file")).toBe(false);
160
- expect(isGlobPattern("")).toBe(false);
161
- });
162
-
163
- test("should handle null/undefined input", () => {
164
- expect(isGlobPattern(null)).toBe(false);
165
- expect(isGlobPattern(undefined)).toBe(false);
166
- });
167
- });
168
-
169
- // CONTENT PROCESSING TESTS
170
- describe("processContent", () => {
171
- test("should convert relative links to flattened format", () => {
172
- const content = "Check out [API Guide](/api/guide) for details.";
173
- const result = processContent({ content });
174
- expect(result).toBe("Check out [API Guide](./api-guide.md) for details.");
175
- });
176
-
177
- test("should preserve external links", () => {
178
- const content = "Visit [Google](https://google.com) or [Email](mailto:test@example.com).";
179
- const result = processContent({ content });
180
- expect(result).toBe(content);
181
- });
182
-
183
- test("should preserve anchors", () => {
184
- const content = "See [Section](/guide#section) for details.";
185
- const result = processContent({ content });
186
- expect(result).toBe("See [Section](./guide.md#section) for details.");
187
- });
188
-
189
- test("should skip images", () => {
190
- const content = "![Image](/path/to/image) and [Link](/path/to/file)";
191
- const result = processContent({ content });
192
- expect(result).toBe("![Image](/path/to/image) and [Link](./path-to-file.md)");
193
- });
194
-
195
- test("should handle links with existing extensions", () => {
196
- const content = "Download [file](/downloads/file.pdf) here.";
197
- const result = processContent({ content });
198
- expect(result).toBe(content);
199
- });
200
-
201
- test("should handle dot-prefixed paths", () => {
202
- const content = "See [Guide](./guide/intro) for basics.";
203
- const result = processContent({ content });
204
- expect(result).toBe("See [Guide](./guide-intro.md) for basics.");
205
- });
206
- });
207
-
208
- // UTILITY FUNCTIONS TESTS
209
- describe("getContentHash", () => {
210
- test("should generate consistent hash for same content", () => {
211
- const content = "test content";
212
- const hash1 = getContentHash(content);
213
- const hash2 = getContentHash(content);
214
- expect(hash1).toBe(hash2);
215
- expect(typeof hash1).toBe("string");
216
- expect(hash1.length).toBe(64); // SHA256 hex length
217
- });
218
-
219
- test("should trim content by default", () => {
220
- const content = " test content ";
221
- const trimmedHash = getContentHash(content);
222
- const directHash = getContentHash("test content");
223
- expect(trimmedHash).toBe(directHash);
224
- });
225
-
226
- test("should not trim when trim=false", () => {
227
- const content = " test content ";
228
- const noTrimHash = getContentHash(content, { trim: false });
229
- const directHash = getContentHash("test content");
230
- expect(noTrimHash).not.toBe(directHash);
231
- });
232
-
233
- test("should handle different content types", () => {
234
- expect(getContentHash("")).toBe(getContentHash(""));
235
- expect(getContentHash("a")).not.toBe(getContentHash("b"));
236
- });
237
- });
238
-
239
- describe("detectSystemLanguage", () => {
240
- test("should detect language from LANG environment variable", () => {
241
- process.env.LANG = "zh_CN.UTF-8";
242
- const result = detectSystemLanguage();
243
- expect(result).toBe("zh");
244
- });
245
-
246
- test("should detect Traditional Chinese when zh is not in supported languages", () => {
247
- // Mock a scenario where "zh" is not found in SUPPORTED_LANGUAGES
248
- // but the system has zh_TW locale
249
- process.env.LANG = "zh_TW.UTF-8";
250
- const result = detectSystemLanguage();
251
- // The function will find "zh" in SUPPORTED_LANGUAGES and return it
252
- expect(result).toBe("zh");
253
- });
254
-
255
- test("should fall back to English for unsupported languages", () => {
256
- process.env.LANG = "unknown_LANG.UTF-8";
257
- const result = detectSystemLanguage();
258
- expect(result).toBe("en");
259
- });
260
-
261
- test("should handle missing environment variables", () => {
262
- delete process.env.LANG;
263
- delete process.env.LANGUAGE;
264
- delete process.env.LC_ALL;
265
- const result = detectSystemLanguage();
266
- expect(result).toBe("en");
267
- });
268
-
269
- test("should detect from LANGUAGE variable", () => {
270
- delete process.env.LANG;
271
- process.env.LANGUAGE = "fr_FR";
272
- const result = detectSystemLanguage();
273
- expect(result).toBe("fr");
274
- });
275
- });
276
-
277
- // GIT OPERATIONS TESTS
278
- describe("getCurrentGitHead", () => {
279
- test("should return git HEAD commit hash", () => {
280
- execSyncSpy.mockReturnValue("abc123def456\n");
281
- const result = getCurrentGitHead();
282
- expect(result).toBe("abc123def456");
283
- expect(execSyncSpy).toHaveBeenCalledWith("git rev-parse HEAD", {
284
- encoding: "utf8",
285
- stdio: ["pipe", "pipe", "ignore"],
286
- });
287
- });
288
-
289
- test("should return null and warn on git error", () => {
290
- execSyncSpy.mockImplementation(() => {
291
- throw new Error("Not a git repository");
292
- });
293
- const result = getCurrentGitHead();
294
- expect(result).toBeNull();
295
- expect(consoleSpy).toHaveBeenCalledWith("Failed to get git HEAD:", "Not a git repository");
296
- });
297
- });
298
-
299
- describe("getGithubRepoUrl", () => {
300
- test("should return GitHub URL", () => {
301
- execSyncSpy.mockReturnValue("git@github.com:user/repo.git\n");
302
- const result = getGithubRepoUrl();
303
- expect(result).toBe("git@github.com:user/repo.git");
304
- });
305
-
306
- test("should return empty string for non-GitHub repos", () => {
307
- execSyncSpy.mockReturnValue("git@gitlab.com:user/repo.git\n");
308
- const result = getGithubRepoUrl();
309
- expect(result).toBe("");
310
- });
311
-
312
- test("should return empty string on git error", () => {
313
- execSyncSpy.mockImplementation(() => {
314
- throw new Error("No origin remote");
315
- });
316
- const result = getGithubRepoUrl();
317
- expect(result).toBe("");
318
- });
319
- });
320
-
321
- describe("getModifiedFilesBetweenCommits", () => {
322
- test("should return modified files", () => {
323
- execSyncSpy.mockReturnValue("file1.js\nfile2.md\nfile3.txt\n");
324
- const result = getModifiedFilesBetweenCommits("abc123", "def456");
325
- expect(result).toEqual(["file1.js", "file2.md", "file3.txt"]);
326
- });
327
-
328
- test("should filter files by provided paths", () => {
329
- execSyncSpy.mockReturnValue("file1.js\nfile2.md\nfile3.txt\n");
330
- processSpies.cwd.mockReturnValue("/project");
331
- const result = getModifiedFilesBetweenCommits("abc123", "def456", ["/project/file1.js"]);
332
- expect(result).toEqual(["file1.js"]);
333
- });
334
-
335
- test("should handle git command errors", () => {
336
- execSyncSpy.mockImplementation(() => {
337
- throw new Error("Git command failed");
338
- });
339
- const result = getModifiedFilesBetweenCommits("abc123", "def456");
340
- expect(result).toEqual([]);
341
- expect(consoleSpy).toHaveBeenCalled();
342
- });
343
- });
344
-
345
- describe("hasSourceFilesChanged", () => {
346
- test("should return true when source files are modified", () => {
347
- processSpies.cwd.mockReturnValue("/project");
348
- const sourceIds = ["/project/src/file.js"];
349
- const modifiedFiles = ["src/file.js", "other/file.md"];
350
- const result = hasSourceFilesChanged(sourceIds, modifiedFiles);
351
- expect(result).toBe(true);
352
- });
353
-
354
- test("should return false when no source files are modified", () => {
355
- const sourceIds = ["src/file.js"];
356
- const modifiedFiles = ["other/file.md"];
357
- const result = hasSourceFilesChanged(sourceIds, modifiedFiles);
358
- expect(result).toBe(false);
359
- });
360
-
361
- test("should handle empty arrays", () => {
362
- expect(hasSourceFilesChanged([], ["file.js"])).toBe(false);
363
- expect(hasSourceFilesChanged(["file.js"], [])).toBe(false);
364
- expect(hasSourceFilesChanged([], [])).toBe(false);
365
- });
366
-
367
- test("should handle null/undefined inputs", () => {
368
- expect(hasSourceFilesChanged(null, ["file.js"])).toBe(false);
369
- expect(hasSourceFilesChanged(["file.js"], null)).toBe(false);
370
- });
371
- });
372
-
373
- // VALIDATION TESTS
374
- describe("validatePath", () => {
375
- test("should return valid for existing accessible path", () => {
376
- existsSyncSpy.mockReturnValue(true);
377
- accessSyncSpy.mockImplementation(() => {}); // No error means accessible
378
-
379
- const result = validatePath("/existing/path");
380
-
381
- expect(result.isValid).toBe(true);
382
- expect(result.error).toBeNull();
383
- });
384
-
385
- test("should return invalid for non-existent path", () => {
386
- existsSyncSpy.mockReturnValue(false);
387
-
388
- const result = validatePath("/non/existent");
389
-
390
- expect(result.isValid).toBe(false);
391
- expect(result.error).toBe("Path does not exist: /non/existent");
392
- });
393
-
394
- test("should return invalid for inaccessible path", () => {
395
- existsSyncSpy.mockReturnValue(true);
396
- accessSyncSpy.mockImplementation(() => {
397
- throw new Error("Permission denied");
398
- });
399
-
400
- const result = validatePath("/inaccessible");
401
-
402
- expect(result.isValid).toBe(false);
403
- expect(result.error).toBe("Path is not accessible: /inaccessible");
404
- });
405
-
406
- test("should handle path format errors", () => {
407
- // This is a bit tricky to test without modifying the actual function
408
- const result = validatePath("validpath");
409
- expect(result.isValid).toBe(true); // Will be valid if mocks succeed
410
- });
411
- });
412
-
413
- describe("validatePaths", () => {
414
- test("should separate valid and invalid paths", () => {
415
- existsSyncSpy
416
- .mockReturnValueOnce(true) // first path exists
417
- .mockReturnValueOnce(false); // second path doesn't exist
418
- accessSyncSpy.mockImplementation(() => {}); // accessible when it exists
419
-
420
- const paths = ["/valid/path", "/invalid/path"];
421
- const result = validatePaths(paths);
422
-
423
- expect(result.validPaths).toEqual(["/valid/path"]);
424
- expect(result.errors).toEqual([
425
- { path: "/invalid/path", error: "Path does not exist: /invalid/path" },
426
- ]);
427
- });
428
-
429
- test("should handle empty paths array", () => {
430
- const result = validatePaths([]);
431
- expect(result.validPaths).toEqual([]);
432
- expect(result.errors).toEqual([]);
433
- });
434
- });
435
-
436
- // CONFIG MANAGEMENT TESTS
437
- describe("loadConfigFromFile", () => {
438
- test("should return null for non-existent file", async () => {
439
- existsSyncSpy.mockReturnValue(false);
440
-
441
- const result = await loadConfigFromFile();
442
-
443
- expect(result).toBeNull();
444
- });
445
- });
446
-
447
- // GITHUB INTEGRATION TESTS
448
- describe("getGitHubRepoInfo", () => {
449
- test("should fetch repository information", async () => {
450
- const mockRepo = {
451
- name: "test-repo",
452
- description: "A test repository",
453
- owner: { avatar_url: "https://example.com/avatar.png" },
454
- };
455
- fetchSpy.mockResolvedValue({
456
- ok: true,
457
- json: () => Promise.resolve(mockRepo),
458
- });
459
-
460
- const result = await getGitHubRepoInfo("https://github.com/user/test-repo");
461
-
462
- expect(result).toEqual({
463
- name: "test-repo",
464
- description: "A test repository",
465
- icon: "https://example.com/avatar.png",
466
- });
467
- });
468
-
469
- test("should return null for invalid GitHub URL", async () => {
470
- const result = await getGitHubRepoInfo("https://gitlab.com/user/repo");
471
- expect(result).toBeNull();
472
- });
473
-
474
- test("should handle fetch errors", async () => {
475
- fetchSpy.mockRejectedValue(new Error("Network error"));
476
-
477
- const result = await getGitHubRepoInfo("https://github.com/user/repo");
478
-
479
- expect(result).toBeNull();
480
- expect(consoleSpy).toHaveBeenCalled();
481
- });
482
-
483
- test("should handle non-ok response", async () => {
484
- fetchSpy.mockResolvedValue({
485
- ok: false,
486
- statusText: "Not Found",
487
- });
488
-
489
- const result = await getGitHubRepoInfo("https://github.com/user/nonexistent");
490
-
491
- expect(result).toBeNull();
492
- expect(consoleSpy).toHaveBeenCalled();
493
- });
494
- });
495
-
496
- // FILE REFERENCES TESTS (simplified due to complex file system interactions)
497
- describe("resolveFileReferences", () => {
498
- test("should return original value for missing files", async () => {
499
- existsSyncSpy.mockReturnValue(false);
500
-
501
- const config = { notes: "@missing.txt" };
502
- const result = await resolveFileReferences(config);
503
-
504
- expect(result.notes).toBe("@missing.txt");
505
- });
506
-
507
- test("should handle non-file reference strings", async () => {
508
- const config = {
509
- normal: "not a file reference",
510
- email: "user@example.com",
511
- };
512
-
513
- const result = await resolveFileReferences(config);
514
-
515
- expect(result).toEqual(config);
516
- });
517
-
518
- test("should handle unsupported file extensions", async () => {
519
- existsSyncSpy.mockReturnValue(true);
520
-
521
- const config = { data: "@data.exe" };
522
- const result = await resolveFileReferences(config);
523
-
524
- expect(result.data).toBe("@data.exe");
525
- });
526
- });
527
-
528
- // PATH DISCOVERY TESTS
529
- describe("getAvailablePaths", () => {
530
- test("should return current directory contents for empty input", () => {
531
- readdirSyncSpy.mockReturnValue([
532
- { name: "file1.txt", isDirectory: () => false },
533
- { name: "folder1", isDirectory: () => true },
534
- ]);
535
-
536
- const result = getAvailablePaths("");
537
-
538
- expect(result).toHaveLength(2);
539
- expect(result[0].description).toBe("📁 Directory");
540
- expect(result[1].description).toBe("📄 File");
541
- });
542
-
543
- test("should handle absolute paths", () => {
544
- readdirSyncSpy.mockReturnValue([{ name: "config.yaml", isDirectory: () => false }]);
545
-
546
- const result = getAvailablePaths("/etc/config");
547
-
548
- expect(result).toEqual(
549
- expect.arrayContaining([
550
- expect.objectContaining({
551
- name: expect.stringContaining("config.yaml"),
552
- description: "📄 File",
553
- }),
554
- ]),
555
- );
556
- });
557
-
558
- test("should filter results by search term", () => {
559
- readdirSyncSpy.mockReturnValue([
560
- { name: "test.js", isDirectory: () => false },
561
- { name: "example.md", isDirectory: () => false },
562
- { name: "test-utils.js", isDirectory: () => false },
563
- ]);
564
-
565
- const result = getAvailablePaths("test");
566
-
567
- expect(result.length).toBeGreaterThan(0);
568
- expect(result.every((item) => item.name.toLowerCase().includes("test"))).toBe(true);
569
- });
570
- });
571
-
572
- // PROJECT INFO TESTS
573
- describe("getProjectInfo", () => {
574
- test("should get project info from git repository", async () => {
575
- execSyncSpy.mockReturnValue("https://github.com/user/repo.git\n");
576
- fetchSpy.mockResolvedValue({
577
- ok: true,
578
- json: () =>
579
- Promise.resolve({
580
- name: "repo",
581
- description: "Test repository",
582
- owner: { avatar_url: "https://avatar.png" },
583
- }),
584
- });
585
-
586
- const result = await getProjectInfo();
587
-
588
- expect(result.name).toBe("repo");
589
- expect(result.description).toBe("Test repository");
590
- expect(result.icon).toBe("https://avatar.png");
591
- expect(result.fromGitHub).toBe(true);
592
- });
593
-
594
- test("should fallback to directory name for non-git projects", async () => {
595
- execSyncSpy.mockImplementation(() => {
596
- throw new Error("Not a git repository");
597
- });
598
- processSpies.cwd.mockReturnValue("/home/user/my-project");
599
-
600
- const result = await getProjectInfo();
601
-
602
- expect(result.name).toBe("my-project");
603
- expect(result.fromGitHub).toBe(false);
604
- });
605
- });
606
-
607
- // FILE CHANGE DETECTION TESTS
608
- describe("hasFileChangesBetweenCommits", () => {
609
- test("should detect added files", () => {
610
- execSyncSpy.mockReturnValue("A\tnew-file.md\nM\texisting.js\n");
611
-
612
- const result = hasFileChangesBetweenCommits("abc123", "def456");
613
-
614
- expect(result).toBe(true);
615
- });
616
-
617
- test("should detect deleted files", () => {
618
- execSyncSpy.mockReturnValue("D\tdeleted-file.md\nM\texisting.js\n");
619
-
620
- const result = hasFileChangesBetweenCommits("abc123", "def456");
621
-
622
- expect(result).toBe(true);
623
- });
624
-
625
- test("should ignore modifications", () => {
626
- execSyncSpy.mockReturnValue("M\texisting.js\nM\tanother.ts\n");
627
-
628
- const result = hasFileChangesBetweenCommits("abc123", "def456");
629
-
630
- expect(result).toBe(false);
631
- });
632
-
633
- test("should handle git errors", () => {
634
- execSyncSpy.mockImplementation(() => {
635
- throw new Error("Git error");
636
- });
637
-
638
- const result = hasFileChangesBetweenCommits("abc123", "def456");
639
-
640
- expect(result).toBe(false);
641
- expect(consoleSpy).toHaveBeenCalled();
642
- });
643
- });
644
-
645
- describe("saveGitHeadToConfig", () => {
646
- test("should skip if gitHead is null", async () => {
647
- await saveGitHeadToConfig(null);
648
- expect(writeFileSpy).not.toHaveBeenCalled();
649
- });
650
-
651
- test("should skip if in test environment", async () => {
652
- process.env.NODE_ENV = "test";
653
- await saveGitHeadToConfig("abc123");
654
- expect(writeFileSpy).not.toHaveBeenCalled();
655
- });
656
-
657
- test("should handle file operation errors", async () => {
658
- delete process.env.NODE_ENV;
659
- existsSyncSpy.mockImplementation(() => {
660
- throw new Error("File system error");
661
- });
662
-
663
- await saveGitHeadToConfig("abc123");
664
-
665
- expect(consoleSpy).toHaveBeenCalledWith(
666
- "Failed to save git HEAD to config.yaml:",
667
- "File system error",
668
- );
669
- });
670
- });
671
-
672
- describe("saveValueToConfig", () => {
673
- test("should skip if value is undefined", async () => {
674
- await saveValueToConfig("key", undefined);
675
- expect(writeFileSpy).not.toHaveBeenCalled();
676
- });
677
-
678
- test("should handle file operation errors", async () => {
679
- existsSyncSpy.mockImplementation(() => {
680
- throw new Error("File system error");
681
- });
682
-
683
- await saveValueToConfig("key", "value");
684
-
685
- expect(consoleSpy).toHaveBeenCalledWith(
686
- "Failed to save key to config.yaml:",
687
- "File system error",
688
- );
689
- });
690
- });
691
-
692
- // CONFIGURATION PROCESSING TESTS
693
- describe("processConfigFields", () => {
694
- test("should apply default values for missing fields", () => {
695
- const config = {};
696
- const result = processConfigFields(config);
697
-
698
- expect(result.nodeName).toBe("Section");
699
- expect(result.locale).toBe("en");
700
- expect(result.sourcesPath).toEqual(["./"]);
701
- expect(result.docsDir).toBe("./.aigne/doc-smith/docs");
702
- expect(result.outputDir).toBe("./.aigne/doc-smith/output");
703
- expect(result.translateLanguages).toEqual([]);
704
- expect(result.rules).toBe("");
705
- expect(result.targetAudience).toBe("");
706
- });
707
-
708
- test("should process document purpose with valid key", () => {
709
- const config = {
710
- documentPurpose: ["getStarted"],
711
- };
712
- const result = processConfigFields(config);
713
-
714
- expect(result.rules).toContain("Document Purpose");
715
- });
716
-
717
- test("should process target audience types with valid key", () => {
718
- const config = {
719
- targetAudienceTypes: ["developers"],
720
- };
721
- const result = processConfigFields(config);
722
-
723
- expect(result.rules).toContain("Target Audience");
724
- expect(result.targetAudience).toContain("Developers");
725
- });
726
-
727
- test("should combine existing rules with processed content", () => {
728
- const config = {
729
- rules: "Existing rules",
730
- documentPurpose: ["getStarted"],
731
- };
732
- const result = processConfigFields(config);
733
-
734
- expect(result.rules).toContain("Existing rules");
735
- expect(result.rules).toContain("Document Purpose");
736
- });
737
- });
738
-
739
- // PATH DISCOVERY EDGE CASES
740
- describe("getAvailablePaths edge cases", () => {
741
- test("should handle directory reading errors", () => {
742
- readdirSyncSpy.mockImplementation(() => {
743
- throw new Error("Permission denied");
744
- });
745
-
746
- const result = getAvailablePaths("");
747
-
748
- expect(result).toEqual([]);
749
- expect(consoleSpy).toHaveBeenCalled();
750
- });
751
-
752
- test("should handle non-existent directory in relative path", () => {
753
- existsSyncSpy.mockReturnValue(false);
754
-
755
- const result = getAvailablePaths("./nonexistent/");
756
-
757
- expect(result).toEqual(
758
- expect.arrayContaining([
759
- expect.objectContaining({
760
- description: expect.stringContaining("does not exist"),
761
- }),
762
- ]),
763
- );
764
- });
765
-
766
- test("should handle path validation in relative path search", () => {
767
- existsSyncSpy.mockReturnValue(false);
768
-
769
- const result = getAvailablePaths("./invalid/path");
770
-
771
- expect(result).toEqual(
772
- expect.arrayContaining([
773
- expect.objectContaining({
774
- description: expect.stringContaining("Path does not exist"),
775
- }),
776
- ]),
777
- );
778
- });
779
-
780
- test("should add exact path match for valid files", () => {
781
- existsSyncSpy.mockReturnValue(true);
782
- accessSyncSpy.mockImplementation(() => {});
783
- statSyncSpy.mockReturnValue({ isDirectory: () => false });
784
- readdirSyncSpy.mockReturnValue([]);
785
-
786
- const result = getAvailablePaths("existing-file.txt");
787
-
788
- expect(result[0]).toEqual({
789
- name: "existing-file.txt",
790
- value: "existing-file.txt",
791
- description: "📄 File",
792
- });
793
- });
794
-
795
- test("should preserve ./ prefix in directory paths", () => {
796
- readdirSyncSpy.mockReturnValue([{ name: "test.js", isDirectory: () => false }]);
797
-
798
- const result = getAvailablePaths("");
799
-
800
- expect(result[0].name).toMatch(/^\.\//);
801
- });
802
- });
803
-
804
- // DOCUMENT MANAGEMENT TESTS
805
- describe("saveDocWithTranslations", () => {
806
- beforeEach(() => {
807
- // Reset mocks for each test
808
- mkdirSpy?.mockClear();
809
- writeFileSpy?.mockClear();
810
- });
811
-
812
- test("should save main document without translations", async () => {
813
- const params = {
814
- path: "/api/guide",
815
- content: "API Guide content",
816
- docsDir: "/docs",
817
- locale: "en",
818
- };
819
-
820
- const result = await saveDocWithTranslations(params);
821
-
822
- expect(result).toBeDefined();
823
- expect(Array.isArray(result)).toBe(true);
824
- expect(result).toHaveLength(1);
825
- expect(result[0]).toEqual({
826
- path: "/docs/api-guide.md",
827
- success: true,
828
- });
829
- });
830
-
831
- test("should save document with translations", async () => {
832
- const params = {
833
- path: "/api/guide",
834
- content: "API Guide content",
835
- docsDir: "/docs",
836
- locale: "en",
837
- translates: [{ language: "zh", translation: "API指南内容" }],
838
- };
839
-
840
- const result = await saveDocWithTranslations(params);
841
-
842
- expect(result).toHaveLength(2);
843
- expect(result.every((r) => r.success)).toBe(true);
844
- expect(result[0]).toEqual({
845
- path: "/docs/api-guide.md",
846
- success: true,
847
- });
848
- expect(result[1]).toEqual({
849
- path: "/docs/api-guide.zh.md",
850
- success: true,
851
- });
852
- });
853
-
854
- test("should skip main content when isTranslate is true", async () => {
855
- const params = {
856
- path: "/api/guide",
857
- content: "Content",
858
- docsDir: "/docs",
859
- locale: "en",
860
- isTranslate: true,
861
- translates: [{ language: "zh", translation: "翻译内容" }],
862
- };
863
-
864
- const result = await saveDocWithTranslations(params);
865
-
866
- expect(result).toHaveLength(1);
867
- expect(result[0]).toEqual({
868
- path: "/docs/api-guide.zh.md",
869
- success: true,
870
- });
871
- });
872
-
873
- test("should add labels front matter", async () => {
874
- const params = {
875
- path: "/guide",
876
- content: "Content",
877
- docsDir: "/docs",
878
- locale: "en",
879
- labels: ["test"],
880
- };
881
-
882
- const result = await saveDocWithTranslations(params);
883
-
884
- expect(result).toHaveLength(1);
885
- expect(result[0]).toEqual({
886
- path: "/docs/guide.md",
887
- success: true,
888
- });
889
- });
890
-
891
- test("should handle non-English locale", async () => {
892
- const params = {
893
- path: "/guide",
894
- content: "Contenu français",
895
- docsDir: "/docs",
896
- locale: "fr",
897
- };
898
-
899
- const result = await saveDocWithTranslations(params);
900
-
901
- expect(result).toHaveLength(1);
902
- expect(result[0]).toEqual({
903
- path: "/docs/guide.fr.md",
904
- success: true,
905
- });
906
- });
907
-
908
- test("should flatten complex paths", async () => {
909
- const params = {
910
- path: "/deep/nested/path",
911
- content: "Content",
912
- docsDir: "/docs",
913
- locale: "en",
914
- };
915
-
916
- const result = await saveDocWithTranslations(params);
917
-
918
- expect(result).toHaveLength(1);
919
- expect(result[0]).toEqual({
920
- path: "/docs/deep-nested-path.md",
921
- success: true,
922
- });
923
- });
924
-
925
- test("should handle errors gracefully", async () => {
926
- // Mock fs promises mkdir to throw error
927
- spyOn(fsPromisesDefault, "mkdir").mockRejectedValueOnce(new Error("Test error"));
928
-
929
- const params = {
930
- path: "/guide",
931
- content: "Content",
932
- docsDir: "/docs",
933
- locale: "en",
934
- };
935
-
936
- const result = await saveDocWithTranslations(params);
937
-
938
- expect(result).toEqual([{ path: "/guide", success: false, error: "Test error" }]);
939
- });
940
- });
941
- });