@aigne/doc-smith 0.8.12-beta.1 → 0.8.12-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.12-beta.1"
2
+ ".": "0.8.12-beta.2"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.12-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12-beta.1...v0.8.12-beta.2) (2025-10-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fix update document include path ([#166](https://github.com/AIGNE-io/aigne-doc-smith/issues/166)) ([40c3aa2](https://github.com/AIGNE-io/aigne-doc-smith/commit/40c3aa2739cb35d36fb1daed87a0e23c81285328))
9
+ * resolve failure in document update and generate ([#168](https://github.com/AIGNE-io/aigne-doc-smith/issues/168)) ([c00c759](https://github.com/AIGNE-io/aigne-doc-smith/commit/c00c759473a01ce3d16b89f2bfa974f43420b82a))
10
+
3
11
  ## [0.8.12-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12-beta...v0.8.12-beta.1) (2025-10-08)
4
12
 
5
13
 
@@ -112,7 +112,11 @@ export default async function checkDocument(
112
112
 
113
113
  const teamAgent = TeamAgent.from({
114
114
  name: "generateDocument",
115
- skills: [options.context.agents["handleDocumentUpdate"]],
115
+ skills: [
116
+ options.context.agents["handleDocumentUpdate"],
117
+ options.context.agents["translateMultilingual"],
118
+ options.context.agents["saveSingleDoc"],
119
+ ],
116
120
  });
117
121
 
118
122
  const result = await options.context.invoke(teamAgent, {
@@ -3,6 +3,10 @@ import { recordUpdate } from "../../utils/history-utils.mjs";
3
3
  export default async function saveAndTranslateDocument(input, options) {
4
4
  const { selectedDocs, docsDir, translateLanguages, locale } = input;
5
5
 
6
+ if (!Array.isArray(selectedDocs) || selectedDocs.length === 0) {
7
+ return {};
8
+ }
9
+
6
10
  // Saves a document with optional translation data
7
11
  const saveDocument = async (doc, translates = null, isTranslate = false) => {
8
12
  const saveAgent = options.context.agents["saveSingleDoc"];
@@ -40,7 +44,7 @@ export default async function saveAndTranslateDocument(input, options) {
40
44
  shouldTranslate = choice === "yes";
41
45
  }
42
46
 
43
- // Process documents in batches for better performance
47
+ // Save documents in batches
44
48
  const batchSize = 3;
45
49
  for (let i = 0; i < selectedDocs.length; i += batchSize) {
46
50
  const batch = selectedDocs.slice(i, i + batchSize);
@@ -56,8 +60,6 @@ export default async function saveAndTranslateDocument(input, options) {
56
60
  feedback: doc.feedback.trim(),
57
61
  documentPath: doc.path,
58
62
  });
59
- // clear feedback
60
- doc.feedback = "";
61
63
  }
62
64
  } catch (error) {
63
65
  console.error(`❌ Failed to save document ${doc.path}:`, error.message);
@@ -80,8 +82,11 @@ export default async function saveAndTranslateDocument(input, options) {
80
82
 
81
83
  const translatePromises = batch.map(async (doc) => {
82
84
  try {
85
+ // Clear feedback to ensure translation is not affected by update feedback
86
+ doc.feedback = "";
87
+
83
88
  const result = await options.context.invoke(translateAgent, {
84
- ...input, // Pass context for translation
89
+ ...input, // context is required
85
90
  content: doc.content,
86
91
  translates: doc.translates,
87
92
  title: doc.title,
@@ -245,7 +245,13 @@ export default async function userReviewDocument(
245
245
  }
246
246
  }
247
247
 
248
- return { content: options.context.userContext.currentContent, feedback: feedbacks.join(". ") };
248
+ return {
249
+ title,
250
+ description,
251
+ ...rest,
252
+ content: options.context.userContext.currentContent,
253
+ feedback: feedbacks.join(". "),
254
+ };
249
255
  }
250
256
 
251
257
  userReviewDocument.taskTitle = "User review and modify document content";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.8.12-beta.1",
3
+ "version": "0.8.12-beta.2",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -9,7 +9,7 @@
9
9
  </user_rules>
10
10
 
11
11
  {% set operation_type = "optimizing" %}
12
- {% include "../common/document/user-preferences.md" %}
12
+ {% include "../../common/document/user-preferences.md" %}
13
13
 
14
14
  <original_page_content>
15
15
  {{originalContent}}
@@ -25,7 +25,7 @@
25
25
  {{ assetsContent }}
26
26
  </media_list>
27
27
 
28
- {% include "../common/document/media-handling-rules.md" %}
28
+ {% include "../../common/document/media-handling-rules.md" %}
29
29
  </datasources>
30
30
 
31
31
  <user_feedback>
@@ -0,0 +1,369 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import saveAndTranslateDocument from "../../../agents/update/save-and-translate-document.mjs";
3
+ import * as historyUtils from "../../../utils/history-utils.mjs";
4
+
5
+ describe("save-and-translate-document", () => {
6
+ let mockOptions;
7
+ let consoleErrorSpy;
8
+ let recordUpdateSpy;
9
+
10
+ beforeEach(() => {
11
+ // Reset all mocks
12
+ mock.restore();
13
+
14
+ mockOptions = {
15
+ prompts: {
16
+ select: mock(async () => "no"),
17
+ },
18
+ context: {
19
+ agents: {
20
+ saveSingleDoc: { mockSaveAgent: true },
21
+ translateMultilingual: { mockTranslateAgent: true },
22
+ },
23
+ invoke: mock(async () => ({ mockResult: true })),
24
+ },
25
+ };
26
+
27
+ consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
28
+ recordUpdateSpy = spyOn(historyUtils, "recordUpdate").mockImplementation(() => {});
29
+
30
+ // Clear context mock call history
31
+ mockOptions.prompts.select.mockClear();
32
+ mockOptions.context.invoke.mockClear();
33
+ });
34
+
35
+ afterEach(() => {
36
+ consoleErrorSpy?.mockRestore();
37
+ recordUpdateSpy?.mockRestore();
38
+ });
39
+
40
+ // INPUT VALIDATION TESTS
41
+ test("should handle empty or invalid selectedDocs", async () => {
42
+ const testCases = [
43
+ { selectedDocs: [], description: "empty array" },
44
+ { selectedDocs: null, description: "null" },
45
+ { selectedDocs: undefined, description: "undefined" },
46
+ { selectedDocs: "not-array", description: "non-array" },
47
+ ];
48
+
49
+ for (const testCase of testCases) {
50
+ const input = {
51
+ selectedDocs: testCase.selectedDocs,
52
+ docsDir: "./docs",
53
+ translateLanguages: ["en", "zh"],
54
+ locale: "en",
55
+ };
56
+
57
+ const result = await saveAndTranslateDocument(input, mockOptions);
58
+
59
+ expect(result).toEqual({});
60
+ expect(mockOptions.context.invoke).not.toHaveBeenCalled();
61
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
62
+ }
63
+ });
64
+
65
+ // SCENARIO 1: NO TRANSLATION CONFIGURATION
66
+ test("should skip translation when no translation languages configured", async () => {
67
+ const testCases = [
68
+ { translateLanguages: null, description: "null" },
69
+ { translateLanguages: undefined, description: "undefined" },
70
+ { translateLanguages: [], description: "empty array" },
71
+ { translateLanguages: ["en"], description: "only current locale" },
72
+ ];
73
+
74
+ for (const testCase of testCases) {
75
+ const input = {
76
+ selectedDocs: [
77
+ {
78
+ path: "/docs/test.md",
79
+ content: "# Test Document",
80
+ translates: {},
81
+ labels: {},
82
+ feedback: "Good content",
83
+ },
84
+ ],
85
+ docsDir: "./docs",
86
+ translateLanguages: testCase.translateLanguages,
87
+ locale: "en",
88
+ };
89
+
90
+ const result = await saveAndTranslateDocument(input, mockOptions);
91
+
92
+ expect(result).toEqual({});
93
+ expect(mockOptions.prompts.select).not.toHaveBeenCalled();
94
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(1);
95
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
96
+ operation: "document_update",
97
+ feedback: "Good content",
98
+ documentPath: "/docs/test.md",
99
+ });
100
+
101
+ // Reset mocks for next iteration
102
+ mockOptions.prompts.select.mockClear();
103
+ mockOptions.context.invoke.mockClear();
104
+ recordUpdateSpy.mockClear();
105
+ }
106
+ });
107
+
108
+ // SCENARIO 2: USER CHOOSES NOT TO TRANSLATE
109
+ test("should save documents and skip translation when user chooses no", async () => {
110
+ const input = {
111
+ selectedDocs: [
112
+ {
113
+ path: "/docs/test1.md",
114
+ content: "# Test Document 1",
115
+ translates: {},
116
+ labels: {},
117
+ feedback: "Update needed",
118
+ },
119
+ {
120
+ path: "/docs/test2.md",
121
+ content: "# Test Document 2",
122
+ translates: {},
123
+ labels: {},
124
+ feedback: "Second feedback",
125
+ },
126
+ {
127
+ path: "/docs/test3.md",
128
+ content: "# Test Document 3",
129
+ translates: {},
130
+ labels: {},
131
+ feedback: " ", // Whitespace only
132
+ },
133
+ ],
134
+ docsDir: "./docs",
135
+ translateLanguages: ["en", "zh", "ja"],
136
+ locale: "en",
137
+ };
138
+
139
+ mockOptions.prompts.select.mockResolvedValue("no");
140
+
141
+ const result = await saveAndTranslateDocument(input, mockOptions);
142
+
143
+ expect(result).toEqual({});
144
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith({
145
+ message: "Document update completed. Would you like to translate these documents now?",
146
+ choices: [
147
+ {
148
+ name: "Review documents first, translate later",
149
+ value: "no",
150
+ },
151
+ {
152
+ name: "Translate now",
153
+ value: "yes",
154
+ },
155
+ ],
156
+ });
157
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(3); // Only saveDocument calls
158
+ expect(recordUpdateSpy).toHaveBeenCalledTimes(2); // Only documents with non-empty feedback
159
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
160
+ operation: "document_update",
161
+ feedback: "Update needed",
162
+ documentPath: "/docs/test1.md",
163
+ });
164
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
165
+ operation: "document_update",
166
+ feedback: "Second feedback",
167
+ documentPath: "/docs/test2.md",
168
+ });
169
+ });
170
+
171
+ // SCENARIO 3: USER CHOOSES TO TRANSLATE
172
+ test("should save and translate documents when user chooses yes", async () => {
173
+ const input = {
174
+ selectedDocs: [
175
+ {
176
+ path: "/docs/test1.md",
177
+ content: "# Test Document 1",
178
+ translates: {},
179
+ labels: {},
180
+ feedback: "Translation needed",
181
+ title: "Test Document 1",
182
+ },
183
+ {
184
+ path: "/docs/test2.md",
185
+ content: "# Test Document 2",
186
+ translates: {},
187
+ labels: {},
188
+ title: "Test Document 2",
189
+ },
190
+ ],
191
+ docsDir: "./docs",
192
+ translateLanguages: ["en", "zh"],
193
+ locale: "en",
194
+ };
195
+
196
+ mockOptions.prompts.select.mockResolvedValue("yes");
197
+ mockOptions.context.invoke
198
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument 1
199
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument 2
200
+ .mockResolvedValueOnce({ translates: { zh: "# 测试文档 1" } }) // translateMultilingual 1
201
+ .mockResolvedValueOnce({ translates: { zh: "# 测试文档 2" } }) // translateMultilingual 2
202
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument with translation 1
203
+ .mockResolvedValueOnce({ mockSaveResult: true }); // saveDocument with translation 2
204
+
205
+ const result = await saveAndTranslateDocument(input, mockOptions);
206
+
207
+ expect(result).toEqual({});
208
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith({
209
+ message: "Document update completed. Would you like to translate these documents now?",
210
+ choices: [
211
+ {
212
+ name: "Review documents first, translate later",
213
+ value: "no",
214
+ },
215
+ {
216
+ name: "Translate now",
217
+ value: "yes",
218
+ },
219
+ ],
220
+ });
221
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(6);
222
+
223
+ // Verify feedback is cleared before translation
224
+ expect(input.selectedDocs[0].feedback).toBe("");
225
+ expect(input.selectedDocs[1].feedback).toBe("");
226
+
227
+ expect(recordUpdateSpy).toHaveBeenCalledWith({
228
+ operation: "document_update",
229
+ feedback: "Translation needed",
230
+ documentPath: "/docs/test1.md",
231
+ });
232
+ });
233
+
234
+ // ERROR HANDLING TESTS
235
+ test("should handle errors gracefully", async () => {
236
+ // Test saveDocument error
237
+ const saveErrorInput = {
238
+ selectedDocs: [
239
+ {
240
+ path: "/docs/test1.md",
241
+ content: "# Test Document 1",
242
+ translates: {},
243
+ labels: {},
244
+ feedback: "Error test",
245
+ },
246
+ ],
247
+ docsDir: "./docs",
248
+ translateLanguages: ["en", "zh"],
249
+ locale: "en",
250
+ };
251
+
252
+ mockOptions.prompts.select.mockResolvedValue("no");
253
+ mockOptions.context.invoke.mockRejectedValue(new Error("Save failed"));
254
+
255
+ const saveErrorResult = await saveAndTranslateDocument(saveErrorInput, mockOptions);
256
+
257
+ expect(saveErrorResult).toEqual({});
258
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
259
+ "❌ Failed to save document /docs/test1.md:",
260
+ "Save failed",
261
+ );
262
+ expect(recordUpdateSpy).not.toHaveBeenCalled(); // Should not record if save failed
263
+
264
+ // Reset mocks
265
+ mockOptions.prompts.select.mockClear();
266
+ mockOptions.context.invoke.mockClear();
267
+ consoleErrorSpy.mockClear();
268
+ recordUpdateSpy.mockClear();
269
+
270
+ // Test translateMultilingual error
271
+ const translateErrorInput = {
272
+ selectedDocs: [
273
+ {
274
+ path: "/docs/test2.md",
275
+ content: "# Test Document 2",
276
+ translates: {},
277
+ labels: {},
278
+ title: "Test Document 2",
279
+ },
280
+ ],
281
+ docsDir: "./docs",
282
+ translateLanguages: ["en", "zh"],
283
+ locale: "en",
284
+ };
285
+
286
+ mockOptions.prompts.select.mockResolvedValue("yes");
287
+ mockOptions.context.invoke
288
+ .mockResolvedValueOnce({ mockSaveResult: true }) // saveDocument succeeds
289
+ .mockRejectedValueOnce(new Error("Translation failed")); // translateMultilingual fails
290
+
291
+ const translateErrorResult = await saveAndTranslateDocument(translateErrorInput, mockOptions);
292
+
293
+ expect(translateErrorResult).toEqual({});
294
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
295
+ "❌ Failed to translate document /docs/test2.md:",
296
+ "Translation failed",
297
+ );
298
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(2);
299
+ });
300
+
301
+ // EDGE CASES AND INTEGRATION
302
+ test("should handle edge cases and complex scenarios", async () => {
303
+ // Test edge cases with different document properties
304
+ const edgeCaseInput = {
305
+ selectedDocs: [
306
+ {
307
+ path: "/docs/test1.md",
308
+ content: "# Test Document 1",
309
+ translates: null, // null translates
310
+ labels: {},
311
+ feedback: "", // empty feedback
312
+ },
313
+ {
314
+ path: "/docs/test2.md",
315
+ content: "# Test Document 2",
316
+ translates: undefined, // undefined translates
317
+ labels: {},
318
+ feedback: " ", // whitespace feedback
319
+ },
320
+ {
321
+ path: "/docs/test3.md",
322
+ content: "# Test Document 3",
323
+ translates: {},
324
+ labels: {},
325
+ // no title
326
+ },
327
+ ],
328
+ docsDir: "./docs",
329
+ translateLanguages: ["en", "zh"],
330
+ locale: "en",
331
+ };
332
+
333
+ mockOptions.prompts.select.mockResolvedValue("no");
334
+
335
+ const edgeCaseResult = await saveAndTranslateDocument(edgeCaseInput, mockOptions);
336
+
337
+ expect(edgeCaseResult).toEqual({});
338
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(3);
339
+ expect(recordUpdateSpy).not.toHaveBeenCalled(); // No valid feedback
340
+
341
+ // Reset mocks
342
+ mockOptions.prompts.select.mockClear();
343
+ mockOptions.context.invoke.mockClear();
344
+ recordUpdateSpy.mockClear();
345
+
346
+ // Test batch processing with multiple documents
347
+ const batchInput = {
348
+ selectedDocs: Array.from({ length: 5 }, (_, i) => ({
349
+ path: `/docs/batch${i + 1}.md`,
350
+ content: `# Batch Document ${i + 1}`,
351
+ translates: {},
352
+ labels: {},
353
+ title: `Batch Document ${i + 1}`,
354
+ })),
355
+ docsDir: "./docs",
356
+ translateLanguages: ["en", "zh"],
357
+ locale: "en",
358
+ };
359
+
360
+ mockOptions.prompts.select.mockResolvedValue("yes");
361
+ mockOptions.context.invoke.mockResolvedValue({ mockResult: true });
362
+
363
+ const batchResult = await saveAndTranslateDocument(batchInput, mockOptions);
364
+
365
+ expect(batchResult).toEqual({});
366
+ // 5 documents * 3 calls each (save, translate, save) = 15 calls
367
+ expect(mockOptions.context.invoke).toHaveBeenCalledTimes(15);
368
+ });
369
+ });