gsd-pi 2.5.0 → 2.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.
- package/README.md +1 -0
- package/dist/cli.js +7 -1
- package/dist/loader.js +21 -3
- package/dist/logo.d.ts +3 -3
- package/dist/logo.js +2 -2
- package/package.json +1 -1
- package/src/resources/extensions/get-secrets-from-user.ts +331 -59
- package/src/resources/extensions/gsd/auto.ts +80 -18
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/doctor.ts +23 -4
- package/src/resources/extensions/gsd/files.ts +115 -1
- package/src/resources/extensions/gsd/git-service.ts +67 -105
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +6 -3
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +7 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -6
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
- package/src/resources/extensions/gsd/session-forensics.ts +19 -6
- package/src/resources/extensions/gsd/templates/plan.md +8 -10
- package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +401 -65
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- package/src/resources/extensions/gsd/types.ts +27 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseRoadmap, parsePlan, parseSummary, parseContinue, parseRequirementCounts } from '../files.ts';
|
|
1
|
+
import { parseRoadmap, parsePlan, parseSummary, parseContinue, parseRequirementCounts, parseSecretsManifest, formatSecretsManifest } from '../files.ts';
|
|
2
2
|
|
|
3
3
|
let passed = 0;
|
|
4
4
|
let failed = 0;
|
|
@@ -1249,97 +1249,433 @@ console.log('\n=== parseRequirementCounts: total is sum of all section counts ==
|
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
1251
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1252
|
-
//
|
|
1252
|
+
// parseSecretsManifest / formatSecretsManifest tests
|
|
1253
1253
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1254
1254
|
|
|
1255
|
-
console.log('\n===
|
|
1255
|
+
console.log('\n=== parseSecretsManifest: full manifest with 3 keys ===');
|
|
1256
1256
|
{
|
|
1257
|
-
const content =
|
|
1258
|
-
id: T04
|
|
1259
|
-
parent: S03
|
|
1260
|
-
milestone: M001
|
|
1261
|
-
provides:
|
|
1262
|
-
- iOS rules
|
|
1263
|
-
key_files:
|
|
1264
|
-
- .claude/rules/swift-style.md
|
|
1265
|
-
key_decisions: none
|
|
1266
|
-
patterns_established: none
|
|
1267
|
-
drill_down_paths: none
|
|
1268
|
-
observability_surfaces: none — static reference files
|
|
1269
|
-
affects: single-value
|
|
1270
|
-
---
|
|
1257
|
+
const content = `# Secrets Manifest
|
|
1271
1258
|
|
|
1272
|
-
|
|
1259
|
+
**Milestone:** M003
|
|
1260
|
+
**Generated:** 2025-06-15T10:00:00Z
|
|
1273
1261
|
|
|
1274
|
-
|
|
1262
|
+
### OPENAI_API_KEY
|
|
1275
1263
|
|
|
1276
|
-
|
|
1264
|
+
**Service:** OpenAI
|
|
1265
|
+
**Dashboard:** https://platform.openai.com/api-keys
|
|
1266
|
+
**Format hint:** starts with sk-
|
|
1267
|
+
**Status:** pending
|
|
1268
|
+
**Destination:** dotenv
|
|
1277
1269
|
|
|
1278
|
-
|
|
1270
|
+
1. Go to https://platform.openai.com/api-keys
|
|
1271
|
+
2. Click "Create new secret key"
|
|
1272
|
+
3. Copy the key immediately — it won't be shown again
|
|
1279
1273
|
|
|
1280
|
-
|
|
1274
|
+
### STRIPE_SECRET_KEY
|
|
1281
1275
|
|
|
1282
|
-
|
|
1276
|
+
**Service:** Stripe
|
|
1277
|
+
**Dashboard:** https://dashboard.stripe.com/apikeys
|
|
1278
|
+
**Format hint:** starts with sk_test_ or sk_live_
|
|
1279
|
+
**Status:** collected
|
|
1280
|
+
**Destination:** dotenv
|
|
1281
|
+
|
|
1282
|
+
1. Go to https://dashboard.stripe.com/apikeys
|
|
1283
|
+
2. Reveal the secret key
|
|
1284
|
+
3. Copy it
|
|
1285
|
+
|
|
1286
|
+
### SUPABASE_URL
|
|
1287
|
+
|
|
1288
|
+
**Service:** Supabase
|
|
1289
|
+
**Dashboard:** https://app.supabase.com/project/settings/api
|
|
1290
|
+
**Format hint:** https://<project-ref>.supabase.co
|
|
1291
|
+
**Status:** skipped
|
|
1292
|
+
**Destination:** vercel
|
|
1293
|
+
|
|
1294
|
+
1. Go to project settings in Supabase
|
|
1295
|
+
2. Copy the URL from the API section
|
|
1283
1296
|
`;
|
|
1284
1297
|
|
|
1285
|
-
const
|
|
1298
|
+
const m = parseSecretsManifest(content);
|
|
1299
|
+
|
|
1300
|
+
assertEq(m.milestone, 'M003', 'manifest milestone');
|
|
1301
|
+
assertEq(m.generatedAt, '2025-06-15T10:00:00Z', 'manifest generatedAt');
|
|
1302
|
+
assertEq(m.entries.length, 3, 'three entries');
|
|
1303
|
+
|
|
1304
|
+
// First entry
|
|
1305
|
+
assertEq(m.entries[0].key, 'OPENAI_API_KEY', 'entry 0 key');
|
|
1306
|
+
assertEq(m.entries[0].service, 'OpenAI', 'entry 0 service');
|
|
1307
|
+
assertEq(m.entries[0].dashboardUrl, 'https://platform.openai.com/api-keys', 'entry 0 dashboardUrl');
|
|
1308
|
+
assertEq(m.entries[0].formatHint, 'starts with sk-', 'entry 0 formatHint');
|
|
1309
|
+
assertEq(m.entries[0].status, 'pending', 'entry 0 status');
|
|
1310
|
+
assertEq(m.entries[0].destination, 'dotenv', 'entry 0 destination');
|
|
1311
|
+
assertEq(m.entries[0].guidance.length, 3, 'entry 0 guidance count');
|
|
1312
|
+
assertEq(m.entries[0].guidance[0], 'Go to https://platform.openai.com/api-keys', 'entry 0 guidance[0]');
|
|
1313
|
+
assertEq(m.entries[0].guidance[2], 'Copy the key immediately — it won\'t be shown again', 'entry 0 guidance[2]');
|
|
1314
|
+
|
|
1315
|
+
// Second entry
|
|
1316
|
+
assertEq(m.entries[1].key, 'STRIPE_SECRET_KEY', 'entry 1 key');
|
|
1317
|
+
assertEq(m.entries[1].service, 'Stripe', 'entry 1 service');
|
|
1318
|
+
assertEq(m.entries[1].status, 'collected', 'entry 1 status');
|
|
1319
|
+
assertEq(m.entries[1].formatHint, 'starts with sk_test_ or sk_live_', 'entry 1 formatHint');
|
|
1320
|
+
assertEq(m.entries[1].guidance.length, 3, 'entry 1 guidance count');
|
|
1321
|
+
|
|
1322
|
+
// Third entry
|
|
1323
|
+
assertEq(m.entries[2].key, 'SUPABASE_URL', 'entry 2 key');
|
|
1324
|
+
assertEq(m.entries[2].status, 'skipped', 'entry 2 status');
|
|
1325
|
+
assertEq(m.entries[2].destination, 'vercel', 'entry 2 destination');
|
|
1326
|
+
assertEq(m.entries[2].guidance.length, 2, 'entry 2 guidance count');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
console.log('\n=== parseSecretsManifest: single-key manifest ===');
|
|
1330
|
+
{
|
|
1331
|
+
const content = `# Secrets Manifest
|
|
1332
|
+
|
|
1333
|
+
**Milestone:** M001
|
|
1334
|
+
**Generated:** 2025-06-15T12:00:00Z
|
|
1286
1335
|
|
|
1287
|
-
|
|
1288
|
-
assertEq(s.frontmatter.provides.length, 1, '#91: provides array preserved');
|
|
1289
|
-
assertEq(s.frontmatter.provides[0], 'iOS rules', '#91: provides value');
|
|
1290
|
-
assertEq(s.frontmatter.key_files.length, 1, '#91: key_files array preserved');
|
|
1336
|
+
### DATABASE_URL
|
|
1291
1337
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1338
|
+
**Service:** PostgreSQL
|
|
1339
|
+
**Dashboard:** https://console.neon.tech
|
|
1340
|
+
**Format hint:** postgresql://...
|
|
1341
|
+
**Status:** pending
|
|
1342
|
+
**Destination:** dotenv
|
|
1296
1343
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1344
|
+
1. Create a database on Neon
|
|
1345
|
+
2. Copy the connection string
|
|
1346
|
+
`;
|
|
1299
1347
|
|
|
1300
|
-
|
|
1301
|
-
assertEq(
|
|
1348
|
+
const m = parseSecretsManifest(content);
|
|
1349
|
+
assertEq(m.milestone, 'M001', 'single-key milestone');
|
|
1350
|
+
assertEq(m.entries.length, 1, 'single entry');
|
|
1351
|
+
assertEq(m.entries[0].key, 'DATABASE_URL', 'single entry key');
|
|
1352
|
+
assertEq(m.entries[0].service, 'PostgreSQL', 'single entry service');
|
|
1353
|
+
assertEq(m.entries[0].guidance.length, 2, 'single entry guidance count');
|
|
1354
|
+
}
|
|
1302
1355
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
assertEq(s.frontmatter.observability_surfaces[0], 'none — static reference files', '#91: full scalar preserved');
|
|
1356
|
+
console.log('\n=== parseSecretsManifest: empty/no-secrets manifest ===');
|
|
1357
|
+
{
|
|
1358
|
+
const content = `# Secrets Manifest
|
|
1307
1359
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
assertEq(s.frontmatter.affects[0], 'single-value', '#91: affects value');
|
|
1360
|
+
**Milestone:** M002
|
|
1361
|
+
**Generated:** 2025-06-15T14:00:00Z
|
|
1362
|
+
`;
|
|
1312
1363
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
assertEq(
|
|
1364
|
+
const m = parseSecretsManifest(content);
|
|
1365
|
+
assertEq(m.milestone, 'M002', 'empty manifest milestone');
|
|
1366
|
+
assertEq(m.generatedAt, '2025-06-15T14:00:00Z', 'empty manifest generatedAt');
|
|
1367
|
+
assertEq(m.entries.length, 0, 'no entries in empty manifest');
|
|
1316
1368
|
}
|
|
1317
1369
|
|
|
1318
|
-
console.log('\n===
|
|
1370
|
+
console.log('\n=== parseSecretsManifest: missing optional fields default correctly ===');
|
|
1319
1371
|
{
|
|
1320
|
-
const content =
|
|
1321
|
-
id: T05
|
|
1322
|
-
parent: S04
|
|
1323
|
-
milestone: M001
|
|
1324
|
-
---
|
|
1372
|
+
const content = `# Secrets Manifest
|
|
1325
1373
|
|
|
1326
|
-
|
|
1374
|
+
**Milestone:** M004
|
|
1375
|
+
**Generated:** 2025-06-15T16:00:00Z
|
|
1327
1376
|
|
|
1328
|
-
|
|
1377
|
+
### SOME_API_KEY
|
|
1329
1378
|
|
|
1330
|
-
|
|
1379
|
+
**Service:** SomeService
|
|
1331
1380
|
|
|
1332
|
-
|
|
1381
|
+
1. Get the key from the dashboard
|
|
1333
1382
|
`;
|
|
1334
1383
|
|
|
1335
|
-
const
|
|
1336
|
-
assertEq(
|
|
1337
|
-
assertEq(
|
|
1338
|
-
assertEq(
|
|
1339
|
-
assertEq(
|
|
1340
|
-
assertEq(
|
|
1341
|
-
assertEq(
|
|
1342
|
-
assertEq(
|
|
1384
|
+
const m = parseSecretsManifest(content);
|
|
1385
|
+
assertEq(m.entries.length, 1, 'one entry with missing fields');
|
|
1386
|
+
assertEq(m.entries[0].key, 'SOME_API_KEY', 'key parsed');
|
|
1387
|
+
assertEq(m.entries[0].service, 'SomeService', 'service parsed');
|
|
1388
|
+
assertEq(m.entries[0].dashboardUrl, '', 'missing dashboardUrl defaults to empty string');
|
|
1389
|
+
assertEq(m.entries[0].formatHint, '', 'missing formatHint defaults to empty string');
|
|
1390
|
+
assertEq(m.entries[0].status, 'pending', 'missing status defaults to pending');
|
|
1391
|
+
assertEq(m.entries[0].destination, 'dotenv', 'missing destination defaults to dotenv');
|
|
1392
|
+
assertEq(m.entries[0].guidance.length, 1, 'guidance still parsed');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
console.log('\n=== parseSecretsManifest: all three status values parse ===');
|
|
1396
|
+
{
|
|
1397
|
+
for (const status of ['pending', 'collected', 'skipped'] as const) {
|
|
1398
|
+
const content = `# Secrets Manifest
|
|
1399
|
+
|
|
1400
|
+
**Milestone:** M005
|
|
1401
|
+
**Generated:** 2025-06-15T18:00:00Z
|
|
1402
|
+
|
|
1403
|
+
### TEST_KEY
|
|
1404
|
+
|
|
1405
|
+
**Service:** TestService
|
|
1406
|
+
**Status:** ${status}
|
|
1407
|
+
|
|
1408
|
+
1. Do something
|
|
1409
|
+
`;
|
|
1410
|
+
|
|
1411
|
+
const m = parseSecretsManifest(content);
|
|
1412
|
+
assertEq(m.entries[0].status, status, `status variant: ${status}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
console.log('\n=== parseSecretsManifest: invalid status defaults to pending ===');
|
|
1417
|
+
{
|
|
1418
|
+
const content = `# Secrets Manifest
|
|
1419
|
+
|
|
1420
|
+
**Milestone:** M006
|
|
1421
|
+
**Generated:** 2025-06-15T20:00:00Z
|
|
1422
|
+
|
|
1423
|
+
### BAD_STATUS_KEY
|
|
1424
|
+
|
|
1425
|
+
**Service:** TestService
|
|
1426
|
+
**Status:** invalid_value
|
|
1427
|
+
|
|
1428
|
+
1. Some step
|
|
1429
|
+
`;
|
|
1430
|
+
|
|
1431
|
+
const m = parseSecretsManifest(content);
|
|
1432
|
+
assertEq(m.entries[0].status, 'pending', 'invalid status defaults to pending');
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
console.log('\n=== parseSecretsManifest + formatSecretsManifest: round-trip ===');
|
|
1436
|
+
{
|
|
1437
|
+
const original = `# Secrets Manifest
|
|
1438
|
+
|
|
1439
|
+
**Milestone:** M007
|
|
1440
|
+
**Generated:** 2025-06-16T10:00:00Z
|
|
1441
|
+
|
|
1442
|
+
### OPENAI_API_KEY
|
|
1443
|
+
|
|
1444
|
+
**Service:** OpenAI
|
|
1445
|
+
**Dashboard:** https://platform.openai.com/api-keys
|
|
1446
|
+
**Format hint:** starts with sk-
|
|
1447
|
+
**Status:** pending
|
|
1448
|
+
**Destination:** dotenv
|
|
1449
|
+
|
|
1450
|
+
1. Go to the API keys page
|
|
1451
|
+
2. Create a new key
|
|
1452
|
+
3. Copy it
|
|
1453
|
+
|
|
1454
|
+
### REDIS_URL
|
|
1455
|
+
|
|
1456
|
+
**Service:** Upstash
|
|
1457
|
+
**Dashboard:** https://console.upstash.com
|
|
1458
|
+
**Format hint:** redis://...
|
|
1459
|
+
**Status:** collected
|
|
1460
|
+
**Destination:** vercel
|
|
1461
|
+
|
|
1462
|
+
1. Open Upstash console
|
|
1463
|
+
2. Copy the Redis URL
|
|
1464
|
+
`;
|
|
1465
|
+
|
|
1466
|
+
const parsed1 = parseSecretsManifest(original);
|
|
1467
|
+
const formatted = formatSecretsManifest(parsed1);
|
|
1468
|
+
const parsed2 = parseSecretsManifest(formatted);
|
|
1469
|
+
|
|
1470
|
+
// Verify semantic equality after round-trip
|
|
1471
|
+
assertEq(parsed2.milestone, parsed1.milestone, 'round-trip milestone');
|
|
1472
|
+
assertEq(parsed2.generatedAt, parsed1.generatedAt, 'round-trip generatedAt');
|
|
1473
|
+
assertEq(parsed2.entries.length, parsed1.entries.length, 'round-trip entry count');
|
|
1474
|
+
|
|
1475
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
1476
|
+
const e1 = parsed1.entries[i];
|
|
1477
|
+
const e2 = parsed2.entries[i];
|
|
1478
|
+
assertEq(e2.key, e1.key, `round-trip entry ${i} key`);
|
|
1479
|
+
assertEq(e2.service, e1.service, `round-trip entry ${i} service`);
|
|
1480
|
+
assertEq(e2.dashboardUrl, e1.dashboardUrl, `round-trip entry ${i} dashboardUrl`);
|
|
1481
|
+
assertEq(e2.formatHint, e1.formatHint, `round-trip entry ${i} formatHint`);
|
|
1482
|
+
assertEq(e2.status, e1.status, `round-trip entry ${i} status`);
|
|
1483
|
+
assertEq(e2.destination, e1.destination, `round-trip entry ${i} destination`);
|
|
1484
|
+
assertEq(e2.guidance.length, e1.guidance.length, `round-trip entry ${i} guidance length`);
|
|
1485
|
+
for (let j = 0; j < e1.guidance.length; j++) {
|
|
1486
|
+
assertEq(e2.guidance[j], e1.guidance[j], `round-trip entry ${i} guidance[${j}]`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1492
|
+
// LLM-style round-trip tests — realistic manifest variations
|
|
1493
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1494
|
+
|
|
1495
|
+
console.log('\n=== LLM round-trip: extra whitespace ===');
|
|
1496
|
+
{
|
|
1497
|
+
// LLMs often produce inconsistent indentation and trailing spaces
|
|
1498
|
+
const messy = `# Secrets Manifest
|
|
1499
|
+
|
|
1500
|
+
**Milestone:** M010
|
|
1501
|
+
**Generated:** 2025-07-01T12:00:00Z
|
|
1502
|
+
|
|
1503
|
+
### OPENAI_API_KEY
|
|
1504
|
+
|
|
1505
|
+
**Service:** OpenAI
|
|
1506
|
+
**Dashboard:** https://platform.openai.com/api-keys
|
|
1507
|
+
**Format hint:** starts with sk-
|
|
1508
|
+
**Status:** pending
|
|
1509
|
+
**Destination:** dotenv
|
|
1510
|
+
|
|
1511
|
+
1. Go to the API keys page
|
|
1512
|
+
2. Create a new key
|
|
1513
|
+
|
|
1514
|
+
### REDIS_URL
|
|
1515
|
+
|
|
1516
|
+
**Service:** Upstash
|
|
1517
|
+
**Status:** collected
|
|
1518
|
+
**Destination:** vercel
|
|
1519
|
+
|
|
1520
|
+
1. Open console
|
|
1521
|
+
`;
|
|
1522
|
+
|
|
1523
|
+
const parsed1 = parseSecretsManifest(messy);
|
|
1524
|
+
const formatted = formatSecretsManifest(parsed1);
|
|
1525
|
+
const parsed2 = parseSecretsManifest(formatted);
|
|
1526
|
+
|
|
1527
|
+
assertEq(parsed2.milestone, parsed1.milestone, 'whitespace round-trip milestone');
|
|
1528
|
+
assertEq(parsed2.generatedAt, parsed1.generatedAt, 'whitespace round-trip generatedAt');
|
|
1529
|
+
assertEq(parsed2.entries.length, parsed1.entries.length, 'whitespace round-trip entry count');
|
|
1530
|
+
assertEq(parsed2.entries.length, 2, 'whitespace: two entries parsed');
|
|
1531
|
+
|
|
1532
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
1533
|
+
const e1 = parsed1.entries[i];
|
|
1534
|
+
const e2 = parsed2.entries[i];
|
|
1535
|
+
assertEq(e2.key, e1.key, `whitespace round-trip entry ${i} key`);
|
|
1536
|
+
assertEq(e2.service, e1.service, `whitespace round-trip entry ${i} service`);
|
|
1537
|
+
assertEq(e2.dashboardUrl, e1.dashboardUrl, `whitespace round-trip entry ${i} dashboardUrl`);
|
|
1538
|
+
assertEq(e2.formatHint, e1.formatHint, `whitespace round-trip entry ${i} formatHint`);
|
|
1539
|
+
assertEq(e2.status, e1.status, `whitespace round-trip entry ${i} status`);
|
|
1540
|
+
assertEq(e2.destination, e1.destination, `whitespace round-trip entry ${i} destination`);
|
|
1541
|
+
assertEq(e2.guidance.length, e1.guidance.length, `whitespace round-trip entry ${i} guidance length`);
|
|
1542
|
+
for (let j = 0; j < e1.guidance.length; j++) {
|
|
1543
|
+
assertEq(e2.guidance[j], e1.guidance[j], `whitespace round-trip entry ${i} guidance[${j}]`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Verify the parser correctly stripped trailing whitespace
|
|
1548
|
+
assertEq(parsed1.milestone, 'M010', 'whitespace: milestone trimmed');
|
|
1549
|
+
assertEq(parsed1.entries[0].key, 'OPENAI_API_KEY', 'whitespace: key trimmed');
|
|
1550
|
+
assertEq(parsed1.entries[0].service, 'OpenAI', 'whitespace: service trimmed');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
console.log('\n=== LLM round-trip: missing optional fields ===');
|
|
1554
|
+
{
|
|
1555
|
+
// LLMs may omit Dashboard and Format hint lines entirely
|
|
1556
|
+
const minimal = `# Secrets Manifest
|
|
1557
|
+
|
|
1558
|
+
**Milestone:** M011
|
|
1559
|
+
**Generated:** 2025-07-02T08:00:00Z
|
|
1560
|
+
|
|
1561
|
+
### DATABASE_URL
|
|
1562
|
+
|
|
1563
|
+
**Service:** Neon
|
|
1564
|
+
**Status:** pending
|
|
1565
|
+
**Destination:** dotenv
|
|
1566
|
+
|
|
1567
|
+
1. Create a Neon project
|
|
1568
|
+
2. Copy connection string
|
|
1569
|
+
|
|
1570
|
+
### WEBHOOK_SECRET
|
|
1571
|
+
|
|
1572
|
+
**Service:** Stripe
|
|
1573
|
+
**Status:** collected
|
|
1574
|
+
**Destination:** dotenv
|
|
1575
|
+
|
|
1576
|
+
1. Go to webhooks
|
|
1577
|
+
`;
|
|
1578
|
+
|
|
1579
|
+
const parsed1 = parseSecretsManifest(minimal);
|
|
1580
|
+
|
|
1581
|
+
// Verify missing optional fields get defaults
|
|
1582
|
+
assertEq(parsed1.entries[0].dashboardUrl, '', 'missing-optional: no dashboard → empty string');
|
|
1583
|
+
assertEq(parsed1.entries[0].formatHint, '', 'missing-optional: no format hint → empty string');
|
|
1584
|
+
assertEq(parsed1.entries[1].dashboardUrl, '', 'missing-optional: entry 2 no dashboard → empty string');
|
|
1585
|
+
assertEq(parsed1.entries[1].formatHint, '', 'missing-optional: entry 2 no format hint → empty string');
|
|
1586
|
+
|
|
1587
|
+
// Round-trip: formatter omits empty optional fields, re-parse preserves defaults
|
|
1588
|
+
const formatted = formatSecretsManifest(parsed1);
|
|
1589
|
+
const parsed2 = parseSecretsManifest(formatted);
|
|
1590
|
+
|
|
1591
|
+
assertEq(parsed2.entries.length, parsed1.entries.length, 'missing-optional round-trip entry count');
|
|
1592
|
+
|
|
1593
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
1594
|
+
const e1 = parsed1.entries[i];
|
|
1595
|
+
const e2 = parsed2.entries[i];
|
|
1596
|
+
assertEq(e2.key, e1.key, `missing-optional round-trip entry ${i} key`);
|
|
1597
|
+
assertEq(e2.service, e1.service, `missing-optional round-trip entry ${i} service`);
|
|
1598
|
+
assertEq(e2.dashboardUrl, e1.dashboardUrl, `missing-optional round-trip entry ${i} dashboardUrl`);
|
|
1599
|
+
assertEq(e2.formatHint, e1.formatHint, `missing-optional round-trip entry ${i} formatHint`);
|
|
1600
|
+
assertEq(e2.status, e1.status, `missing-optional round-trip entry ${i} status`);
|
|
1601
|
+
assertEq(e2.destination, e1.destination, `missing-optional round-trip entry ${i} destination`);
|
|
1602
|
+
assertEq(e2.guidance.length, e1.guidance.length, `missing-optional round-trip entry ${i} guidance length`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
console.log('\n=== LLM round-trip: extra blank lines ===');
|
|
1607
|
+
{
|
|
1608
|
+
// LLMs sometimes insert excessive blank lines between sections
|
|
1609
|
+
const blanky = `# Secrets Manifest
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
**Milestone:** M012
|
|
1613
|
+
**Generated:** 2025-07-03T14:00:00Z
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
### API_KEY_ONE
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
**Service:** ServiceOne
|
|
1621
|
+
**Dashboard:** https://one.example.com
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
**Format hint:** key_...
|
|
1625
|
+
**Status:** pending
|
|
1626
|
+
**Destination:** dotenv
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
1. Go to settings
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
2. Generate key
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
### API_KEY_TWO
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
**Service:** ServiceTwo
|
|
1642
|
+
**Status:** skipped
|
|
1643
|
+
**Destination:** dotenv
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
1. Not needed
|
|
1647
|
+
`;
|
|
1648
|
+
|
|
1649
|
+
const parsed1 = parseSecretsManifest(blanky);
|
|
1650
|
+
|
|
1651
|
+
assertEq(parsed1.entries.length, 2, 'blank-lines: two entries parsed');
|
|
1652
|
+
assertEq(parsed1.milestone, 'M012', 'blank-lines: milestone parsed');
|
|
1653
|
+
assertEq(parsed1.entries[0].key, 'API_KEY_ONE', 'blank-lines: first key');
|
|
1654
|
+
assertEq(parsed1.entries[0].guidance.length, 2, 'blank-lines: first entry guidance count');
|
|
1655
|
+
assertEq(parsed1.entries[1].key, 'API_KEY_TWO', 'blank-lines: second key');
|
|
1656
|
+
assertEq(parsed1.entries[1].status, 'skipped', 'blank-lines: second entry status');
|
|
1657
|
+
|
|
1658
|
+
// Round-trip produces clean output
|
|
1659
|
+
const formatted = formatSecretsManifest(parsed1);
|
|
1660
|
+
const parsed2 = parseSecretsManifest(formatted);
|
|
1661
|
+
|
|
1662
|
+
assertEq(parsed2.entries.length, parsed1.entries.length, 'blank-lines round-trip entry count');
|
|
1663
|
+
|
|
1664
|
+
for (let i = 0; i < parsed1.entries.length; i++) {
|
|
1665
|
+
const e1 = parsed1.entries[i];
|
|
1666
|
+
const e2 = parsed2.entries[i];
|
|
1667
|
+
assertEq(e2.key, e1.key, `blank-lines round-trip entry ${i} key`);
|
|
1668
|
+
assertEq(e2.service, e1.service, `blank-lines round-trip entry ${i} service`);
|
|
1669
|
+
assertEq(e2.dashboardUrl, e1.dashboardUrl, `blank-lines round-trip entry ${i} dashboardUrl`);
|
|
1670
|
+
assertEq(e2.formatHint, e1.formatHint, `blank-lines round-trip entry ${i} formatHint`);
|
|
1671
|
+
assertEq(e2.status, e1.status, `blank-lines round-trip entry ${i} status`);
|
|
1672
|
+
assertEq(e2.destination, e1.destination, `blank-lines round-trip entry ${i} destination`);
|
|
1673
|
+
assertEq(e2.guidance.length, e1.guidance.length, `blank-lines round-trip entry ${i} guidance length`);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Verify the formatted output is cleaner (fewer consecutive blank lines)
|
|
1677
|
+
const consecutiveBlanks = formatted.match(/\n{4,}/g);
|
|
1678
|
+
assert(consecutiveBlanks === null, 'blank-lines: formatted output has no 4+ consecutive newlines');
|
|
1343
1679
|
}
|
|
1344
1680
|
|
|
1345
1681
|
// ═══════════════════════════════════════════════════════════════════════════
|