@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
|
@@ -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
|
+
});
|