@aigne/doc-smith 0.7.2 → 0.8.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.
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { promises as fs } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import { parse as parseYAML } from "yaml";
3
- import { generateYAML } from "../agents/input-generator.mjs";
6
+ import init, { generateYAML } from "../agents/input-generator.mjs";
4
7
 
5
8
  describe("generateYAML", () => {
6
9
  // Helper function to parse YAML and verify it's valid
@@ -938,3 +941,593 @@ describe("generateYAML", () => {
938
941
  });
939
942
  });
940
943
  });
944
+
945
+ describe("init", () => {
946
+ // Helper function to create mock prompts
947
+ function createMockPrompts(responses) {
948
+ return {
949
+ checkbox: (options) => {
950
+ const key = options.message.match(/\[(\d+)\/\d+\]/)?.[1] || "default";
951
+ const response = responses[`checkbox_${key}`] || responses["checkbox"] || [];
952
+ return Promise.resolve(response);
953
+ },
954
+ select: (options) => {
955
+ const key = options.message.match(/\[(\d+)\/\d+\]/)?.[1] || "default";
956
+ const response = responses[`select_${key}`] || responses["select"] || "";
957
+ return Promise.resolve(response);
958
+ },
959
+ input: (options) => {
960
+ const key = options.message.match(/\[(\d+)\/\d+\]/)?.[1] || "default";
961
+ const response = responses[`input_${key}`] || responses["input"] || options.default || "";
962
+ return Promise.resolve(response);
963
+ },
964
+ search: () => {
965
+ const response = responses["search"] || "";
966
+ return Promise.resolve(response);
967
+ },
968
+ };
969
+ }
970
+
971
+ // Helper function to create temporary directory
972
+ async function createTempDir() {
973
+ const tempDir = join(tmpdir(), `aigne-test-${Date.now()}`);
974
+ await fs.mkdir(tempDir, { recursive: true });
975
+ return tempDir;
976
+ }
977
+
978
+ // Helper function to cleanup temp directory
979
+ async function cleanupTempDir(tempDir) {
980
+ try {
981
+ await fs.rm(tempDir, { recursive: true, force: true });
982
+ } catch {
983
+ // Ignore cleanup errors since they don't affect test results
984
+ }
985
+ }
986
+
987
+ describe("Complete init workflow", () => {
988
+ test("should complete full init workflow with typical developer responses", async () => {
989
+ const tempDir = await createTempDir();
990
+
991
+ try {
992
+ const mockResponses = {
993
+ checkbox_1: ["getStarted", "findAnswers"], // Document purpose
994
+ checkbox_2: ["developers"], // Target audience
995
+ select_3: "domainFamiliar", // Reader knowledge level
996
+ select_4: "balancedCoverage", // Documentation depth
997
+ select_5: "en", // Primary language
998
+ checkbox_6: ["zh", "ja"], // Translation languages
999
+ input_7: join(tempDir, "docs"), // Documentation directory
1000
+ search: "", // Source paths (empty to finish)
1001
+ };
1002
+
1003
+ const mockPrompts = createMockPrompts(mockResponses);
1004
+ const options = { prompts: mockPrompts };
1005
+
1006
+ const result = await init(
1007
+ {
1008
+ outputPath: tempDir,
1009
+ fileName: "config.yaml",
1010
+ skipIfExists: false,
1011
+ },
1012
+ options,
1013
+ );
1014
+
1015
+ // Check that function completed successfully
1016
+ expect(result).toEqual({});
1017
+
1018
+ // Check that config file was created
1019
+ const configPath = join(tempDir, "config.yaml");
1020
+ const configExists = await fs
1021
+ .access(configPath)
1022
+ .then(() => true)
1023
+ .catch(() => false);
1024
+ expect(configExists).toBe(true);
1025
+
1026
+ // Verify the generated config content
1027
+ const configContent = await fs.readFile(configPath, "utf8");
1028
+ const config = parseYAML(configContent);
1029
+
1030
+ expect(config.documentPurpose).toEqual(["getStarted", "findAnswers"]);
1031
+ expect(config.targetAudienceTypes).toEqual(["developers"]);
1032
+ expect(config.readerKnowledgeLevel).toBe("domainFamiliar");
1033
+ expect(config.documentationDepth).toBe("balancedCoverage");
1034
+ expect(config.locale).toBe("en");
1035
+ expect(config.translateLanguages).toEqual(["zh", "ja"]);
1036
+ expect(config.docsDir).toBe(join(tempDir, "docs"));
1037
+ expect(config.sourcesPath).toEqual(["./"]); // Default when no paths provided
1038
+ } finally {
1039
+ await cleanupTempDir(tempDir);
1040
+ }
1041
+ });
1042
+
1043
+ test("should handle mixed purpose workflow with priority selection", async () => {
1044
+ const tempDir = await createTempDir();
1045
+
1046
+ try {
1047
+ const mockResponses = {
1048
+ checkbox_1: ["mixedPurpose"], // Document purpose - triggers follow-up
1049
+ checkbox: ["completeTasks", "findAnswers"], // Top priorities after mixedPurpose
1050
+ checkbox_2: ["developers", "devops"], // Target audience
1051
+ select_3: "experiencedUsers", // Reader knowledge level
1052
+ select_4: "comprehensive", // Documentation depth
1053
+ select_5: "zh-CN", // Primary language
1054
+ checkbox_6: ["en"], // Translation languages
1055
+ input_7: join(tempDir, "documentation"), // Documentation directory
1056
+ search: "", // Source paths (empty to finish)
1057
+ };
1058
+
1059
+ const mockPrompts = createMockPrompts(mockResponses);
1060
+ const options = { prompts: mockPrompts };
1061
+
1062
+ const result = await init(
1063
+ {
1064
+ outputPath: tempDir,
1065
+ fileName: "test-config.yaml",
1066
+ skipIfExists: false,
1067
+ },
1068
+ options,
1069
+ );
1070
+
1071
+ expect(result).toEqual({});
1072
+
1073
+ // Verify the generated config
1074
+ const configPath = join(tempDir, "test-config.yaml");
1075
+ const configContent = await fs.readFile(configPath, "utf8");
1076
+ const config = parseYAML(configContent);
1077
+
1078
+ expect(config.documentPurpose).toEqual(["completeTasks", "findAnswers"]);
1079
+ expect(config.targetAudienceTypes).toEqual(["developers", "devops"]);
1080
+ expect(config.readerKnowledgeLevel).toBe("experiencedUsers");
1081
+ expect(config.documentationDepth).toBe("comprehensive");
1082
+ expect(config.locale).toBe("zh-CN");
1083
+ expect(config.translateLanguages).toEqual(["en"]);
1084
+ } finally {
1085
+ await cleanupTempDir(tempDir);
1086
+ }
1087
+ });
1088
+
1089
+ test("should handle end-user focused minimal configuration", async () => {
1090
+ const tempDir = await createTempDir();
1091
+
1092
+ try {
1093
+ const mockResponses = {
1094
+ checkbox_1: ["getStarted"], // Document purpose
1095
+ checkbox_2: ["endUsers"], // Target audience
1096
+ select_3: "completeBeginners", // Reader knowledge level
1097
+ select_4: "essentialOnly", // Documentation depth
1098
+ select_5: "en", // Primary language
1099
+ checkbox_6: [], // No translation languages
1100
+ input_7: join(tempDir, "simple-docs"), // Documentation directory
1101
+ search: "", // Source paths (empty to finish)
1102
+ };
1103
+
1104
+ const mockPrompts = createMockPrompts(mockResponses);
1105
+ const options = { prompts: mockPrompts };
1106
+
1107
+ const result = await init(
1108
+ {
1109
+ outputPath: tempDir,
1110
+ fileName: "simple-config.yaml",
1111
+ skipIfExists: false,
1112
+ },
1113
+ options,
1114
+ );
1115
+
1116
+ expect(result).toEqual({});
1117
+
1118
+ const configPath = join(tempDir, "simple-config.yaml");
1119
+ const configContent = await fs.readFile(configPath, "utf8");
1120
+ const config = parseYAML(configContent);
1121
+
1122
+ expect(config.documentPurpose).toEqual(["getStarted"]);
1123
+ expect(config.targetAudienceTypes).toEqual(["endUsers"]);
1124
+ expect(config.readerKnowledgeLevel).toBe("completeBeginners");
1125
+ expect(config.documentationDepth).toBe("essentialOnly");
1126
+ expect(config.locale).toBe("en");
1127
+ expect(config.translateLanguages).toBeUndefined();
1128
+ } finally {
1129
+ await cleanupTempDir(tempDir);
1130
+ }
1131
+ });
1132
+ });
1133
+
1134
+ describe("Source paths handling", () => {
1135
+ test("should handle multiple source paths input when search returns valid paths", async () => {
1136
+ const tempDir = await createTempDir();
1137
+
1138
+ try {
1139
+ let searchCallCount = 0;
1140
+ const sourcePaths = ["./src", "./lib", "./packages", ""];
1141
+
1142
+ const mockPrompts = {
1143
+ checkbox: () => Promise.resolve(["getStarted"]),
1144
+ select: () => Promise.resolve("en"),
1145
+ input: () => Promise.resolve(join(tempDir, "docs")),
1146
+ search: () => {
1147
+ const response = sourcePaths[searchCallCount];
1148
+ searchCallCount++;
1149
+ return Promise.resolve(response);
1150
+ },
1151
+ };
1152
+
1153
+ const options = { prompts: mockPrompts };
1154
+
1155
+ // First let's create the directories that will be searched for
1156
+ await fs.mkdir(join(process.cwd(), "src"), { recursive: true }).catch(() => {
1157
+ // Ignore if directory already exists
1158
+ });
1159
+ await fs.mkdir(join(process.cwd(), "lib"), { recursive: true }).catch(() => {
1160
+ // Ignore if directory already exists
1161
+ });
1162
+ await fs.mkdir(join(process.cwd(), "packages"), { recursive: true }).catch(() => {
1163
+ // Ignore if directory already exists
1164
+ });
1165
+
1166
+ try {
1167
+ const result = await init(
1168
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1169
+ options,
1170
+ );
1171
+
1172
+ expect(result).toEqual({});
1173
+
1174
+ const configPath = join(tempDir, "config.yaml");
1175
+ const configContent = await fs.readFile(configPath, "utf8");
1176
+ const config = parseYAML(configContent);
1177
+
1178
+ // Should contain the paths that were added before empty string
1179
+ expect(config.sourcesPath.length).toBeGreaterThan(0);
1180
+ } finally {
1181
+ // Clean up test directories
1182
+ await fs.rm(join(process.cwd(), "src"), { recursive: true, force: true }).catch(() => {
1183
+ // Ignore cleanup errors since directories may not exist
1184
+ });
1185
+ await fs.rm(join(process.cwd(), "lib"), { recursive: true, force: true }).catch(() => {
1186
+ // Ignore cleanup errors since directories may not exist
1187
+ });
1188
+ await fs
1189
+ .rm(join(process.cwd(), "packages"), { recursive: true, force: true })
1190
+ .catch(() => {
1191
+ // Ignore cleanup errors since directories may not exist
1192
+ });
1193
+ }
1194
+ } finally {
1195
+ await cleanupTempDir(tempDir);
1196
+ }
1197
+ });
1198
+
1199
+ test("should use default source path when no paths provided", async () => {
1200
+ const tempDir = await createTempDir();
1201
+
1202
+ try {
1203
+ const mockPrompts = {
1204
+ checkbox: () => Promise.resolve(["getStarted"]),
1205
+ select: () => Promise.resolve("en"),
1206
+ input: () => Promise.resolve(join(tempDir, "docs")),
1207
+ search: () => Promise.resolve(""), // Immediately finish without adding paths
1208
+ };
1209
+
1210
+ const options = { prompts: mockPrompts };
1211
+
1212
+ const result = await init(
1213
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1214
+ options,
1215
+ );
1216
+
1217
+ expect(result).toEqual({});
1218
+
1219
+ const configPath = join(tempDir, "config.yaml");
1220
+ const configContent = await fs.readFile(configPath, "utf8");
1221
+ const config = parseYAML(configContent);
1222
+
1223
+ expect(config.sourcesPath).toEqual(["./"]); // Default value
1224
+ } finally {
1225
+ await cleanupTempDir(tempDir);
1226
+ }
1227
+ });
1228
+ });
1229
+
1230
+ describe("Skip existing configuration", () => {
1231
+ test("should skip if config exists and skipIfExists is true", async () => {
1232
+ const tempDir = await createTempDir();
1233
+
1234
+ try {
1235
+ // Create existing config file
1236
+ const configPath = join(tempDir, "config.yaml");
1237
+ const existingConfig = 'projectName: "Existing Project"\nlocale: "zh"';
1238
+ await fs.writeFile(configPath, existingConfig, "utf8");
1239
+
1240
+ const mockPrompts = {
1241
+ checkbox: () => Promise.resolve(["getStarted"]),
1242
+ select: () => Promise.resolve("en"),
1243
+ input: () => Promise.resolve("docs"),
1244
+ search: () => Promise.resolve(""),
1245
+ };
1246
+
1247
+ const options = { prompts: mockPrompts };
1248
+
1249
+ const result = await init(
1250
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: true },
1251
+ options,
1252
+ );
1253
+
1254
+ expect(result).toEqual({});
1255
+
1256
+ // Config should remain unchanged
1257
+ const configContent = await fs.readFile(configPath, "utf8");
1258
+ expect(configContent).toBe(existingConfig);
1259
+ } finally {
1260
+ await cleanupTempDir(tempDir);
1261
+ }
1262
+ });
1263
+
1264
+ test("should not skip if config file is empty even when skipIfExists is true", async () => {
1265
+ const tempDir = await createTempDir();
1266
+
1267
+ try {
1268
+ // Create empty config file
1269
+ const configPath = join(tempDir, "config.yaml");
1270
+ await fs.writeFile(configPath, "", "utf8");
1271
+
1272
+ const mockPrompts = {
1273
+ checkbox: () => Promise.resolve(["getStarted"]),
1274
+ select: () => Promise.resolve("en"),
1275
+ input: () => Promise.resolve(join(tempDir, "docs")),
1276
+ search: () => Promise.resolve(""),
1277
+ };
1278
+
1279
+ const options = { prompts: mockPrompts };
1280
+
1281
+ const result = await init(
1282
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: true },
1283
+ options,
1284
+ );
1285
+
1286
+ expect(result).toEqual({});
1287
+
1288
+ // Config should be generated since original was empty
1289
+ const configContent = await fs.readFile(configPath, "utf8");
1290
+ expect(configContent).toContain("projectName:");
1291
+ expect(configContent).toContain("locale: en");
1292
+ } finally {
1293
+ await cleanupTempDir(tempDir);
1294
+ }
1295
+ });
1296
+ });
1297
+
1298
+ describe("Validation error handling", () => {
1299
+ test("should handle empty document purpose validation", async () => {
1300
+ const tempDir = await createTempDir();
1301
+
1302
+ try {
1303
+ // Mock prompts that will trigger validation errors by calling validate function
1304
+ let validateCalled = false;
1305
+ const mockPrompts = {
1306
+ checkbox: (options) => {
1307
+ if (options.message.includes("[1/8]") && options.validate) {
1308
+ // Test the validation function directly
1309
+ const validationResult = options.validate([]);
1310
+ expect(validationResult).toBe("Please select at least one purpose.");
1311
+ validateCalled = true;
1312
+ // Return valid result after testing validation
1313
+ return Promise.resolve(["getStarted"]);
1314
+ }
1315
+ return Promise.resolve(["getStarted"]);
1316
+ },
1317
+ select: () => Promise.resolve("en"),
1318
+ input: () => Promise.resolve(join(tempDir, "docs")),
1319
+ search: () => Promise.resolve(""),
1320
+ };
1321
+
1322
+ const options = { prompts: mockPrompts };
1323
+
1324
+ const result = await init(
1325
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1326
+ options,
1327
+ );
1328
+
1329
+ expect(result).toEqual({});
1330
+ expect(validateCalled).toBe(true);
1331
+ } finally {
1332
+ await cleanupTempDir(tempDir);
1333
+ }
1334
+ });
1335
+
1336
+ test("should handle empty target audience validation", async () => {
1337
+ const tempDir = await createTempDir();
1338
+
1339
+ try {
1340
+ let audienceValidateCalled = false;
1341
+ const mockPrompts = {
1342
+ checkbox: (options) => {
1343
+ if (options.message.includes("[1/8]")) {
1344
+ return Promise.resolve(["getStarted"]); // Valid document purpose
1345
+ }
1346
+ if (options.message.includes("[2/8]") && options.validate) {
1347
+ // Test the validation function for target audience
1348
+ const validationResult = options.validate([]);
1349
+ expect(validationResult).toBe("Please select at least one audience.");
1350
+ audienceValidateCalled = true;
1351
+ return Promise.resolve(["developers"]); // Valid result after testing
1352
+ }
1353
+ return Promise.resolve(["developers"]);
1354
+ },
1355
+ select: () => Promise.resolve("en"),
1356
+ input: () => Promise.resolve(join(tempDir, "docs")),
1357
+ search: () => Promise.resolve(""),
1358
+ };
1359
+
1360
+ const options = { prompts: mockPrompts };
1361
+
1362
+ const result = await init(
1363
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1364
+ options,
1365
+ );
1366
+
1367
+ expect(result).toEqual({});
1368
+ expect(audienceValidateCalled).toBe(true);
1369
+ } finally {
1370
+ await cleanupTempDir(tempDir);
1371
+ }
1372
+ });
1373
+
1374
+ test("should handle mixed purpose priority validation errors", async () => {
1375
+ const tempDir = await createTempDir();
1376
+
1377
+ try {
1378
+ let priorityValidateCalled = false;
1379
+ const mockPrompts = {
1380
+ checkbox: (options) => {
1381
+ if (options.message.includes("[1/8]")) {
1382
+ return Promise.resolve(["mixedPurpose"]); // Trigger follow-up question
1383
+ }
1384
+ // This is the follow-up priority selection
1385
+ if (options.message.includes("Which is most important?") && options.validate) {
1386
+ // Test validation for empty selection
1387
+ let validationResult = options.validate([]);
1388
+ expect(validationResult).toBe("Please select at least one priority.");
1389
+
1390
+ // Test validation for too many selections
1391
+ validationResult = options.validate(["getStarted", "completeTasks", "findAnswers"]);
1392
+ expect(validationResult).toBe("Please select maximum 2 priorities.");
1393
+
1394
+ // Test validation for valid selection
1395
+ validationResult = options.validate(["getStarted", "completeTasks"]);
1396
+ expect(validationResult).toBe(true);
1397
+
1398
+ priorityValidateCalled = true;
1399
+ return Promise.resolve(["getStarted", "completeTasks"]); // Valid selection
1400
+ }
1401
+ return Promise.resolve(["getStarted", "completeTasks"]);
1402
+ },
1403
+ select: () => Promise.resolve("en"),
1404
+ input: () => Promise.resolve(join(tempDir, "docs")),
1405
+ search: () => Promise.resolve(""),
1406
+ };
1407
+
1408
+ const options = { prompts: mockPrompts };
1409
+
1410
+ const result = await init(
1411
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1412
+ options,
1413
+ );
1414
+
1415
+ expect(result).toEqual({});
1416
+ expect(priorityValidateCalled).toBe(true);
1417
+ } finally {
1418
+ await cleanupTempDir(tempDir);
1419
+ }
1420
+ });
1421
+
1422
+ test("should handle file write errors", async () => {
1423
+ const invalidPath = "/invalid/path/that/does/not/exist";
1424
+
1425
+ const mockPrompts = {
1426
+ checkbox: () => Promise.resolve(["getStarted"]),
1427
+ select: () => Promise.resolve("en"),
1428
+ input: () => Promise.resolve("docs"),
1429
+ search: () => Promise.resolve(""),
1430
+ };
1431
+
1432
+ const options = { prompts: mockPrompts };
1433
+
1434
+ const result = await init(
1435
+ { outputPath: invalidPath, fileName: "config.yaml", skipIfExists: false },
1436
+ options,
1437
+ );
1438
+
1439
+ expect(result).toHaveProperty("inputGeneratorStatus", false);
1440
+ expect(result).toHaveProperty("inputGeneratorError");
1441
+ expect(typeof result.inputGeneratorError).toBe("string");
1442
+ });
1443
+ });
1444
+
1445
+ describe("Advanced source path scenarios", () => {
1446
+ test("should handle source path validation and duplicate detection", async () => {
1447
+ const tempDir = await createTempDir();
1448
+
1449
+ try {
1450
+ let searchCallCount = 0;
1451
+ const responses = [
1452
+ "invalid/path/that/does/not/exist", // Should trigger validation error
1453
+ "invalid/path/that/does/not/exist", // Duplicate path
1454
+ join(tempDir, "valid-path"), // Valid path after creating it
1455
+ join(tempDir, "valid-path"), // Duplicate of valid path
1456
+ "**/*.glob", // Glob pattern (should be accepted)
1457
+ "**/*.glob", // Duplicate glob pattern
1458
+ "", // End input
1459
+ ];
1460
+
1461
+ // Create a valid directory for testing
1462
+ await fs.mkdir(join(tempDir, "valid-path"), { recursive: true });
1463
+
1464
+ const mockPrompts = {
1465
+ checkbox: () => Promise.resolve(["getStarted"]),
1466
+ select: () => Promise.resolve("en"),
1467
+ input: () => Promise.resolve(join(tempDir, "docs")),
1468
+ search: () => {
1469
+ const response = responses[searchCallCount];
1470
+ searchCallCount++;
1471
+ return Promise.resolve(response);
1472
+ },
1473
+ };
1474
+
1475
+ const options = { prompts: mockPrompts };
1476
+
1477
+ const result = await init(
1478
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1479
+ options,
1480
+ );
1481
+
1482
+ expect(result).toEqual({});
1483
+
1484
+ const configPath = join(tempDir, "config.yaml");
1485
+ const configContent = await fs.readFile(configPath, "utf8");
1486
+ const config = parseYAML(configContent);
1487
+
1488
+ // Should contain the valid paths that were successfully added
1489
+ expect(config.sourcesPath.length).toBeGreaterThan(0);
1490
+ } finally {
1491
+ await cleanupTempDir(tempDir);
1492
+ }
1493
+ });
1494
+
1495
+ test("should handle search source function callback", async () => {
1496
+ const tempDir = await createTempDir();
1497
+
1498
+ try {
1499
+ const mockPrompts = {
1500
+ checkbox: () => Promise.resolve(["getStarted"]),
1501
+ select: () => Promise.resolve("en"),
1502
+ input: () => Promise.resolve(join(tempDir, "docs")),
1503
+ search: (options) => {
1504
+ // Test the source callback function
1505
+ if (options?.source) {
1506
+ // Call the source function with different inputs to test the logic
1507
+ const sourceResults1 = options.source("");
1508
+ const sourceResults2 = options.source("src");
1509
+ const sourceResults3 = options.source("**/*.js");
1510
+
1511
+ // Verify these are promises
1512
+ expect(sourceResults1).toBeInstanceOf(Promise);
1513
+ expect(sourceResults2).toBeInstanceOf(Promise);
1514
+ expect(sourceResults3).toBeInstanceOf(Promise);
1515
+ }
1516
+ return Promise.resolve(""); // End input
1517
+ },
1518
+ };
1519
+
1520
+ const options = { prompts: mockPrompts };
1521
+
1522
+ const result = await init(
1523
+ { outputPath: tempDir, fileName: "config.yaml", skipIfExists: false },
1524
+ options,
1525
+ );
1526
+
1527
+ expect(result).toEqual({});
1528
+ } finally {
1529
+ await cleanupTempDir(tempDir);
1530
+ }
1531
+ });
1532
+ });
1533
+ });