@aigne/doc-smith 0.8.11-beta → 0.8.11-beta.2

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 (257) hide show
  1. package/.aigne/doc-smith/config.yaml +2 -0
  2. package/.aigne/doc-smith/output/structure-plan.json +3 -3
  3. package/.aigne/doc-smith/upload-cache.yaml +252 -0
  4. package/.github/workflows/publish-docs.yml +67 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +22 -0
  7. package/README.md +45 -115
  8. package/agents/clear/choose-contents.mjs +170 -0
  9. package/agents/clear/clear-auth-tokens.mjs +111 -0
  10. package/agents/clear/clear-document-config.mjs +39 -0
  11. package/agents/clear/clear-document-structure.mjs +106 -0
  12. package/agents/clear/clear-generated-docs.mjs +51 -0
  13. package/agents/clear/index.yaml +23 -0
  14. package/agents/evaluate/code-snippet.mjs +93 -0
  15. package/agents/evaluate/document-structure.yaml +70 -0
  16. package/agents/evaluate/document.yaml +79 -0
  17. package/agents/evaluate/generate-report.mjs +78 -0
  18. package/agents/evaluate/index.yaml +39 -0
  19. package/agents/generate/document-structure-tools/add-document.mjs +56 -0
  20. package/agents/generate/document-structure-tools/delete-document.mjs +49 -0
  21. package/agents/generate/document-structure-tools/move-document.mjs +82 -0
  22. package/agents/generate/document-structure-tools/update-document.mjs +50 -0
  23. package/agents/generate/generate-structure.yaml +1 -1
  24. package/agents/generate/update-document-structure.yaml +42 -0
  25. package/agents/generate/user-review-document-structure.mjs +6 -4
  26. package/agents/init/index.mjs +1 -1
  27. package/agents/publish/publish-docs.mjs +12 -3
  28. package/agents/translate/choose-language.mjs +1 -1
  29. package/agents/update/batch-update-document.yaml +7 -0
  30. package/agents/update/check-update-is-single.mjs +38 -0
  31. package/agents/update/document-tools/update-document-content.mjs +293 -0
  32. package/agents/update/index.yaml +4 -10
  33. package/agents/update/update-document-detail.yaml +52 -0
  34. package/agents/update/update-single-document.yaml +15 -0
  35. package/agents/update/user-review-document.mjs +248 -0
  36. package/agents/utils/choose-docs.mjs +4 -2
  37. package/agents/utils/format-document-structure.mjs +12 -2
  38. package/agents/utils/load-document-all-content.mjs +84 -0
  39. package/agents/utils/load-sources.mjs +4 -1
  40. package/aigne.yaml +59 -20
  41. package/assets/report-template/report.html +198 -0
  42. package/biome.json +14 -2
  43. package/docs/advanced-how-it-works.ja.md +101 -0
  44. package/docs/advanced-how-it-works.zh-TW.md +101 -0
  45. package/docs/advanced-how-it-works.zh.md +20 -20
  46. package/docs/advanced-quality-assurance.ja.md +96 -0
  47. package/docs/advanced-quality-assurance.zh-TW.md +96 -0
  48. package/docs/advanced-quality-assurance.zh.md +18 -18
  49. package/docs/advanced.ja.md +16 -0
  50. package/docs/advanced.zh-TW.md +16 -0
  51. package/docs/advanced.zh.md +4 -4
  52. package/docs/changelog.ja.md +309 -0
  53. package/docs/changelog.zh-TW.md +309 -0
  54. package/docs/changelog.zh.md +23 -23
  55. package/docs/cli-reference.ja.md +210 -0
  56. package/docs/cli-reference.zh-TW.md +210 -0
  57. package/docs/cli-reference.zh.md +21 -21
  58. package/docs/configuration-interactive-setup.ja.md +135 -0
  59. package/docs/configuration-interactive-setup.zh-TW.md +135 -0
  60. package/docs/configuration-interactive-setup.zh.md +29 -29
  61. package/docs/configuration-language-support.ja.md +94 -0
  62. package/docs/configuration-language-support.zh-TW.md +94 -0
  63. package/docs/configuration-language-support.zh.md +13 -13
  64. package/docs/configuration-llm-setup.ja.md +54 -0
  65. package/docs/configuration-llm-setup.zh-TW.md +54 -0
  66. package/docs/configuration-llm-setup.zh.md +12 -12
  67. package/docs/configuration-preferences.ja.md +129 -0
  68. package/docs/configuration-preferences.zh-TW.md +129 -0
  69. package/docs/configuration-preferences.zh.md +36 -36
  70. package/docs/configuration.ja.md +172 -0
  71. package/docs/configuration.zh-TW.md +172 -0
  72. package/docs/configuration.zh.md +49 -49
  73. package/docs/features-generate-documentation.ja.md +101 -0
  74. package/docs/features-generate-documentation.zh-TW.md +101 -0
  75. package/docs/features-generate-documentation.zh.md +17 -17
  76. package/docs/features-publish-your-docs.ja.md +107 -0
  77. package/docs/features-publish-your-docs.zh-TW.md +107 -0
  78. package/docs/features-publish-your-docs.zh.md +22 -22
  79. package/docs/features-translate-documentation.ja.md +79 -0
  80. package/docs/features-translate-documentation.zh-TW.md +79 -0
  81. package/docs/features-translate-documentation.zh.md +12 -12
  82. package/docs/features-update-and-refine.ja.md +138 -0
  83. package/docs/features-update-and-refine.zh-TW.md +138 -0
  84. package/docs/features-update-and-refine.zh.md +21 -21
  85. package/docs/features.ja.md +52 -0
  86. package/docs/features.zh-TW.md +52 -0
  87. package/docs/features.zh.md +8 -8
  88. package/docs/getting-started.ja.md +123 -0
  89. package/docs/getting-started.zh-TW.md +123 -0
  90. package/docs/getting-started.zh.md +24 -24
  91. package/docs/overview.ja.md +30 -0
  92. package/docs/overview.zh-TW.md +30 -0
  93. package/docs/overview.zh.md +8 -8
  94. package/package.json +19 -11
  95. package/prompts/common/document/content-rules-core.md +19 -0
  96. package/prompts/common/document/media-handling-rules.md +9 -0
  97. package/prompts/common/document/role-and-personality.md +15 -0
  98. package/prompts/common/document/user-preferences.md +9 -0
  99. package/prompts/common/document-structure/conflict-resolution-guidance.md +16 -0
  100. package/prompts/common/document-structure/document-structure-rules.md +45 -0
  101. package/prompts/common/document-structure/glossary.md +7 -0
  102. package/prompts/common/document-structure/intj-traits.md +5 -0
  103. package/prompts/common/document-structure/output-constraints.md +9 -0
  104. package/prompts/common/document-structure/user-locale-rules.md +10 -0
  105. package/prompts/common/document-structure/user-preferences.md +9 -0
  106. package/prompts/detail/custom/custom-components.md +9 -1
  107. package/prompts/detail/document-rules.md +6 -6
  108. package/prompts/detail/generate-document.md +5 -45
  109. package/prompts/detail/update-document.md +145 -0
  110. package/prompts/evaluate/document-structure.md +94 -0
  111. package/prompts/evaluate/document.md +149 -0
  112. package/prompts/structure/document-rules.md +1 -1
  113. package/prompts/structure/generate-structure-system.md +74 -0
  114. package/prompts/structure/generate-structure-user.md +41 -0
  115. package/prompts/structure/update-document-structure.md +118 -0
  116. package/prompts/translate/translate-document.md +1 -1
  117. package/prompts/utils/feedback-refiner.md +3 -3
  118. package/release-please-config.json +1 -7
  119. package/tests/agents/clear/choose-contents.test.mjs +280 -0
  120. package/tests/agents/clear/clear-auth-tokens.test.mjs +268 -0
  121. package/tests/agents/clear/clear-document-config.test.mjs +167 -0
  122. package/tests/agents/clear/clear-document-structure.test.mjs +374 -0
  123. package/tests/agents/clear/clear-generated-docs.test.mjs +222 -0
  124. package/tests/agents/evaluate/code-snippet.test.mjs +163 -0
  125. package/tests/agents/evaluate/fixtures/api-services.md +87 -0
  126. package/tests/agents/evaluate/fixtures/js-sdk.md +94 -0
  127. package/tests/agents/evaluate/generate-report.test.mjs +312 -0
  128. package/tests/agents/generate/check-document-structure.test.mjs +0 -6
  129. package/tests/agents/generate/document-structure-tools/add-document.test.mjs +449 -0
  130. package/tests/agents/generate/document-structure-tools/delete-document.test.mjs +410 -0
  131. package/tests/agents/generate/document-structure-tools/move-document.test.mjs +476 -0
  132. package/tests/agents/generate/document-structure-tools/update-document.test.mjs +548 -0
  133. package/tests/agents/generate/generate-structure.test.mjs +0 -6
  134. package/tests/agents/generate/user-review-document-structure.test.mjs +9 -9
  135. package/tests/agents/publish/publish-docs.test.mjs +2 -2
  136. package/tests/agents/update/check-update-is-single.test.mjs +300 -0
  137. package/tests/agents/update/document-tools/update-document-content.test.mjs +326 -0
  138. package/tests/agents/update/user-review-document.test.mjs +561 -0
  139. package/tests/agents/utils/format-document-structure.test.mjs +100 -0
  140. package/tests/utils/auth-utils.test.mjs +239 -1
  141. package/tests/utils/blocklet.test.mjs +9 -7
  142. package/tests/utils/constants.test.mjs +1 -1
  143. package/tests/utils/d2-utils.test.mjs +1 -1
  144. package/tests/utils/deploy.test.mjs +310 -366
  145. package/tests/utils/kroki-utils.test.mjs +2 -15
  146. package/tests/utils/linter/fixtures/css/keyword-error.css +1 -0
  147. package/tests/utils/linter/fixtures/css/missing-semicolon.css +1 -0
  148. package/tests/utils/linter/fixtures/css/syntax-error.css +1 -0
  149. package/tests/utils/linter/fixtures/css/undeclare-variable.css +1 -0
  150. package/tests/utils/linter/fixtures/css/unused-variable.css +2 -0
  151. package/tests/utils/linter/fixtures/css/valid-code.css +1 -0
  152. package/tests/utils/linter/fixtures/dockerfile/keyword-error.dockerfile +1 -0
  153. package/tests/utils/linter/fixtures/dockerfile/missing-semicolon.dockerfile +2 -0
  154. package/tests/utils/linter/fixtures/dockerfile/syntax-error.dockerfile +2 -0
  155. package/tests/utils/linter/fixtures/dockerfile/undeclare-variable.dockerfile +1 -0
  156. package/tests/utils/linter/fixtures/dockerfile/unused-variable.dockerfile +1 -0
  157. package/tests/utils/linter/fixtures/dockerfile/valid-code.dockerfile +2 -0
  158. package/tests/utils/linter/fixtures/go/keyword-error.go +5 -0
  159. package/tests/utils/linter/fixtures/go/missing-semicolon.go +5 -0
  160. package/tests/utils/linter/fixtures/go/syntax-error.go +6 -0
  161. package/tests/utils/linter/fixtures/go/undeclare-variable.go +5 -0
  162. package/tests/utils/linter/fixtures/go/unused-variable.go +5 -0
  163. package/tests/utils/linter/fixtures/go/valid-code.go +7 -0
  164. package/tests/utils/linter/fixtures/js/keyword-error.js +3 -0
  165. package/tests/utils/linter/fixtures/js/missing-semicolon.js +6 -0
  166. package/tests/utils/linter/fixtures/js/syntax-error.js +4 -0
  167. package/tests/utils/linter/fixtures/js/undeclare-variable.js +3 -0
  168. package/tests/utils/linter/fixtures/js/unused-variable.js +7 -0
  169. package/tests/utils/linter/fixtures/js/valid-code.js +15 -0
  170. package/tests/utils/linter/fixtures/json/keyword-error.json +1 -0
  171. package/tests/utils/linter/fixtures/json/missing-semicolon.json +1 -0
  172. package/tests/utils/linter/fixtures/json/syntax-error.json +1 -0
  173. package/tests/utils/linter/fixtures/json/undeclare-variable.json +1 -0
  174. package/tests/utils/linter/fixtures/json/unused-variable.json +1 -0
  175. package/tests/utils/linter/fixtures/json/valid-code.json +1 -0
  176. package/tests/utils/linter/fixtures/jsx/keyword-error.jsx +5 -0
  177. package/tests/utils/linter/fixtures/jsx/missing-semicolon.jsx +5 -0
  178. package/tests/utils/linter/fixtures/jsx/syntax-error.jsx +5 -0
  179. package/tests/utils/linter/fixtures/jsx/undeclare-variable.jsx +5 -0
  180. package/tests/utils/linter/fixtures/jsx/unused-variable.jsx +4 -0
  181. package/tests/utils/linter/fixtures/jsx/valid-code.jsx +5 -0
  182. package/tests/utils/linter/fixtures/python/keyword-error.py +3 -0
  183. package/tests/utils/linter/fixtures/python/missing-semicolon.py +2 -0
  184. package/tests/utils/linter/fixtures/python/syntax-error.py +3 -0
  185. package/tests/utils/linter/fixtures/python/undeclare-variable.py +3 -0
  186. package/tests/utils/linter/fixtures/python/unused-variable.py +6 -0
  187. package/tests/utils/linter/fixtures/python/valid-code.py +12 -0
  188. package/tests/utils/linter/fixtures/ruby/keyword-error.rb +2 -0
  189. package/tests/utils/linter/fixtures/ruby/missing-semicolon.rb +1 -0
  190. package/tests/utils/linter/fixtures/ruby/syntax-error.rb +2 -0
  191. package/tests/utils/linter/fixtures/ruby/undeclare-variable.rb +1 -0
  192. package/tests/utils/linter/fixtures/ruby/unused-variable.rb +2 -0
  193. package/tests/utils/linter/fixtures/ruby/valid-code.rb +1 -0
  194. package/tests/utils/linter/fixtures/sass/keyword-error.sass +2 -0
  195. package/tests/utils/linter/fixtures/sass/missing-semicolon.sass +3 -0
  196. package/tests/utils/linter/fixtures/sass/syntax-error.sass +3 -0
  197. package/tests/utils/linter/fixtures/sass/undeclare-variable.sass +2 -0
  198. package/tests/utils/linter/fixtures/sass/unused-variable.sass +4 -0
  199. package/tests/utils/linter/fixtures/sass/valid-code.sass +2 -0
  200. package/tests/utils/linter/fixtures/scss/keyword-error.scss +1 -0
  201. package/tests/utils/linter/fixtures/scss/missing-semicolon.scss +1 -0
  202. package/tests/utils/linter/fixtures/scss/syntax-error.scss +1 -0
  203. package/tests/utils/linter/fixtures/scss/undeclare-variable.scss +1 -0
  204. package/tests/utils/linter/fixtures/scss/unused-variable.scss +2 -0
  205. package/tests/utils/linter/fixtures/scss/valid-code.scss +1 -0
  206. package/tests/utils/linter/fixtures/shell/keyword-error.sh +5 -0
  207. package/tests/utils/linter/fixtures/shell/missing-semicolon.sh +3 -0
  208. package/tests/utils/linter/fixtures/shell/syntax-error.sh +4 -0
  209. package/tests/utils/linter/fixtures/shell/undeclare-variable.sh +3 -0
  210. package/tests/utils/linter/fixtures/shell/unused-variable.sh +4 -0
  211. package/tests/utils/linter/fixtures/shell/valid-code.sh +3 -0
  212. package/tests/utils/linter/fixtures/ts/keyword-error.ts +1 -0
  213. package/tests/utils/linter/fixtures/ts/missing-semicolon.ts +1 -0
  214. package/tests/utils/linter/fixtures/ts/syntax-error.ts +1 -0
  215. package/tests/utils/linter/fixtures/ts/undeclare-variable.ts +1 -0
  216. package/tests/utils/linter/fixtures/ts/unused-variable.ts +3 -0
  217. package/tests/utils/linter/fixtures/ts/valid-code.ts +3 -0
  218. package/tests/utils/linter/fixtures/tsx/keyword-error.tsx +5 -0
  219. package/tests/utils/linter/fixtures/tsx/missing-semicolon.tsx +5 -0
  220. package/tests/utils/linter/fixtures/tsx/syntax-error.tsx +5 -0
  221. package/tests/utils/linter/fixtures/tsx/undeclare-variable.tsx +6 -0
  222. package/tests/utils/linter/fixtures/tsx/unused-variable.tsx +6 -0
  223. package/tests/utils/linter/fixtures/tsx/valid-code.tsx +5 -0
  224. package/tests/utils/linter/fixtures/vue/keyword-error.vue +6 -0
  225. package/tests/utils/linter/fixtures/vue/missing-semicolon.vue +6 -0
  226. package/tests/utils/linter/fixtures/vue/syntax-error.vue +6 -0
  227. package/tests/utils/linter/fixtures/vue/undeclare-variable.vue +6 -0
  228. package/tests/utils/linter/fixtures/vue/unused-variable.vue +7 -0
  229. package/tests/utils/linter/fixtures/vue/valid-code.vue +6 -0
  230. package/tests/utils/linter/fixtures/yaml/keyword-error.yml +1 -0
  231. package/tests/utils/linter/fixtures/yaml/missing-semicolon.yml +2 -0
  232. package/tests/utils/linter/fixtures/yaml/syntax-error.yml +1 -0
  233. package/tests/utils/linter/fixtures/yaml/undeclare-variable.yml +1 -0
  234. package/tests/utils/linter/fixtures/yaml/unused-variable.yml +2 -0
  235. package/tests/utils/linter/fixtures/yaml/valid-code.yml +3 -0
  236. package/tests/utils/linter/index.test.mjs +440 -0
  237. package/tests/utils/linter/scan-results.mjs +42 -0
  238. package/tests/utils/markdown/index.test.mjs +478 -0
  239. package/tests/utils/mermaid-validator.test.mjs +2 -2
  240. package/tests/utils/utils.test.mjs +3 -1
  241. package/types/document-schema.mjs +54 -0
  242. package/types/document-structure-schema.mjs +244 -0
  243. package/utils/auth-utils.mjs +131 -6
  244. package/utils/conflict-detector.mjs +5 -1
  245. package/utils/{constants.mjs → constants/index.mjs} +109 -0
  246. package/utils/constants/linter.mjs +102 -0
  247. package/utils/d2-utils.mjs +2 -4
  248. package/utils/debug.mjs +3 -0
  249. package/utils/deploy.mjs +81 -385
  250. package/utils/evaluate/report-utils.mjs +131 -0
  251. package/utils/file-utils.mjs +36 -1
  252. package/utils/kroki-utils.mjs +1 -1
  253. package/utils/linter/index.mjs +50 -0
  254. package/utils/markdown/index.mjs +26 -0
  255. package/utils/markdown-checker.mjs +1 -1
  256. package/utils/utils.mjs +19 -7
  257. package/prompts/structure/generate-structure.md +0 -161
@@ -0,0 +1,561 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import * as markedModule from "marked";
3
+ import userReviewDocument from "../../../agents/update/user-review-document.mjs";
4
+ import * as preferencesUtils from "../../../utils/preferences-utils.mjs";
5
+
6
+ describe("user-review-document", () => {
7
+ let mockOptions;
8
+ let mockContent;
9
+
10
+ // Spies for internal utils
11
+ let getActiveRulesForScopeSpy;
12
+ let consoleSpy;
13
+ let consoleErrorSpy;
14
+ let consoleWarnSpy;
15
+ let markedLexerSpy;
16
+ let markedSpy;
17
+
18
+ beforeEach(() => {
19
+ // Reset all mocks
20
+ mock.restore();
21
+
22
+ mockContent =
23
+ "# Getting Started\n\n## Installation\n\nThis is a test document.\n\n### Prerequisites\n\nSome prerequisites here.";
24
+
25
+ mockOptions = {
26
+ prompts: {
27
+ select: mock(async () => "finish"),
28
+ input: mock(async () => ""),
29
+ },
30
+ context: {
31
+ agents: {
32
+ updateDocumentDetail: {},
33
+ checkFeedbackRefiner: {},
34
+ },
35
+ invoke: mock(async () => ({
36
+ updatedContent: "# Updated Content\n\nThis is updated content.",
37
+ operationSummary: "Document updated successfully",
38
+ })),
39
+ },
40
+ };
41
+
42
+ // Set up spies for internal utils
43
+ getActiveRulesForScopeSpy = spyOn(preferencesUtils, "getActiveRulesForScope").mockReturnValue(
44
+ [],
45
+ );
46
+ consoleSpy = spyOn(console, "log").mockImplementation(() => {});
47
+ consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
48
+ consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
49
+
50
+ // Mock marked library
51
+ markedLexerSpy = spyOn(markedModule.marked, "lexer").mockImplementation(() => [
52
+ { type: "heading", depth: 1, text: "Getting Started" },
53
+ { type: "heading", depth: 2, text: "Installation" },
54
+ { type: "heading", depth: 3, text: "Prerequisites" },
55
+ ]);
56
+ markedSpy = spyOn(markedModule.marked, "setOptions").mockImplementation(() => {});
57
+
58
+ // Clear context mock call history
59
+ mockOptions.prompts.select.mockClear();
60
+ mockOptions.prompts.input.mockClear();
61
+ mockOptions.context.invoke.mockClear();
62
+ });
63
+
64
+ afterEach(() => {
65
+ // Restore all spies
66
+ getActiveRulesForScopeSpy?.mockRestore();
67
+ consoleSpy?.mockRestore();
68
+ consoleErrorSpy?.mockRestore();
69
+ consoleWarnSpy?.mockRestore();
70
+ markedLexerSpy?.mockRestore();
71
+ markedSpy?.mockRestore();
72
+ });
73
+
74
+ // CONTENT VALIDATION TESTS
75
+ test("should return original content when no content provided", async () => {
76
+ const result = await userReviewDocument({}, mockOptions);
77
+
78
+ expect(result).toBeDefined();
79
+ expect(result.content).toBeUndefined();
80
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
81
+ expect(consoleSpy).toHaveBeenCalledWith("Please provide document content to review.");
82
+ });
83
+
84
+ test("should return original content when empty content provided", async () => {
85
+ const result = await userReviewDocument({ content: "" }, mockOptions);
86
+
87
+ expect(result).toBeDefined();
88
+ expect(result.content).toBe("");
89
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
90
+ expect(consoleSpy).toHaveBeenCalledWith("Please provide document content to review.");
91
+ });
92
+
93
+ test("should return original content when only whitespace provided", async () => {
94
+ const result = await userReviewDocument({ content: " \n\t " }, mockOptions);
95
+
96
+ expect(result).toBeDefined();
97
+ expect(result.content).toBe(" \n\t ");
98
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
99
+ expect(consoleSpy).toHaveBeenCalledWith("Please provide document content to review.");
100
+ });
101
+
102
+ // HEADING EXTRACTION TESTS
103
+ test("should extract markdown headings correctly", async () => {
104
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
105
+
106
+ const result = await userReviewDocument(
107
+ { content: mockContent, title: "Test Doc" },
108
+ mockOptions,
109
+ );
110
+
111
+ expect(result).toBeDefined();
112
+ expect(result.content).toBe(mockContent);
113
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Current Document: Test Doc"));
114
+ expect(markedLexerSpy).toHaveBeenCalledWith(mockContent);
115
+ });
116
+
117
+ test("should handle markdown parsing errors with fallback", async () => {
118
+ markedLexerSpy.mockImplementation(() => {
119
+ throw new Error("Parsing error");
120
+ });
121
+
122
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
123
+
124
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
125
+
126
+ expect(result).toBeDefined();
127
+ expect(result.content).toBe(mockContent);
128
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
129
+ "Failed to parse markdown with marked library, falling back to regex:",
130
+ "Parsing error",
131
+ );
132
+ });
133
+
134
+ test("should handle empty content in heading extraction", async () => {
135
+ const emptyContent = "";
136
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
137
+
138
+ markedLexerSpy.mockReturnValue([]);
139
+
140
+ const result = await userReviewDocument({ content: emptyContent }, mockOptions);
141
+
142
+ expect(result.content).toBe(emptyContent);
143
+ expect(consoleSpy).toHaveBeenCalledWith("Please provide document content to review.");
144
+ });
145
+
146
+ // USER INTERACTION TESTS
147
+ test("should finish immediately when user selects finish", async () => {
148
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
149
+
150
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
151
+
152
+ expect(result.content).toBe(mockContent);
153
+ expect(mockOptions.prompts.select).toHaveBeenCalledTimes(1);
154
+ expect(mockOptions.prompts.input).not.toHaveBeenCalled();
155
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
156
+ });
157
+
158
+ test("should show document details when user selects view", async () => {
159
+ mockOptions.prompts.select.mockImplementation(async () => "view");
160
+ mockOptions.prompts.input.mockImplementation(async () => ""); // Empty feedback after view
161
+
162
+ // Mock the marked function directly
163
+ const markedFunctionSpy = spyOn(markedModule, "marked").mockReturnValue(
164
+ "Rendered markdown content",
165
+ );
166
+
167
+ const result = await userReviewDocument(
168
+ { content: mockContent, title: "Test Doc" },
169
+ mockOptions,
170
+ );
171
+
172
+ expect(result.content).toBe(mockContent);
173
+ expect(mockOptions.prompts.select).toHaveBeenCalledTimes(1);
174
+ expect(mockOptions.prompts.input).toHaveBeenCalledTimes(1);
175
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Document: Test Doc"));
176
+
177
+ markedFunctionSpy?.mockRestore();
178
+ });
179
+
180
+ test("should handle marked-terminal rendering errors gracefully", async () => {
181
+ mockOptions.prompts.select.mockImplementation(async () => "view");
182
+ mockOptions.prompts.input.mockImplementation(async () => ""); // Empty feedback after view
183
+
184
+ // Mock the marked function to throw error
185
+ const markedFunctionSpy = spyOn(markedModule, "marked").mockImplementation(() => {
186
+ throw new Error("Rendering error");
187
+ });
188
+
189
+ const result = await userReviewDocument(
190
+ { content: mockContent, title: "Test Doc" },
191
+ mockOptions,
192
+ );
193
+
194
+ expect(result.content).toBe(mockContent);
195
+ expect(consoleSpy).toHaveBeenCalledWith(
196
+ expect.stringContaining("Falling back to plain text display"),
197
+ );
198
+
199
+ markedFunctionSpy?.mockRestore();
200
+ });
201
+
202
+ // FEEDBACK PROCESSING TESTS
203
+ test("should process user feedback and update content", async () => {
204
+ const feedback = "Please add more examples";
205
+ const updatedContent = "# Updated Content\n\nThis has more examples.";
206
+
207
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
208
+ mockOptions.prompts.input
209
+ .mockImplementationOnce(async () => feedback)
210
+ .mockImplementationOnce(async () => ""); // Exit loop
211
+
212
+ mockOptions.context.invoke.mockImplementation(async () => ({
213
+ updatedContent,
214
+ operationSummary: "Added examples successfully",
215
+ }));
216
+
217
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
218
+
219
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
220
+ mockOptions.context.agents.updateDocumentDetail,
221
+ expect.objectContaining({
222
+ originalContent: mockContent,
223
+ feedback: feedback,
224
+ userPreferences: "",
225
+ }),
226
+ );
227
+ expect(result.content).toBe(updatedContent);
228
+ expect(consoleSpy).toHaveBeenCalledWith(
229
+ expect.stringContaining("✅ Added examples successfully"),
230
+ );
231
+ });
232
+
233
+ test("should handle empty feedback by exiting loop", async () => {
234
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
235
+ mockOptions.prompts.input.mockImplementation(async () => "");
236
+
237
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
238
+
239
+ expect(result.content).toBe(mockContent);
240
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
241
+ });
242
+
243
+ test("should handle whitespace-only feedback by exiting loop", async () => {
244
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
245
+ mockOptions.prompts.input.mockImplementation(async () => " \n\t ");
246
+
247
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
248
+
249
+ expect(result.content).toBe(mockContent);
250
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
251
+ });
252
+
253
+ // USER PREFERENCES TESTS
254
+ test("should include user preferences in update call", async () => {
255
+ const feedback = "Improve clarity";
256
+ const mockRules = [{ rule: "Keep sections concise" }, { rule: "Use clear headings" }];
257
+ const expectedPreferences = "Keep sections concise\n\nUse clear headings";
258
+
259
+ getActiveRulesForScopeSpy
260
+ .mockImplementationOnce(() => mockRules) // document rules
261
+ .mockImplementationOnce(() => []); // global rules
262
+
263
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
264
+ mockOptions.prompts.input
265
+ .mockImplementationOnce(async () => feedback)
266
+ .mockImplementationOnce(async () => "");
267
+
268
+ await userReviewDocument({ content: mockContent, path: "/test-doc" }, mockOptions);
269
+
270
+ expect(getActiveRulesForScopeSpy).toHaveBeenCalledWith("document", ["/test-doc"]);
271
+ expect(getActiveRulesForScopeSpy).toHaveBeenCalledWith("global");
272
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
273
+ mockOptions.context.agents.updateDocumentDetail,
274
+ expect.objectContaining({
275
+ userPreferences: expectedPreferences,
276
+ }),
277
+ );
278
+ });
279
+
280
+ test("should combine document and global rules correctly", async () => {
281
+ const feedback = "Add examples";
282
+ const documentRules = [{ rule: "Document rule 1" }];
283
+ const globalRules = [{ rule: "Global rule 1" }, { rule: "Global rule 2" }];
284
+ const expectedPreferences = "Document rule 1\n\nGlobal rule 1\n\nGlobal rule 2";
285
+
286
+ getActiveRulesForScopeSpy
287
+ .mockImplementationOnce(() => documentRules)
288
+ .mockImplementationOnce(() => globalRules);
289
+
290
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
291
+ mockOptions.prompts.input
292
+ .mockImplementationOnce(async () => feedback)
293
+ .mockImplementationOnce(async () => "");
294
+
295
+ await userReviewDocument({ content: mockContent, path: "/test-doc" }, mockOptions);
296
+
297
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
298
+ mockOptions.context.agents.updateDocumentDetail,
299
+ expect.objectContaining({
300
+ userPreferences: expectedPreferences,
301
+ }),
302
+ );
303
+ });
304
+
305
+ // AGENT ERROR HANDLING TESTS
306
+ test("should handle missing updateDocumentDetail agent", async () => {
307
+ const feedback = "Some feedback";
308
+ mockOptions.context.agents = {}; // No updateDocumentDetail agent
309
+
310
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
311
+ mockOptions.prompts.input.mockImplementation(async () => feedback);
312
+
313
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
314
+
315
+ expect(result.content).toBe(mockContent);
316
+ expect(consoleSpy).toHaveBeenCalledWith(
317
+ "We can't process your feedback right now. The document update feature is temporarily unavailable.",
318
+ );
319
+ expect(consoleSpy).toHaveBeenCalledWith(
320
+ "Please try again later or contact support if this continues.",
321
+ );
322
+ });
323
+
324
+ test("should handle updateDocumentDetail agent errors", async () => {
325
+ const feedback = "Some feedback";
326
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
327
+ mockOptions.prompts.input
328
+ .mockImplementationOnce(async () => feedback)
329
+ .mockImplementationOnce(async () => "");
330
+
331
+ mockOptions.context.invoke.mockImplementation(async () => {
332
+ const error = new Error("Agent failed");
333
+ error.name = "AgentError";
334
+ error.stack = "Stack trace here";
335
+ throw error;
336
+ });
337
+
338
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
339
+
340
+ expect(result.content).toBe(mockContent);
341
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Error processing your feedback:");
342
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Type: AgentError");
343
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Message: Agent failed");
344
+ expect(consoleErrorSpy).toHaveBeenCalledWith("Stack: Stack trace here");
345
+ });
346
+
347
+ test("should handle updateDocumentDetail agent returning no content", async () => {
348
+ const feedback = "Some feedback";
349
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
350
+ mockOptions.prompts.input
351
+ .mockImplementationOnce(async () => feedback)
352
+ .mockImplementationOnce(async () => "");
353
+
354
+ mockOptions.context.invoke.mockImplementation(async () => ({})); // No updatedContent
355
+
356
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
357
+
358
+ expect(result.content).toBe(mockContent);
359
+ expect(consoleSpy).toHaveBeenCalledWith(
360
+ "\n❌ We couldn't update the document. Please try rephrasing your feedback.\n",
361
+ );
362
+ });
363
+
364
+ // FEEDBACK REFINER TESTS
365
+ test("should call checkFeedbackRefiner agent when available", async () => {
366
+ const feedback = "Improve examples";
367
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
368
+ mockOptions.prompts.input
369
+ .mockImplementationOnce(async () => feedback)
370
+ .mockImplementationOnce(async () => "");
371
+
372
+ mockOptions.context.invoke
373
+ .mockImplementationOnce(async () => ({
374
+ updatedContent: "Updated content",
375
+ operationSummary: "Updated successfully",
376
+ })) // updateDocumentDetail
377
+ .mockImplementationOnce(async () => ({})); // checkFeedbackRefiner
378
+
379
+ await userReviewDocument({ content: mockContent }, mockOptions);
380
+
381
+ expect(mockOptions.context.invoke).toHaveBeenCalledWith(
382
+ mockOptions.context.agents.checkFeedbackRefiner,
383
+ expect.objectContaining({
384
+ documentContentFeedback: feedback,
385
+ stage: "document_refine",
386
+ }),
387
+ );
388
+ });
389
+
390
+ test("should handle missing checkFeedbackRefiner agent gracefully", async () => {
391
+ const feedback = "Some feedback";
392
+ mockOptions.context.agents = { updateDocumentDetail: {} }; // No checkFeedbackRefiner
393
+
394
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
395
+ mockOptions.prompts.input
396
+ .mockImplementationOnce(async () => feedback)
397
+ .mockImplementationOnce(async () => "");
398
+
399
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
400
+
401
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(1); // Only updateDocumentDetail called
402
+ expect(result.content).toBe("# Updated Content\n\nThis is updated content.");
403
+ });
404
+
405
+ test("should handle checkFeedbackRefiner errors gracefully", async () => {
406
+ const feedback = "Some feedback";
407
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
408
+ mockOptions.prompts.input
409
+ .mockImplementationOnce(async () => feedback)
410
+ .mockImplementationOnce(async () => "");
411
+
412
+ mockOptions.context.invoke
413
+ .mockImplementationOnce(async () => ({
414
+ updatedContent: "Updated content",
415
+ operationSummary: "Updated successfully",
416
+ })) // updateDocumentDetail
417
+ .mockImplementationOnce(async () => {
418
+ throw new Error("Refiner failed");
419
+ }); // checkFeedbackRefiner
420
+
421
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
422
+
423
+ expect(result.content).toBe("Updated content");
424
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
425
+ "We couldn't save your feedback as a preference:",
426
+ "Refiner failed",
427
+ );
428
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
429
+ "Your feedback was applied, but we couldn't save it as a preference.",
430
+ );
431
+ });
432
+
433
+ // MULTIPLE ROUNDS TESTS
434
+ test("should handle multiple feedback rounds", async () => {
435
+ const firstFeedback = "Add more examples";
436
+ const secondFeedback = "Improve clarity";
437
+ const firstUpdate = "# Content with examples";
438
+ const secondUpdate = "# Clear content with examples";
439
+
440
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
441
+ mockOptions.prompts.input
442
+ .mockImplementationOnce(async () => firstFeedback)
443
+ .mockImplementationOnce(async () => secondFeedback)
444
+ .mockImplementationOnce(async () => ""); // Exit loop
445
+
446
+ mockOptions.context.invoke
447
+ .mockImplementationOnce(async () => ({
448
+ updatedContent: firstUpdate,
449
+ operationSummary: "Added examples",
450
+ })) // First update
451
+ .mockImplementationOnce(async () => ({})) // First refiner
452
+ .mockImplementationOnce(async () => ({
453
+ updatedContent: secondUpdate,
454
+ operationSummary: "Improved clarity",
455
+ })) // Second update
456
+ .mockImplementationOnce(async () => ({})); // Second refiner
457
+
458
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
459
+
460
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(4);
461
+ expect(result.content).toBe(secondUpdate);
462
+ });
463
+
464
+ test("should stop at maximum iterations to prevent infinite loops", async () => {
465
+ mockOptions.prompts.select.mockImplementation(async () => "feedback");
466
+ mockOptions.prompts.input.mockImplementation(async () => "Keep giving feedback");
467
+
468
+ // Mock a long running process
469
+ let callCount = 0;
470
+ mockOptions.context.invoke.mockImplementation(async () => {
471
+ callCount++;
472
+ return {
473
+ updatedContent: `Updated content ${callCount}`,
474
+ operationSummary: `Update ${callCount}`,
475
+ };
476
+ });
477
+
478
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
479
+
480
+ // Should have stopped due to MAX_ITERATIONS (100)
481
+ expect(mockOptions.prompts.input).toHaveBeenCalledTimes(100);
482
+ expect(result.content).toBeDefined();
483
+ });
484
+
485
+ // EDGE CASES
486
+ test("should handle document with no title", async () => {
487
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
488
+
489
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
490
+
491
+ expect(result.content).toBe(mockContent);
492
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Untitled Document"));
493
+ });
494
+
495
+ test("should handle nested tokens in marked parsing", async () => {
496
+ const tokensWithNested = [
497
+ {
498
+ type: "heading",
499
+ depth: 1,
500
+ text: "Main Heading",
501
+ },
502
+ {
503
+ type: "list",
504
+ items: [],
505
+ tokens: [
506
+ {
507
+ type: "heading",
508
+ depth: 2,
509
+ text: "Nested Heading",
510
+ },
511
+ ],
512
+ },
513
+ ];
514
+
515
+ markedLexerSpy.mockReturnValue(tokensWithNested);
516
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
517
+
518
+ const result = await userReviewDocument({ content: mockContent }, mockOptions);
519
+
520
+ expect(result.content).toBe(mockContent);
521
+ // Should process both the main heading and nested heading
522
+ expect(markedLexerSpy).toHaveBeenCalledWith(mockContent);
523
+ });
524
+
525
+ test("should handle non-string content gracefully", async () => {
526
+ const result = await userReviewDocument({ content: null }, mockOptions);
527
+
528
+ expect(result.content).toBeNull();
529
+ expect(consoleSpy).toHaveBeenCalledWith("Please provide document content to review.");
530
+ });
531
+
532
+ // FALLBACK HEADING EXTRACTION TESTS
533
+ test("should use fallback regex extraction when marked fails", async () => {
534
+ const contentWithHeadings = "# Title\n## Section\n### Subsection\nContent here.";
535
+ markedLexerSpy.mockImplementation(() => {
536
+ throw new Error("Marked failed");
537
+ });
538
+
539
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
540
+
541
+ const result = await userReviewDocument({ content: contentWithHeadings }, mockOptions);
542
+
543
+ expect(result.content).toBe(contentWithHeadings);
544
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
545
+ "Failed to parse markdown with marked library, falling back to regex:",
546
+ "Marked failed",
547
+ );
548
+ });
549
+
550
+ test("should handle content with no headings", async () => {
551
+ const contentNoHeadings = "Just some plain text content without any headings.";
552
+ markedLexerSpy.mockReturnValue([]);
553
+
554
+ mockOptions.prompts.select.mockImplementation(async () => "finish");
555
+
556
+ const result = await userReviewDocument({ content: contentNoHeadings }, mockOptions);
557
+
558
+ expect(result.content).toBe(contentNoHeadings);
559
+ expect(consoleSpy).toHaveBeenCalledWith(" This document has no headings.");
560
+ });
561
+ });
@@ -3,6 +3,106 @@ import { stringify } from "yaml";
3
3
  import formatDocumentStructure from "../../../agents/utils/format-document-structure.mjs";
4
4
 
5
5
  describe("format-document-structure", () => {
6
+ // EDGE CASE TESTS
7
+ test("should handle when both documentStructure and originalDocumentStructure are null", async () => {
8
+ const result = await formatDocumentStructure({
9
+ documentStructure: null,
10
+ originalDocumentStructure: null,
11
+ });
12
+
13
+ expect(result).toEqual({
14
+ documentStructureYaml: "",
15
+ documentStructure: [],
16
+ });
17
+ });
18
+
19
+ test("should handle when both documentStructure and originalDocumentStructure are undefined", async () => {
20
+ const result = await formatDocumentStructure({
21
+ documentStructure: undefined,
22
+ originalDocumentStructure: undefined,
23
+ });
24
+
25
+ expect(result).toEqual({
26
+ documentStructureYaml: "",
27
+ documentStructure: [],
28
+ });
29
+ });
30
+
31
+ test("should handle when both parameters are missing", async () => {
32
+ const result = await formatDocumentStructure({});
33
+
34
+ expect(result).toEqual({
35
+ documentStructureYaml: "",
36
+ documentStructure: [],
37
+ });
38
+ });
39
+
40
+ test("should use originalDocumentStructure when documentStructure is null", async () => {
41
+ const originalDocumentStructure = [
42
+ {
43
+ title: "Original Document",
44
+ path: "/original",
45
+ parentId: null,
46
+ description: "From original structure",
47
+ extraField: "should be filtered out",
48
+ },
49
+ ];
50
+
51
+ const result = await formatDocumentStructure({
52
+ documentStructure: null,
53
+ originalDocumentStructure,
54
+ });
55
+
56
+ const expectedData = [
57
+ {
58
+ title: "Original Document",
59
+ path: "/original",
60
+ parentId: null,
61
+ description: "From original structure",
62
+ },
63
+ ];
64
+ const expectedYaml = stringify(expectedData, {
65
+ indent: 2,
66
+ lineWidth: 120,
67
+ minContentWidth: 20,
68
+ });
69
+
70
+ expect(result.documentStructureYaml).toBe(expectedYaml);
71
+ expect(result.documentStructure).toBe(null);
72
+ });
73
+
74
+ test("should use originalDocumentStructure when documentStructure is undefined", async () => {
75
+ const originalDocumentStructure = [
76
+ {
77
+ title: "Fallback Document",
78
+ path: "/fallback",
79
+ parentId: "parent-id",
80
+ description: "Fallback to original",
81
+ },
82
+ ];
83
+
84
+ const result = await formatDocumentStructure({
85
+ originalDocumentStructure,
86
+ });
87
+
88
+ const expectedData = [
89
+ {
90
+ title: "Fallback Document",
91
+ path: "/fallback",
92
+ parentId: "parent-id",
93
+ description: "Fallback to original",
94
+ },
95
+ ];
96
+ const expectedYaml = stringify(expectedData, {
97
+ indent: 2,
98
+ lineWidth: 120,
99
+ minContentWidth: 20,
100
+ });
101
+
102
+ expect(result.documentStructureYaml).toBe(expectedYaml);
103
+ expect(result.documentStructure).toBe(undefined);
104
+ });
105
+
6
106
  // BASIC FUNCTIONALITY TESTS
7
107
  test("should format empty document structure", async () => {
8
108
  const result = await formatDocumentStructure({