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

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 (116) hide show
  1. package/.aigne/doc-smith/config.yaml +2 -0
  2. package/.aigne/doc-smith/output/structure-plan.json +2 -2
  3. package/.aigne/doc-smith/preferences.yml +28 -20
  4. package/.aigne/doc-smith/upload-cache.yaml +702 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +20 -0
  7. package/README.md +1 -1
  8. package/agents/generate/document-structure-tools/add-document.mjs +35 -10
  9. package/agents/generate/document-structure-tools/delete-document.mjs +35 -12
  10. package/agents/generate/document-structure-tools/move-document.mjs +43 -17
  11. package/agents/generate/document-structure-tools/update-document.mjs +37 -10
  12. package/agents/generate/update-document-structure.yaml +1 -7
  13. package/agents/generate/user-review-document-structure.mjs +5 -4
  14. package/agents/translate/translate-document.yaml +1 -9
  15. package/agents/update/check-update-is-single.mjs +2 -1
  16. package/agents/update/document-tools/update-document-content.mjs +24 -14
  17. package/agents/update/fs-tools/glob.mjs +184 -0
  18. package/agents/update/fs-tools/grep.mjs +317 -0
  19. package/agents/update/fs-tools/read-file.mjs +307 -0
  20. package/agents/update/generate-document.yaml +4 -7
  21. package/agents/update/update-document-detail.yaml +6 -10
  22. package/agents/update/user-review-document.mjs +13 -13
  23. package/assets/screenshots/doc-complete-setup.png +0 -0
  24. package/assets/screenshots/doc-generate-docs.png +0 -0
  25. package/assets/screenshots/doc-generate.png +0 -0
  26. package/assets/screenshots/doc-generated-successfully.png +0 -0
  27. package/assets/screenshots/doc-publish.png +0 -0
  28. package/assets/screenshots/doc-regenerate.png +0 -0
  29. package/assets/screenshots/doc-translate-langs.png +0 -0
  30. package/assets/screenshots/doc-translate.png +0 -0
  31. package/assets/screenshots/doc-update.png +0 -0
  32. package/docs/advanced-how-it-works.ja.md +31 -31
  33. package/docs/advanced-how-it-works.md +9 -9
  34. package/docs/advanced-how-it-works.zh-TW.md +24 -24
  35. package/docs/advanced-how-it-works.zh.md +20 -20
  36. package/docs/advanced-quality-assurance.ja.md +57 -61
  37. package/docs/advanced-quality-assurance.md +57 -61
  38. package/docs/advanced-quality-assurance.zh-TW.md +57 -61
  39. package/docs/advanced-quality-assurance.zh.md +57 -61
  40. package/docs/advanced.ja.md +8 -4
  41. package/docs/advanced.md +7 -3
  42. package/docs/advanced.zh-TW.md +9 -5
  43. package/docs/advanced.zh.md +9 -5
  44. package/docs/changelog.ja.md +206 -29
  45. package/docs/changelog.md +177 -0
  46. package/docs/changelog.zh-TW.md +229 -52
  47. package/docs/changelog.zh.md +204 -27
  48. package/docs/cli-reference.ja.md +82 -52
  49. package/docs/cli-reference.md +56 -26
  50. package/docs/cli-reference.zh-TW.md +82 -52
  51. package/docs/cli-reference.zh.md +70 -40
  52. package/docs/configuration-interactive-setup.ja.md +45 -42
  53. package/docs/configuration-interactive-setup.md +8 -5
  54. package/docs/configuration-interactive-setup.zh-TW.md +26 -23
  55. package/docs/configuration-interactive-setup.zh.md +25 -22
  56. package/docs/configuration-language-support.ja.md +33 -63
  57. package/docs/configuration-language-support.md +32 -62
  58. package/docs/configuration-language-support.zh-TW.md +35 -65
  59. package/docs/configuration-language-support.zh.md +32 -62
  60. package/docs/configuration-llm-setup.ja.md +25 -23
  61. package/docs/configuration-llm-setup.md +20 -18
  62. package/docs/configuration-llm-setup.zh-TW.md +21 -19
  63. package/docs/configuration-llm-setup.zh.md +20 -18
  64. package/docs/configuration-preferences.ja.md +67 -52
  65. package/docs/configuration-preferences.md +56 -41
  66. package/docs/configuration-preferences.zh-TW.md +69 -54
  67. package/docs/configuration-preferences.zh.md +68 -53
  68. package/docs/configuration.ja.md +65 -81
  69. package/docs/configuration.md +19 -35
  70. package/docs/configuration.zh-TW.md +62 -79
  71. package/docs/configuration.zh.md +50 -67
  72. package/docs/features-generate-documentation.ja.md +44 -69
  73. package/docs/features-generate-documentation.md +36 -61
  74. package/docs/features-generate-documentation.zh-TW.md +42 -67
  75. package/docs/features-generate-documentation.zh.md +41 -67
  76. package/docs/features-publish-your-docs.ja.md +36 -36
  77. package/docs/features-publish-your-docs.md +2 -2
  78. package/docs/features-publish-your-docs.zh-TW.md +21 -21
  79. package/docs/features-publish-your-docs.zh.md +23 -23
  80. package/docs/features-translate-documentation.ja.md +40 -31
  81. package/docs/features-translate-documentation.md +15 -6
  82. package/docs/features-translate-documentation.zh-TW.md +37 -28
  83. package/docs/features-translate-documentation.zh.md +23 -14
  84. package/docs/features-update-and-refine.ja.md +68 -118
  85. package/docs/features-update-and-refine.md +58 -108
  86. package/docs/features-update-and-refine.zh-TW.md +67 -116
  87. package/docs/features-update-and-refine.zh.md +64 -114
  88. package/docs/features.ja.md +29 -19
  89. package/docs/features.md +25 -15
  90. package/docs/features.zh-TW.md +28 -18
  91. package/docs/features.zh.md +31 -21
  92. package/docs/getting-started.ja.md +40 -43
  93. package/docs/getting-started.md +36 -39
  94. package/docs/getting-started.zh-TW.md +38 -41
  95. package/docs/getting-started.zh.md +45 -48
  96. package/docs/overview.ja.md +63 -11
  97. package/docs/overview.md +60 -8
  98. package/docs/overview.zh-TW.md +67 -15
  99. package/docs/overview.zh.md +62 -10
  100. package/media.md +9 -9
  101. package/package.json +1 -1
  102. package/prompts/detail/custom/custom-components.md +304 -188
  103. package/prompts/detail/document-rules.md +4 -4
  104. package/prompts/detail/generate-document.md +21 -8
  105. package/prompts/detail/update-document.md +8 -12
  106. package/prompts/structure/update-document-structure.md +12 -8
  107. package/prompts/utils/feedback-refiner.md +3 -3
  108. package/tests/agents/generate/document-structure-tools/move-document.test.mjs +9 -9
  109. package/tests/agents/generate/user-review-document-structure.test.mjs +29 -8
  110. package/tests/agents/update/document-tools/update-document-content.test.mjs +115 -112
  111. package/tests/agents/update/fs-tools/glob.test.mjs +438 -0
  112. package/tests/agents/update/fs-tools/grep.test.mjs +279 -0
  113. package/tests/agents/update/fs-tools/read-file.test.mjs +553 -0
  114. package/tests/agents/update/user-review-document.test.mjs +48 -27
  115. package/types/document-schema.mjs +5 -6
  116. package/types/document-structure-schema.mjs +20 -8
@@ -0,0 +1,553 @@
1
+ import assert from "node:assert";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "bun:test";
6
+ import readFile from "../../../../agents/update/fs-tools/read-file.mjs";
7
+
8
+ describe("read-file tool", () => {
9
+ let tempDir;
10
+
11
+ beforeEach(async () => {
12
+ // Create temporary directory for test files
13
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "read-file-test-"));
14
+ });
15
+
16
+ afterEach(async () => {
17
+ // Clean up temporary directory
18
+ await fs.rm(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ describe("basic functionality", () => {
22
+ it("should read a simple text file", async () => {
23
+ const filePath = path.join(tempDir, "test.txt");
24
+ const content = "Hello, World!\nThis is a test file.\nLine 3.";
25
+ await fs.writeFile(filePath, content, "utf8");
26
+
27
+ const result = await readFile({
28
+ path: filePath,
29
+ });
30
+
31
+ assert.strictEqual(result.command, "read_file");
32
+ assert.strictEqual(result.error, null);
33
+ assert.strictEqual(result.result.content, content);
34
+ assert.strictEqual(result.result.metadata.path, filePath);
35
+ assert.strictEqual(result.result.metadata.mimeType, "text/plain");
36
+ assert.strictEqual(result.result.metadata.isBinary, false);
37
+ assert.strictEqual(result.result.metadata.encoding, "utf8");
38
+ assert.strictEqual(result.result.metadata.lineCount, 3);
39
+ });
40
+
41
+ it("should support backwards compatibility with path parameter", async () => {
42
+ const filePath = path.join(tempDir, "compat.txt");
43
+ await fs.writeFile(filePath, "Compatible content", "utf8");
44
+
45
+ const result = await readFile({
46
+ path: filePath,
47
+ });
48
+
49
+ assert.strictEqual(result.error, null);
50
+ assert.strictEqual(result.result.content, "Compatible content");
51
+ assert.strictEqual(result.arguments.path, filePath);
52
+ });
53
+
54
+ it("should read JavaScript files with correct MIME type", async () => {
55
+ const filePath = path.join(tempDir, "script.js");
56
+ const content = 'function hello() {\n return "world";\n}';
57
+ await fs.writeFile(filePath, content, "utf8");
58
+
59
+ const result = await readFile({
60
+ path: filePath,
61
+ });
62
+
63
+ assert.strictEqual(result.error, null);
64
+ assert.strictEqual(result.result.content, content);
65
+ assert.strictEqual(result.result.metadata.mimeType, "text/javascript");
66
+ assert.strictEqual(result.result.metadata.lineCount, 3);
67
+ });
68
+
69
+ it("should read JSON files with correct MIME type", async () => {
70
+ const filePath = path.join(tempDir, "data.json");
71
+ const content = '{\n "name": "test",\n "version": "1.0.0"\n}';
72
+ await fs.writeFile(filePath, content, "utf8");
73
+
74
+ const result = await readFile({
75
+ path: filePath,
76
+ });
77
+
78
+ assert.strictEqual(result.error, null);
79
+ assert.strictEqual(result.result.content, content);
80
+ assert.strictEqual(result.result.metadata.mimeType, "application/json");
81
+ });
82
+
83
+ it("should handle empty files", async () => {
84
+ const filePath = path.join(tempDir, "empty.txt");
85
+ await fs.writeFile(filePath, "", "utf8");
86
+
87
+ const result = await readFile({
88
+ path: filePath,
89
+ });
90
+
91
+ assert.strictEqual(result.error, null);
92
+ assert.strictEqual(result.result.content, "");
93
+ assert.strictEqual(result.result.metadata.lineCount, 1); // Empty file has 1 line
94
+ });
95
+ });
96
+
97
+ describe("offset and limit functionality", () => {
98
+ let multiLineFile;
99
+
100
+ beforeEach(async () => {
101
+ multiLineFile = path.join(tempDir, "multiline.txt");
102
+ const lines = [];
103
+ for (let i = 1; i <= 20; i++) {
104
+ lines.push(`Line ${i}: This is line number ${i}`);
105
+ }
106
+ await fs.writeFile(multiLineFile, lines.join("\n"), "utf8");
107
+ });
108
+
109
+ it("should read specific lines with offset and limit", async () => {
110
+ const result = await readFile({
111
+ path: multiLineFile,
112
+ offset: 5,
113
+ limit: 3,
114
+ });
115
+
116
+ assert.strictEqual(result.error, null);
117
+ assert(result.result.content.includes("Line 6:"));
118
+ assert(result.result.content.includes("Line 7:"));
119
+ assert(result.result.content.includes("Line 8:"));
120
+ assert(!result.result.content.includes("Line 5:"));
121
+ assert(!result.result.content.includes("Line 9:"));
122
+
123
+ assert.strictEqual(result.result.truncated.isTruncated, true);
124
+ assert.deepStrictEqual(result.result.truncated.linesShown, [6, 8]);
125
+ assert.strictEqual(result.result.truncated.totalLines, 20);
126
+ assert(result.result.message.includes("truncated"));
127
+ });
128
+
129
+ it("should read from offset to end when no limit specified", async () => {
130
+ const result = await readFile({
131
+ path: multiLineFile,
132
+ offset: 15,
133
+ });
134
+
135
+ assert.strictEqual(result.error, null);
136
+ assert(result.result.content.includes("Line 16:"));
137
+ assert(result.result.content.includes("Line 20:"));
138
+ assert(!result.result.content.includes("Line 15:"));
139
+
140
+ assert.strictEqual(result.result.truncated.isTruncated, true);
141
+ assert.deepStrictEqual(result.result.truncated.linesShown, [16, 20]);
142
+ });
143
+
144
+ it("should handle offset beyond file end", async () => {
145
+ const result = await readFile({
146
+ path: multiLineFile,
147
+ offset: 25,
148
+ limit: 5,
149
+ });
150
+
151
+ assert.strictEqual(result.error, null);
152
+ assert.strictEqual(result.result.content, "");
153
+ assert(result.result.message.includes("beyond file end"));
154
+ });
155
+
156
+ it("should read first N lines with limit only", async () => {
157
+ const result = await readFile({
158
+ path: multiLineFile,
159
+ limit: 5,
160
+ });
161
+
162
+ assert.strictEqual(result.error, null);
163
+ assert(result.result.content.includes("Line 1:"));
164
+ assert(result.result.content.includes("Line 5:"));
165
+ assert(!result.result.content.includes("Line 6:"));
166
+
167
+ assert.strictEqual(result.result.truncated.isTruncated, true);
168
+ assert.deepStrictEqual(result.result.truncated.linesShown, [1, 5]);
169
+ });
170
+ });
171
+
172
+ describe("binary file handling", () => {
173
+ it("should detect binary files by null bytes", async () => {
174
+ const filePath = path.join(tempDir, "binary.bin");
175
+ const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x0a, 0x1a, 0x0a]); // PNG header with null byte
176
+ await fs.writeFile(filePath, binaryData);
177
+
178
+ const result = await readFile({
179
+ path: filePath,
180
+ });
181
+
182
+ assert.strictEqual(result.error, null);
183
+ assert.strictEqual(result.result.content, "[Binary file: binary.bin]");
184
+ assert.strictEqual(result.result.metadata.isBinary, true);
185
+ assert.strictEqual(result.result.metadata.encoding, null);
186
+ assert.strictEqual(result.result.metadata.lineCount, null);
187
+ });
188
+
189
+ it("should detect image files by extension", async () => {
190
+ const filePath = path.join(tempDir, "image.png");
191
+ await fs.writeFile(filePath, "fake png content", "utf8");
192
+
193
+ const result = await readFile({
194
+ path: filePath,
195
+ });
196
+
197
+ assert.strictEqual(result.error, null);
198
+ assert.strictEqual(result.result.content, "[Binary file: image.png]");
199
+ assert.strictEqual(result.result.metadata.mimeType, "image/png");
200
+ assert.strictEqual(result.result.metadata.isBinary, true);
201
+ });
202
+
203
+ it("should detect PDF files by extension", async () => {
204
+ const filePath = path.join(tempDir, "document.pdf");
205
+ await fs.writeFile(filePath, "fake pdf content", "utf8");
206
+
207
+ const result = await readFile({
208
+ path: filePath,
209
+ });
210
+
211
+ assert.strictEqual(result.error, null);
212
+ assert.strictEqual(result.result.content, "[Binary file: document.pdf]");
213
+ assert.strictEqual(result.result.metadata.mimeType, "application/pdf");
214
+ assert.strictEqual(result.result.metadata.isBinary, true);
215
+ });
216
+
217
+ it("should handle large files as binary", async () => {
218
+ const filePath = path.join(tempDir, "large.txt");
219
+ // Create a file larger than 10MB
220
+ const largeContent = "x".repeat(11 * 1024 * 1024);
221
+ await fs.writeFile(filePath, largeContent, "utf8");
222
+
223
+ const result = await readFile({
224
+ path: filePath,
225
+ });
226
+
227
+ assert.strictEqual(result.error, null);
228
+ assert.strictEqual(result.result.content, "[Binary file: large.txt]");
229
+ assert.strictEqual(result.result.metadata.isBinary, true);
230
+ });
231
+ });
232
+
233
+ describe("MIME type detection", () => {
234
+ const testCases = [
235
+ { ext: ".md", mimeType: "text/markdown" },
236
+ { ext: ".ts", mimeType: "text/typescript" },
237
+ { ext: ".jsx", mimeType: "text/javascript" },
238
+ { ext: ".py", mimeType: "text/x-python" },
239
+ { ext: ".css", mimeType: "text/css" },
240
+ { ext: ".html", mimeType: "text/html" },
241
+ { ext: ".yaml", mimeType: "text/yaml" },
242
+ { ext: ".sh", mimeType: "text/x-shellscript" },
243
+ { ext: ".jpg", mimeType: "image/jpeg" },
244
+ { ext: ".gif", mimeType: "image/gif" },
245
+ { ext: ".unknown", mimeType: "application/octet-stream" },
246
+ ];
247
+
248
+ testCases.forEach(({ ext, mimeType }) => {
249
+ it(`should detect ${mimeType} for ${ext} files`, async () => {
250
+ const filePath = path.join(tempDir, `test${ext}`);
251
+
252
+ if (mimeType.startsWith("image/")) {
253
+ // For image files, just create an empty file (will be treated as binary)
254
+ await fs.writeFile(filePath, "", "utf8");
255
+ } else {
256
+ // For text files, create with content
257
+ await fs.writeFile(filePath, "test content", "utf8");
258
+ }
259
+
260
+ const result = await readFile({
261
+ path: filePath,
262
+ });
263
+
264
+ assert.strictEqual(result.error, null);
265
+ assert.strictEqual(result.result.metadata.mimeType, mimeType);
266
+ });
267
+ });
268
+ });
269
+
270
+ describe("encoding support", () => {
271
+ it("should read files with different encodings", async () => {
272
+ const filePath = path.join(tempDir, "encoded.txt");
273
+ const content = "Hello, 世界! 🌍";
274
+ await fs.writeFile(filePath, content, "utf8");
275
+
276
+ const result = await readFile({
277
+ path: filePath,
278
+ encoding: "utf8",
279
+ });
280
+
281
+ assert.strictEqual(result.error, null);
282
+ assert.strictEqual(result.result.content, content);
283
+ assert.strictEqual(result.result.metadata.encoding, "utf8");
284
+ });
285
+
286
+ it("should handle latin1 encoding", async () => {
287
+ const filePath = path.join(tempDir, "latin1.txt");
288
+ const content = "Hello, résumé!";
289
+ await fs.writeFile(filePath, content, "latin1");
290
+
291
+ const result = await readFile({
292
+ path: filePath,
293
+ encoding: "latin1",
294
+ });
295
+
296
+ assert.strictEqual(result.error, null);
297
+ assert.strictEqual(result.result.content, content);
298
+ assert.strictEqual(result.result.metadata.encoding, "latin1");
299
+ });
300
+ });
301
+
302
+ describe("auto-truncation", () => {
303
+ it("should auto-truncate very large text files", async () => {
304
+ const filePath = path.join(tempDir, "huge.txt");
305
+ const lines = [];
306
+ for (let i = 1; i <= 15000; i++) {
307
+ lines.push(`Line ${i}`);
308
+ }
309
+ await fs.writeFile(filePath, lines.join("\n"), "utf8");
310
+
311
+ const result = await readFile({
312
+ path: filePath,
313
+ });
314
+
315
+ assert.strictEqual(result.error, null);
316
+ assert.strictEqual(result.result.metadata.lineCount, 15000);
317
+ assert.strictEqual(result.result.truncated.isTruncated, true);
318
+ assert.deepStrictEqual(result.result.truncated.linesShown, [1, 10000]);
319
+ assert(result.result.content.includes("Line 1"));
320
+ assert(result.result.content.includes("Line 10000"));
321
+ assert(!result.result.content.includes("Line 10001"));
322
+ });
323
+
324
+ it("should not truncate files under the limit", async () => {
325
+ const filePath = path.join(tempDir, "medium.txt");
326
+ const lines = [];
327
+ for (let i = 1; i <= 5000; i++) {
328
+ lines.push(`Line ${i}`);
329
+ }
330
+ await fs.writeFile(filePath, lines.join("\n"), "utf8");
331
+
332
+ const result = await readFile({
333
+ path: filePath,
334
+ });
335
+
336
+ assert.strictEqual(result.error, null);
337
+ assert.strictEqual(result.result.metadata.lineCount, 5000);
338
+ assert.strictEqual(result.result.truncated, undefined);
339
+ assert(result.result.content.includes("Line 5000"));
340
+ });
341
+ });
342
+
343
+ describe("error handling", () => {
344
+ it("should require a file path", async () => {
345
+ const result = await readFile({});
346
+
347
+ assert.notStrictEqual(result.error, null);
348
+ assert(result.error.message.includes("required"));
349
+ });
350
+
351
+ it("should require absolute path", async () => {
352
+ const result = await readFile({
353
+ path: "relative/path.txt",
354
+ });
355
+
356
+ assert.notStrictEqual(result.error, null);
357
+ assert(result.error.message.includes("absolute"));
358
+ });
359
+
360
+ it("should handle non-existent files", async () => {
361
+ const result = await readFile({
362
+ path: path.join(tempDir, "nonexistent.txt"),
363
+ });
364
+
365
+ assert.notStrictEqual(result.error, null);
366
+ assert(result.error.message.includes("does not exist"));
367
+ });
368
+
369
+ it("should handle directories instead of files", async () => {
370
+ const dirPath = path.join(tempDir, "directory");
371
+ await fs.mkdir(dirPath);
372
+
373
+ const result = await readFile({
374
+ path: dirPath,
375
+ });
376
+
377
+ assert.notStrictEqual(result.error, null);
378
+ assert(result.error.message.includes("directory, not a file"));
379
+ });
380
+
381
+ it("should validate offset parameter", async () => {
382
+ const filePath = path.join(tempDir, "test.txt");
383
+ await fs.writeFile(filePath, "test content", "utf8");
384
+
385
+ const result = await readFile({
386
+ path: filePath,
387
+ offset: -1,
388
+ });
389
+
390
+ assert.notStrictEqual(result.error, null);
391
+ assert(result.error.message.includes("non-negative"));
392
+ });
393
+
394
+ it("should validate limit parameter", async () => {
395
+ const filePath = path.join(tempDir, "test.txt");
396
+ await fs.writeFile(filePath, "test content", "utf8");
397
+
398
+ const result = await readFile({
399
+ path: filePath,
400
+ limit: 0,
401
+ });
402
+
403
+ assert.notStrictEqual(result.error, null);
404
+ assert(result.error.message.includes("positive"));
405
+ });
406
+
407
+ it("should handle invalid offset type", async () => {
408
+ const filePath = path.join(tempDir, "test.txt");
409
+ await fs.writeFile(filePath, "test content", "utf8");
410
+
411
+ const result = await readFile({
412
+ path: filePath,
413
+ offset: "invalid",
414
+ });
415
+
416
+ assert.notStrictEqual(result.error, null);
417
+ assert(result.error.message.includes("non-negative number"));
418
+ });
419
+
420
+ it("should handle permission errors gracefully", async () => {
421
+ // This test might not work on all systems, but we include it for completeness
422
+ const filePath = path.join(tempDir, "test.txt");
423
+ await fs.writeFile(filePath, "test content", "utf8");
424
+
425
+ const result = await readFile({
426
+ path: filePath,
427
+ });
428
+
429
+ // Should either succeed or fail gracefully
430
+ assert.strictEqual(typeof result, "object");
431
+ assert.strictEqual(typeof result.result, "object");
432
+ });
433
+ });
434
+
435
+ describe("edge cases", () => {
436
+ it("should handle files with only newlines", async () => {
437
+ const filePath = path.join(tempDir, "newlines.txt");
438
+ await fs.writeFile(filePath, "\n\n\n", "utf8");
439
+
440
+ const result = await readFile({
441
+ path: filePath,
442
+ });
443
+
444
+ assert.strictEqual(result.error, null);
445
+ assert.strictEqual(result.result.content, "\n\n\n");
446
+ assert.strictEqual(result.result.metadata.lineCount, 4); // 3 newlines = 4 lines
447
+ });
448
+
449
+ it("should handle files with mixed line endings", async () => {
450
+ const filePath = path.join(tempDir, "mixed.txt");
451
+ await fs.writeFile(filePath, "Line 1\nLine 2\r\nLine 3\r", "utf8");
452
+
453
+ const result = await readFile({
454
+ path: filePath,
455
+ });
456
+
457
+ assert.strictEqual(result.error, null);
458
+ assert(result.result.content.includes("Line 1"));
459
+ assert(result.result.content.includes("Line 2"));
460
+ assert(result.result.content.includes("Line 3"));
461
+ });
462
+
463
+ it("should handle very long file names", async () => {
464
+ const longName = `${"a".repeat(100)}.txt`;
465
+ const filePath = path.join(tempDir, longName);
466
+ await fs.writeFile(filePath, "content", "utf8");
467
+
468
+ const result = await readFile({
469
+ path: filePath,
470
+ });
471
+
472
+ assert.strictEqual(result.error, null);
473
+ assert.strictEqual(result.result.content, "content");
474
+ });
475
+
476
+ it("should handle files with special characters in content", async () => {
477
+ const filePath = path.join(tempDir, "special.txt");
478
+ const content = "Special chars: @#$%^&*()[]{}|\\:\";'<>?,./";
479
+ await fs.writeFile(filePath, content, "utf8");
480
+
481
+ const result = await readFile({
482
+ path: filePath,
483
+ });
484
+
485
+ assert.strictEqual(result.error, null);
486
+ assert.strictEqual(result.result.content, content);
487
+ });
488
+
489
+ it("should handle zero-byte file", async () => {
490
+ const filePath = path.join(tempDir, "zero.txt");
491
+ await fs.writeFile(filePath, Buffer.alloc(0));
492
+
493
+ const result = await readFile({
494
+ path: filePath,
495
+ });
496
+
497
+ assert.strictEqual(result.error, null);
498
+ assert.strictEqual(result.result.content, "");
499
+ assert.strictEqual(result.result.metadata.fileSize, 0);
500
+ });
501
+ });
502
+
503
+ describe("metadata validation", () => {
504
+ it("should return correct file metadata", async () => {
505
+ const filePath = path.join(tempDir, "metadata.txt");
506
+ const content = "Line 1\nLine 2\nLine 3";
507
+ await fs.writeFile(filePath, content, "utf8");
508
+
509
+ const result = await readFile({
510
+ path: filePath,
511
+ });
512
+
513
+ assert.strictEqual(result.error, null);
514
+ assert.strictEqual(result.result.metadata.path, filePath);
515
+ assert.strictEqual(result.result.metadata.mimeType, "text/plain");
516
+ assert.strictEqual(typeof result.result.metadata.fileSize, "number");
517
+ assert(result.result.metadata.fileSize > 0);
518
+ assert.strictEqual(result.result.metadata.isBinary, false);
519
+ assert.strictEqual(result.result.metadata.encoding, "utf8");
520
+ assert.strictEqual(result.result.metadata.lineCount, 3);
521
+ });
522
+
523
+ it("should handle metadata for binary files", async () => {
524
+ const filePath = path.join(tempDir, "binary.bin");
525
+ const binaryData = Buffer.from([0x00, 0x01, 0x02, 0x03]);
526
+ await fs.writeFile(filePath, binaryData);
527
+
528
+ const result = await readFile({
529
+ path: filePath,
530
+ });
531
+
532
+ assert.strictEqual(result.error, null);
533
+ assert.strictEqual(result.result.metadata.isBinary, true);
534
+ assert.strictEqual(result.result.metadata.encoding, null);
535
+ assert.strictEqual(result.result.metadata.lineCount, null);
536
+ assert.strictEqual(result.result.metadata.fileSize, 4);
537
+ });
538
+ });
539
+ });
540
+
541
+ describe("schema validation", () => {
542
+ it("should have correct input schema", () => {
543
+ assert.strictEqual(typeof readFile.input_schema, "object");
544
+ assert.strictEqual(readFile.input_schema.type, "object");
545
+ assert(Array.isArray(readFile.input_schema.required));
546
+ assert(readFile.input_schema.required.includes("path"));
547
+ assert.strictEqual(typeof readFile.input_schema.properties, "object");
548
+ assert.strictEqual(typeof readFile.input_schema.properties.path, "object");
549
+ assert.strictEqual(typeof readFile.input_schema.properties.offset, "object");
550
+ assert.strictEqual(typeof readFile.input_schema.properties.limit, "object");
551
+ assert.strictEqual(typeof readFile.input_schema.properties.encoding, "object");
552
+ });
553
+ });
@@ -36,6 +36,9 @@ describe("user-review-document", () => {
36
36
  updatedContent: "# Updated Content\n\nThis is updated content.",
37
37
  operationSummary: "Document updated successfully",
38
38
  })),
39
+ userContext: {
40
+ currentContent: "",
41
+ },
39
42
  },
40
43
  };
41
44
 
@@ -209,10 +212,14 @@ describe("user-review-document", () => {
209
212
  .mockImplementationOnce(async () => feedback)
210
213
  .mockImplementationOnce(async () => ""); // Exit loop
211
214
 
212
- mockOptions.context.invoke.mockImplementation(async () => ({
213
- updatedContent,
214
- operationSummary: "Added examples successfully",
215
- }));
215
+ mockOptions.context.invoke.mockImplementation(async () => {
216
+ // Simulate the agent updating the shared context
217
+ mockOptions.context.userContext.currentContent = updatedContent;
218
+ return {
219
+ updatedContent,
220
+ operationSummary: "Added examples successfully",
221
+ };
222
+ });
216
223
 
217
224
  const result = await userReviewDocument({ content: mockContent }, mockOptions);
218
225
 
@@ -225,9 +232,6 @@ describe("user-review-document", () => {
225
232
  }),
226
233
  );
227
234
  expect(result.content).toBe(updatedContent);
228
- expect(consoleSpy).toHaveBeenCalledWith(
229
- expect.stringContaining("✅ Added examples successfully"),
230
- );
231
235
  });
232
236
 
233
237
  test("should handle empty feedback by exiting loop", async () => {
@@ -351,14 +355,13 @@ describe("user-review-document", () => {
351
355
  .mockImplementationOnce(async () => feedback)
352
356
  .mockImplementationOnce(async () => "");
353
357
 
358
+ // Agent doesn't update the shared context (simulating failure)
354
359
  mockOptions.context.invoke.mockImplementation(async () => ({})); // No updatedContent
355
360
 
356
361
  const result = await userReviewDocument({ content: mockContent }, mockOptions);
357
362
 
363
+ // Content should remain unchanged since agent didn't update it
358
364
  expect(result.content).toBe(mockContent);
359
- expect(consoleSpy).toHaveBeenCalledWith(
360
- "\n❌ We couldn't update the document. Please try rephrasing your feedback.\n",
361
- );
362
365
  });
363
366
 
364
367
  // FEEDBACK REFINER TESTS
@@ -389,6 +392,7 @@ describe("user-review-document", () => {
389
392
 
390
393
  test("should handle missing checkFeedbackRefiner agent gracefully", async () => {
391
394
  const feedback = "Some feedback";
395
+ const updatedContent = "# Updated Content\n\nThis is updated content.";
392
396
  mockOptions.context.agents = { updateDocumentDetail: {} }; // No checkFeedbackRefiner
393
397
 
394
398
  mockOptions.prompts.select.mockImplementation(async () => "feedback");
@@ -396,31 +400,40 @@ describe("user-review-document", () => {
396
400
  .mockImplementationOnce(async () => feedback)
397
401
  .mockImplementationOnce(async () => "");
398
402
 
403
+ mockOptions.context.invoke.mockImplementation(async () => {
404
+ mockOptions.context.userContext.currentContent = updatedContent;
405
+ return { updatedContent, operationSummary: "Updated" };
406
+ });
407
+
399
408
  const result = await userReviewDocument({ content: mockContent }, mockOptions);
400
409
 
401
410
  expect(mockOptions.context.invoke).toHaveBeenCalledTimes(1); // Only updateDocumentDetail called
402
- expect(result.content).toBe("# Updated Content\n\nThis is updated content.");
411
+ expect(result.content).toBe(updatedContent);
403
412
  });
404
413
 
405
414
  test("should handle checkFeedbackRefiner errors gracefully", async () => {
406
415
  const feedback = "Some feedback";
416
+ const updatedContent = "Updated content";
407
417
  mockOptions.prompts.select.mockImplementation(async () => "feedback");
408
418
  mockOptions.prompts.input
409
419
  .mockImplementationOnce(async () => feedback)
410
420
  .mockImplementationOnce(async () => "");
411
421
 
412
422
  mockOptions.context.invoke
413
- .mockImplementationOnce(async () => ({
414
- updatedContent: "Updated content",
415
- operationSummary: "Updated successfully",
416
- })) // updateDocumentDetail
423
+ .mockImplementationOnce(async () => {
424
+ mockOptions.context.userContext.currentContent = updatedContent;
425
+ return {
426
+ updatedContent,
427
+ operationSummary: "Updated successfully",
428
+ };
429
+ }) // updateDocumentDetail
417
430
  .mockImplementationOnce(async () => {
418
431
  throw new Error("Refiner failed");
419
432
  }); // checkFeedbackRefiner
420
433
 
421
434
  const result = await userReviewDocument({ content: mockContent }, mockOptions);
422
435
 
423
- expect(result.content).toBe("Updated content");
436
+ expect(result.content).toBe(updatedContent);
424
437
  expect(consoleWarnSpy).toHaveBeenCalledWith(
425
438
  "We couldn't save your feedback as a preference:",
426
439
  "Refiner failed",
@@ -443,17 +456,25 @@ describe("user-review-document", () => {
443
456
  .mockImplementationOnce(async () => secondFeedback)
444
457
  .mockImplementationOnce(async () => ""); // Exit loop
445
458
 
446
- mockOptions.context.invoke
447
- .mockImplementationOnce(async () => ({
448
- updatedContent: firstUpdate,
449
- operationSummary: "Added examples",
450
- })) // First update
451
- .mockImplementationOnce(async () => ({})) // First refiner
452
- .mockImplementationOnce(async () => ({
453
- updatedContent: secondUpdate,
454
- operationSummary: "Improved clarity",
455
- })) // Second update
456
- .mockImplementationOnce(async () => ({})); // Second refiner
459
+ let invokeCount = 0;
460
+ mockOptions.context.invoke.mockImplementation(async () => {
461
+ invokeCount++;
462
+ if (invokeCount === 1) {
463
+ // First update
464
+ mockOptions.context.userContext.currentContent = firstUpdate;
465
+ return { updatedContent: firstUpdate, operationSummary: "Added examples" };
466
+ } else if (invokeCount === 2) {
467
+ // First refiner
468
+ return {};
469
+ } else if (invokeCount === 3) {
470
+ // Second update
471
+ mockOptions.context.userContext.currentContent = secondUpdate;
472
+ return { updatedContent: secondUpdate, operationSummary: "Improved clarity" };
473
+ } else {
474
+ // Second refiner
475
+ return {};
476
+ }
477
+ });
457
478
 
458
479
  const result = await userReviewDocument({ content: mockContent }, mockOptions);
459
480