@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.
Files changed (117) hide show
  1. package/.aigne/doc-smith/output/structure-plan.json +1 -5
  2. package/CHANGELOG.md +14 -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} +25 -13
  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} +17 -11
  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 +413 -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 +517 -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,523 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import * as aigneCore from "@aigne/core";
4
+ import checkDocument from "../../../agents/update/check-document.mjs";
5
+ import * as checkDetailResultModule from "../../../agents/utils/check-detail-result.mjs";
6
+ import * as utils from "../../../utils/utils.mjs";
7
+
8
+ describe("check-document", () => {
9
+ let mockOptions;
10
+
11
+ // Spies for external dependencies
12
+ let teamAgentFromSpy;
13
+
14
+ // Spies for internal utils and fs operations
15
+ let hasSourceFilesChangedSpy;
16
+ let checkDetailResultSpy;
17
+ let consoleSpy;
18
+ let accessSpy;
19
+ let readFileSpy;
20
+
21
+ beforeEach(() => {
22
+ // Set up spy for external dependencies
23
+ teamAgentFromSpy = spyOn(aigneCore.TeamAgent, "from").mockReturnValue({ mockTeamAgent: true });
24
+
25
+ mockOptions = {
26
+ context: {
27
+ agents: {
28
+ generateAndTranslateDocument: { mockAgent: true },
29
+ },
30
+ invoke: mock(async () => ({ mockResult: true })),
31
+ },
32
+ };
33
+
34
+ // Set up spies for internal utils
35
+ hasSourceFilesChangedSpy = spyOn(utils, "hasSourceFilesChanged").mockReturnValue(false);
36
+ checkDetailResultSpy = spyOn(checkDetailResultModule, "default").mockResolvedValue({
37
+ isApproved: true,
38
+ detailFeedback: "",
39
+ });
40
+ consoleSpy = spyOn(console, "log").mockImplementation(() => {});
41
+
42
+ // Use spyOn for fs operations instead of module mocking
43
+ accessSpy = spyOn(fsPromises, "access").mockResolvedValue(undefined);
44
+ readFileSpy = spyOn(fsPromises, "readFile").mockResolvedValue("# Test Content\n\nSome content");
45
+
46
+ // Clear context mock call history
47
+ mockOptions.context.invoke.mockClear();
48
+ });
49
+
50
+ afterEach(() => {
51
+ // Restore all spies
52
+ teamAgentFromSpy?.mockRestore();
53
+ hasSourceFilesChangedSpy?.mockRestore();
54
+ checkDetailResultSpy?.mockRestore();
55
+ consoleSpy?.mockRestore();
56
+ accessSpy?.mockRestore();
57
+ readFileSpy?.mockRestore();
58
+ });
59
+
60
+ // FILE EXISTENCE TESTS
61
+ test("should return early when file exists and no changes detected", async () => {
62
+ // File exists, no changes
63
+ accessSpy.mockResolvedValue();
64
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
65
+
66
+ const result = await checkDocument(
67
+ {
68
+ path: "/getting-started",
69
+ docsDir: "./docs",
70
+ sourceIds: ["file1.js"],
71
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
72
+ documentStructure: [{ path: "/getting-started" }],
73
+ modifiedFiles: [],
74
+ },
75
+ mockOptions,
76
+ );
77
+
78
+ expect(result.detailGenerated).toBe(true);
79
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
80
+ });
81
+
82
+ test("should generate when file does not exist", async () => {
83
+ // File doesn't exist
84
+ accessSpy.mockRejectedValue(new Error("File not found"));
85
+
86
+ const result = await checkDocument(
87
+ {
88
+ path: "/getting-started",
89
+ docsDir: "./docs",
90
+ sourceIds: ["file1.js"],
91
+ },
92
+ mockOptions,
93
+ );
94
+
95
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
96
+ expect(result.path).toBe("/getting-started");
97
+ });
98
+
99
+ // SOURCE IDS CHANGE TESTS
100
+ test("should regenerate when sourceIds have changed", async () => {
101
+ accessSpy.mockResolvedValue();
102
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
103
+
104
+ await checkDocument(
105
+ {
106
+ path: "/getting-started",
107
+ docsDir: "./docs",
108
+ sourceIds: ["file1.js", "file2.js"], // Different from original
109
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
110
+ documentStructure: [{ path: "/getting-started" }],
111
+ },
112
+ mockOptions,
113
+ );
114
+
115
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
116
+ });
117
+
118
+ test("should regenerate when sourceIds count changed", async () => {
119
+ accessSpy.mockResolvedValue();
120
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
121
+
122
+ await checkDocument(
123
+ {
124
+ path: "/getting-started",
125
+ docsDir: "./docs",
126
+ sourceIds: ["file1.js", "file2.js", "file3.js"], // More files
127
+ originalDocumentStructure: [
128
+ { path: "/getting-started", sourceIds: ["file1.js", "file2.js"] },
129
+ ],
130
+ documentStructure: [{ path: "/getting-started" }],
131
+ },
132
+ mockOptions,
133
+ );
134
+
135
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
136
+ });
137
+
138
+ test("should not regenerate when sourceIds are same (different order)", async () => {
139
+ accessSpy.mockResolvedValue();
140
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
141
+
142
+ const result = await checkDocument(
143
+ {
144
+ path: "/getting-started",
145
+ docsDir: "./docs",
146
+ sourceIds: ["file2.js", "file1.js"], // Same files, different order
147
+ originalDocumentStructure: [
148
+ { path: "/getting-started", sourceIds: ["file1.js", "file2.js"] },
149
+ ],
150
+ documentStructure: [{ path: "/getting-started" }],
151
+ modifiedFiles: [],
152
+ },
153
+ mockOptions,
154
+ );
155
+
156
+ expect(result.detailGenerated).toBe(true);
157
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
158
+ });
159
+
160
+ test("should handle missing originalDocumentStructure gracefully", async () => {
161
+ accessSpy.mockResolvedValue();
162
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
163
+
164
+ const result = await checkDocument(
165
+ {
166
+ path: "/getting-started",
167
+ docsDir: "./docs",
168
+ sourceIds: ["file1.js"],
169
+ originalDocumentStructure: null,
170
+ documentStructure: [{ path: "/getting-started" }],
171
+ modifiedFiles: [],
172
+ },
173
+ mockOptions,
174
+ );
175
+
176
+ expect(result.detailGenerated).toBe(true);
177
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
178
+ });
179
+
180
+ test("should handle missing original node in document structure", async () => {
181
+ accessSpy.mockResolvedValue();
182
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
183
+
184
+ const result = await checkDocument(
185
+ {
186
+ path: "/getting-started",
187
+ docsDir: "./docs",
188
+ sourceIds: ["file1.js"],
189
+ originalDocumentStructure: [{ path: "/different-path", sourceIds: ["file1.js"] }],
190
+ documentStructure: [{ path: "/getting-started" }],
191
+ modifiedFiles: [],
192
+ },
193
+ mockOptions,
194
+ );
195
+
196
+ expect(result.detailGenerated).toBe(true);
197
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
198
+ });
199
+
200
+ // SOURCE FILES CHANGE TESTS
201
+ test("should regenerate when source files have changed", async () => {
202
+ accessSpy.mockResolvedValue();
203
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
204
+ hasSourceFilesChangedSpy.mockReturnValue(true);
205
+
206
+ await checkDocument(
207
+ {
208
+ path: "/getting-started",
209
+ docsDir: "./docs",
210
+ sourceIds: ["file1.js"],
211
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
212
+ documentStructure: [{ path: "/getting-started" }],
213
+ modifiedFiles: ["file1.js"],
214
+ },
215
+ mockOptions,
216
+ );
217
+
218
+ expect(hasSourceFilesChangedSpy).toHaveBeenCalledWith(["file1.js"], ["file1.js"]);
219
+ expect(consoleSpy).toHaveBeenCalledWith(
220
+ "Source files changed for /getting-started, will regenerate",
221
+ );
222
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
223
+ });
224
+
225
+ test("should not check source files when no sourceIds provided", async () => {
226
+ accessSpy.mockResolvedValue();
227
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
228
+
229
+ const result = await checkDocument(
230
+ {
231
+ path: "/getting-started",
232
+ docsDir: "./docs",
233
+ sourceIds: [],
234
+ documentStructure: [{ path: "/getting-started" }],
235
+ modifiedFiles: ["file1.js"],
236
+ },
237
+ mockOptions,
238
+ );
239
+
240
+ expect(hasSourceFilesChangedSpy).not.toHaveBeenCalled();
241
+ expect(result.detailGenerated).toBe(true);
242
+ });
243
+
244
+ test("should not check source files when no modifiedFiles provided", async () => {
245
+ accessSpy.mockResolvedValue();
246
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
247
+
248
+ const result = await checkDocument(
249
+ {
250
+ path: "/getting-started",
251
+ docsDir: "./docs",
252
+ sourceIds: ["file1.js"],
253
+ documentStructure: [{ path: "/getting-started" }],
254
+ modifiedFiles: null,
255
+ },
256
+ mockOptions,
257
+ );
258
+
259
+ expect(hasSourceFilesChangedSpy).not.toHaveBeenCalled();
260
+ expect(result.detailGenerated).toBe(true);
261
+ });
262
+
263
+ // CONTENT VALIDATION TESTS
264
+ test("should regenerate when content validation fails", async () => {
265
+ accessSpy.mockResolvedValue();
266
+ readFileSpy.mockResolvedValue("# Test Content");
267
+ checkDetailResultSpy.mockResolvedValue({
268
+ isApproved: false,
269
+ detailFeedback: "Content needs improvement",
270
+ });
271
+
272
+ await checkDocument(
273
+ {
274
+ path: "/getting-started",
275
+ docsDir: "./docs",
276
+ sourceIds: ["file1.js"],
277
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
278
+ documentStructure: [{ path: "/getting-started" }],
279
+ modifiedFiles: [],
280
+ },
281
+ mockOptions,
282
+ );
283
+
284
+ expect(checkDetailResultSpy).toHaveBeenCalledWith({
285
+ documentStructure: [{ path: "/getting-started" }],
286
+ reviewContent: "# Test Content",
287
+ docsDir: "./docs",
288
+ });
289
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
290
+ { mockTeamAgent: true },
291
+ expect.objectContaining({
292
+ detailFeedback: "Content needs improvement",
293
+ }),
294
+ );
295
+ });
296
+
297
+ test("should not validate content when file doesn't exist", async () => {
298
+ accessSpy.mockRejectedValue(new Error("File not found"));
299
+
300
+ await checkDocument(
301
+ {
302
+ path: "/getting-started",
303
+ docsDir: "./docs",
304
+ documentStructure: [{ path: "/getting-started" }],
305
+ },
306
+ mockOptions,
307
+ );
308
+
309
+ expect(checkDetailResultSpy).not.toHaveBeenCalled();
310
+ });
311
+
312
+ test("should not validate content when no documentStructure provided", async () => {
313
+ accessSpy.mockResolvedValue();
314
+ readFileSpy.mockResolvedValue("# Test Content");
315
+
316
+ const result = await checkDocument(
317
+ {
318
+ path: "/getting-started",
319
+ docsDir: "./docs",
320
+ sourceIds: ["file1.js"],
321
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
322
+ documentStructure: null,
323
+ modifiedFiles: [],
324
+ },
325
+ mockOptions,
326
+ );
327
+
328
+ expect(checkDetailResultSpy).not.toHaveBeenCalled();
329
+ expect(result.detailGenerated).toBe(true);
330
+ });
331
+
332
+ // FORCE REGENERATE TESTS
333
+ test("should regenerate when forceRegenerate is true", async () => {
334
+ accessSpy.mockResolvedValue();
335
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
336
+
337
+ await checkDocument(
338
+ {
339
+ path: "/getting-started",
340
+ docsDir: "./docs",
341
+ sourceIds: ["file1.js"],
342
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
343
+ documentStructure: [{ path: "/getting-started" }],
344
+ modifiedFiles: [],
345
+ forceRegenerate: true,
346
+ },
347
+ mockOptions,
348
+ );
349
+
350
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
351
+ });
352
+
353
+ // TEAM AGENT TESTS
354
+ test("should create team agent with correct configuration", async () => {
355
+ accessSpy.mockRejectedValue(new Error("File not found"));
356
+
357
+ await checkDocument(
358
+ {
359
+ path: "/getting-started",
360
+ docsDir: "./docs",
361
+ },
362
+ mockOptions,
363
+ );
364
+
365
+ expect(teamAgentFromSpy).toHaveBeenCalledWith({
366
+ name: "generateDocument",
367
+ skills: [{ mockAgent: true }],
368
+ });
369
+ });
370
+
371
+ test("should invoke team agent with correct parameters", async () => {
372
+ accessSpy.mockRejectedValue(new Error("File not found"));
373
+
374
+ await checkDocument(
375
+ {
376
+ path: "/getting-started",
377
+ docsDir: "./docs",
378
+ sourceIds: ["file1.js"],
379
+ originalDocumentStructure: [{ path: "/getting-started" }],
380
+ documentStructure: [{ path: "/getting-started" }],
381
+ customParam: "test",
382
+ },
383
+ mockOptions,
384
+ );
385
+
386
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
387
+ { mockTeamAgent: true },
388
+ expect.objectContaining({
389
+ path: "/getting-started",
390
+ docsDir: "./docs",
391
+ sourceIds: ["file1.js"],
392
+ originalDocumentStructure: [{ path: "/getting-started" }],
393
+ documentStructure: [{ path: "/getting-started" }],
394
+ customParam: "test",
395
+ detailFeedback: "",
396
+ }),
397
+ );
398
+ });
399
+
400
+ // PATH PROCESSING TESTS
401
+ test("should handle root path correctly", async () => {
402
+ accessSpy.mockRejectedValue(new Error("File not found"));
403
+
404
+ await checkDocument(
405
+ {
406
+ path: "/",
407
+ docsDir: "./docs",
408
+ },
409
+ mockOptions,
410
+ );
411
+
412
+ // Root path "/" -> flatName "" -> fileFullName ".md"
413
+ expect(accessSpy).toHaveBeenCalledWith(expect.stringMatching(/\.md$/));
414
+ });
415
+
416
+ test("should handle nested path correctly", async () => {
417
+ accessSpy.mockRejectedValue(new Error("File not found"));
418
+
419
+ await checkDocument(
420
+ {
421
+ path: "/api/users/create",
422
+ docsDir: "./docs",
423
+ },
424
+ mockOptions,
425
+ );
426
+
427
+ // "/api/users/create" -> flatName "api-users-create" -> fileFullName "api-users-create.md"
428
+ expect(accessSpy).toHaveBeenCalledWith(expect.stringMatching(/api-users-create\.md$/));
429
+ });
430
+
431
+ // RESULT STRUCTURE TESTS
432
+ test("should return correct result structure when regenerating", async () => {
433
+ accessSpy.mockRejectedValue(new Error("File not found"));
434
+ mockOptions.context.invoke.mockResolvedValue({ generatedContent: "test" });
435
+
436
+ const result = await checkDocument(
437
+ {
438
+ path: "/getting-started",
439
+ docsDir: "./docs",
440
+ customParam: "test",
441
+ },
442
+ mockOptions,
443
+ );
444
+
445
+ expect(result).toEqual({
446
+ path: "/getting-started",
447
+ docsDir: "./docs",
448
+ customParam: "test",
449
+ result: { generatedContent: "test" },
450
+ });
451
+ });
452
+
453
+ test("should return correct result structure when not regenerating", async () => {
454
+ accessSpy.mockResolvedValue();
455
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
456
+
457
+ const result = await checkDocument(
458
+ {
459
+ path: "/getting-started",
460
+ docsDir: "./docs",
461
+ sourceIds: ["file1.js"],
462
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
463
+ documentStructure: [{ path: "/getting-started" }],
464
+ modifiedFiles: [],
465
+ customParam: "test",
466
+ },
467
+ mockOptions,
468
+ );
469
+
470
+ expect(result).toEqual({
471
+ path: "/getting-started",
472
+ docsDir: "./docs",
473
+ customParam: "test",
474
+ detailGenerated: true,
475
+ });
476
+ });
477
+
478
+ // EDGE CASES
479
+ test("should handle file read errors gracefully", async () => {
480
+ accessSpy.mockResolvedValue();
481
+ readFileSpy.mockRejectedValue(new Error("Read error"));
482
+
483
+ // When file read fails, content validation is skipped, and since file exists
484
+ // but can't be read properly, no early return occurs, so regeneration happens
485
+ const result = await checkDocument(
486
+ {
487
+ path: "/getting-started",
488
+ docsDir: "./docs",
489
+ sourceIds: ["file1.js"],
490
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: ["file1.js"] }],
491
+ documentStructure: [{ path: "/getting-started" }],
492
+ modifiedFiles: [],
493
+ },
494
+ mockOptions,
495
+ );
496
+
497
+ // When file read fails, no content validation happens, and since no early return
498
+ // condition is met, the function should proceed to regeneration
499
+ expect(result.result).toBeDefined();
500
+ expect(mockOptions.context.invoke).toHaveBeenCalled();
501
+ expect(checkDetailResultSpy).not.toHaveBeenCalled();
502
+ });
503
+
504
+ test("should handle empty sourceIds arrays", async () => {
505
+ accessSpy.mockResolvedValue();
506
+ checkDetailResultSpy.mockResolvedValue({ isApproved: true });
507
+
508
+ const result = await checkDocument(
509
+ {
510
+ path: "/getting-started",
511
+ docsDir: "./docs",
512
+ sourceIds: [],
513
+ originalDocumentStructure: [{ path: "/getting-started", sourceIds: [] }],
514
+ documentStructure: [{ path: "/getting-started" }],
515
+ modifiedFiles: [],
516
+ },
517
+ mockOptions,
518
+ );
519
+
520
+ expect(result.detailGenerated).toBe(true);
521
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
522
+ });
523
+ });
@@ -0,0 +1,51 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { join } from "node:path";
3
+ import { AIAgent } from "@aigne/core";
4
+ import { loadAgent } from "@aigne/core/loader/index.js";
5
+ import { loadModel } from "../../utils/mock-chat-model.mjs";
6
+
7
+ describe("generateDocument Agent", () => {
8
+ beforeAll(() => {
9
+ process.env.AIGNE_OBSERVABILITY_DISABLED = "true";
10
+ });
11
+
12
+ afterAll(() => {
13
+ delete process.env.AIGNE_OBSERVABILITY_DISABLED;
14
+ });
15
+ test("should load agent correctly with proper configuration", async () => {
16
+ const agent = await loadAgent(
17
+ join(import.meta.dirname, "../../../agents/update/generate-document.yaml"),
18
+ {
19
+ model: loadModel,
20
+ },
21
+ );
22
+
23
+ expect(agent).toBeDefined();
24
+
25
+ // Verify agent exists and is correct type
26
+ expect(agent).toBeDefined();
27
+ expect(agent).toBeInstanceOf(AIAgent);
28
+ expect(agent.name).toBe("generateDocument");
29
+ });
30
+
31
+ test("should have instructions loaded from file", async () => {
32
+ const agent = await loadAgent(
33
+ join(import.meta.dirname, "../../../agents/update/generate-document.yaml"),
34
+ {
35
+ model: loadModel,
36
+ },
37
+ );
38
+
39
+ expect(agent).toBeDefined();
40
+
41
+ // Verify instructions are loaded
42
+ expect(agent.instructions).toBeDefined();
43
+ const instructions = await agent.instructions.build({});
44
+ expect(instructions.messages).toBeDefined();
45
+ expect(instructions.messages.length).toBeGreaterThan(0);
46
+
47
+ // The instructions should contain content from the prompt file
48
+ const systemMessage = instructions.messages.find((m) => m.role === "system");
49
+ expect(systemMessage).toBeDefined();
50
+ });
51
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import actionSuccess from "../../../agents/utils/action-success.mjs";
3
+
4
+ describe("action-success", () => {
5
+ test("should return success message with action name", async () => {
6
+ const result = await actionSuccess({ action: "✅ Document generation successfully" });
7
+
8
+ expect(result).toBeDefined();
9
+ expect(result).toHaveProperty("message");
10
+ expect(result.message).toBe("✅ Document generation successfully");
11
+ });
12
+
13
+ test("should handle different action names", async () => {
14
+ const actions = [
15
+ "Configuration setup",
16
+ "File processing",
17
+ "Translation generation",
18
+ "Markdown validation",
19
+ ];
20
+
21
+ for (const action of actions) {
22
+ const result = await actionSuccess({ action });
23
+
24
+ expect(result).toBeDefined();
25
+ expect(result.message).toBe(`${action}`);
26
+ }
27
+ });
28
+
29
+ test("should handle empty action name", async () => {
30
+ const result = await actionSuccess({ action: "" });
31
+
32
+ expect(result).toBeDefined();
33
+ expect(result.message).toBe("");
34
+ });
35
+
36
+ test("should handle undefined action", async () => {
37
+ const result = await actionSuccess({ action: undefined });
38
+
39
+ expect(result).toBeDefined();
40
+ expect(result.message).toBe("undefined");
41
+ });
42
+
43
+ test("should have task_render_mode property", () => {
44
+ expect(actionSuccess.task_render_mode).toBe("hide");
45
+ });
46
+
47
+ test("should handle mermaid worker pool shutdown gracefully", async () => {
48
+ // This test ensures the function doesn't throw even if worker pool fails
49
+ const result = await actionSuccess({ action: "Test action" });
50
+
51
+ expect(result).toBeDefined();
52
+ expect(result.message).toBe("Test action");
53
+ });
54
+ });