@aikdna/kdna-studio-cli 0.2.0 → 0.3.1

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/tests/cli.test.js CHANGED
@@ -5,6 +5,9 @@ const path = require('node:path');
5
5
  const { spawnSync } = require('node:child_process');
6
6
  const test = require('node:test');
7
7
 
8
+ let kdnaCore = null;
9
+ try { kdnaCore = require('@aikdna/kdna-core'); } catch { /* optional — runtime digest cross-verification */ }
10
+
8
11
  const root = path.resolve(__dirname, '..');
9
12
  const cli = path.join(root, 'bin', 'kdna-studio.js');
10
13
 
@@ -446,3 +449,220 @@ test('export includes creator and lineage in kdna.json', (t) => {
446
449
  assert.ok(exportedManifest.lineage, 'must have lineage');
447
450
  assert.equal(exportedManifest.lineage.type, 'original');
448
451
  });
452
+
453
+ // ── E2E: Blank workflow with runtime content_digest check ──────────
454
+
455
+ test('E2E blank: create → approve → lock → export → runtime digest match', (t) => {
456
+ const tmp = tmpDir();
457
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
458
+
459
+ const projectDir = path.join(tmp, 'project');
460
+ let result = run(['create', projectDir, '--name', '@test/e2e-blank'], { tmp });
461
+ assert.equal(result.status, 0, result.stderr);
462
+
463
+ // Add axiom with full fields
464
+ result = run([
465
+ 'card', 'add', projectDir, 'axiom',
466
+ '--field', 'one_sentence=E2E blank axiom.',
467
+ '--field', 'full_statement=A complete testable explanation for the end-to-end blank workflow test.',
468
+ '--field', 'why=Without this axiom the end-to-end test would not validate blank creation.',
469
+ '--field', 'applies_when=["e2e testing"]',
470
+ '--field', 'does_not_apply_when=["production"]',
471
+ '--field', 'failure_risk=e2e test may incorrectly pass',
472
+ ], { tmp });
473
+ assert.equal(result.status, 0, result.stderr);
474
+ const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
475
+
476
+ // Add a misunderstanding
477
+ result = run([
478
+ 'card', 'add', projectDir, 'misunderstanding',
479
+ '--field', 'wrong=E2E tests are unnecessary.',
480
+ '--field', 'correct=E2E tests catch integration issues not visible in unit tests.',
481
+ '--field', 'key_distinction=E2E tests validate the full pipeline, not individual functions.',
482
+ ], { tmp });
483
+ assert.equal(result.status, 0, result.stderr);
484
+
485
+ // Approve all cards
486
+ let cards = run(['card', 'list', projectDir], { tmp });
487
+ for (const line of cards.stdout.trim().split('\n')) {
488
+ const id = line.split(/\s+/)[0];
489
+ result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Confirmed.'], { tmp });
490
+ assert.equal(result.status, 0, result.stderr);
491
+ }
492
+
493
+ // Lock and export
494
+ result = run(['lock', projectDir], { tmp });
495
+ assert.equal(result.status, 0, result.stderr);
496
+
497
+ const outFile = path.join(tmp, 'blank.kdna');
498
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
499
+ result = run(['export', projectDir, '--out', outFile], { tmp });
500
+ assert.equal(result.status, 0, result.stderr);
501
+
502
+ // ── Verify: source_mode, lineage, digest chain ────────────
503
+ const distDir = path.dirname(outFile);
504
+ const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
505
+ const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
506
+ const provenance = JSON.parse(fs.readFileSync(path.join(distDir, 'provenance-report.json'), 'utf8'));
507
+
508
+ assert.equal(manifest.authoring.source_mode, 'blank');
509
+ assert.equal(manifest.lineage.type, 'original');
510
+ assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
511
+ assert.equal(manifest.content_digest, receipt.content_digest, 'manifest vs receipt digest mismatch');
512
+ assert.equal(manifest.content_digest, provenance.content_digest, 'manifest vs provenance digest mismatch');
513
+ assert.equal(manifest.authoring.compiler, '@aikdna/kdna-studio-core');
514
+ assert.equal(manifest.authoring.human_confirmed, true);
515
+
516
+ // ── Runtime cross-verification: content digest consistency ──
517
+ // Compute content digest from the .kdna ZIP entries and verify it
518
+ // matches the manifest, receipt, and provenance report.
519
+ if (kdnaCore) {
520
+ const reader = kdnaCore.createKdnaAssetReader();
521
+ const asset = reader.openSync(outFile);
522
+ const runtimeResult = reader.verifySync(asset);
523
+ const runtimeDigest = runtimeResult.content_digest;
524
+
525
+ assert.ok(runtimeDigest, 'runtime content_digest must be present');
526
+ assert.match(runtimeDigest, /^sha256:[0-9a-f]{64}$/, 'runtime content_digest must be valid sha256');
527
+
528
+ // Read ZIP-internal manifest (avoid sidecar timing issues)
529
+ const zipManifest = JSON.parse(asset.readEntry('kdna.json').toString());
530
+ assert.equal(zipManifest.content_digest, receipt.content_digest,
531
+ `ZIP manifest digest != receipt digest`);
532
+ assert.equal(zipManifest.content_digest, provenance.content_digest,
533
+ `ZIP manifest digest != provenance digest`);
534
+
535
+ // Verify: runtime computed digest matches ZIP manifest
536
+ assert.equal(zipManifest.content_digest, runtimeDigest,
537
+ `ZIP manifest content_digest (${zipManifest.content_digest.slice(0, 20)}...) != runtime computed (${runtimeDigest.slice(0, 20)}...) — trust chain broken`);
538
+ } else {
539
+ console.log(' (kdna-core not available, skipping runtime assetReader cross-verification)');
540
+ }
541
+ });
542
+
543
+ // ── E2E: Fork workflow ────────────────────────────────────────────
544
+
545
+ test('E2E fork: parent → fork → approve → export → lineage check', (t) => {
546
+ const tmp = tmpDir();
547
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
548
+
549
+ // Step 1: Create and export a parent domain
550
+ const parentDir = path.join(tmp, 'parent');
551
+ let result = run(['create', parentDir, '--name', '@test/parent-domain'], { tmp });
552
+ result = run([
553
+ 'card', 'add', parentDir, 'axiom',
554
+ '--field', 'one_sentence=Parent axiom for forking test.',
555
+ '--field', 'full_statement=A complete testable explanation from the parent domain for fork lineage verification.',
556
+ '--field', 'why=Without this parent axiom the fork test would have no source to verify lineage against.',
557
+ '--field', 'applies_when=["fork testing"]',
558
+ '--field', 'does_not_apply_when=["not forking"]',
559
+ '--field', 'failure_risk=forked content may lose lineage',
560
+ ], { tmp });
561
+ const pCards = run(['card', 'list', parentDir], { tmp });
562
+ for (const line of pCards.stdout.trim().split('\n')) {
563
+ const id = line.split(/\s+/)[0];
564
+ run(['card', 'approve', parentDir, id, '--by', 'tester', '--statement', 'Parent OK.'], { tmp });
565
+ }
566
+ run(['lock', parentDir], { tmp });
567
+ const parentKdna = path.join(tmp, 'parent.kdna');
568
+ fs.mkdirSync(path.dirname(parentKdna), { recursive: true });
569
+ result = run(['export', parentDir, '--out', parentKdna], { tmp });
570
+ assert.equal(result.status, 0, result.stderr);
571
+
572
+ // Step 2: Fork from parent
573
+ const forkDir = path.join(tmp, 'forked');
574
+ result = run(['create', forkDir, '--from-kdna', parentKdna, '--name', '@test/forked-domain'], { tmp });
575
+ assert.equal(result.status, 0, result.stderr);
576
+
577
+ const fProject = JSON.parse(fs.readFileSync(path.join(forkDir, 'studio.project.json'), 'utf8'));
578
+ assert.equal(fProject.source_mode, 'kdna_asset');
579
+ assert.equal(fProject.lineage.type, 'fork');
580
+ assert.equal(fProject.lineage.parent_name, '@test/parent-domain');
581
+
582
+ // Step 3: Approve and export fork
583
+ const fCards = run(['card', 'list', forkDir], { tmp });
584
+ for (const line of fCards.stdout.trim().split('\n')) {
585
+ const id = line.split(/\s+/)[0];
586
+ run(['card', 'approve', forkDir, id, '--by', 'tester', '--statement', 'Fork confirmed.'], { tmp });
587
+ }
588
+ run(['lock', forkDir], { tmp });
589
+ const forkKdna = path.join(tmp, 'forked.kdna');
590
+ fs.mkdirSync(path.dirname(forkKdna), { recursive: true });
591
+ result = run(['export', forkDir, '--out', forkKdna], { tmp });
592
+ assert.equal(result.status, 0, result.stderr);
593
+
594
+ // Step 4: Verify lineage in exported fork
595
+ const fManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'kdna.json'), 'utf8'));
596
+ assert.equal(fManifest.authoring.source_mode, 'kdna_asset');
597
+ assert.equal(fManifest.lineage.type, 'fork');
598
+ assert.equal(fManifest.lineage.parent_name, '@test/parent-domain');
599
+ assert.match(fManifest.content_digest, /^sha256:/);
600
+ });
601
+
602
+ // ── E2E: Migrate workflow ─────────────────────────────────────────
603
+
604
+ test('E2E migrate: source folder → import → approve → export → verify', (t) => {
605
+ const tmp = tmpDir();
606
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
607
+
608
+ // Step 1: Create legacy source folder
609
+ const sourceDir = path.join(tmp, 'legacy');
610
+ fs.mkdirSync(sourceDir, { recursive: true });
611
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify({
612
+ meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'Legacy migrate test', load_condition: 'always' },
613
+ axioms: [{
614
+ id: 'ax_mig', one_sentence: 'Migrated axiom for E2E test.',
615
+ full_statement: 'A complete testable explanation for the migrated legacy axiom in the end-to-end workflow.',
616
+ why: 'Without this migrated axiom the e2e test cannot verify legacy folder migration.',
617
+ applies_when: ['legacy migration testing'], does_not_apply_when: ['not migrating'],
618
+ failure_risk: 'migrated content may not be re-locked',
619
+ }],
620
+ ontology: [],
621
+ stances: [],
622
+ }));
623
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
624
+ meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'legacy patterns', load_condition: 'always' },
625
+ misunderstandings: [{ id: 'ms_mig', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key difference between legacy wrong and correct behavior.' }],
626
+ self_check: ['Is the migrated content properly re-locked?'],
627
+ }));
628
+
629
+ // Step 2: Import via --from-folder
630
+ const projectDir = path.join(tmp, 'imported');
631
+ let result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/e2e-migrate'], { tmp });
632
+ assert.equal(result.status, 0, result.stderr);
633
+ const audit = JSON.parse(result.stdout);
634
+ assert.ok(audit.imported > 0, `No cards imported: ${result.stdout}`);
635
+ assert.equal(audit.source_mode, 'source_folder');
636
+
637
+ // Verify project has source_folder mode
638
+ const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
639
+ assert.equal(project.source_mode, 'source_folder');
640
+ assert.equal(project.lineage.type, 'migrated');
641
+
642
+ // Step 3: Approve and export
643
+ const cards = run(['card', 'list', projectDir], { tmp });
644
+ for (const line of cards.stdout.trim().split('\n')) {
645
+ const id = line.split(/\s+/)[0];
646
+ result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Migrated content confirmed.'], { tmp });
647
+ assert.equal(result.status, 0, result.stderr);
648
+ }
649
+ result = run(['lock', projectDir], { tmp });
650
+ assert.equal(result.status, 0, result.stderr);
651
+
652
+ const outFile = path.join(tmp, 'migrated.kdna');
653
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
654
+ result = run(['export', projectDir, '--out', outFile], { tmp });
655
+ assert.equal(result.status, 0, result.stderr);
656
+
657
+ // Verify exported manifest
658
+ const distDir = path.dirname(outFile);
659
+ const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
660
+ assert.equal(manifest.authoring.source_mode, 'source_folder');
661
+ assert.equal(manifest.lineage.type, 'migrated');
662
+ assert.match(manifest.content_digest, /^sha256:/);
663
+ assert.equal(manifest.authoring.human_confirmed, true);
664
+
665
+ // Digest chain
666
+ const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
667
+ assert.equal(manifest.content_digest, receipt.content_digest);
668
+ });