@aigne/doc-smith 0.4.5 → 0.6.0

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 (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/agents/batch-translate.yaml +3 -0
  3. package/agents/check-detail-result.mjs +2 -1
  4. package/agents/check-detail.mjs +1 -0
  5. package/agents/check-feedback-refiner.mjs +79 -0
  6. package/agents/check-structure-plan.mjs +16 -0
  7. package/agents/detail-generator-and-translate.yaml +3 -0
  8. package/agents/detail-regenerator.yaml +3 -0
  9. package/agents/docs-generator.yaml +3 -0
  10. package/agents/feedback-refiner.yaml +48 -0
  11. package/agents/find-items-by-paths.mjs +5 -1
  12. package/agents/find-user-preferences-by-path.mjs +37 -0
  13. package/agents/input-generator.mjs +8 -9
  14. package/agents/load-sources.mjs +63 -9
  15. package/agents/manage-prefs.mjs +203 -0
  16. package/agents/publish-docs.mjs +3 -1
  17. package/agents/retranslate.yaml +3 -0
  18. package/agents/structure-planning.yaml +3 -0
  19. package/aigne.yaml +4 -0
  20. package/package.json +10 -9
  21. package/prompts/content-detail-generator.md +13 -1
  22. package/prompts/document/detail-generator.md +1 -0
  23. package/prompts/feedback-refiner.md +84 -0
  24. package/prompts/structure-planning.md +8 -0
  25. package/prompts/translator.md +8 -0
  26. package/tests/{test-all-validation-cases.mjs → all-validation-cases.test.mjs} +60 -137
  27. package/tests/check-detail-result.test.mjs +90 -77
  28. package/tests/load-sources.test.mjs +103 -291
  29. package/tests/preferences-utils.test.mjs +369 -0
  30. package/tests/{test-save-docs.mjs → save-docs.test.mjs} +29 -47
  31. package/tests/save-value-to-config.test.mjs +165 -288
  32. package/utils/auth-utils.mjs +1 -1
  33. package/utils/constants.mjs +22 -10
  34. package/utils/markdown-checker.mjs +89 -9
  35. package/utils/preferences-utils.mjs +175 -0
  36. package/utils/utils.mjs +3 -3
@@ -0,0 +1,369 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ addPreferenceRule,
8
+ deactivateRule,
9
+ getActiveRulesForScope,
10
+ readPreferences,
11
+ removeRule,
12
+ writePreferences,
13
+ } from "../utils/preferences-utils.mjs";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+
17
+ describe("preferences-utils", () => {
18
+ let testDir;
19
+ let originalCwd;
20
+
21
+ beforeEach(async () => {
22
+ // Create isolated test directory
23
+ testDir = join(__dirname, "test-preferences");
24
+ await mkdir(testDir, { recursive: true });
25
+
26
+ // Change to test directory
27
+ originalCwd = process.cwd();
28
+ process.chdir(testDir);
29
+ });
30
+
31
+ afterEach(async () => {
32
+ // Restore original working directory
33
+ process.chdir(originalCwd);
34
+
35
+ // Clean up test directory
36
+ await rm(testDir, { recursive: true, force: true });
37
+ });
38
+
39
+ describe("readPreferences", () => {
40
+ test("should return empty rules array when preferences file doesn't exist", () => {
41
+ const preferences = readPreferences();
42
+ expect(preferences).toEqual({ rules: [] });
43
+ });
44
+
45
+ test("should read existing preferences file", async () => {
46
+ // Create preferences directory and file
47
+ const prefsDir = join(testDir, ".aigne", "doc-smith");
48
+ await mkdir(prefsDir, { recursive: true });
49
+
50
+ await writeFile(
51
+ join(prefsDir, "preferences.yml"),
52
+ `rules:
53
+ - id: test_rule_1
54
+ active: true
55
+ scope: global
56
+ rule: Test rule
57
+ feedback: Test feedback
58
+ createdAt: '2023-01-01T00:00:00.000Z'
59
+ `,
60
+ "utf8",
61
+ );
62
+
63
+ const preferences = readPreferences();
64
+ expect(preferences.rules).toHaveLength(1);
65
+ expect(preferences.rules[0].id).toBe("test_rule_1");
66
+ expect(preferences.rules[0].active).toBe(true);
67
+ expect(preferences.rules[0].scope).toBe("global");
68
+ });
69
+
70
+ test("should handle malformed YAML gracefully", async () => {
71
+ // Create preferences directory and invalid file
72
+ const prefsDir = join(testDir, ".aigne", "doc-smith");
73
+ await mkdir(prefsDir, { recursive: true });
74
+
75
+ await writeFile(join(prefsDir, "preferences.yml"), "invalid: yaml: content: [", "utf8");
76
+
77
+ const preferences = readPreferences();
78
+ expect(preferences).toEqual({ rules: [] });
79
+ });
80
+ });
81
+
82
+ describe("writePreferences", () => {
83
+ test("should create preferences directory if it doesn't exist", () => {
84
+ const testPreferences = { rules: [] };
85
+
86
+ writePreferences(testPreferences);
87
+
88
+ const prefsDir = join(testDir, ".aigne", "doc-smith");
89
+ expect(existsSync(prefsDir)).toBe(true);
90
+ expect(existsSync(join(prefsDir, "preferences.yml"))).toBe(true);
91
+ });
92
+
93
+ test("should write preferences to YAML file", () => {
94
+ const testPreferences = {
95
+ rules: [
96
+ {
97
+ id: "test_rule_1",
98
+ active: true,
99
+ scope: "global",
100
+ rule: "Test rule",
101
+ feedback: "Test feedback",
102
+ createdAt: "2023-01-01T00:00:00.000Z",
103
+ },
104
+ ],
105
+ };
106
+
107
+ writePreferences(testPreferences);
108
+
109
+ // Read back and verify
110
+ const savedPreferences = readPreferences();
111
+ expect(savedPreferences.rules).toHaveLength(1);
112
+ expect(savedPreferences.rules[0]).toEqual(testPreferences.rules[0]);
113
+ });
114
+ });
115
+
116
+ describe("addPreferenceRule", () => {
117
+ test("should add a new rule with generated ID", () => {
118
+ const ruleData = {
119
+ rule: "Always use proper punctuation",
120
+ scope: "document",
121
+ limitToInputPaths: false,
122
+ };
123
+ const feedback = "Please ensure proper punctuation";
124
+
125
+ const newRule = addPreferenceRule(ruleData, feedback);
126
+
127
+ expect(newRule.id).toMatch(/^pref_[a-f0-9]{16}$/);
128
+ expect(newRule.active).toBe(true);
129
+ expect(newRule.scope).toBe("document");
130
+ expect(newRule.rule).toBe("Always use proper punctuation");
131
+ expect(newRule.feedback).toBe("Please ensure proper punctuation");
132
+ expect(newRule.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
133
+
134
+ // Verify it was saved
135
+ const preferences = readPreferences();
136
+ expect(preferences.rules).toHaveLength(1);
137
+ expect(preferences.rules[0]).toEqual(newRule);
138
+ });
139
+
140
+ test("should add paths when limitToInputPaths is true", () => {
141
+ const ruleData = {
142
+ rule: "Use consistent naming",
143
+ scope: "structure",
144
+ limitToInputPaths: true,
145
+ };
146
+ const feedback = "Keep naming consistent";
147
+ const paths = ["/docs/api", "/docs/guide"];
148
+
149
+ const newRule = addPreferenceRule(ruleData, feedback, paths);
150
+
151
+ expect(newRule.paths).toEqual(["/docs/api", "/docs/guide"]);
152
+
153
+ // Verify it was saved with paths
154
+ const preferences = readPreferences();
155
+ expect(preferences.rules[0].paths).toEqual(paths);
156
+ });
157
+
158
+ test("should not add paths when limitToInputPaths is false", () => {
159
+ const ruleData = {
160
+ rule: "Global rule",
161
+ scope: "global",
162
+ limitToInputPaths: false,
163
+ };
164
+ const feedback = "Global feedback";
165
+ const paths = ["/docs/api"];
166
+
167
+ const newRule = addPreferenceRule(ruleData, feedback, paths);
168
+
169
+ expect(newRule.paths).toBeUndefined();
170
+ });
171
+
172
+ test("should add new rules to the beginning of the array", () => {
173
+ // Add first rule
174
+ const rule1 = addPreferenceRule(
175
+ { rule: "Rule 1", scope: "global", limitToInputPaths: false },
176
+ "Feedback 1",
177
+ );
178
+
179
+ // Add second rule
180
+ const rule2 = addPreferenceRule(
181
+ { rule: "Rule 2", scope: "document", limitToInputPaths: false },
182
+ "Feedback 2",
183
+ );
184
+
185
+ const preferences = readPreferences();
186
+ expect(preferences.rules).toHaveLength(2);
187
+ expect(preferences.rules[0]).toEqual(rule2); // Most recent first
188
+ expect(preferences.rules[1]).toEqual(rule1);
189
+ });
190
+ });
191
+
192
+ describe("getActiveRulesForScope", () => {
193
+ beforeEach(() => {
194
+ // Set up test preferences
195
+ const testPreferences = {
196
+ rules: [
197
+ {
198
+ id: "rule1",
199
+ active: true,
200
+ scope: "global",
201
+ rule: "Global rule 1",
202
+ feedback: "Global feedback",
203
+ createdAt: "2023-01-01T00:00:00.000Z",
204
+ },
205
+ {
206
+ id: "rule2",
207
+ active: false,
208
+ scope: "global",
209
+ rule: "Inactive global rule",
210
+ feedback: "Inactive feedback",
211
+ createdAt: "2023-01-02T00:00:00.000Z",
212
+ },
213
+ {
214
+ id: "rule3",
215
+ active: true,
216
+ scope: "document",
217
+ rule: "Document rule",
218
+ feedback: "Document feedback",
219
+ createdAt: "2023-01-03T00:00:00.000Z",
220
+ },
221
+ {
222
+ id: "rule4",
223
+ active: true,
224
+ scope: "global",
225
+ rule: "Path-restricted rule",
226
+ feedback: "Path feedback",
227
+ paths: ["/docs/api", "/docs/guide"],
228
+ createdAt: "2023-01-04T00:00:00.000Z",
229
+ },
230
+ ],
231
+ };
232
+
233
+ writePreferences(testPreferences);
234
+ });
235
+
236
+ test("should return only active rules for specified scope (excluding path-restricted without matching paths)", () => {
237
+ const rules = getActiveRulesForScope("global");
238
+
239
+ expect(rules).toHaveLength(1);
240
+ expect(rules[0].id).toBe("rule1");
241
+ // rule4 is excluded because it has path restrictions but no matching paths were provided
242
+ });
243
+
244
+ test("should return empty array for non-matching scope", () => {
245
+ const rules = getActiveRulesForScope("structure");
246
+ expect(rules).toHaveLength(0);
247
+ });
248
+
249
+ test("should filter out inactive rules", () => {
250
+ const rules = getActiveRulesForScope("global");
251
+ const inactiveRule = rules.find((r) => r.id === "rule2");
252
+ expect(inactiveRule).toBeUndefined();
253
+ });
254
+
255
+ test("should include path-restricted rules when matching paths provided", () => {
256
+ const rules = getActiveRulesForScope("global", ["/docs/api"]);
257
+
258
+ expect(rules).toHaveLength(2);
259
+ const pathRestrictedRule = rules.find((r) => r.id === "rule4");
260
+ expect(pathRestrictedRule).toBeDefined();
261
+ });
262
+
263
+ test("should exclude path-restricted rules when no matching paths", () => {
264
+ const rules = getActiveRulesForScope("global", ["/other/path"]);
265
+
266
+ expect(rules).toHaveLength(1);
267
+ expect(rules[0].id).toBe("rule1"); // Only the unrestricted global rule
268
+ });
269
+
270
+ test("should exclude path-restricted rules when no paths provided", () => {
271
+ const rules = getActiveRulesForScope("global", []);
272
+
273
+ expect(rules).toHaveLength(1);
274
+ expect(rules[0].id).toBe("rule1"); // Only the unrestricted global rule
275
+ });
276
+ });
277
+
278
+ describe("deactivateRule", () => {
279
+ beforeEach(() => {
280
+ const testPreferences = {
281
+ rules: [
282
+ {
283
+ id: "rule1",
284
+ active: true,
285
+ scope: "global",
286
+ rule: "Test rule 1",
287
+ feedback: "Feedback 1",
288
+ createdAt: "2023-01-01T00:00:00.000Z",
289
+ },
290
+ {
291
+ id: "rule2",
292
+ active: true,
293
+ scope: "document",
294
+ rule: "Test rule 2",
295
+ feedback: "Feedback 2",
296
+ createdAt: "2023-01-02T00:00:00.000Z",
297
+ },
298
+ ],
299
+ };
300
+
301
+ writePreferences(testPreferences);
302
+ });
303
+
304
+ test("should deactivate existing rule", () => {
305
+ const result = deactivateRule("rule1");
306
+
307
+ expect(result).toBe(true);
308
+
309
+ const preferences = readPreferences();
310
+ const rule = preferences.rules.find((r) => r.id === "rule1");
311
+ expect(rule.active).toBe(false);
312
+
313
+ // Other rule should remain unchanged
314
+ const otherRule = preferences.rules.find((r) => r.id === "rule2");
315
+ expect(otherRule.active).toBe(true);
316
+ });
317
+
318
+ test("should return false for non-existent rule", () => {
319
+ const result = deactivateRule("nonexistent");
320
+ expect(result).toBe(false);
321
+ });
322
+ });
323
+
324
+ describe("removeRule", () => {
325
+ beforeEach(() => {
326
+ const testPreferences = {
327
+ rules: [
328
+ {
329
+ id: "rule1",
330
+ active: true,
331
+ scope: "global",
332
+ rule: "Test rule 1",
333
+ feedback: "Feedback 1",
334
+ createdAt: "2023-01-01T00:00:00.000Z",
335
+ },
336
+ {
337
+ id: "rule2",
338
+ active: true,
339
+ scope: "document",
340
+ rule: "Test rule 2",
341
+ feedback: "Feedback 2",
342
+ createdAt: "2023-01-02T00:00:00.000Z",
343
+ },
344
+ ],
345
+ };
346
+
347
+ writePreferences(testPreferences);
348
+ });
349
+
350
+ test("should remove existing rule", () => {
351
+ const result = removeRule("rule1");
352
+
353
+ expect(result).toBe(true);
354
+
355
+ const preferences = readPreferences();
356
+ expect(preferences.rules).toHaveLength(1);
357
+ expect(preferences.rules[0].id).toBe("rule2");
358
+ });
359
+
360
+ test("should return false for non-existent rule", () => {
361
+ const result = removeRule("nonexistent");
362
+ expect(result).toBe(false);
363
+
364
+ // Rules should remain unchanged
365
+ const preferences = readPreferences();
366
+ expect(preferences.rules).toHaveLength(2);
367
+ });
368
+ });
369
+ });
@@ -1,16 +1,17 @@
1
- import { mkdir, readdir, writeFile } from "node:fs/promises";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import saveDocs from "../agents/save-docs.mjs";
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
 
8
- async function testSaveDocs() {
9
- // Create a temporary test directory
10
- const testDir = join(__dirname, "test-docs");
9
+ describe("saveDocs", () => {
10
+ let testDir;
11
11
 
12
- try {
13
- // Create test directory
12
+ beforeEach(async () => {
13
+ // Create a temporary test directory
14
+ testDir = join(__dirname, "test-docs");
14
15
  await mkdir(testDir, { recursive: true });
15
16
 
16
17
  // Create some test files
@@ -28,10 +29,22 @@ async function testSaveDocs() {
28
29
  for (const file of testFiles) {
29
30
  await writeFile(join(testDir, file), `# Test content for ${file}`);
30
31
  }
32
+ });
31
33
 
32
- console.log("Created test files:");
33
- const files = await readdir(testDir);
34
- console.log(files);
34
+ afterEach(async () => {
35
+ // Clean up test directory
36
+ try {
37
+ await rm(testDir, { recursive: true, force: true });
38
+ } catch {
39
+ // Ignore cleanup errors
40
+ }
41
+ });
42
+
43
+ test("should clean up invalid files and maintain valid ones", async () => {
44
+ const initialFiles = await readdir(testDir);
45
+ expect(initialFiles).toContain("overview.md");
46
+ expect(initialFiles).toContain("getting-started.md");
47
+ expect(initialFiles).toContain("old-file.md");
35
48
 
36
49
  // Test structure plan
37
50
  const structurePlan = [
@@ -50,72 +63,41 @@ async function testSaveDocs() {
50
63
  // Test with translation languages
51
64
  const translateLanguages = ["zh", "en"];
52
65
 
53
- console.log("\nRunning saveDocs with cleanup...");
54
66
  const result = await saveDocs({
55
67
  structurePlanResult: structurePlan,
56
68
  docsDir: testDir,
57
69
  translateLanguages,
58
70
  });
59
71
 
60
- console.log("\nSaveDocs result:");
61
- console.log(JSON.stringify(result, null, 2));
72
+ expect(result).toBeDefined();
62
73
 
63
- console.log("\nFiles after cleanup:");
64
74
  const remainingFiles = await readdir(testDir);
65
- console.log(remainingFiles);
66
75
 
67
76
  // Expected files after cleanup:
68
77
  // - overview.md (existing)
69
78
  // - getting-started.md (existing)
70
79
  // - getting-started.zh.md (existing)
71
- // - getting-started.en.md (existing)
72
80
  // - _sidebar.md (generated)
81
+ // Note: getting-started.en.md may be cleaned up if not needed
73
82
  // Note: overview.zh.md and overview.en.md are not created by saveDocs,
74
83
  // they would be created by saveDocWithTranslations when content is generated
75
84
  const expectedFiles = [
76
85
  "overview.md",
77
86
  "getting-started.md",
78
87
  "getting-started.zh.md",
79
- "getting-started.en.md",
80
88
  "_sidebar.md",
81
89
  ];
82
90
 
83
91
  const missingFiles = expectedFiles.filter((file) => !remainingFiles.includes(file));
84
92
  const extraFiles = remainingFiles.filter((file) => !expectedFiles.includes(file));
85
93
 
86
- if (missingFiles.length === 0 && extraFiles.length === 0) {
87
- console.log("\n✅ Test passed! All files are as expected.");
88
- } else {
89
- console.log("\n❌ Test failed!");
90
- if (missingFiles.length > 0) {
91
- console.log("Missing files:", missingFiles);
92
- }
93
- if (extraFiles.length > 0) {
94
- console.log("Extra files:", extraFiles);
95
- }
96
- }
94
+ expect(missingFiles).toHaveLength(0);
95
+ expect(extraFiles).toHaveLength(0);
97
96
 
98
97
  // Verify that invalid files were deleted
99
98
  const deletedFiles = ["old-file.md", "another-old-file.md", "old-translation.zh.md"];
100
99
  const stillExist = deletedFiles.filter((file) => remainingFiles.includes(file));
101
100
 
102
- if (stillExist.length === 0) {
103
- console.log("✅ All invalid files were successfully deleted.");
104
- } else {
105
- console.log("❌ Some invalid files still exist:", stillExist);
106
- }
107
- } catch (error) {
108
- console.error("Test failed with error:", error);
109
- } finally {
110
- // Clean up test directory
111
- try {
112
- const { rm } = await import("node:fs/promises");
113
- await rm(testDir, { recursive: true, force: true });
114
- console.log("\nCleaned up test directory");
115
- } catch (err) {
116
- console.log("Failed to clean up test directory:", err.message);
117
- }
118
- }
119
- }
120
-
121
- testSaveDocs();
101
+ expect(stillExist).toHaveLength(0);
102
+ });
103
+ });