gentle-pi 0.7.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,9 +5,12 @@ import { tmpdir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import test from "node:test";
7
7
  import {
8
+ isNonAuthoritativeStatus,
8
9
  listActiveOpenSpecChanges,
9
10
  parseSddStatusCommandArgs,
11
+ renderNativeSddPhasePrompt,
10
12
  renderPhaseInstructions,
13
+ renderSddDispatcherMarkdown,
11
14
  renderSddStatusMarkdown,
12
15
  resolveSddStatus,
13
16
  } from "../lib/sdd-status.ts";
@@ -279,6 +282,76 @@ test("renderSddStatusMarkdown includes structured JSON", async () => {
279
282
  assert.match(markdown, /"schemaName": "gentle-pi.sdd-status"/);
280
283
  });
281
284
 
285
+ test("resolveSddStatus with artifactStore engram returns non-authoritative status without disk scan", async () => {
286
+ const cwd = await workspace();
287
+ // No openspec directory — simulates an engram-only session
288
+
289
+ const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "my-change" });
290
+
291
+ assert.equal(status.artifactStore, "engram");
292
+ assert.equal(status.changeName, "my-change");
293
+ assert.deepEqual(status.blockedReasons, []);
294
+ assert.equal(status.nextRecommended, "resolve-via-engram");
295
+ assert.equal(status.dependencies.apply, "not_applicable");
296
+ assert.equal(status.applyState, "not_applicable");
297
+ });
298
+
299
+ test("resolveSddStatus with artifactStore none returns non-authoritative status without disk scan", async () => {
300
+ const cwd = await workspace();
301
+ // No openspec directory
302
+
303
+ const status = resolveSddStatus({ cwd, artifactStore: "none", changeName: "my-change" });
304
+
305
+ assert.equal(status.artifactStore, "none");
306
+ assert.deepEqual(status.blockedReasons, []);
307
+ assert.equal(status.nextRecommended, "resolve-via-engram");
308
+ });
309
+
310
+ test("resolveSddStatus with artifactStore both uses disk scan and reflects store", async () => {
311
+ const cwd = await workspace();
312
+ seedChange(cwd);
313
+
314
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
315
+
316
+ assert.equal(status.artifactStore, "both");
317
+ assert.equal(status.changeName, "add-auth");
318
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
319
+ });
320
+
321
+ test("resolveSddStatus with undefined store and existing openspec dir defaults to openspec and blocks", async () => {
322
+ const cwd = await workspace();
323
+ // Create openspec/ directory to signal an openspec workspace
324
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
325
+
326
+ const status = resolveSddStatus({ cwd });
327
+
328
+ // openspec workspace with no active changes → blocked (back-compat)
329
+ assert.equal(status.artifactStore, "openspec");
330
+ assert.match(status.blockedReasons[0] ?? "", /No active SDD changes/);
331
+ });
332
+
333
+ test("resolveSddStatus with undefined store and NO openspec dir returns non-authoritative status", async () => {
334
+ const cwd = await workspace();
335
+ // No openspec directory at all — unknown store, no disk evidence
336
+
337
+ const status = resolveSddStatus({ cwd });
338
+
339
+ // Safety net: should not emit the openspec false-block
340
+ assert.equal(status.artifactStore, "none");
341
+ assert.deepEqual(status.blockedReasons, []);
342
+ assert.equal(status.nextRecommended, "resolve-via-engram");
343
+ assert.equal(status.applyState, "not_applicable");
344
+ });
345
+
346
+ test("resolveSddStatus non-authoritative status has neutral planningHome (no misleading openspec path)", async () => {
347
+ const cwd = await workspace();
348
+
349
+ const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-auth" });
350
+
351
+ assert.equal(status.planningHome.changesDir, "");
352
+ assert.equal(status.planningHome.root, status.actionContext.workspaceRoot);
353
+ });
354
+
282
355
  test("parseSddStatusCommandArgs extracts change and json flag", () => {
283
356
  assert.deepEqual(parseSddStatusCommandArgs("add-auth --json"), {
284
357
  changeName: "add-auth",
@@ -289,3 +362,250 @@ test("parseSddStatusCommandArgs extracts change and json flag", () => {
289
362
  json: true,
290
363
  });
291
364
  });
365
+
366
+ test("resolveSddStatus with artifactStore both and NO openspec dir returns non-authoritative status", async () => {
367
+ const cwd = await workspace();
368
+ // No openspec directory — both store without disk backing is non-authoritative
369
+
370
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "my-change" });
371
+
372
+ assert.equal(status.artifactStore, "both");
373
+ assert.equal(status.changeName, "my-change");
374
+ assert.deepEqual(status.blockedReasons, []);
375
+ assert.equal(status.nextRecommended, "resolve-via-engram");
376
+ assert.equal(status.applyState, "not_applicable");
377
+ assert.equal(status.dependencies.apply, "not_applicable");
378
+ assert.equal(status.dependencies.archive, "not_applicable");
379
+ });
380
+
381
+ test("resolveSddStatus with artifactStore both and existing openspec dir runs authoritative disk scan", async () => {
382
+ const cwd = await workspace();
383
+ seedChange(cwd);
384
+
385
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
386
+
387
+ assert.equal(status.artifactStore, "both");
388
+ assert.equal(status.changeName, "add-auth");
389
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
390
+ assert.equal(status.artifacts.proposal, "done");
391
+ });
392
+
393
+ // Renamed: previously "returns true only when nextRecommended is resolve-via-engram" — now
394
+ // explicitly asserts BOTH the typed isNonAuthoritative field and the sentinel together.
395
+ test("isNonAuthoritativeStatus reads typed isNonAuthoritative field and matches resolve-via-engram sentinel", async () => {
396
+ const cwd = await workspace();
397
+
398
+ const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
399
+ assert.equal(engram.isNonAuthoritative, true);
400
+ assert.equal(isNonAuthoritativeStatus(engram), true);
401
+ assert.equal(engram.nextRecommended, "resolve-via-engram");
402
+
403
+ const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
404
+ assert.equal(none.isNonAuthoritative, true);
405
+ assert.equal(isNonAuthoritativeStatus(none), true);
406
+ assert.equal(none.nextRecommended, "resolve-via-engram");
407
+
408
+ const bothWithoutOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
409
+ assert.equal(bothWithoutOpenspec.isNonAuthoritative, true);
410
+ assert.equal(isNonAuthoritativeStatus(bothWithoutOpenspec), true);
411
+ assert.equal(bothWithoutOpenspec.nextRecommended, "resolve-via-engram");
412
+
413
+ seedChange(cwd);
414
+ const bothWithOpenspec = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
415
+ assert.equal(bothWithOpenspec.isNonAuthoritative, false);
416
+ assert.equal(isNonAuthoritativeStatus(bothWithOpenspec), false);
417
+ assert.notEqual(bothWithOpenspec.nextRecommended, "resolve-via-engram");
418
+ });
419
+
420
+ // Fix 4 item 4 — isNonAuthoritative boolean is set correctly on the typed field
421
+ test("isNonAuthoritative boolean field is set correctly across all store/disk combinations", async () => {
422
+ const cwd = await workspace();
423
+
424
+ // engram → non-authoritative
425
+ const engram = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "x" });
426
+ assert.equal(engram.isNonAuthoritative, true);
427
+
428
+ // none → non-authoritative
429
+ const none = resolveSddStatus({ cwd, artifactStore: "none", changeName: "x" });
430
+ assert.equal(none.isNonAuthoritative, true);
431
+
432
+ // both without openspec/ → non-authoritative
433
+ const bothWithout = resolveSddStatus({ cwd, artifactStore: "both", changeName: "x" });
434
+ assert.equal(bothWithout.isNonAuthoritative, true);
435
+
436
+ // both WITH openspec/ and seeded change → authoritative
437
+ seedChange(cwd);
438
+ const bothWith = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
439
+ assert.equal(bothWith.isNonAuthoritative, false);
440
+
441
+ // openspec (default disk scan, seeded) → authoritative
442
+ const openspec = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "add-auth" });
443
+ assert.equal(openspec.isNonAuthoritative, false);
444
+ });
445
+
446
+ // Fix 4 item 1 — both + openspec/ dir present + change NOT on disk → non-authoritative
447
+ test("resolveSddStatus with artifactStore both, openspec dir present but change not on disk returns non-authoritative", async () => {
448
+ const cwd = await workspace();
449
+ // Create an openspec/changes dir with a different change — not the requested one
450
+ mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
451
+
452
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "missing-change" });
453
+
454
+ assert.equal(status.isNonAuthoritative, true);
455
+ assert.equal(status.nextRecommended, "resolve-via-engram");
456
+ assert.deepEqual(status.blockedReasons, []);
457
+ assert.equal(status.applyState, "not_applicable");
458
+ assert.equal(status.artifactStore, "both");
459
+ // Must NOT be treated as blocked
460
+ assert.notEqual(status.applyState, "blocked");
461
+ });
462
+
463
+ // Fix 4 item 2 — strengthen existing both-with-openspec-and-seeded-change test
464
+ test("resolveSddStatus with artifactStore both, openspec dir present and change on disk is authoritative", async () => {
465
+ const cwd = await workspace();
466
+ seedChange(cwd);
467
+
468
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "add-auth" });
469
+
470
+ assert.equal(status.artifactStore, "both");
471
+ assert.equal(status.changeName, "add-auth");
472
+ // Must be authoritative
473
+ assert.equal(isNonAuthoritativeStatus(status), false);
474
+ assert.equal(status.isNonAuthoritative, false);
475
+ // Must not be not_applicable — real disk scan ran
476
+ assert.notEqual(status.applyState, "not_applicable");
477
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
478
+ assert.equal(status.artifacts.proposal, "done");
479
+ });
480
+
481
+ // Fix 4 item 3 — pure openspec store + change not found STILL blocks (guard against over-broadening Fix 2)
482
+ test("resolveSddStatus with artifactStore openspec and change not found still blocks", async () => {
483
+ const cwd = await workspace();
484
+ // Create openspec dir with a different change — simulate openspec store with no matching change
485
+ mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
486
+
487
+ const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
488
+
489
+ // Must block, not return non-authoritative
490
+ assert.equal(status.isNonAuthoritative, false);
491
+ assert.match(status.blockedReasons.join("\n"), /Active change not found/);
492
+ assert.equal(status.applyState, "blocked");
493
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
494
+ });
495
+
496
+ test("renderSddDispatcherMarkdown for both-without-openspec does NOT render Ready", async () => {
497
+ const cwd = await workspace();
498
+ // No openspec directory — both store is non-authoritative
499
+
500
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
501
+ const markdown = renderSddDispatcherMarkdown(status);
502
+
503
+ assert.doesNotMatch(markdown, /### Ready/);
504
+ assert.match(markdown, /resolve via Engram/i);
505
+ });
506
+
507
+ test("renderNativeSddPhasePrompt for both-without-openspec emits non-authoritative line", async () => {
508
+ const cwd = await workspace();
509
+
510
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
511
+ const prompt = renderNativeSddPhasePrompt(status, "apply");
512
+
513
+ assert.match(prompt, /non-authoritative/);
514
+ assert.doesNotMatch(prompt, /deterministically/);
515
+ });
516
+
517
+ test("renderPhaseInstructions for not_applicable applyState emits neutral line", async () => {
518
+ const cwd = await workspace();
519
+
520
+ const status = resolveSddStatus({ cwd, artifactStore: "engram", changeName: "fix-x" });
521
+ const instructions = renderPhaseInstructions(status);
522
+
523
+ assert.match(instructions.apply.join("\n"), /Readiness is resolved from Engram/);
524
+ assert.match(instructions.archive.join("\n"), /Readiness is resolved from Engram/);
525
+ });
526
+
527
+ // Fix 4 item 1 — both + openspec/ + ZERO changes + no changeName → non-authoritative
528
+ test("resolveSddStatus both + openspec/ dir + zero active changes + no changeName returns non-authoritative", async () => {
529
+ const cwd = await workspace();
530
+ // openspec/ dir exists but holds no active changes (only the changes/ subdir)
531
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
532
+
533
+ const status = resolveSddStatus({ cwd, artifactStore: "both" });
534
+
535
+ assert.equal(status.isNonAuthoritative, true);
536
+ assert.equal(status.nextRecommended, "resolve-via-engram");
537
+ assert.deepEqual(status.blockedReasons, []);
538
+ assert.equal(status.artifactStore, "both");
539
+ assert.equal(status.applyState, "not_applicable");
540
+ assert.equal(status.dependencies.apply, "not_applicable");
541
+ assert.equal(status.dependencies.archive, "not_applicable");
542
+ // Must NOT be treated as blocked
543
+ assert.notEqual(status.applyState, "blocked");
544
+ });
545
+
546
+ // Fix 4 item 2 — both + openspec/ + MULTIPLE changes + no changeName → authoritative select-change
547
+ test("resolveSddStatus both + openspec/ dir + multiple active changes + no changeName stays authoritative", async () => {
548
+ const cwd = await workspace();
549
+ mkdirSync(join(cwd, "openspec", "changes", "alpha"), { recursive: true });
550
+ mkdirSync(join(cwd, "openspec", "changes", "beta"), { recursive: true });
551
+
552
+ const status = resolveSddStatus({ cwd, artifactStore: "both" });
553
+
554
+ // Authoritative ambiguous-selection behavior must be preserved
555
+ assert.equal(status.isNonAuthoritative, false);
556
+ assert.match(status.blockedReasons.join("\n"), /ambiguous/);
557
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
558
+ });
559
+
560
+ // Fix 4 item 3 — both + openspec/ + ONE resolvable change → authoritative
561
+ test("resolveSddStatus both + openspec/ dir + exactly one active change is authoritative", async () => {
562
+ const cwd = await workspace();
563
+ seedChange(cwd);
564
+
565
+ // No changeName supplied — should auto-select the single change
566
+ const status = resolveSddStatus({ cwd, artifactStore: "both" });
567
+
568
+ assert.equal(status.isNonAuthoritative, false);
569
+ assert.equal(status.changeName, "add-auth");
570
+ assert.equal(status.artifactStore, "both");
571
+ assert.notEqual(status.applyState, "not_applicable");
572
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
573
+ assert.equal(status.artifacts.proposal, "done");
574
+ });
575
+
576
+ // Fix 4 item 4 — pure openspec + zero/missing change STILL blocks (guard against over-broadening)
577
+ test("resolveSddStatus openspec + zero active changes still blocks", async () => {
578
+ const cwd = await workspace();
579
+ mkdirSync(join(cwd, "openspec", "changes"), { recursive: true });
580
+
581
+ const status = resolveSddStatus({ cwd, artifactStore: "openspec" });
582
+
583
+ assert.equal(status.isNonAuthoritative, false);
584
+ assert.match(status.blockedReasons.join("\n"), /No active SDD changes/);
585
+ assert.equal(status.applyState, "blocked");
586
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
587
+ });
588
+
589
+ test("resolveSddStatus openspec + named change missing still blocks", async () => {
590
+ const cwd = await workspace();
591
+ mkdirSync(join(cwd, "openspec", "changes", "other-change"), { recursive: true });
592
+
593
+ const status = resolveSddStatus({ cwd, artifactStore: "openspec", changeName: "nonexistent" });
594
+
595
+ assert.equal(status.isNonAuthoritative, false);
596
+ assert.match(status.blockedReasons.join("\n"), /Active change not found/);
597
+ assert.equal(status.applyState, "blocked");
598
+ assert.notEqual(status.nextRecommended, "resolve-via-engram");
599
+ });
600
+
601
+ // Fix 4 render test — non-authoritative both status → dispatcher shows "both" not "Engram or none"
602
+ test("renderSddDispatcherMarkdown for non-authoritative both status shows artifact store 'both'", async () => {
603
+ const cwd = await workspace();
604
+
605
+ const status = resolveSddStatus({ cwd, artifactStore: "both", changeName: "fix-x" });
606
+ const markdown = renderSddDispatcherMarkdown(status);
607
+
608
+ assert.match(markdown, /artifact store: both/);
609
+ assert.doesNotMatch(markdown, /Engram or none/);
610
+ assert.match(markdown, /resolve via Engram/i);
611
+ });