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