@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,358 @@
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 * as fs from "node:fs";
13
+ import * as fsPromises from "node:fs/promises";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import * as yaml from "yaml";
17
+ import { getAccessToken } from "../../utils/auth-utils.mjs";
18
+ import * as blockletUtils from "../../utils/blocklet.mjs";
19
+
20
+ // Mock external modules that involve network requests
21
+ const mockCreateConnect = mock(() => Promise.resolve({ accessKeySecret: "new-access-token" }));
22
+ const mockOpen = mock(() => Promise.resolve());
23
+ const mockJoinURL = mock((base, path) => `${base}${path}`);
24
+
25
+ describe("auth-utils", () => {
26
+ let originalEnv;
27
+
28
+ // Spies for internal operations
29
+ let existsSyncSpy;
30
+ let readFileSpy;
31
+ let writeFileSpy;
32
+ let mkdirSyncSpy;
33
+ let homedirSpy;
34
+ let joinSpy;
35
+ let parseSpy;
36
+ let stringifySpy;
37
+ let getComponentMountPointSpy;
38
+ let consoleWarnSpy;
39
+ let consoleDebugSpy;
40
+
41
+ beforeAll(() => {
42
+ // Apply mocks for external dependencies that involve network requests
43
+ mock.module("@aigne/cli/utils/aigne-hub/credential.js", () => ({
44
+ createConnect: mockCreateConnect,
45
+ }));
46
+ mock.module("open", () => ({ default: mockOpen }));
47
+ mock.module("join-url", () => ({ default: mockJoinURL }));
48
+ });
49
+
50
+ afterAll(() => {
51
+ // Restore all mocks when this test file is complete
52
+ mock.restore();
53
+ });
54
+
55
+ beforeEach(() => {
56
+ originalEnv = { ...process.env };
57
+ delete process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN;
58
+
59
+ // Reset external mocks
60
+ mockCreateConnect.mockClear();
61
+ mockCreateConnect.mockImplementation(() =>
62
+ Promise.resolve({ accessKeySecret: "new-access-token" }),
63
+ );
64
+ mockOpen.mockClear();
65
+ mockOpen.mockImplementation(() => Promise.resolve());
66
+ mockJoinURL.mockClear();
67
+ mockJoinURL.mockImplementation((base, path) => `${base}${path}`);
68
+
69
+ // Spy on filesystem operations
70
+ existsSyncSpy = spyOn(fs, "existsSync").mockReturnValue(false);
71
+ readFileSpy = spyOn(fsPromises, "readFile").mockResolvedValue("");
72
+ writeFileSpy = spyOn(fsPromises, "writeFile").mockResolvedValue();
73
+ mkdirSyncSpy = spyOn(fs, "mkdirSync").mockImplementation(() => {});
74
+
75
+ // Spy on path operations
76
+ homedirSpy = spyOn(os, "homedir").mockReturnValue("/mock/home");
77
+ joinSpy = spyOn(path, "join").mockImplementation((...paths) => paths.join("/"));
78
+
79
+ // Spy on YAML operations
80
+ parseSpy = spyOn(yaml, "parse").mockReturnValue({});
81
+ stringifySpy = spyOn(yaml, "stringify").mockReturnValue("mock yaml");
82
+
83
+ // Spy on blocklet operations
84
+ getComponentMountPointSpy = spyOn(blockletUtils, "getComponentMountPoint").mockResolvedValue();
85
+
86
+ // Spy on console methods
87
+ consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
88
+ consoleDebugSpy = spyOn(console, "debug").mockImplementation(() => {});
89
+ });
90
+
91
+ afterEach(() => {
92
+ process.env = originalEnv;
93
+
94
+ // Restore all spies
95
+ existsSyncSpy?.mockRestore();
96
+ readFileSpy?.mockRestore();
97
+ writeFileSpy?.mockRestore();
98
+ mkdirSyncSpy?.mockRestore();
99
+ homedirSpy?.mockRestore();
100
+ joinSpy?.mockRestore();
101
+ parseSpy?.mockRestore();
102
+ stringifySpy?.mockRestore();
103
+ getComponentMountPointSpy?.mockRestore();
104
+ consoleWarnSpy?.mockRestore();
105
+ consoleDebugSpy?.mockRestore();
106
+ });
107
+
108
+ test("should return access token from environment variable", async () => {
109
+ process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN = "env-token";
110
+
111
+ const result = await getAccessToken("https://example.com");
112
+
113
+ expect(result).toBe("env-token");
114
+ });
115
+
116
+ test("should handle different URL formats", async () => {
117
+ process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN = "test-token";
118
+
119
+ // Test different URL formats
120
+ const urls = [
121
+ "https://example.com",
122
+ "http://example.com",
123
+ "https://example.com:8080",
124
+ "https://sub.example.com/path",
125
+ ];
126
+
127
+ for (const url of urls) {
128
+ const result = await getAccessToken(url);
129
+ expect(result).toBe("test-token");
130
+ }
131
+ });
132
+
133
+ test("should handle invalid URL", async () => {
134
+ process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN = "test-token";
135
+
136
+ await expect(getAccessToken("invalid-url")).rejects.toThrow();
137
+ });
138
+
139
+ test("should work with localhost URLs", async () => {
140
+ process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN = "local-token";
141
+
142
+ const result = await getAccessToken("http://localhost:3000");
143
+
144
+ expect(result).toBe("local-token");
145
+ });
146
+
147
+ test("should preserve environment variable after function call", async () => {
148
+ process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN = "persistent-token";
149
+
150
+ await getAccessToken("https://example.com");
151
+
152
+ expect(process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN).toBe("persistent-token");
153
+ });
154
+
155
+ // CONFIG FILE READING TESTS
156
+ test("should read access token from config file", async () => {
157
+ existsSyncSpy.mockReturnValue(true);
158
+ readFileSpy.mockResolvedValue("DOC_DISCUSS_KIT_ACCESS_TOKEN: config-token");
159
+ parseSpy.mockReturnValue({
160
+ "example.com": {
161
+ DOC_DISCUSS_KIT_ACCESS_TOKEN: "config-token",
162
+ },
163
+ });
164
+
165
+ const result = await getAccessToken("https://example.com");
166
+
167
+ expect(result).toBe("config-token");
168
+ expect(joinSpy).toHaveBeenCalledWith("/mock/home", ".aigne", "doc-smith-connected.yaml");
169
+ expect(existsSyncSpy).toHaveBeenCalledWith("/mock/home/.aigne/doc-smith-connected.yaml");
170
+ expect(readFileSpy).toHaveBeenCalledWith("/mock/home/.aigne/doc-smith-connected.yaml", "utf8");
171
+ expect(parseSpy).toHaveBeenCalled();
172
+ });
173
+
174
+ test("should handle config file without token field", async () => {
175
+ existsSyncSpy.mockReturnValue(true);
176
+ readFileSpy.mockResolvedValue("other: value");
177
+ parseSpy.mockReturnValue({
178
+ "example.com": {
179
+ other: "value",
180
+ },
181
+ });
182
+
183
+ // Since we now have successful authorization flow mocked, this should succeed
184
+ const result = await getAccessToken("https://example.com");
185
+ expect(result).toBe("new-access-token");
186
+ expect(getComponentMountPointSpy).toHaveBeenCalled();
187
+ });
188
+
189
+ test("should handle missing hostname in config", async () => {
190
+ existsSyncSpy.mockReturnValue(true);
191
+ readFileSpy.mockResolvedValue("DOC_DISCUSS_KIT_ACCESS_TOKEN: token");
192
+ parseSpy.mockReturnValue({
193
+ "other-domain.com": {
194
+ DOC_DISCUSS_KIT_ACCESS_TOKEN: "other-token",
195
+ },
196
+ });
197
+
198
+ // Since we now have successful authorization flow mocked, this should succeed
199
+ const result = await getAccessToken("https://example.com");
200
+ expect(result).toBe("new-access-token");
201
+ expect(getComponentMountPointSpy).toHaveBeenCalled();
202
+ });
203
+
204
+ test("should handle config file read errors", async () => {
205
+ existsSyncSpy.mockReturnValue(true);
206
+ readFileSpy.mockRejectedValue(new Error("File read error"));
207
+ // Make createConnect fail to test the error path
208
+ mockCreateConnect.mockRejectedValueOnce(new Error("Network error"));
209
+
210
+ await expect(getAccessToken("https://example.com")).rejects.toThrow(
211
+ "Failed to obtain access token. Please check your network connection and try again later.",
212
+ );
213
+ expect(consoleWarnSpy).toHaveBeenCalledWith("Failed to read config file:", "File read error");
214
+ });
215
+
216
+ test("should handle config file without DOC_DISCUSS_KIT_ACCESS_TOKEN keyword", async () => {
217
+ existsSyncSpy.mockReturnValue(true);
218
+ readFileSpy.mockResolvedValue("some other content");
219
+
220
+ // Should succeed with authorization flow
221
+ const result = await getAccessToken("https://example.com");
222
+ expect(result).toBe("new-access-token");
223
+ // Verify that the config file was read but the flow proceeded to authorization
224
+ expect(readFileSpy).toHaveBeenCalled();
225
+ });
226
+
227
+ // ERROR HANDLING TESTS
228
+ test("should throw error for invalid blocklet", async () => {
229
+ const InvalidBlockletError = (await import("../../utils/blocklet.mjs")).InvalidBlockletError;
230
+ getComponentMountPointSpy.mockRejectedValue(new InvalidBlockletError());
231
+
232
+ await expect(getAccessToken("https://example.com")).rejects.toThrow(
233
+ "The provided URL is not a valid website on ArcBlock platform",
234
+ );
235
+ });
236
+
237
+ test("should throw error for missing component", async () => {
238
+ const ComponentNotFoundError = (await import("../../utils/blocklet.mjs"))
239
+ .ComponentNotFoundError;
240
+ getComponentMountPointSpy.mockRejectedValue(new ComponentNotFoundError());
241
+
242
+ await expect(getAccessToken("https://example.com")).rejects.toThrow(
243
+ "This website does not have required components for publishing",
244
+ );
245
+ });
246
+
247
+ test("should throw error for network issues", async () => {
248
+ getComponentMountPointSpy.mockRejectedValue(new Error("Network error"));
249
+
250
+ await expect(getAccessToken("https://example.com")).rejects.toThrow("Unable to connect to:");
251
+ });
252
+
253
+ // AUTHORIZATION FLOW TESTS
254
+ test("should successfully complete authorization flow", async () => {
255
+ // Mock successful component check
256
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
257
+
258
+ const result = await getAccessToken("https://example.com");
259
+
260
+ expect(result).toBe("new-access-token");
261
+
262
+ // Verify the authorization flow
263
+ expect(getComponentMountPointSpy).toHaveBeenCalledWith(
264
+ "https://example.com",
265
+ expect.any(String),
266
+ );
267
+ expect(mockCreateConnect).toHaveBeenCalledWith(
268
+ expect.objectContaining({
269
+ connectAction: "gen-simple-access-key",
270
+ source: "AIGNE DocSmith connect to website",
271
+ closeOnSuccess: true,
272
+ appName: "AIGNE DocSmith",
273
+ openPage: expect.any(Function),
274
+ }),
275
+ );
276
+
277
+ // Verify environment variable is set
278
+ expect(process.env.DOC_DISCUSS_KIT_ACCESS_TOKEN).toBe("new-access-token");
279
+
280
+ // Verify config file is saved
281
+ expect(writeFileSpy).toHaveBeenCalledWith(
282
+ "/mock/home/.aigne/doc-smith-connected.yaml",
283
+ "mock yaml",
284
+ );
285
+ expect(stringifySpy).toHaveBeenCalledWith(
286
+ expect.objectContaining({
287
+ "example.com": {
288
+ DOC_DISCUSS_KIT_ACCESS_TOKEN: "new-access-token",
289
+ DOC_DISCUSS_KIT_URL: "https://example.com",
290
+ },
291
+ }),
292
+ );
293
+ });
294
+
295
+ test("should create .aigne directory if it doesn't exist", async () => {
296
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
297
+ existsSyncSpy.mockReturnValueOnce(false); // .aigne directory doesn't exist
298
+
299
+ const result = await getAccessToken("https://example.com");
300
+
301
+ expect(result).toBe("new-access-token");
302
+ expect(mkdirSyncSpy).toHaveBeenCalledWith("/mock/home/.aigne", { recursive: true });
303
+ });
304
+
305
+ test("should merge with existing config file", async () => {
306
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
307
+ existsSyncSpy
308
+ .mockReturnValueOnce(true) // .aigne directory exists
309
+ .mockReturnValueOnce(true); // config file exists at save time
310
+ readFileSpy.mockResolvedValue("other.com:\n token: other-token");
311
+ parseSpy.mockReturnValue({ "other.com": { token: "other-token" } });
312
+
313
+ const result = await getAccessToken("https://example.com");
314
+
315
+ expect(result).toBe("new-access-token");
316
+ expect(stringifySpy).toHaveBeenCalled();
317
+ // Verify that config file writing was attempted
318
+ expect(writeFileSpy).toHaveBeenCalled();
319
+ });
320
+
321
+ test("should call openPage function with correct URL", async () => {
322
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
323
+
324
+ let capturedOpenPage;
325
+ mockCreateConnect.mockImplementation((options) => {
326
+ capturedOpenPage = options.openPage;
327
+ return Promise.resolve({ accessKeySecret: "new-access-token" });
328
+ });
329
+
330
+ const result = await getAccessToken("https://example.com");
331
+
332
+ expect(result).toBe("new-access-token");
333
+ expect(typeof capturedOpenPage).toBe("function");
334
+
335
+ // Test that openPage calls the mock open function
336
+ await capturedOpenPage("https://auth.example.com");
337
+ expect(mockOpen).toHaveBeenCalledWith("https://auth.example.com/");
338
+ });
339
+
340
+ test("should handle authorization failure", async () => {
341
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
342
+ mockCreateConnect.mockRejectedValue(new Error("Authorization failed"));
343
+
344
+ await expect(getAccessToken("https://example.com")).rejects.toThrow(
345
+ "Failed to obtain access token. Please check your network connection and try again later.",
346
+ );
347
+ expect(consoleDebugSpy).toHaveBeenCalledWith(expect.any(Error));
348
+ });
349
+
350
+ test("should handle createConnect with different error types", async () => {
351
+ getComponentMountPointSpy.mockResolvedValue({ endpoint: "https://example.com/api" });
352
+ mockCreateConnect.mockRejectedValue(new TypeError("Network error"));
353
+
354
+ await expect(getAccessToken("https://example.com")).rejects.toThrow(
355
+ "Failed to obtain access token. Please check your network connection and try again later.",
356
+ );
357
+ });
358
+ });
@@ -0,0 +1,334 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import {
3
+ ComponentNotFoundError,
4
+ getComponentMountPoint,
5
+ InvalidBlockletError,
6
+ } from "../../utils/blocklet.mjs";
7
+
8
+ // Mock global fetch
9
+ const mockFetch = mock();
10
+ global.fetch = mockFetch;
11
+
12
+ describe("blocklet", () => {
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ });
16
+
17
+ afterEach(() => {
18
+ mockFetch.mockRestore?.();
19
+ });
20
+
21
+ // ERROR CLASSES TESTS
22
+ describe("InvalidBlockletError", () => {
23
+ test("should create error with correct properties", () => {
24
+ const url = "https://example.com";
25
+ const status = 404;
26
+ const statusText = "Not Found";
27
+
28
+ const error = new InvalidBlockletError(url, status, statusText);
29
+
30
+ expect(error).toBeInstanceOf(Error);
31
+ expect(error.name).toBe("InvalidBlockletError");
32
+ expect(error.message).toBe(
33
+ 'Invalid application URL: "https://example.com". Unable to fetch configuration.',
34
+ );
35
+ expect(error.url).toBe(url);
36
+ expect(error.status).toBe(status);
37
+ expect(error.statusText).toBe(statusText);
38
+ });
39
+
40
+ test("should handle null status and statusText", () => {
41
+ const url = "https://example.com";
42
+
43
+ const error = new InvalidBlockletError(url, null, "Network error");
44
+
45
+ expect(error.url).toBe(url);
46
+ expect(error.status).toBeNull();
47
+ expect(error.statusText).toBe("Network error");
48
+ });
49
+
50
+ test("should be instanceof Error", () => {
51
+ const error = new InvalidBlockletError("https://example.com");
52
+ expect(error instanceof Error).toBe(true);
53
+ expect(error instanceof InvalidBlockletError).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe("ComponentNotFoundError", () => {
58
+ test("should create error with correct properties", () => {
59
+ const did = "z8ia28nJVd6UMcS4dcJf5NLhv3rLmrFCK";
60
+ const appUrl = "https://example.com";
61
+
62
+ const error = new ComponentNotFoundError(did, appUrl);
63
+
64
+ expect(error).toBeInstanceOf(Error);
65
+ expect(error.name).toBe("ComponentNotFoundError");
66
+ expect(error.message).toBe(
67
+ 'Your website "https://example.com" missing required component to host your docs.',
68
+ );
69
+ expect(error.did).toBe(did);
70
+ expect(error.appUrl).toBe(appUrl);
71
+ });
72
+
73
+ test("should be instanceof Error", () => {
74
+ const error = new ComponentNotFoundError("test-did", "https://example.com");
75
+ expect(error instanceof Error).toBe(true);
76
+ expect(error instanceof ComponentNotFoundError).toBe(true);
77
+ });
78
+ });
79
+
80
+ // GETCOMPONENTMOUNTPOINT FUNCTION TESTS
81
+ describe("getComponentMountPoint", () => {
82
+ const testAppUrl = "https://example.com";
83
+ const testDid = "z8ia28nJVd6UMcS4dcJf5NLhv3rLmrFCK";
84
+ const expectedUrl = "https://example.com/__blocklet__.js?type=json";
85
+
86
+ test("should return mount point for existing component", async () => {
87
+ const mockConfig = {
88
+ componentMountPoints: [
89
+ { did: "other-did", mountPoint: "/other" },
90
+ { did: testDid, mountPoint: "/api/discuss" },
91
+ { did: "another-did", mountPoint: "/another" },
92
+ ],
93
+ };
94
+
95
+ mockFetch.mockResolvedValue({
96
+ ok: true,
97
+ json: () => Promise.resolve(mockConfig),
98
+ });
99
+
100
+ const result = await getComponentMountPoint(testAppUrl, testDid);
101
+
102
+ expect(result).toBe("/api/discuss");
103
+ expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
104
+ method: "GET",
105
+ headers: { Accept: "application/json" },
106
+ });
107
+ });
108
+
109
+ // NETWORK ERROR TESTS
110
+ test("should throw InvalidBlockletError when fetch fails", async () => {
111
+ mockFetch.mockRejectedValue(new Error("Network timeout"));
112
+
113
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
114
+ InvalidBlockletError,
115
+ );
116
+
117
+ try {
118
+ await getComponentMountPoint(testAppUrl, testDid);
119
+ } catch (error) {
120
+ expect(error.url).toBe(testAppUrl);
121
+ expect(error.status).toBeNull();
122
+ expect(error.statusText).toBe("Network timeout");
123
+ }
124
+ });
125
+
126
+ test("should throw InvalidBlockletError when response is not ok", async () => {
127
+ mockFetch.mockResolvedValue({
128
+ ok: false,
129
+ status: 404,
130
+ statusText: "Not Found",
131
+ });
132
+
133
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
134
+ InvalidBlockletError,
135
+ );
136
+
137
+ try {
138
+ await getComponentMountPoint(testAppUrl, testDid);
139
+ } catch (error) {
140
+ expect(error.url).toBe(testAppUrl);
141
+ expect(error.status).toBe(404);
142
+ expect(error.statusText).toBe("Not Found");
143
+ }
144
+ });
145
+
146
+ test("should throw InvalidBlockletError when JSON parsing fails", async () => {
147
+ mockFetch.mockResolvedValue({
148
+ ok: true,
149
+ json: () => Promise.reject(new Error("Invalid JSON")),
150
+ });
151
+
152
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
153
+ InvalidBlockletError,
154
+ );
155
+
156
+ try {
157
+ await getComponentMountPoint(testAppUrl, testDid);
158
+ } catch (error) {
159
+ expect(error.url).toBe(testAppUrl);
160
+ expect(error.status).toBeNull();
161
+ expect(error.statusText).toBe("Invalid JSON response");
162
+ }
163
+ });
164
+
165
+ // COMPONENT NOT FOUND TESTS
166
+ test("should throw ComponentNotFoundError when component is not found", async () => {
167
+ const mockConfig = {
168
+ componentMountPoints: [
169
+ { did: "other-did", mountPoint: "/other" },
170
+ { did: "another-did", mountPoint: "/another" },
171
+ ],
172
+ };
173
+
174
+ mockFetch.mockResolvedValue({
175
+ ok: true,
176
+ json: () => Promise.resolve(mockConfig),
177
+ });
178
+
179
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
180
+ ComponentNotFoundError,
181
+ );
182
+
183
+ try {
184
+ await getComponentMountPoint(testAppUrl, testDid);
185
+ } catch (error) {
186
+ expect(error.did).toBe(testDid);
187
+ expect(error.appUrl).toBe(testAppUrl);
188
+ }
189
+ });
190
+
191
+ test("should throw ComponentNotFoundError when componentMountPoints is undefined", async () => {
192
+ const mockConfig = {};
193
+
194
+ mockFetch.mockResolvedValue({
195
+ ok: true,
196
+ json: () => Promise.resolve(mockConfig),
197
+ });
198
+
199
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
200
+ ComponentNotFoundError,
201
+ );
202
+ });
203
+
204
+ test("should throw ComponentNotFoundError when componentMountPoints is empty", async () => {
205
+ const mockConfig = {
206
+ componentMountPoints: [],
207
+ };
208
+
209
+ mockFetch.mockResolvedValue({
210
+ ok: true,
211
+ json: () => Promise.resolve(mockConfig),
212
+ });
213
+
214
+ await expect(getComponentMountPoint(testAppUrl, testDid)).rejects.toThrow(
215
+ ComponentNotFoundError,
216
+ );
217
+ });
218
+
219
+ // EDGE CASES
220
+ test("should handle component at the beginning of array", async () => {
221
+ const mockConfig = {
222
+ componentMountPoints: [
223
+ { did: testDid, mountPoint: "/first" },
224
+ { did: "other-did", mountPoint: "/other" },
225
+ ],
226
+ };
227
+
228
+ mockFetch.mockResolvedValue({
229
+ ok: true,
230
+ json: () => Promise.resolve(mockConfig),
231
+ });
232
+
233
+ const result = await getComponentMountPoint(testAppUrl, testDid);
234
+ expect(result).toBe("/first");
235
+ });
236
+
237
+ test("should handle component at the end of array", async () => {
238
+ const mockConfig = {
239
+ componentMountPoints: [
240
+ { did: "other-did", mountPoint: "/other" },
241
+ { did: testDid, mountPoint: "/last" },
242
+ ],
243
+ };
244
+
245
+ mockFetch.mockResolvedValue({
246
+ ok: true,
247
+ json: () => Promise.resolve(mockConfig),
248
+ });
249
+
250
+ const result = await getComponentMountPoint(testAppUrl, testDid);
251
+ expect(result).toBe("/last");
252
+ });
253
+
254
+ test("should handle single component", async () => {
255
+ const mockConfig = {
256
+ componentMountPoints: [{ did: testDid, mountPoint: "/single" }],
257
+ };
258
+
259
+ mockFetch.mockResolvedValue({
260
+ ok: true,
261
+ json: () => Promise.resolve(mockConfig),
262
+ });
263
+
264
+ const result = await getComponentMountPoint(testAppUrl, testDid);
265
+ expect(result).toBe("/single");
266
+ });
267
+
268
+ test("should handle complex mount points", async () => {
269
+ const mockConfig = {
270
+ componentMountPoints: [{ did: testDid, mountPoint: "/api/v1/discuss-kit/endpoint" }],
271
+ };
272
+
273
+ mockFetch.mockResolvedValue({
274
+ ok: true,
275
+ json: () => Promise.resolve(mockConfig),
276
+ });
277
+
278
+ const result = await getComponentMountPoint(testAppUrl, testDid);
279
+ expect(result).toBe("/api/v1/discuss-kit/endpoint");
280
+ });
281
+
282
+ test("should handle empty mount point", async () => {
283
+ const mockConfig = {
284
+ componentMountPoints: [{ did: testDid, mountPoint: "" }],
285
+ };
286
+
287
+ mockFetch.mockResolvedValue({
288
+ ok: true,
289
+ json: () => Promise.resolve(mockConfig),
290
+ });
291
+
292
+ const result = await getComponentMountPoint(testAppUrl, testDid);
293
+ expect(result).toBe("");
294
+ });
295
+
296
+ test("should handle null/undefined mount point", async () => {
297
+ const mockConfig = {
298
+ componentMountPoints: [{ did: testDid, mountPoint: null }],
299
+ };
300
+
301
+ mockFetch.mockResolvedValue({
302
+ ok: true,
303
+ json: () => Promise.resolve(mockConfig),
304
+ });
305
+
306
+ const result = await getComponentMountPoint(testAppUrl, testDid);
307
+ expect(result).toBeNull();
308
+ });
309
+
310
+ // NETWORK TIMEOUT AND RETRY SCENARIOS
311
+ test("should handle network errors with different error types", async () => {
312
+ const errorTypes = [
313
+ new TypeError("Failed to fetch"),
314
+ new Error("Connection refused"),
315
+ new Error("Timeout"),
316
+ new ReferenceError("Network error"),
317
+ ];
318
+
319
+ for (const error of errorTypes) {
320
+ mockFetch.mockRejectedValueOnce(error);
321
+
322
+ try {
323
+ await getComponentMountPoint(testAppUrl, testDid);
324
+ expect(true).toBe(false); // Should not reach here
325
+ } catch (caughtError) {
326
+ expect(caughtError).toBeInstanceOf(InvalidBlockletError);
327
+ expect(caughtError.url).toBe(testAppUrl);
328
+ expect(caughtError.status).toBeNull();
329
+ expect(caughtError.statusText).toBe(error.message);
330
+ }
331
+ }
332
+ });
333
+ });
334
+ });
@@ -3,10 +3,10 @@ import {
3
3
  detectResolvableConflicts,
4
4
  generateConflictResolutionRules,
5
5
  getFilteredOptions,
6
- } from "../utils/conflict-detector.mjs";
7
- import { processConfigFields } from "../utils/utils.mjs";
6
+ } from "../../utils/conflict-detector.mjs";
7
+ import { processConfigFields } from "../../utils/utils.mjs";
8
8
 
9
- describe("conflict resolution", () => {
9
+ describe("conflict-detector", () => {
10
10
  describe("getFilteredOptions", () => {
11
11
  test("should filter experiencedUsers when documentPurpose is getStarted", () => {
12
12
  const allOptions = {