@aigne/doc-smith 0.8.6 → 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 (117) hide show
  1. package/.aigne/doc-smith/output/structure-plan.json +1 -5
  2. package/CHANGELOG.md +7 -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} +20 -20
  7. package/agents/{structure-planning.yaml → generate/generate-structure.yaml} +6 -6
  8. package/agents/{docs-generator.yaml → generate/index.yaml} +11 -12
  9. package/agents/generate/refine-document-structure.yaml +12 -0
  10. package/agents/{input-generator.mjs → init/index.mjs} +12 -5
  11. package/agents/{manage-prefs.mjs → prefs/index.mjs} +1 -1
  12. package/agents/{team-publish-docs.yaml → publish/index.yaml} +1 -2
  13. package/agents/{publish-docs.mjs → publish/publish-docs.mjs} +6 -5
  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} +11 -12
  18. package/agents/{translate.yaml → translate/translate-document.yaml} +3 -2
  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} +10 -11
  25. package/agents/{action-success.mjs → utils/action-success.mjs} +1 -1
  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} +24 -9
  29. package/agents/{docs-fs.yaml → utils/docs-fs-actor.yaml} +3 -1
  30. package/agents/{feedback-refiner.yaml → utils/feedback-refiner.yaml} +2 -4
  31. package/agents/{find-item-by-path.mjs → utils/find-item-by-path.mjs} +16 -6
  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-mcp/analyze-docs-relevance.yaml +10 -10
  40. package/docs-mcp/docs-search.yaml +5 -3
  41. package/package.json +10 -8
  42. package/prompts/{document → detail/custom}/custom-code-block.md +6 -6
  43. package/prompts/detail/custom/custom-components.md +172 -0
  44. package/prompts/{document → detail}/d2-chart/rules.md +95 -1
  45. package/prompts/{document → detail}/detail-example.md +80 -61
  46. package/prompts/{document/detail-generator.md → detail/document-rules.md} +4 -8
  47. package/prompts/{content-detail-generator.md → detail/generate-document.md} +48 -25
  48. package/prompts/{check-structure-planning-result.md → structure/check-document-structure.md} +23 -17
  49. package/prompts/{document/structure-planning.md → structure/document-rules.md} +0 -2
  50. package/prompts/{structure-planning.md → structure/generate-structure.md} +51 -30
  51. package/prompts/{document → structure}/structure-example.md +2 -2
  52. package/prompts/{document → structure}/structure-getting-started.md +2 -2
  53. package/prompts/translate/glossary.md +6 -0
  54. package/prompts/{translator.md → translate/translate-document.md} +29 -10
  55. package/prompts/{feedback-refiner.md → utils/feedback-refiner.md} +8 -8
  56. package/tests/agents/chat/chat.test.mjs +46 -0
  57. package/tests/agents/generate/check-document-structure.test.mjs +51 -0
  58. package/tests/agents/generate/check-need-generate-structure.test.mjs +292 -0
  59. package/tests/agents/generate/generate-structure.test.mjs +51 -0
  60. package/tests/{input-generator.test.mjs → agents/init/init.test.mjs} +13 -13
  61. package/tests/agents/prefs/prefs.test.mjs +431 -0
  62. package/tests/agents/publish/publish-docs.test.mjs +642 -0
  63. package/tests/agents/translate/choose-language.test.mjs +311 -0
  64. package/tests/agents/translate/translate-document.test.mjs +51 -0
  65. package/tests/agents/update/check-document.test.mjs +523 -0
  66. package/tests/agents/update/generate-document.test.mjs +51 -0
  67. package/tests/agents/utils/action-success.test.mjs +54 -0
  68. package/tests/{check-detail-result.test.mjs → agents/utils/check-detail-result.test.mjs} +98 -98
  69. package/tests/agents/utils/check-feedback-refiner.test.mjs +478 -0
  70. package/tests/agents/utils/choose-docs.test.mjs +417 -0
  71. package/tests/agents/utils/exit.test.mjs +70 -0
  72. package/tests/agents/utils/feedback-refiner.test.mjs +51 -0
  73. package/tests/agents/utils/find-item-by-path.test.mjs +526 -0
  74. package/tests/agents/utils/find-user-preferences-by-path.test.mjs +382 -0
  75. package/tests/agents/utils/format-document-structure.test.mjs +264 -0
  76. package/tests/agents/utils/fs.test.mjs +267 -0
  77. package/tests/{load-sources.test.mjs → agents/utils/load-sources.test.mjs} +153 -25
  78. package/tests/{save-docs.test.mjs → agents/utils/save-docs.test.mjs} +11 -5
  79. package/tests/agents/utils/save-output.test.mjs +315 -0
  80. package/tests/agents/utils/save-single-doc.test.mjs +364 -0
  81. package/tests/agents/utils/transform-detail-datasources.test.mjs +363 -0
  82. package/tests/utils/auth-utils.test.mjs +358 -0
  83. package/tests/utils/blocklet.test.mjs +334 -0
  84. package/tests/{conflict-resolution.test.mjs → utils/conflict-detector.test.mjs} +3 -3
  85. package/tests/utils/constants.test.mjs +295 -0
  86. package/tests/utils/d2-utils.test.mjs +423 -0
  87. package/tests/{deploy.test.mjs → utils/deploy.test.mjs} +25 -36
  88. package/tests/utils/docs-finder-utils.test.mjs +625 -0
  89. package/tests/utils/file-utils.test.mjs +213 -0
  90. package/tests/{kroki-utils.test.mjs → utils/kroki-utils.test.mjs} +2 -2
  91. package/tests/utils/load-config.test.mjs +141 -0
  92. package/tests/{mermaid-validation.test.mjs → utils/mermaid-validator.test.mjs} +2 -2
  93. package/tests/utils/mock-chat-model.mjs +12 -0
  94. package/tests/{preferences-utils.test.mjs → utils/preferences-utils.test.mjs} +1 -1
  95. package/tests/{save-value-to-config.test.mjs → utils/save-value-to-config.test.mjs} +61 -4
  96. package/tests/utils/utils.test.mjs +939 -0
  97. package/utils/conflict-detector.mjs +1 -1
  98. package/utils/constants.mjs +5 -3
  99. package/utils/d2-utils.mjs +194 -0
  100. package/utils/docs-finder-utils.mjs +26 -26
  101. package/utils/icon-map.mjs +26 -0
  102. package/{agents → utils}/load-config.mjs +2 -18
  103. package/utils/markdown-checker.mjs +5 -5
  104. package/agents/batch-docs-detail-generator.yaml +0 -19
  105. package/agents/check-structure-planning-result.yaml +0 -30
  106. package/agents/content-detail-generator.yaml +0 -50
  107. package/agents/format-structure-plan.mjs +0 -25
  108. package/agents/reflective-structure-planner.yaml +0 -12
  109. package/agents/schema/structure-plan.yaml +0 -26
  110. package/prompts/document/custom-components.md +0 -104
  111. package/tests/README.md +0 -93
  112. package/tests/utils.test.mjs +0 -2067
  113. /package/agents/{exit.mjs → utils/exit.mjs} +0 -0
  114. /package/agents/{fs.mjs → utils/fs.mjs} +0 -0
  115. /package/agents/{save-output.mjs → utils/save-output.mjs} +0 -0
  116. /package/prompts/{document → detail}/d2-chart/official-examples.md +0 -0
  117. /package/prompts/{document → detail}/jsx/rules.md +0 -0
@@ -0,0 +1,267 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import fs from "../../../agents/utils/fs.mjs";
5
+
6
+ describe("fs utility", () => {
7
+ let readFileSpy;
8
+ let writeFileSpy;
9
+ let mkdirSpy;
10
+ let rmSpy;
11
+ let readdirSpy;
12
+ let joinSpy;
13
+ let dirnameSpy;
14
+
15
+ beforeEach(() => {
16
+ // Spy on fs/promises methods
17
+ readFileSpy = spyOn(fsPromises, "readFile").mockResolvedValue("mocked file content");
18
+ writeFileSpy = spyOn(fsPromises, "writeFile").mockResolvedValue();
19
+ mkdirSpy = spyOn(fsPromises, "mkdir").mockResolvedValue();
20
+ rmSpy = spyOn(fsPromises, "rm").mockResolvedValue();
21
+ readdirSpy = spyOn(fsPromises, "readdir").mockResolvedValue([
22
+ { name: "file1.txt", parentPath: "/test/root", isDirectory: () => false },
23
+ { name: "subdir", parentPath: "/test/root", isDirectory: () => true },
24
+ ]);
25
+
26
+ // Spy on path methods
27
+ joinSpy = spyOn(path, "join").mockImplementation((...paths) => paths.join("/"));
28
+ dirnameSpy = spyOn(path, "dirname").mockImplementation(
29
+ (path) => path.split("/").slice(0, -1).join("/") || "/",
30
+ );
31
+ });
32
+
33
+ afterEach(() => {
34
+ // Restore all spies
35
+ readFileSpy?.mockRestore();
36
+ writeFileSpy?.mockRestore();
37
+ mkdirSpy?.mockRestore();
38
+ rmSpy?.mockRestore();
39
+ readdirSpy?.mockRestore();
40
+ joinSpy?.mockRestore();
41
+ dirnameSpy?.mockRestore();
42
+ });
43
+
44
+ // ERROR HANDLING TESTS
45
+ test("should throw error when rootDir is not specified", async () => {
46
+ await expect(
47
+ fs({
48
+ action: "read_file",
49
+ path: "test.txt",
50
+ }),
51
+ ).rejects.toThrow("Root directory is not specified");
52
+ });
53
+
54
+ test("should throw error when rootDir is empty string", async () => {
55
+ await expect(
56
+ fs({
57
+ rootDir: "",
58
+ action: "read_file",
59
+ path: "test.txt",
60
+ }),
61
+ ).rejects.toThrow("Root directory is not specified");
62
+ });
63
+
64
+ // READ_FILE ACTION TESTS
65
+ test("should read file successfully", async () => {
66
+ readFileSpy.mockResolvedValue("test file content");
67
+
68
+ const result = await fs({
69
+ rootDir: "/test/root",
70
+ action: "read_file",
71
+ path: "test.txt",
72
+ });
73
+
74
+ expect(joinSpy).toHaveBeenCalledWith("/test/root", "test.txt");
75
+ expect(readFileSpy).toHaveBeenCalledWith("/test/root/test.txt", "utf-8");
76
+ expect(result).toEqual({
77
+ status: "ok",
78
+ path: "/test/root/test.txt",
79
+ content: "test file content",
80
+ });
81
+ });
82
+
83
+ test("should handle read file with nested path", async () => {
84
+ await fs({
85
+ rootDir: "/project",
86
+ action: "read_file",
87
+ path: "src/components/Button.tsx",
88
+ });
89
+
90
+ expect(joinSpy).toHaveBeenCalledWith("/project", "src/components/Button.tsx");
91
+ expect(readFileSpy).toHaveBeenCalledWith("/project/src/components/Button.tsx", "utf-8");
92
+ });
93
+
94
+ // WRITE_FILE ACTION TESTS
95
+ test("should write file successfully", async () => {
96
+ const result = await fs({
97
+ rootDir: "/test/root",
98
+ action: "write_file",
99
+ path: "output.txt",
100
+ content: "Hello, world!",
101
+ });
102
+
103
+ expect(dirnameSpy).toHaveBeenCalledWith("/test/root/output.txt");
104
+ expect(mkdirSpy).toHaveBeenCalledWith("/test/root", { recursive: true });
105
+ expect(writeFileSpy).toHaveBeenCalledWith("/test/root/output.txt", "Hello, world!");
106
+ expect(result).toEqual({
107
+ status: "ok",
108
+ path: "/test/root/output.txt",
109
+ content: "Hello, world!",
110
+ });
111
+ });
112
+
113
+ test("should write file with empty content when content is not provided", async () => {
114
+ const result = await fs({
115
+ rootDir: "/test/root",
116
+ action: "write_file",
117
+ path: "empty.txt",
118
+ });
119
+
120
+ expect(writeFileSpy).toHaveBeenCalledWith("/test/root/empty.txt", "");
121
+ expect(result.content).toBeUndefined(); // The function returns the original content parameter
122
+ });
123
+
124
+ test("should create directories recursively when writing file", async () => {
125
+ dirnameSpy.mockReturnValue("/test/root/deep/nested");
126
+
127
+ await fs({
128
+ rootDir: "/test/root",
129
+ action: "write_file",
130
+ path: "deep/nested/file.txt",
131
+ content: "nested content",
132
+ });
133
+
134
+ expect(mkdirSpy).toHaveBeenCalledWith("/test/root/deep/nested", {
135
+ recursive: true,
136
+ });
137
+ });
138
+
139
+ // DELETE_FILE ACTION TESTS
140
+ test("should delete file successfully", async () => {
141
+ const result = await fs({
142
+ rootDir: "/test/root",
143
+ action: "delete_file",
144
+ path: "to-delete.txt",
145
+ });
146
+
147
+ expect(joinSpy).toHaveBeenCalledWith("/test/root", "to-delete.txt");
148
+ expect(rmSpy).toHaveBeenCalledWith("/test/root/to-delete.txt", {
149
+ recursive: true,
150
+ force: true,
151
+ });
152
+ expect(result).toEqual({
153
+ status: "ok",
154
+ path: "/test/root/to-delete.txt",
155
+ });
156
+ });
157
+
158
+ test("should delete directory recursively", async () => {
159
+ await fs({
160
+ rootDir: "/project",
161
+ action: "delete_file",
162
+ path: "build",
163
+ });
164
+
165
+ expect(rmSpy).toHaveBeenCalledWith("/project/build", {
166
+ recursive: true,
167
+ force: true,
168
+ });
169
+ });
170
+
171
+ // LIST_DIRECTORY ACTION TESTS
172
+ test("should list directory entries successfully", async () => {
173
+ const result = await fs({
174
+ rootDir: "/test/root",
175
+ action: "list_directory",
176
+ path: "src",
177
+ });
178
+
179
+ expect(joinSpy).toHaveBeenCalledWith("/test/root", "src");
180
+ expect(readdirSpy).toHaveBeenCalledWith("/test/root/src", { withFileTypes: true });
181
+ expect(result).toEqual({
182
+ status: "ok",
183
+ entries: [
184
+ { path: "/test/root/file1.txt", isDirectory: false },
185
+ { path: "/test/root/subdir", isDirectory: true },
186
+ ],
187
+ });
188
+ });
189
+
190
+ test("should handle empty directory listing", async () => {
191
+ readdirSpy.mockResolvedValue([]);
192
+
193
+ const result = await fs({
194
+ rootDir: "/test/root",
195
+ action: "list_directory",
196
+ path: "empty-dir",
197
+ });
198
+
199
+ expect(result.entries).toEqual([]);
200
+ });
201
+
202
+ // PATH HANDLING TESTS
203
+ test("should handle root directory with trailing slash", async () => {
204
+ await fs({
205
+ rootDir: "/test/root/",
206
+ action: "read_file",
207
+ path: "file.txt",
208
+ });
209
+
210
+ expect(joinSpy).toHaveBeenCalledWith("/test/root/", "file.txt");
211
+ });
212
+
213
+ test("should handle path with leading slash", async () => {
214
+ await fs({
215
+ rootDir: "/test/root",
216
+ action: "read_file",
217
+ path: "/absolute/path.txt",
218
+ });
219
+
220
+ expect(joinSpy).toHaveBeenCalledWith("/test/root", "/absolute/path.txt");
221
+ });
222
+
223
+ test("should handle complex nested paths", async () => {
224
+ await fs({
225
+ rootDir: "/project",
226
+ action: "write_file",
227
+ path: "src/components/ui/Button/index.tsx",
228
+ content: "export default Button;",
229
+ });
230
+
231
+ expect(joinSpy).toHaveBeenCalledWith("/project", "src/components/ui/Button/index.tsx");
232
+ });
233
+
234
+ // UNDEFINED ACTION TEST
235
+ test("should return undefined for unknown action", async () => {
236
+ const result = await fs({
237
+ rootDir: "/test/root",
238
+ action: "unknown_action",
239
+ path: "test.txt",
240
+ });
241
+
242
+ expect(result).toBeUndefined();
243
+ });
244
+
245
+ // EDGE CASES
246
+ test("should handle special characters in paths", async () => {
247
+ await fs({
248
+ rootDir: "/test/root",
249
+ action: "read_file",
250
+ path: "文件名 with spaces & symbols!.txt",
251
+ });
252
+
253
+ expect(joinSpy).toHaveBeenCalledWith("/test/root", "文件名 with spaces & symbols!.txt");
254
+ });
255
+
256
+ test("should handle null content for write_file", async () => {
257
+ const result = await fs({
258
+ rootDir: "/test/root",
259
+ action: "write_file",
260
+ path: "null-content.txt",
261
+ content: null,
262
+ });
263
+
264
+ expect(writeFileSpy).toHaveBeenCalledWith("/test/root/null-content.txt", "");
265
+ expect(result.content).toBe(null);
266
+ });
267
+ });
@@ -1,18 +1,19 @@
1
- import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, rm, writeFile } from "node:fs/promises";
1
+ import { afterAll, afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import path, { dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import loadSources from "../agents/load-sources.mjs";
5
+ import loadSources from "../../../agents/utils/load-sources.mjs";
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
 
9
- describe("loadSources", () => {
9
+ describe("load-sources", () => {
10
10
  let testDir;
11
11
  let tempDir;
12
12
 
13
13
  beforeEach(async () => {
14
- // Create test directory structure
15
- testDir = path.join(__dirname, "test-content-generator");
14
+ // Create test directory structure with unique name to avoid conflicts
15
+ const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
16
+ testDir = path.join(__dirname, `test-content-generator-${uniqueId}`);
16
17
  tempDir = path.join(testDir, "temp");
17
18
 
18
19
  await mkdir(testDir, { recursive: true });
@@ -687,6 +688,9 @@ describe("loadSources", () => {
687
688
  await mkdir(deepPath, { recursive: true });
688
689
  await writeFile(path.join(deepPath, "deep.js"), "console.log('deep');");
689
690
 
691
+ const content = await readFile(path.join(deepPath, "deep.js"), "utf8");
692
+ console.log("deep content", content);
693
+
690
694
  const result = await loadSources({
691
695
  sourcesPath: testDir,
692
696
  includePatterns: ["**/*.js"],
@@ -1026,14 +1030,14 @@ describe("loadSources", () => {
1026
1030
  });
1027
1031
  });
1028
1032
 
1029
- describe("Document path and structure plan handling", () => {
1030
- test("should load existing structure plan", async () => {
1031
- const structurePlan = {
1033
+ describe("Document path and document structure handling", () => {
1034
+ test("should load existing document structure", async () => {
1035
+ const documentStructure = {
1032
1036
  sections: ["Introduction", "API", "Examples"],
1033
1037
  lastUpdated: new Date().toISOString(),
1034
1038
  };
1035
1039
 
1036
- await writeFile(path.join(tempDir, "structure-plan.json"), JSON.stringify(structurePlan));
1040
+ await writeFile(path.join(tempDir, "structure-plan.json"), JSON.stringify(documentStructure));
1037
1041
 
1038
1042
  const result = await loadSources({
1039
1043
  sourcesPath: testDir,
@@ -1043,10 +1047,10 @@ describe("loadSources", () => {
1043
1047
  docsDir: path.join(testDir, "docs"),
1044
1048
  });
1045
1049
 
1046
- expect(result.originalStructurePlan).toEqual(structurePlan);
1050
+ expect(result.originalDocumentStructure).toEqual(documentStructure);
1047
1051
  });
1048
1052
 
1049
- test("should handle malformed structure plan JSON", async () => {
1053
+ test("should handle malformed document structure JSON", async () => {
1050
1054
  await writeFile(path.join(tempDir, "structure-plan.json"), "{ invalid json content");
1051
1055
 
1052
1056
  const result = await loadSources({
@@ -1057,7 +1061,120 @@ describe("loadSources", () => {
1057
1061
  docsDir: path.join(testDir, "docs"),
1058
1062
  });
1059
1063
 
1060
- expect(result.originalStructurePlan).toBeUndefined();
1064
+ expect(result.originalDocumentStructure).toBeUndefined();
1065
+ });
1066
+
1067
+ test("should handle non-ENOENT errors when reading document structure JSON", async () => {
1068
+ // Import fs promises module to spy on
1069
+ const fsPromises = await import("node:fs/promises");
1070
+
1071
+ // Get the original readFile function
1072
+ const originalReadFile = fsPromises.readFile;
1073
+
1074
+ // Create a spy that only throws error for structure-plan.json
1075
+ const readFileSpy = spyOn(fsPromises, "readFile").mockImplementation((filePath, encoding) => {
1076
+ if (filePath.includes("structure-plan.json")) {
1077
+ const error = new Error("Permission denied");
1078
+ error.code = "EACCES";
1079
+ throw error;
1080
+ }
1081
+ // For all other files, use the original readFile
1082
+ return originalReadFile(filePath, encoding);
1083
+ });
1084
+
1085
+ try {
1086
+ const result = await loadSources({
1087
+ sourcesPath: testDir,
1088
+ includePatterns: ["*.js"],
1089
+ useDefaultPatterns: false,
1090
+ outputDir: tempDir,
1091
+ docsDir: path.join(testDir, "docs"),
1092
+ });
1093
+
1094
+ expect(result.originalDocumentStructure).toBeUndefined();
1095
+ } finally {
1096
+ // Restore the original readFile function
1097
+ readFileSpy.mockRestore();
1098
+ }
1099
+ });
1100
+
1101
+ test("should handle non-ENOENT errors when reading docPath file", async () => {
1102
+ // Import fs promises module to spy on
1103
+ const fsPromises = await import("node:fs/promises");
1104
+
1105
+ // Get the original readFile function
1106
+ const originalReadFile = fsPromises.readFile;
1107
+
1108
+ // Create a spy that only throws error for the specific docPath file
1109
+ const readFileSpy = spyOn(fsPromises, "readFile").mockImplementation((filePath, encoding) => {
1110
+ // Check if this is the docPath file (api-overview.md)
1111
+ if (filePath.includes("api-overview.md")) {
1112
+ const error = new Error("Permission denied");
1113
+ error.code = "EACCES";
1114
+ throw error;
1115
+ }
1116
+ // For all other files, use the original readFile
1117
+ return originalReadFile(filePath, encoding);
1118
+ });
1119
+
1120
+ try {
1121
+ const result = await loadSources({
1122
+ sourcesPath: testDir,
1123
+ "doc-path": "/api/overview",
1124
+ includePatterns: ["*.js"],
1125
+ useDefaultPatterns: false,
1126
+ outputDir: tempDir,
1127
+ docsDir: path.join(testDir, "docs"),
1128
+ });
1129
+
1130
+ expect(result.content).toBeUndefined();
1131
+ } finally {
1132
+ // Restore the original readFile function
1133
+ readFileSpy.mockRestore();
1134
+ }
1135
+ });
1136
+
1137
+ test("should handle non-ENOENT errors when reading boardId-based docPath file", async () => {
1138
+ // Import fs promises module to spy on
1139
+ const fsPromises = await import("node:fs/promises");
1140
+
1141
+ // Get the original readFile function
1142
+ const originalReadFile = fsPromises.readFile;
1143
+
1144
+ // Create a spy that only throws error for the boardId file
1145
+ const readFileSpy = spyOn(fsPromises, "readFile").mockImplementation((filePath, encoding) => {
1146
+ // First call will fail for the original format (board123-api-overview.md)
1147
+ // Second call should fail for the boardId format (api-overview.md)
1148
+ if (filePath.includes("board123-api-overview.md")) {
1149
+ const error = new Error("File not found");
1150
+ error.code = "ENOENT";
1151
+ throw error;
1152
+ }
1153
+ if (filePath.includes("api-overview.md") && !filePath.includes("board123-")) {
1154
+ const error = new Error("Permission denied");
1155
+ error.code = "EACCES";
1156
+ throw error;
1157
+ }
1158
+ // For all other files, use the original readFile
1159
+ return originalReadFile(filePath, encoding);
1160
+ });
1161
+
1162
+ try {
1163
+ const result = await loadSources({
1164
+ sourcesPath: testDir,
1165
+ "doc-path": "board123-api-overview",
1166
+ boardId: "board123",
1167
+ includePatterns: ["*.js"],
1168
+ useDefaultPatterns: false,
1169
+ outputDir: tempDir,
1170
+ docsDir: path.join(testDir, "docs"),
1171
+ });
1172
+
1173
+ expect(result.content).toBeUndefined();
1174
+ } finally {
1175
+ // Restore the original readFile function
1176
+ readFileSpy.mockRestore();
1177
+ }
1061
1178
  });
1062
1179
 
1063
1180
  test("should load document content by docPath", async () => {
@@ -1323,20 +1440,31 @@ describe("loadSources", () => {
1323
1440
 
1324
1441
  // Global cleanup to ensure test directories are fully removed
1325
1442
  afterAll(async () => {
1326
- const testDirBase = path.join(__dirname, "test-content-generator");
1443
+ // Clean up all test-content-generator directories (including unique ones)
1327
1444
  try {
1328
- await rm(testDirBase, { recursive: true, force: true });
1329
- } catch {
1330
- // Try with system command as final fallback
1331
- try {
1332
- const { exec } = await import("node:child_process");
1333
- const { promisify } = await import("node:util");
1334
- const execAsync = promisify(exec);
1335
- await execAsync(`rm -rf "${testDirBase}"`);
1336
- } catch {
1337
- // If we still can't clean up, warn but don't fail
1338
- console.warn(`Warning: Could not fully clean up test directory: ${testDirBase}`);
1445
+ const { readdir } = await import("node:fs/promises");
1446
+ const entries = await readdir(__dirname, { withFileTypes: true });
1447
+
1448
+ for (const entry of entries) {
1449
+ if (entry.isDirectory() && entry.name.startsWith("test-content-generator")) {
1450
+ const testDirToClean = path.join(__dirname, entry.name);
1451
+ try {
1452
+ await rm(testDirToClean, { recursive: true, force: true });
1453
+ } catch {
1454
+ // Try with system command as fallback
1455
+ try {
1456
+ const { exec } = await import("node:child_process");
1457
+ const { promisify } = await import("node:util");
1458
+ const execAsync = promisify(exec);
1459
+ await execAsync(`rm -rf "${testDirToClean}"`);
1460
+ } catch {
1461
+ console.warn(`Warning: Could not fully clean up test directory: ${testDirToClean}`);
1462
+ }
1463
+ }
1464
+ }
1339
1465
  }
1466
+ } catch (error) {
1467
+ console.warn(`Warning: Could not clean up test directories: ${error.message}`);
1340
1468
  }
1341
1469
  });
1342
1470
  });
@@ -2,14 +2,17 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import saveDocs from "../agents/save-docs.mjs";
5
+ import saveDocs from "../../../agents/utils/save-docs.mjs";
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
 
9
- describe("saveDocs", () => {
9
+ describe("save-docs", () => {
10
10
  let testDir;
11
11
 
12
12
  beforeEach(async () => {
13
+ // Set test environment variable to prevent actual config file modification
14
+ process.env.NODE_ENV = "test";
15
+
13
16
  // Create a temporary test directory
14
17
  testDir = join(__dirname, "test-docs");
15
18
  await mkdir(testDir, { recursive: true });
@@ -32,6 +35,9 @@ describe("saveDocs", () => {
32
35
  });
33
36
 
34
37
  afterEach(async () => {
38
+ // Clean up environment variable
39
+ delete process.env.NODE_ENV;
40
+
35
41
  // Clean up test directory
36
42
  try {
37
43
  await rm(testDir, { recursive: true, force: true });
@@ -46,8 +52,8 @@ describe("saveDocs", () => {
46
52
  expect(initialFiles).toContain("getting-started.md");
47
53
  expect(initialFiles).toContain("old-file.md");
48
54
 
49
- // Test structure plan
50
- const structurePlan = [
55
+ // Test document structure
56
+ const documentStructure = [
51
57
  {
52
58
  path: "/overview",
53
59
  title: "Overview",
@@ -64,7 +70,7 @@ describe("saveDocs", () => {
64
70
  const translateLanguages = ["zh", "en"];
65
71
 
66
72
  const result = await saveDocs({
67
- structurePlanResult: structurePlan,
73
+ documentExecutionStructure: documentStructure,
68
74
  docsDir: testDir,
69
75
  translateLanguages,
70
76
  });