@aigne/doc-smith 0.8.5 → 0.8.7

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 (126) hide show
  1. package/.aigne/doc-smith/output/structure-plan.json +1 -5
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +3 -3
  4. package/agents/{chat.yaml → chat/index.yaml} +7 -7
  5. package/agents/generate/check-document-structure.yaml +30 -0
  6. package/agents/{check-structure-plan.mjs → generate/check-need-generate-structure.mjs} +21 -21
  7. package/agents/generate/generate-structure.yaml +58 -0
  8. package/agents/{docs-generator.yaml → generate/index.yaml} +15 -16
  9. package/agents/generate/refine-document-structure.yaml +12 -0
  10. package/agents/{input-generator.mjs → init/index.mjs} +34 -27
  11. package/agents/{manage-prefs.mjs → prefs/index.mjs} +16 -16
  12. package/agents/publish/index.yaml +17 -0
  13. package/agents/{publish-docs.mjs → publish/publish-docs.mjs} +15 -16
  14. package/agents/schema/{structure-plan-result.yaml → document-execution-structure.yaml} +3 -3
  15. package/agents/schema/document-structure.yaml +26 -0
  16. package/agents/{language-selector.mjs → translate/choose-language.mjs} +5 -5
  17. package/agents/{retranslate.yaml → translate/index.yaml} +17 -18
  18. package/agents/translate/translate-document.yaml +32 -0
  19. package/agents/{batch-translate.yaml → translate/translate-multilingual.yaml} +5 -5
  20. package/agents/update/batch-generate-document.yaml +19 -0
  21. package/agents/{check-detail.mjs → update/check-document.mjs} +16 -16
  22. package/agents/{detail-generator-and-translate.yaml → update/generate-and-translate-document.yaml} +23 -23
  23. package/agents/update/generate-document.yaml +50 -0
  24. package/agents/{detail-regenerator.yaml → update/index.yaml} +16 -17
  25. package/agents/{action-success.mjs → utils/action-success.mjs} +2 -2
  26. package/agents/{check-detail-result.mjs → utils/check-detail-result.mjs} +3 -3
  27. package/agents/{check-feedback-refiner.mjs → utils/check-feedback-refiner.mjs} +6 -6
  28. package/agents/{find-items-by-paths.mjs → utils/choose-docs.mjs} +25 -10
  29. package/agents/{docs-fs.yaml → utils/docs-fs-actor.yaml} +3 -1
  30. package/agents/utils/feedback-refiner.yaml +50 -0
  31. package/agents/{find-item-by-path.mjs → utils/find-item-by-path.mjs} +17 -7
  32. package/agents/{find-user-preferences-by-path.mjs → utils/find-user-preferences-by-path.mjs} +1 -1
  33. package/agents/utils/format-document-structure.mjs +25 -0
  34. package/agents/{load-sources.mjs → utils/load-sources.mjs} +41 -28
  35. package/agents/{save-docs.mjs → utils/save-docs.mjs} +16 -16
  36. package/agents/{save-single-doc.mjs → utils/save-single-doc.mjs} +2 -2
  37. package/agents/{transform-detail-datasources.mjs → utils/transform-detail-datasources.mjs} +1 -1
  38. package/aigne.yaml +35 -35
  39. package/docs/cli-reference.md +1 -1
  40. package/docs/features-generate-documentation.md +1 -1
  41. package/docs/features-update-and-refine.md +2 -2
  42. package/docs-mcp/analyze-docs-relevance.yaml +10 -10
  43. package/docs-mcp/docs-search.yaml +5 -3
  44. package/package.json +10 -8
  45. package/prompts/{document → detail/custom}/custom-code-block.md +6 -6
  46. package/prompts/detail/custom/custom-components.md +172 -0
  47. package/prompts/{document → detail}/d2-chart/rules.md +95 -1
  48. package/prompts/{document → detail}/detail-example.md +80 -61
  49. package/prompts/{document/detail-generator.md → detail/document-rules.md} +4 -8
  50. package/prompts/{content-detail-generator.md → detail/generate-document.md} +48 -25
  51. package/prompts/{check-structure-planning-result.md → structure/check-document-structure.md} +23 -17
  52. package/prompts/{document/structure-planning.md → structure/document-rules.md} +0 -2
  53. package/prompts/{structure-planning.md → structure/generate-structure.md} +51 -30
  54. package/prompts/{document → structure}/structure-example.md +2 -2
  55. package/prompts/{document → structure}/structure-getting-started.md +2 -2
  56. package/prompts/translate/glossary.md +6 -0
  57. package/prompts/{translator.md → translate/translate-document.md} +29 -10
  58. package/prompts/{feedback-refiner.md → utils/feedback-refiner.md} +8 -8
  59. package/tests/agents/chat/chat.test.mjs +46 -0
  60. package/tests/agents/generate/check-document-structure.test.mjs +51 -0
  61. package/tests/agents/generate/check-need-generate-structure.test.mjs +292 -0
  62. package/tests/agents/generate/generate-structure.test.mjs +51 -0
  63. package/tests/{input-generator.test.mjs → agents/init/init.test.mjs} +19 -17
  64. package/tests/agents/prefs/prefs.test.mjs +431 -0
  65. package/tests/agents/publish/publish-docs.test.mjs +642 -0
  66. package/tests/agents/translate/choose-language.test.mjs +311 -0
  67. package/tests/agents/translate/translate-document.test.mjs +51 -0
  68. package/tests/agents/update/check-document.test.mjs +523 -0
  69. package/tests/agents/update/generate-document.test.mjs +51 -0
  70. package/tests/agents/utils/action-success.test.mjs +54 -0
  71. package/tests/{check-detail-result.test.mjs → agents/utils/check-detail-result.test.mjs} +98 -98
  72. package/tests/agents/utils/check-feedback-refiner.test.mjs +478 -0
  73. package/tests/agents/utils/choose-docs.test.mjs +417 -0
  74. package/tests/agents/utils/exit.test.mjs +70 -0
  75. package/tests/agents/utils/feedback-refiner.test.mjs +51 -0
  76. package/tests/agents/utils/find-item-by-path.test.mjs +526 -0
  77. package/tests/agents/utils/find-user-preferences-by-path.test.mjs +382 -0
  78. package/tests/agents/utils/format-document-structure.test.mjs +264 -0
  79. package/tests/agents/utils/fs.test.mjs +267 -0
  80. package/tests/{load-sources.test.mjs → agents/utils/load-sources.test.mjs} +153 -25
  81. package/tests/{save-docs.test.mjs → agents/utils/save-docs.test.mjs} +11 -5
  82. package/tests/agents/utils/save-output.test.mjs +315 -0
  83. package/tests/agents/utils/save-single-doc.test.mjs +364 -0
  84. package/tests/agents/utils/transform-detail-datasources.test.mjs +363 -0
  85. package/tests/utils/auth-utils.test.mjs +358 -0
  86. package/tests/utils/blocklet.test.mjs +334 -0
  87. package/tests/{conflict-resolution.test.mjs → utils/conflict-detector.test.mjs} +3 -3
  88. package/tests/utils/constants.test.mjs +295 -0
  89. package/tests/utils/d2-utils.test.mjs +423 -0
  90. package/tests/utils/deploy.test.mjs +365 -0
  91. package/tests/utils/docs-finder-utils.test.mjs +625 -0
  92. package/tests/utils/file-utils.test.mjs +213 -0
  93. package/tests/{kroki-utils.test.mjs → utils/kroki-utils.test.mjs} +2 -2
  94. package/tests/utils/load-config.test.mjs +141 -0
  95. package/tests/{mermaid-validation.test.mjs → utils/mermaid-validator.test.mjs} +2 -2
  96. package/tests/utils/mock-chat-model.mjs +12 -0
  97. package/tests/{preferences-utils.test.mjs → utils/preferences-utils.test.mjs} +1 -1
  98. package/tests/{save-value-to-config.test.mjs → utils/save-value-to-config.test.mjs} +61 -4
  99. package/tests/utils/utils.test.mjs +939 -0
  100. package/utils/auth-utils.mjs +1 -1
  101. package/utils/conflict-detector.mjs +1 -1
  102. package/utils/constants.mjs +5 -3
  103. package/utils/d2-utils.mjs +194 -0
  104. package/utils/deploy.mjs +3 -3
  105. package/utils/docs-finder-utils.mjs +26 -26
  106. package/utils/icon-map.mjs +26 -0
  107. package/{agents → utils}/load-config.mjs +2 -18
  108. package/utils/markdown-checker.mjs +5 -5
  109. package/agents/batch-docs-detail-generator.yaml +0 -19
  110. package/agents/check-structure-planning-result.yaml +0 -30
  111. package/agents/content-detail-generator.yaml +0 -50
  112. package/agents/feedback-refiner.yaml +0 -52
  113. package/agents/format-structure-plan.mjs +0 -25
  114. package/agents/reflective-structure-planner.yaml +0 -12
  115. package/agents/schema/structure-plan.yaml +0 -26
  116. package/agents/structure-planning.yaml +0 -58
  117. package/agents/team-publish-docs.yaml +0 -18
  118. package/agents/translate.yaml +0 -31
  119. package/prompts/document/custom-components.md +0 -104
  120. package/tests/README.md +0 -93
  121. package/tests/utils.test.mjs +0 -2067
  122. /package/agents/{exit.mjs → utils/exit.mjs} +0 -0
  123. /package/agents/{fs.mjs → utils/fs.mjs} +0 -0
  124. /package/agents/{save-output.mjs → utils/save-output.mjs} +0 -0
  125. /package/prompts/{document → detail}/d2-chart/official-examples.md +0 -0
  126. /package/prompts/{document → detail}/jsx/rules.md +0 -0
@@ -0,0 +1,315 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import saveOutput from "../../../agents/utils/save-output.mjs";
5
+
6
+ describe("saveOutput utility", () => {
7
+ let mkdirSpy;
8
+ let writeFileSpy;
9
+ let joinSpy;
10
+ let consoleWarnSpy;
11
+
12
+ beforeEach(() => {
13
+ // Spy on fs.promises methods
14
+ mkdirSpy = spyOn(fs.promises, "mkdir").mockResolvedValue();
15
+ writeFileSpy = spyOn(fs.promises, "writeFile").mockResolvedValue();
16
+
17
+ // Spy on path methods
18
+ joinSpy = spyOn(path, "join").mockImplementation((...paths) => paths.join("/"));
19
+
20
+ // Spy on console.warn
21
+ consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
22
+ });
23
+
24
+ afterEach(() => {
25
+ // Restore all spies
26
+ mkdirSpy?.mockRestore();
27
+ writeFileSpy?.mockRestore();
28
+ joinSpy?.mockRestore();
29
+ consoleWarnSpy?.mockRestore();
30
+ });
31
+
32
+ // SUCCESSFUL SAVE TESTS
33
+ test("should save string content successfully", async () => {
34
+ const result = await saveOutput({
35
+ savePath: "/output/dir",
36
+ fileName: "result.txt",
37
+ saveKey: "textContent",
38
+ textContent: "Hello, World!",
39
+ });
40
+
41
+ expect(mkdirSpy).toHaveBeenCalledWith("/output/dir", { recursive: true });
42
+ expect(joinSpy).toHaveBeenCalledWith("/output/dir", "result.txt");
43
+ expect(writeFileSpy).toHaveBeenCalledWith("/output/dir/result.txt", "Hello, World!", "utf8");
44
+ expect(result).toEqual({
45
+ saveOutputStatus: true,
46
+ saveOutputPath: "/output/dir/result.txt",
47
+ });
48
+ });
49
+
50
+ test("should save object content as formatted JSON", async () => {
51
+ const objectData = {
52
+ title: "Test Document",
53
+ tags: ["test", "sample"],
54
+ metadata: { version: 1 },
55
+ };
56
+
57
+ const result = await saveOutput({
58
+ savePath: "/data",
59
+ fileName: "config.json",
60
+ saveKey: "configData",
61
+ configData: objectData,
62
+ });
63
+
64
+ const expectedContent = JSON.stringify(objectData, null, 2);
65
+ expect(writeFileSpy).toHaveBeenCalledWith("/data/config.json", expectedContent, "utf8");
66
+ expect(result.saveOutputStatus).toBe(true);
67
+ expect(result.saveOutputPath).toBe("/data/config.json");
68
+ });
69
+
70
+ test("should save number content as string", async () => {
71
+ const result = await saveOutput({
72
+ savePath: "/numbers",
73
+ fileName: "count.txt",
74
+ saveKey: "count",
75
+ count: 42,
76
+ });
77
+
78
+ expect(writeFileSpy).toHaveBeenCalledWith("/numbers/count.txt", "42", "utf8");
79
+ expect(result.saveOutputStatus).toBe(true);
80
+ });
81
+
82
+ test("should save boolean content as string", async () => {
83
+ const result = await saveOutput({
84
+ savePath: "/flags",
85
+ fileName: "flag.txt",
86
+ saveKey: "isEnabled",
87
+ isEnabled: true,
88
+ });
89
+
90
+ expect(writeFileSpy).toHaveBeenCalledWith("/flags/flag.txt", "true", "utf8");
91
+ expect(result.saveOutputStatus).toBe(true);
92
+ });
93
+
94
+ test("should save array content as formatted JSON", async () => {
95
+ const arrayData = ["item1", "item2", { nested: "object" }];
96
+
97
+ const result = await saveOutput({
98
+ savePath: "/arrays",
99
+ fileName: "list.json",
100
+ saveKey: "items",
101
+ items: arrayData,
102
+ });
103
+
104
+ const expectedContent = JSON.stringify(arrayData, null, 2);
105
+ expect(writeFileSpy).toHaveBeenCalledWith("/arrays/list.json", expectedContent, "utf8");
106
+ expect(result.saveOutputStatus).toBe(true);
107
+ });
108
+
109
+ // NULL AND UNDEFINED HANDLING
110
+ test("should handle null values by converting to JSON string", async () => {
111
+ const result = await saveOutput({
112
+ savePath: "/null-test",
113
+ fileName: "null.json",
114
+ saveKey: "nullValue",
115
+ nullValue: null,
116
+ });
117
+
118
+ expect(writeFileSpy).toHaveBeenCalledWith("/null-test/null.json", "null", "utf8");
119
+ expect(result.saveOutputStatus).toBe(true);
120
+ });
121
+
122
+ test("should handle undefined values by converting to string", async () => {
123
+ const result = await saveOutput({
124
+ savePath: "/undefined-test",
125
+ fileName: "undefined.txt",
126
+ saveKey: "undefinedValue",
127
+ undefinedValue: undefined,
128
+ });
129
+
130
+ expect(writeFileSpy).toHaveBeenCalledWith("/undefined-test/undefined.txt", "undefined", "utf8");
131
+ expect(result.saveOutputStatus).toBe(true);
132
+ });
133
+
134
+ // MISSING SAVE KEY TESTS
135
+ test("should warn and return false when saveKey is not found", async () => {
136
+ const result = await saveOutput({
137
+ savePath: "/output",
138
+ fileName: "missing.txt",
139
+ saveKey: "nonExistentKey",
140
+ existingKey: "some value",
141
+ });
142
+
143
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
144
+ 'saveKey "nonExistentKey" not found in input, skip saving.',
145
+ );
146
+ expect(result).toEqual({
147
+ saveOutputStatus: false,
148
+ saveOutputPath: null,
149
+ });
150
+ expect(mkdirSpy).not.toHaveBeenCalled();
151
+ expect(writeFileSpy).not.toHaveBeenCalled();
152
+ });
153
+
154
+ test("should not save when saveKey exists but is undefined", async () => {
155
+ const result = await saveOutput({
156
+ savePath: "/output",
157
+ fileName: "test.txt",
158
+ saveKey: "undefinedKey",
159
+ // undefinedKey is not provided, so it will be undefined
160
+ });
161
+
162
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
163
+ 'saveKey "undefinedKey" not found in input, skip saving.',
164
+ );
165
+ expect(result.saveOutputStatus).toBe(false);
166
+ });
167
+
168
+ // COMPLEX PATH HANDLING
169
+ test("should handle nested directory paths", async () => {
170
+ await saveOutput({
171
+ savePath: "/deep/nested/directory/structure",
172
+ fileName: "file.txt",
173
+ saveKey: "content",
174
+ content: "test content",
175
+ });
176
+
177
+ expect(mkdirSpy).toHaveBeenCalledWith("/deep/nested/directory/structure", {
178
+ recursive: true,
179
+ });
180
+ expect(joinSpy).toHaveBeenCalledWith("/deep/nested/directory/structure", "file.txt");
181
+ });
182
+
183
+ test("should handle paths with special characters", async () => {
184
+ await saveOutput({
185
+ savePath: "/path with spaces/特殊字符/symbols!@#",
186
+ fileName: "file-name_with-symbols.json",
187
+ saveKey: "data",
188
+ data: { test: "value" },
189
+ });
190
+
191
+ expect(mkdirSpy).toHaveBeenCalledWith("/path with spaces/特殊字符/symbols!@#", {
192
+ recursive: true,
193
+ });
194
+ expect(joinSpy).toHaveBeenCalledWith(
195
+ "/path with spaces/特殊字符/symbols!@#",
196
+ "file-name_with-symbols.json",
197
+ );
198
+ });
199
+
200
+ // EMPTY AND EDGE CASES
201
+ test("should save empty string content", async () => {
202
+ const result = await saveOutput({
203
+ savePath: "/empty",
204
+ fileName: "empty.txt",
205
+ saveKey: "emptyString",
206
+ emptyString: "",
207
+ });
208
+
209
+ expect(writeFileSpy).toHaveBeenCalledWith("/empty/empty.txt", "", "utf8");
210
+ expect(result.saveOutputStatus).toBe(true);
211
+ });
212
+
213
+ test("should save empty object as formatted JSON", async () => {
214
+ const result = await saveOutput({
215
+ savePath: "/empty",
216
+ fileName: "empty.json",
217
+ saveKey: "emptyObject",
218
+ emptyObject: {},
219
+ });
220
+
221
+ expect(writeFileSpy).toHaveBeenCalledWith("/empty/empty.json", "{}", "utf8");
222
+ expect(result.saveOutputStatus).toBe(true);
223
+ });
224
+
225
+ test("should save empty array as formatted JSON", async () => {
226
+ const result = await saveOutput({
227
+ savePath: "/empty",
228
+ fileName: "empty-array.json",
229
+ saveKey: "emptyArray",
230
+ emptyArray: [],
231
+ });
232
+
233
+ expect(writeFileSpy).toHaveBeenCalledWith("/empty/empty-array.json", "[]", "utf8");
234
+ expect(result.saveOutputStatus).toBe(true);
235
+ });
236
+
237
+ // COMPLEX OBJECT SERIALIZATION
238
+ test("should handle complex nested objects", async () => {
239
+ const complexObject = {
240
+ users: [
241
+ { id: 1, name: "Alice", settings: { theme: "dark", notifications: true } },
242
+ { id: 2, name: "Bob", settings: { theme: "light", notifications: false } },
243
+ ],
244
+ metadata: {
245
+ version: "1.0.0",
246
+ created: "2024-01-01",
247
+ features: ["auth", "api", "ui"],
248
+ },
249
+ };
250
+
251
+ const result = await saveOutput({
252
+ savePath: "/complex",
253
+ fileName: "data.json",
254
+ saveKey: "complexData",
255
+ complexData: complexObject,
256
+ });
257
+
258
+ const expectedContent = JSON.stringify(complexObject, null, 2);
259
+ expect(writeFileSpy).toHaveBeenCalledWith("/complex/data.json", expectedContent, "utf8");
260
+ expect(result.saveOutputStatus).toBe(true);
261
+ });
262
+
263
+ // MULTIPLE KEYS IN INPUT
264
+ test("should only save the specified saveKey among multiple keys", async () => {
265
+ const result = await saveOutput({
266
+ savePath: "/selective",
267
+ fileName: "selected.txt",
268
+ saveKey: "targetKey",
269
+ targetKey: "This should be saved",
270
+ otherKey: "This should be ignored",
271
+ anotherKey: { ignored: "data" },
272
+ });
273
+
274
+ expect(writeFileSpy).toHaveBeenCalledWith(
275
+ "/selective/selected.txt",
276
+ "This should be saved",
277
+ "utf8",
278
+ );
279
+ expect(result.saveOutputStatus).toBe(true);
280
+ });
281
+
282
+ // FUNCTION CONTENT HANDLING
283
+ test("should convert function to string", async () => {
284
+ const testFunction = function testFn() {
285
+ return "hello";
286
+ };
287
+
288
+ const result = await saveOutput({
289
+ savePath: "/functions",
290
+ fileName: "function.txt",
291
+ saveKey: "fn",
292
+ fn: testFunction,
293
+ });
294
+
295
+ expect(writeFileSpy).toHaveBeenCalledWith(
296
+ "/functions/function.txt",
297
+ testFunction.toString(),
298
+ "utf8",
299
+ );
300
+ expect(result.saveOutputStatus).toBe(true);
301
+ });
302
+
303
+ // ZERO VALUES
304
+ test("should save zero values correctly", async () => {
305
+ const result = await saveOutput({
306
+ savePath: "/zeros",
307
+ fileName: "zero.txt",
308
+ saveKey: "zeroValue",
309
+ zeroValue: 0,
310
+ });
311
+
312
+ expect(writeFileSpy).toHaveBeenCalledWith("/zeros/zero.txt", "0", "utf8");
313
+ expect(result.saveOutputStatus).toBe(true);
314
+ });
315
+ });
@@ -0,0 +1,364 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import saveSingleDoc from "../../../agents/utils/save-single-doc.mjs";
3
+ import * as mermaidWorkerPool from "../../../utils/mermaid-worker-pool.mjs";
4
+ import * as utils from "../../../utils/utils.mjs";
5
+
6
+ describe("saveSingleDoc utility", () => {
7
+ let consoleWarnSpy;
8
+ let shutdownMermaidWorkerPoolSpy;
9
+ let saveDocWithTranslationsSpy;
10
+
11
+ beforeEach(() => {
12
+ shutdownMermaidWorkerPoolSpy = spyOn(
13
+ mermaidWorkerPool,
14
+ "shutdownMermaidWorkerPool",
15
+ ).mockResolvedValue();
16
+ saveDocWithTranslationsSpy = spyOn(utils, "saveDocWithTranslations").mockResolvedValue({
17
+ success: true,
18
+ });
19
+ consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Restore all spies
24
+ shutdownMermaidWorkerPoolSpy?.mockRestore();
25
+ saveDocWithTranslationsSpy?.mockRestore();
26
+ consoleWarnSpy?.mockRestore();
27
+ });
28
+
29
+ // BASIC FUNCTIONALITY TESTS
30
+ test("should save document without showing message", async () => {
31
+ const options = {
32
+ path: "/docs/guide.md",
33
+ content: "# User Guide\n\nThis is a guide.",
34
+ docsDir: "/project/docs",
35
+ translates: [],
36
+ labels: {},
37
+ locale: "en",
38
+ isTranslate: false,
39
+ isShowMessage: false,
40
+ };
41
+
42
+ const result = await saveSingleDoc(options);
43
+
44
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith({
45
+ path: "/docs/guide.md",
46
+ content: "# User Guide\n\nThis is a guide.",
47
+ docsDir: "/project/docs",
48
+ translates: [],
49
+ labels: {},
50
+ locale: "en",
51
+ isTranslate: false,
52
+ });
53
+ expect(shutdownMermaidWorkerPoolSpy).not.toHaveBeenCalled();
54
+ expect(result).toEqual({});
55
+ });
56
+
57
+ test("should save document with success message for regular update", async () => {
58
+ const options = {
59
+ path: "/docs/api.md",
60
+ content: "# API Reference",
61
+ docsDir: "/project/docs",
62
+ translates: ["zh", "ja"],
63
+ labels: { api: "API" },
64
+ locale: "en",
65
+ isTranslate: false,
66
+ isShowMessage: true,
67
+ };
68
+
69
+ const result = await saveSingleDoc(options);
70
+
71
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith({
72
+ path: "/docs/api.md",
73
+ content: "# API Reference",
74
+ docsDir: "/project/docs",
75
+ translates: ["zh", "ja"],
76
+ labels: { api: "API" },
77
+ locale: "en",
78
+ isTranslate: false,
79
+ });
80
+ expect(shutdownMermaidWorkerPoolSpy).toHaveBeenCalled();
81
+ expect(result).toEqual({
82
+ message: "✅ Document updated successfully",
83
+ });
84
+ });
85
+
86
+ test("should save document with success message for translation", async () => {
87
+ const options = {
88
+ path: "/docs/zh/guide.md",
89
+ content: "# 用户指南\n\n这是一个指南。",
90
+ docsDir: "/project/docs",
91
+ translates: ["zh", "ja"],
92
+ labels: { guide: "指南" },
93
+ locale: "zh",
94
+ isTranslate: true,
95
+ isShowMessage: true,
96
+ };
97
+
98
+ const result = await saveSingleDoc(options);
99
+
100
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith({
101
+ path: "/docs/zh/guide.md",
102
+ content: "# 用户指南\n\n这是一个指南。",
103
+ docsDir: "/project/docs",
104
+ translates: ["zh", "ja"],
105
+ labels: { guide: "指南" },
106
+ locale: "zh",
107
+ isTranslate: true,
108
+ });
109
+ expect(shutdownMermaidWorkerPoolSpy).toHaveBeenCalled();
110
+ expect(result).toEqual({
111
+ message: "✅ Translation completed successfully",
112
+ });
113
+ });
114
+
115
+ // DEFAULT VALUES TESTS
116
+ test("should use default values for optional parameters", async () => {
117
+ const options = {
118
+ path: "/docs/minimal.md",
119
+ content: "# Minimal",
120
+ docsDir: "/docs",
121
+ translates: [],
122
+ labels: {},
123
+ locale: "en",
124
+ };
125
+
126
+ const result = await saveSingleDoc(options);
127
+
128
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith({
129
+ path: "/docs/minimal.md",
130
+ content: "# Minimal",
131
+ docsDir: "/docs",
132
+ translates: [],
133
+ labels: {},
134
+ locale: "en",
135
+ isTranslate: false, // Default value
136
+ });
137
+ expect(shutdownMermaidWorkerPoolSpy).not.toHaveBeenCalled();
138
+ expect(result).toEqual({});
139
+ });
140
+
141
+ test("should handle explicit false values", async () => {
142
+ const options = {
143
+ path: "/docs/explicit.md",
144
+ content: "# Explicit",
145
+ docsDir: "/docs",
146
+ translates: [],
147
+ labels: {},
148
+ locale: "en",
149
+ isTranslate: false,
150
+ isShowMessage: false,
151
+ };
152
+
153
+ const result = await saveSingleDoc(options);
154
+
155
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith(
156
+ expect.objectContaining({
157
+ isTranslate: false,
158
+ }),
159
+ );
160
+ expect(result).toEqual({});
161
+ });
162
+
163
+ // MERMAID WORKER POOL SHUTDOWN TESTS
164
+ test("should handle mermaid worker pool shutdown error gracefully", async () => {
165
+ const shutdownError = new Error("Worker pool shutdown failed");
166
+ shutdownMermaidWorkerPoolSpy.mockRejectedValue(shutdownError);
167
+
168
+ const options = {
169
+ path: "/docs/with-error.md",
170
+ content: "# Document with shutdown error",
171
+ docsDir: "/docs",
172
+ translates: [],
173
+ labels: {},
174
+ locale: "en",
175
+ isTranslate: false,
176
+ isShowMessage: true,
177
+ };
178
+
179
+ const result = await saveSingleDoc(options);
180
+
181
+ expect(shutdownMermaidWorkerPoolSpy).toHaveBeenCalled();
182
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
183
+ "Failed to shutdown mermaid worker pool:",
184
+ "Worker pool shutdown failed",
185
+ );
186
+ expect(result).toEqual({
187
+ message: "✅ Document updated successfully",
188
+ });
189
+ });
190
+
191
+ test("should handle mermaid worker pool shutdown error for translation", async () => {
192
+ const shutdownError = new Error("Pool cleanup failed");
193
+ shutdownMermaidWorkerPoolSpy.mockRejectedValue(shutdownError);
194
+
195
+ const options = {
196
+ path: "/docs/zh/with-error.md",
197
+ content: "# 带错误的文档",
198
+ docsDir: "/docs",
199
+ translates: ["zh"],
200
+ labels: {},
201
+ locale: "zh",
202
+ isTranslate: true,
203
+ isShowMessage: true,
204
+ };
205
+
206
+ const result = await saveSingleDoc(options);
207
+
208
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
209
+ "Failed to shutdown mermaid worker pool:",
210
+ "Pool cleanup failed",
211
+ );
212
+ expect(result).toEqual({
213
+ message: "✅ Translation completed successfully",
214
+ });
215
+ });
216
+
217
+ // COMPREHENSIVE PARAMETER TESTS
218
+ test("should pass all parameters correctly to saveDocWithTranslations", async () => {
219
+ const complexOptions = {
220
+ path: "/docs/complex/nested/file.md",
221
+ content: "# Complex Document\n\nWith multiple sections.",
222
+ docsDir: "/project/documentation",
223
+ translates: ["zh-CN", "ja-JP", "ko-KR"],
224
+ labels: {
225
+ title: "标题",
226
+ section: "部分",
227
+ example: "例子",
228
+ },
229
+ locale: "zh-CN",
230
+ isTranslate: true,
231
+ isShowMessage: false,
232
+ };
233
+
234
+ await saveSingleDoc(complexOptions);
235
+
236
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith({
237
+ path: "/docs/complex/nested/file.md",
238
+ content: "# Complex Document\n\nWith multiple sections.",
239
+ docsDir: "/project/documentation",
240
+ translates: ["zh-CN", "ja-JP", "ko-KR"],
241
+ labels: {
242
+ title: "标题",
243
+ section: "部分",
244
+ example: "例子",
245
+ },
246
+ locale: "zh-CN",
247
+ isTranslate: true,
248
+ });
249
+ });
250
+
251
+ // EDGE CASES
252
+ test("should handle empty content", async () => {
253
+ const options = {
254
+ path: "/docs/empty.md",
255
+ content: "",
256
+ docsDir: "/docs",
257
+ translates: [],
258
+ labels: {},
259
+ locale: "en",
260
+ isShowMessage: true,
261
+ };
262
+
263
+ const result = await saveSingleDoc(options);
264
+
265
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith(
266
+ expect.objectContaining({
267
+ content: "",
268
+ }),
269
+ );
270
+ expect(result).toEqual({
271
+ message: "✅ Document updated successfully",
272
+ });
273
+ });
274
+
275
+ test("should handle empty translations array", async () => {
276
+ const options = {
277
+ path: "/docs/no-translations.md",
278
+ content: "# No Translations",
279
+ docsDir: "/docs",
280
+ translates: [],
281
+ labels: {},
282
+ locale: "en",
283
+ };
284
+
285
+ await saveSingleDoc(options);
286
+
287
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith(
288
+ expect.objectContaining({
289
+ translates: [],
290
+ }),
291
+ );
292
+ });
293
+
294
+ test("should handle empty labels object", async () => {
295
+ const options = {
296
+ path: "/docs/no-labels.md",
297
+ content: "# No Labels",
298
+ docsDir: "/docs",
299
+ translates: ["zh"],
300
+ labels: {},
301
+ locale: "en",
302
+ };
303
+
304
+ await saveSingleDoc(options);
305
+
306
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith(
307
+ expect.objectContaining({
308
+ labels: {},
309
+ }),
310
+ );
311
+ });
312
+
313
+ // SPECIAL CHARACTERS AND PATHS
314
+ test("should handle paths with special characters", async () => {
315
+ const options = {
316
+ path: "/docs/特殊字符/file with spaces.md",
317
+ content: "# Special Characters 特殊字符",
318
+ docsDir: "/project/docs",
319
+ translates: ["zh-CN"],
320
+ labels: { special: "特殊" },
321
+ locale: "zh-CN",
322
+ isTranslate: true,
323
+ };
324
+
325
+ await saveSingleDoc(options);
326
+
327
+ expect(saveDocWithTranslationsSpy).toHaveBeenCalledWith(
328
+ expect.objectContaining({
329
+ path: "/docs/特殊字符/file with spaces.md",
330
+ content: "# Special Characters 特殊字符",
331
+ labels: { special: "特殊" },
332
+ locale: "zh-CN",
333
+ }),
334
+ );
335
+ });
336
+
337
+ // RETURN VALUE CONSISTENCY
338
+ test("should always return object structure", async () => {
339
+ const withoutMessage = await saveSingleDoc({
340
+ path: "/docs/test1.md",
341
+ content: "Test",
342
+ docsDir: "/docs",
343
+ translates: [],
344
+ labels: {},
345
+ locale: "en",
346
+ isShowMessage: false,
347
+ });
348
+
349
+ const withMessage = await saveSingleDoc({
350
+ path: "/docs/test2.md",
351
+ content: "Test",
352
+ docsDir: "/docs",
353
+ translates: [],
354
+ labels: {},
355
+ locale: "en",
356
+ isShowMessage: true,
357
+ });
358
+
359
+ expect(typeof withoutMessage).toBe("object");
360
+ expect(typeof withMessage).toBe("object");
361
+ expect(withoutMessage).toEqual({});
362
+ expect(withMessage).toHaveProperty("message");
363
+ });
364
+ });