@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,642 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ mock,
9
+ spyOn,
10
+ test,
11
+ } from "bun:test";
12
+ import publishDocs from "../../../agents/publish/publish-docs.mjs";
13
+
14
+ // Import internal utils for selective spying
15
+ import * as authUtils from "../../../utils/auth-utils.mjs";
16
+ import * as d2Utils from "../../../utils/d2-utils.mjs";
17
+ import * as utils from "../../../utils/utils.mjs";
18
+
19
+ // Mock all external dependencies
20
+ const mockPublishDocs = {
21
+ publishDocs: mock(() => Promise.resolve({ success: true, boardId: "new-board-id" })),
22
+ };
23
+
24
+ const mockChalk = {
25
+ bold: mock((text) => text),
26
+ cyan: mock((text) => text),
27
+ blue: mock((text) => text),
28
+ green: mock((text) => text),
29
+ yellow: mock((text) => text),
30
+ };
31
+
32
+ const mockFsExtra = {
33
+ rm: mock(() => Promise.resolve()),
34
+ mkdir: mock(() => Promise.resolve()),
35
+ cp: mock(() => Promise.resolve()),
36
+ };
37
+
38
+ const mockPath = {
39
+ basename: mock(() => "test-project"),
40
+ join: mock((...paths) => paths.join("/")),
41
+ };
42
+
43
+ describe("publish-docs", () => {
44
+ let mockOptions;
45
+ let originalEnv;
46
+
47
+ // Spies for internal utils
48
+ let getAccessTokenSpy;
49
+ let beforePublishHookSpy;
50
+ let ensureTmpDirSpy;
51
+ let getGithubRepoUrlSpy;
52
+ let loadConfigFromFileSpy;
53
+ let saveValueToConfigSpy;
54
+
55
+ beforeAll(() => {
56
+ // Apply mocks for external dependencies only
57
+ mock.module("@aigne/publish-docs", () => mockPublishDocs);
58
+ mock.module("chalk", () => ({ default: mockChalk }));
59
+ mock.module("fs-extra", () => ({ default: mockFsExtra }));
60
+ mock.module("node:path", () => mockPath);
61
+ });
62
+
63
+ afterAll(() => {
64
+ // Restore all mocks when this test file is complete
65
+ mock.restore();
66
+ });
67
+
68
+ beforeEach(() => {
69
+ // Save original environment
70
+ originalEnv = { ...process.env };
71
+
72
+ // Reset external mocks and clear call history
73
+ mockPublishDocs.publishDocs.mockClear();
74
+ mockPublishDocs.publishDocs.mockImplementation(() =>
75
+ Promise.resolve({ success: true, boardId: "new-board-id" }),
76
+ );
77
+ mockFsExtra.rm.mockClear();
78
+ mockFsExtra.rm.mockImplementation(() => Promise.resolve());
79
+ mockFsExtra.mkdir.mockClear();
80
+ mockFsExtra.mkdir.mockImplementation(() => Promise.resolve());
81
+ mockFsExtra.cp.mockClear();
82
+ mockFsExtra.cp.mockImplementation(() => Promise.resolve());
83
+ mockPath.basename.mockClear();
84
+ mockPath.basename.mockImplementation(() => "test-project");
85
+ mockPath.join.mockClear();
86
+ mockPath.join.mockImplementation((...paths) => paths.join("/"));
87
+ mockChalk.bold.mockClear();
88
+ mockChalk.bold.mockImplementation((text) => text);
89
+ mockChalk.cyan.mockClear();
90
+ mockChalk.cyan.mockImplementation((text) => text);
91
+
92
+ // Set up spies for internal utils
93
+ getAccessTokenSpy = spyOn(authUtils, "getAccessToken").mockResolvedValue("mock-token");
94
+ beforePublishHookSpy = spyOn(d2Utils, "beforePublishHook").mockResolvedValue();
95
+ ensureTmpDirSpy = spyOn(d2Utils, "ensureTmpDir").mockResolvedValue();
96
+ getGithubRepoUrlSpy = spyOn(utils, "getGithubRepoUrl").mockReturnValue(
97
+ "https://github.com/user/repo",
98
+ );
99
+ loadConfigFromFileSpy = spyOn(utils, "loadConfigFromFile").mockResolvedValue({});
100
+ saveValueToConfigSpy = spyOn(utils, "saveValueToConfig").mockResolvedValue();
101
+
102
+ mockOptions = {
103
+ prompts: {
104
+ select: mock(async () => "default"),
105
+ input: mock(async () => "https://example.com"),
106
+ },
107
+ };
108
+
109
+ // Clear prompts mock call history
110
+ mockOptions.prompts.select.mockClear();
111
+ mockOptions.prompts.input.mockClear();
112
+
113
+ // Clear environment variable
114
+ delete process.env.DOC_DISCUSS_KIT_URL;
115
+ });
116
+
117
+ afterEach(() => {
118
+ // Restore original environment
119
+ process.env = originalEnv;
120
+
121
+ // Restore all spies
122
+ getAccessTokenSpy?.mockRestore();
123
+ beforePublishHookSpy?.mockRestore();
124
+ ensureTmpDirSpy?.mockRestore();
125
+ getGithubRepoUrlSpy?.mockRestore();
126
+ loadConfigFromFileSpy?.mockRestore();
127
+ saveValueToConfigSpy?.mockRestore();
128
+ });
129
+
130
+ // BASIC FUNCTIONALITY TESTS
131
+ test("should publish docs successfully with default settings", async () => {
132
+ loadConfigFromFileSpy.mockResolvedValue({ appUrl: "https://docsmith.aigne.io" });
133
+
134
+ const result = await publishDocs(
135
+ {
136
+ docsDir: "./docs",
137
+ appUrl: "https://docsmith.aigne.io",
138
+ boardId: "board-123",
139
+ },
140
+ mockOptions,
141
+ );
142
+
143
+ expect(ensureTmpDirSpy).toHaveBeenCalled();
144
+ expect(mockFsExtra.cp).toHaveBeenCalled();
145
+ expect(beforePublishHookSpy).toHaveBeenCalled();
146
+ expect(getAccessTokenSpy).toHaveBeenCalledWith("https://docsmith.aigne.io", "");
147
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalled();
148
+ expect(result.message).toBe("✅ Documentation Published Successfully!");
149
+ });
150
+
151
+ // ENVIRONMENT VARIABLE TESTS
152
+ test("should use environment variable DOC_DISCUSS_KIT_URL when set", async () => {
153
+ process.env.DOC_DISCUSS_KIT_URL = "https://env.example.com";
154
+
155
+ await publishDocs(
156
+ {
157
+ docsDir: "./docs",
158
+ appUrl: "https://docsmith.aigne.io",
159
+ },
160
+ mockOptions,
161
+ );
162
+
163
+ expect(getAccessTokenSpy).toHaveBeenCalledWith("https://env.example.com", "");
164
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ appUrl: "https://env.example.com",
167
+ }),
168
+ );
169
+ // Should not save appUrl when using environment variable
170
+ expect(saveValueToConfigSpy).not.toHaveBeenCalledWith("appUrl", expect.anything());
171
+ });
172
+
173
+ // USER INTERACTION TESTS
174
+ test("should prompt user to select platform when using default URL without config", async () => {
175
+ loadConfigFromFileSpy.mockResolvedValue({});
176
+ mockOptions.prompts.select.mockResolvedValue("default");
177
+
178
+ await publishDocs(
179
+ {
180
+ docsDir: "./docs",
181
+ appUrl: "https://docsmith.aigne.io",
182
+ },
183
+ mockOptions,
184
+ );
185
+
186
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ message: expect.stringContaining("Select platform"),
189
+ choices: expect.any(Array),
190
+ }),
191
+ );
192
+ });
193
+
194
+ test("should handle custom platform selection", async () => {
195
+ loadConfigFromFileSpy.mockResolvedValue({});
196
+ mockOptions.prompts.select.mockResolvedValue("custom");
197
+ mockOptions.prompts.input.mockResolvedValue("https://custom.example.com");
198
+
199
+ const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
200
+
201
+ await publishDocs(
202
+ {
203
+ docsDir: "./docs",
204
+ appUrl: "https://docsmith.aigne.io",
205
+ },
206
+ mockOptions,
207
+ );
208
+
209
+ expect(consoleSpy).toHaveBeenCalled();
210
+ expect(mockOptions.prompts.input).toHaveBeenCalledWith({
211
+ message: "Please enter your website URL:",
212
+ validate: expect.any(Function),
213
+ });
214
+ expect(getAccessTokenSpy).toHaveBeenCalledWith("https://custom.example.com", "");
215
+ });
216
+
217
+ test("should validate URL input and accept valid URLs", async () => {
218
+ loadConfigFromFileSpy.mockResolvedValue({});
219
+ mockOptions.prompts.select.mockResolvedValue("custom");
220
+ mockOptions.prompts.input.mockResolvedValue("https://valid.example.com");
221
+
222
+ await publishDocs(
223
+ {
224
+ docsDir: "./docs",
225
+ appUrl: "https://docsmith.aigne.io",
226
+ },
227
+ mockOptions,
228
+ );
229
+
230
+ const validateFn = mockOptions.prompts.input.mock.calls[0][0].validate;
231
+
232
+ expect(validateFn("https://valid.com")).toBe(true);
233
+ expect(validateFn("valid.com")).toBe(true); // Should work without protocol
234
+ expect(validateFn("")).toBe("Please enter a valid URL");
235
+ });
236
+
237
+ test("should add https protocol when not provided in URL", async () => {
238
+ loadConfigFromFileSpy.mockResolvedValue({});
239
+ mockOptions.prompts.select.mockResolvedValue("custom");
240
+ mockOptions.prompts.input.mockResolvedValue("example.com");
241
+
242
+ await publishDocs(
243
+ {
244
+ docsDir: "./docs",
245
+ appUrl: "https://docsmith.aigne.io",
246
+ },
247
+ mockOptions,
248
+ );
249
+
250
+ expect(getAccessTokenSpy).toHaveBeenCalledWith("https://example.com", "");
251
+ });
252
+
253
+ // PROJECT INFO TESTS
254
+ test("should use provided project info parameters", async () => {
255
+ await publishDocs(
256
+ {
257
+ docsDir: "./docs",
258
+ appUrl: "https://example.com",
259
+ projectName: "Test Project",
260
+ projectDesc: "Test Description",
261
+ projectLogo: "logo.png",
262
+ },
263
+ mockOptions,
264
+ );
265
+
266
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
267
+ expect.objectContaining({
268
+ boardName: "Test Project",
269
+ boardDesc: "Test Description",
270
+ boardCover: "logo.png",
271
+ }),
272
+ );
273
+ });
274
+
275
+ test("should fallback to config values for project info", async () => {
276
+ loadConfigFromFileSpy.mockResolvedValue({
277
+ projectName: "Config Project",
278
+ projectDesc: "Config Description",
279
+ projectLogo: "config-logo.png",
280
+ });
281
+
282
+ await publishDocs(
283
+ {
284
+ docsDir: "./docs",
285
+ appUrl: "https://example.com",
286
+ },
287
+ mockOptions,
288
+ );
289
+
290
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
291
+ expect.objectContaining({
292
+ boardName: "Config Project",
293
+ boardDesc: "Config Description",
294
+ boardCover: "config-logo.png",
295
+ }),
296
+ );
297
+ });
298
+
299
+ test("should use default project name from current directory", async () => {
300
+ mockPath.basename.mockReturnValue("default-project");
301
+
302
+ await publishDocs(
303
+ {
304
+ docsDir: "./docs",
305
+ appUrl: "https://example.com",
306
+ },
307
+ mockOptions,
308
+ );
309
+
310
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
311
+ expect.objectContaining({
312
+ boardName: "default-project",
313
+ boardDesc: "",
314
+ boardCover: "",
315
+ }),
316
+ );
317
+ });
318
+
319
+ // BOARD META TESTS
320
+ test("should construct board meta correctly", async () => {
321
+ loadConfigFromFileSpy.mockResolvedValue({
322
+ documentPurpose: ["API", "Tutorial"],
323
+ locale: "en",
324
+ translateLanguages: ["zh", "ja"],
325
+ lastGitHead: "abc123",
326
+ });
327
+
328
+ await publishDocs(
329
+ {
330
+ docsDir: "./docs",
331
+ appUrl: "https://example.com",
332
+ },
333
+ mockOptions,
334
+ );
335
+
336
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
337
+ expect.objectContaining({
338
+ boardMeta: {
339
+ category: ["API", "Tutorial"],
340
+ githubRepoUrl: "https://github.com/user/repo",
341
+ commitSha: "abc123",
342
+ languages: ["en", "zh", "ja"],
343
+ },
344
+ }),
345
+ );
346
+ });
347
+
348
+ test("should handle duplicate languages in board meta", async () => {
349
+ loadConfigFromFileSpy.mockResolvedValue({
350
+ locale: "en",
351
+ translateLanguages: ["en", "zh", "en"], // Duplicates
352
+ });
353
+
354
+ await publishDocs(
355
+ {
356
+ docsDir: "./docs",
357
+ appUrl: "https://example.com",
358
+ },
359
+ mockOptions,
360
+ );
361
+
362
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
363
+ expect.objectContaining({
364
+ boardMeta: expect.objectContaining({
365
+ languages: ["en", "zh"], // Duplicates removed
366
+ }),
367
+ }),
368
+ );
369
+ });
370
+
371
+ // CONFIG SAVING TESTS
372
+ test("should save appUrl when not using environment variable", async () => {
373
+ await publishDocs(
374
+ {
375
+ docsDir: "./docs",
376
+ appUrl: "https://custom.example.com",
377
+ },
378
+ mockOptions,
379
+ );
380
+
381
+ expect(saveValueToConfigSpy).toHaveBeenCalledWith("appUrl", "https://custom.example.com");
382
+ });
383
+
384
+ test("should save new boardId when auto-created", async () => {
385
+ mockPublishDocs.publishDocs.mockResolvedValue({
386
+ success: true,
387
+ boardId: "auto-created-id",
388
+ });
389
+
390
+ await publishDocs(
391
+ {
392
+ docsDir: "./docs",
393
+ appUrl: "https://example.com",
394
+ boardId: "original-id",
395
+ },
396
+ mockOptions,
397
+ );
398
+
399
+ expect(saveValueToConfigSpy).toHaveBeenCalledWith("boardId", "auto-created-id");
400
+ });
401
+
402
+ test("should not save boardId when it hasn't changed", async () => {
403
+ mockPublishDocs.publishDocs.mockResolvedValue({
404
+ success: true,
405
+ boardId: "same-id",
406
+ });
407
+
408
+ await publishDocs(
409
+ {
410
+ docsDir: "./docs",
411
+ appUrl: "https://example.com",
412
+ boardId: "same-id",
413
+ },
414
+ mockOptions,
415
+ );
416
+
417
+ expect(saveValueToConfigSpy).not.toHaveBeenCalledWith("boardId", expect.anything());
418
+ });
419
+
420
+ // ERROR HANDLING TESTS
421
+ test("should handle publish failure", async () => {
422
+ mockPublishDocs.publishDocs.mockRejectedValue(new Error("Publish failed"));
423
+
424
+ const result = await publishDocs(
425
+ {
426
+ docsDir: "./docs",
427
+ appUrl: "https://example.com",
428
+ },
429
+ mockOptions,
430
+ );
431
+
432
+ expect(result.message).toBe("❌ Failed to publish docs: Publish failed");
433
+ });
434
+
435
+ test("should handle unsuccessful publish", async () => {
436
+ mockPublishDocs.publishDocs.mockResolvedValue({
437
+ success: false,
438
+ boardId: "failed-id",
439
+ });
440
+
441
+ const result = await publishDocs(
442
+ {
443
+ docsDir: "./docs",
444
+ appUrl: "https://example.com",
445
+ },
446
+ mockOptions,
447
+ );
448
+
449
+ expect(result).toEqual({});
450
+ });
451
+
452
+ test("should clean up temporary directory on success", async () => {
453
+ await publishDocs(
454
+ {
455
+ docsDir: "./docs",
456
+ appUrl: "https://example.com",
457
+ },
458
+ mockOptions,
459
+ );
460
+
461
+ expect(mockFsExtra.rm).toHaveBeenCalledWith(
462
+ expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
463
+ expect.objectContaining({
464
+ recursive: true,
465
+ force: true,
466
+ }),
467
+ );
468
+ });
469
+
470
+ test("should clean up temporary directory on error", async () => {
471
+ mockPublishDocs.publishDocs.mockRejectedValue(new Error("Test error"));
472
+
473
+ await publishDocs(
474
+ {
475
+ docsDir: "./docs",
476
+ appUrl: "https://example.com",
477
+ },
478
+ mockOptions,
479
+ );
480
+
481
+ expect(mockFsExtra.rm).toHaveBeenCalledWith(
482
+ expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
483
+ expect.objectContaining({
484
+ recursive: true,
485
+ force: true,
486
+ }),
487
+ );
488
+ });
489
+
490
+ // FILESYSTEM OPERATION TESTS
491
+ test("should set up temporary directory correctly", async () => {
492
+ await publishDocs(
493
+ {
494
+ docsDir: "./docs",
495
+ appUrl: "https://example.com",
496
+ },
497
+ mockOptions,
498
+ );
499
+
500
+ expect(ensureTmpDirSpy).toHaveBeenCalled();
501
+ expect(mockFsExtra.rm).toHaveBeenCalledWith(
502
+ expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
503
+ expect.objectContaining({ recursive: true, force: true }),
504
+ );
505
+ expect(mockFsExtra.mkdir).toHaveBeenCalledWith(
506
+ expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
507
+ expect.objectContaining({ recursive: true }),
508
+ );
509
+ expect(mockFsExtra.cp).toHaveBeenCalledWith(
510
+ "./docs",
511
+ expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
512
+ expect.objectContaining({ recursive: true }),
513
+ );
514
+ });
515
+
516
+ test("should call beforePublishHook with correct docsDir", async () => {
517
+ await publishDocs(
518
+ {
519
+ docsDir: "./docs",
520
+ appUrl: "https://example.com",
521
+ },
522
+ mockOptions,
523
+ );
524
+
525
+ expect(beforePublishHookSpy).toHaveBeenCalledWith({
526
+ docsDir: expect.stringContaining(".aigne/doc-smith/.tmp/docs"),
527
+ });
528
+ });
529
+
530
+ test("should set DOC_ROOT_DIR environment variable", async () => {
531
+ await publishDocs(
532
+ {
533
+ docsDir: "./docs",
534
+ appUrl: "https://example.com",
535
+ },
536
+ mockOptions,
537
+ );
538
+
539
+ expect(process.env.DOC_ROOT_DIR).toContain(".aigne/doc-smith/.tmp/docs");
540
+ });
541
+
542
+ // EDGE CASES
543
+ test("should handle missing config file", async () => {
544
+ loadConfigFromFileSpy.mockResolvedValue(null);
545
+
546
+ const result = await publishDocs(
547
+ {
548
+ docsDir: "./docs",
549
+ appUrl: "https://example.com",
550
+ },
551
+ mockOptions,
552
+ );
553
+
554
+ expect(result.message).toBe("✅ Documentation Published Successfully!");
555
+ });
556
+
557
+ test("should handle empty config", async () => {
558
+ loadConfigFromFileSpy.mockResolvedValue({});
559
+
560
+ await publishDocs(
561
+ {
562
+ docsDir: "./docs",
563
+ appUrl: "https://example.com",
564
+ },
565
+ mockOptions,
566
+ );
567
+
568
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
569
+ expect.objectContaining({
570
+ boardMeta: {
571
+ category: [],
572
+ githubRepoUrl: "https://github.com/user/repo",
573
+ commitSha: "",
574
+ languages: [],
575
+ },
576
+ }),
577
+ );
578
+ });
579
+
580
+ test("should skip platform selection when appUrl is in config", async () => {
581
+ loadConfigFromFileSpy.mockResolvedValue({
582
+ appUrl: "https://existing.com",
583
+ });
584
+
585
+ await publishDocs(
586
+ {
587
+ docsDir: "./docs",
588
+ appUrl: "https://docsmith.aigne.io", // Default URL
589
+ },
590
+ mockOptions,
591
+ );
592
+
593
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
594
+ });
595
+
596
+ test("should handle URL validation edge cases", async () => {
597
+ loadConfigFromFileSpy.mockResolvedValue({});
598
+ mockOptions.prompts.select.mockResolvedValue("custom");
599
+
600
+ await publishDocs(
601
+ {
602
+ docsDir: "./docs",
603
+ appUrl: "https://docsmith.aigne.io",
604
+ },
605
+ mockOptions,
606
+ );
607
+
608
+ const validateFn = mockOptions.prompts.input.mock.calls[0][0].validate;
609
+
610
+ expect(validateFn("")).toBe("Please enter a valid URL");
611
+ expect(validateFn(" ")).toBe("Please enter a valid URL");
612
+ expect(validateFn("http://valid.com")).toBe(true);
613
+ expect(validateFn("https://valid.com")).toBe(true);
614
+ expect(validateFn("valid.com")).toBe(true);
615
+ });
616
+
617
+ test("should handle parameters priority correctly", async () => {
618
+ // Parameters > Config > Defaults
619
+ loadConfigFromFileSpy.mockResolvedValue({
620
+ projectName: "Config Name",
621
+ projectDesc: "Config Desc",
622
+ });
623
+
624
+ await publishDocs(
625
+ {
626
+ docsDir: "./docs",
627
+ appUrl: "https://example.com",
628
+ projectName: "Param Name", // Should override config
629
+ // projectDesc not provided - should use config
630
+ },
631
+ mockOptions,
632
+ );
633
+
634
+ expect(mockPublishDocs.publishDocs).toHaveBeenCalledWith(
635
+ expect.objectContaining({
636
+ boardName: "Param Name", // From parameter
637
+ boardDesc: "Config Desc", // From config
638
+ boardCover: "", // Default (empty)
639
+ }),
640
+ );
641
+ });
642
+ });