@junghanacs/entwurf 0.15.0 → 0.15.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.
@@ -42,6 +42,7 @@ import {
42
42
  readdirSync,
43
43
  readFileSync,
44
44
  rmSync,
45
+ symlinkSync,
45
46
  writeFileSync,
46
47
  } from "node:fs";
47
48
  import { tmpdir } from "node:os";
@@ -400,65 +401,144 @@ ok(
400
401
  const OURS = "entwurf-meta-receive-copilot@meta-bridge-copilot-local";
401
402
  const STALE = "entwurf-meta-receive@meta-bridge-local";
402
403
  const FOREIGN = "entwurf-meta-receive@someone-elses-marketplace";
404
+ const MKT = "meta-bridge-copilot-local";
405
+ const SHIPPED_VERSION = (
406
+ JSON.parse(
407
+ readFileSync(path.join(REPO, "pi", "meta-bridge-copilot", PLUGIN, ".claude-plugin", "plugin.json"), "utf8"),
408
+ ) as {
409
+ version: string;
410
+ }
411
+ ).version;
403
412
 
413
+ interface FakeHost {
414
+ home: string;
415
+ env: NodeJS.ProcessEnv;
416
+ asm: string;
417
+ /** package ownership state file inside the sandboxed XDG root */
418
+ stateFile: string;
419
+ log: string;
420
+ pluginState: string;
421
+ mktState: string;
422
+ }
423
+ interface FakeOpts {
424
+ installed?: string[];
425
+ /** registered marketplaces as [name, localPath] rows */
426
+ marketplaces?: Array<[string, string]>;
427
+ /** verbatim `plugin list` body override — for malformed/garbled listing cells */
428
+ pluginListRaw?: string;
429
+ uninstallFails?: boolean;
430
+ listFails?: boolean;
431
+ mktListFails?: boolean;
432
+ mktRemoveFails?: boolean;
433
+ }
404
434
  interface FakeRun {
405
435
  status: number | null;
406
436
  stdout: string;
407
437
  stderr: string;
408
- /** every `copilot …` argv the installer issued, in order */
438
+ /** every `copilot …` argv the driven surface issued, in order */
409
439
  calls: string[];
410
- /** the plugin ids the fake still holds when the installer is done */
440
+ /** the plugin ids the fake still holds when the surface is done */
411
441
  installed: string[];
442
+ /** the marketplace rows the fake still holds */
443
+ marketplaces: string[];
412
444
  }
413
- function runInstall(opts: {
414
- installed: string[];
415
- uninstallFails?: boolean;
416
- listFails?: boolean;
417
- label: string;
418
- }): FakeRun {
419
- const home = path.join(root, `install-${opts.label}`);
445
+ /** A fake that ANSWERS like the MEASURED CLI (copilot 1.0.80, 2026-08-27): `plugin
446
+ * list` prints qualified ids with a `(vX)` suffix, `plugin marketplace list` prints a
447
+ * registered local marketplace as `<name> (Local: <abs path>)`, `plugin uninstall <id>`
448
+ * removes exactly that id — and `--force` anywhere is refused loudly, because the real
449
+ * `marketplace remove --force` uninstalls that marketplace's plugins as a side effect
450
+ * and no entwurf surface may reach for it. Marketplace subverbs are modeled SEPARATELY
451
+ * (the old generic `exit 0` arm could not kill an inverse that removed a foreign
452
+ * marketplace). */
453
+ function makeFakeHost(label: string, opts: FakeOpts): FakeHost {
454
+ const home = path.join(root, `fake-${label}`);
420
455
  const bin = path.join(home, "bin");
421
456
  mkdirSync(bin, { recursive: true });
422
- const state = path.join(home, "installed.txt");
457
+ const xdg = path.join(home, "xdg");
458
+ mkdirSync(xdg, { recursive: true });
459
+ const pluginState = path.join(home, "installed.txt");
460
+ const mktState = path.join(home, "marketplaces.txt");
461
+ const rawList = path.join(home, "rawlist.txt");
423
462
  const log = path.join(home, "calls.log");
424
- writeFileSync(state, opts.installed.join("\n") + (opts.installed.length ? "\n" : ""));
463
+ const installed = opts.installed ?? [];
464
+ const marketplaces = opts.marketplaces ?? [];
465
+ if (opts.pluginListRaw !== undefined) writeFileSync(rawList, `${opts.pluginListRaw}\n`);
466
+ writeFileSync(pluginState, installed.join("\n") + (installed.length ? "\n" : ""));
467
+ writeFileSync(mktState, marketplaces.map(([n, p]) => `${n}\t${p}`).join("\n") + (marketplaces.length ? "\n" : ""));
425
468
  writeFileSync(log, "");
426
- // A fake that ANSWERS like the measured CLI: `plugin list` prints qualified ids,
427
- // `plugin uninstall <id>` removes exactly that id.
428
469
  writeFileSync(
429
470
  path.join(bin, "copilot"),
430
471
  [
431
472
  "#!/usr/bin/env bash",
432
- `STATE=${JSON.stringify(state)}`,
473
+ `STATE=${JSON.stringify(pluginState)}`,
474
+ `MKTS=${JSON.stringify(mktState)}`,
433
475
  `LOG=${JSON.stringify(log)}`,
476
+ `VER=${JSON.stringify(SHIPPED_VERSION)}`,
434
477
  'echo "$*" >> "$LOG"',
478
+ 'for a in "$@"; do [ "$a" = "--force" ] && { echo "fake copilot: --force is forbidden here" >&2; exit 99; }; done',
479
+ 'case "$1 $2 $3" in',
480
+ opts.mktListFails
481
+ ? ' "plugin marketplace list") echo "not authenticated" >&2; exit 1 ;;'
482
+ : [
483
+ ' "plugin marketplace list")',
484
+ ' echo "Included with GitHub Copilot:"',
485
+ ' while IFS=$\'\\t\' read -r n p; do [ -n "$n" ] && echo " • $n (Local: $p)"; done < "$MKTS"',
486
+ " exit 0 ;;",
487
+ ].join("\n"),
488
+ ' "plugin marketplace add") printf "%s\\t%s\\n" ' + JSON.stringify(MKT) + ' "$4" >> "$MKTS"; exit 0 ;;',
489
+ opts.mktRemoveFails
490
+ ? ' "plugin marketplace remove") echo "mkt boom" >&2; exit 1 ;;'
491
+ : ' "plugin marketplace remove") awk -F"\\t" -v n="$4" \'$1 != n\' "$MKTS" > "$MKTS.tmp"; mv "$MKTS.tmp" "$MKTS"; exit 0 ;;',
492
+ "esac",
435
493
  'case "$1 $2" in',
436
494
  opts.listFails
437
495
  ? ' "plugin list") echo "not authenticated" >&2; exit 1 ;;'
438
- : ' "plugin list") echo "Installed plugins:"; sed "s/^/ • /" "$STATE"; exit 0 ;;',
496
+ : opts.pluginListRaw !== undefined
497
+ ? ` "plugin list") echo "Installed plugins:"; cat ${JSON.stringify(rawList)}; exit 0 ;;`
498
+ : ' "plugin list") echo "Installed plugins:"; sed "s/^/ • /;s/$/ (v$VER)/" "$STATE"; exit 0 ;;',
439
499
  ' "plugin uninstall")',
440
500
  opts.uninstallFails
441
501
  ? ' echo "boom" >&2; exit 1 ;;'
442
- : ' grep -Fvx "$3" "$STATE" > "$STATE.tmp" || true; mv "$STATE.tmp" "$STATE"; exit 0 ;;',
502
+ : ' grep -Fvx "$3" "$STATE" > "$STATE.tmp"; mv "$STATE.tmp" "$STATE"; exit 0 ;;',
443
503
  ' "plugin install") echo "$3" >> "$STATE"; exit 0 ;;',
444
- ' "plugin marketplace") exit 0 ;;',
445
504
  "esac",
446
505
  "exit 0",
447
506
  ].join("\n"),
448
507
  );
449
508
  chmodSync(path.join(bin, "copilot"), 0o755);
450
- const res = spawnSync("bash", [path.join(REPO, "run.sh"), "install-copilot-bridge"], {
451
- env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, ENTWURF_COPILOT_ASM: path.join(home, "asm") },
452
- encoding: "utf8",
453
- });
509
+ return {
510
+ home,
511
+ asm: path.join(home, "asm"),
512
+ stateFile: path.join(xdg, "entwurf", "copilot-bridge", "install-state.json"),
513
+ log,
514
+ pluginState,
515
+ mktState,
516
+ env: {
517
+ ...process.env,
518
+ PATH: `${bin}:${process.env.PATH}`,
519
+ XDG_DATA_HOME: xdg,
520
+ // The doctor reads the record store and hook log from the agent dir; keep the
521
+ // gate hermetic rather than letting it read the operator's real host state.
522
+ PI_CODING_AGENT_DIR: path.join(home, "agent"),
523
+ ENTWURF_COPILOT_ASM: path.join(home, "asm"),
524
+ },
525
+ };
526
+ }
527
+ function runVerb(host: FakeHost, verb: string): FakeRun {
528
+ const res = spawnSync("bash", [path.join(REPO, "run.sh"), verb], { env: host.env, encoding: "utf8" });
454
529
  return {
455
530
  status: res.status,
456
531
  stdout: res.stdout ?? "",
457
532
  stderr: res.stderr ?? "",
458
- calls: readFileSync(log, "utf8").split("\n").filter(Boolean),
459
- installed: readFileSync(state, "utf8").split("\n").filter(Boolean),
533
+ calls: readFileSync(host.log, "utf8").split("\n").filter(Boolean),
534
+ installed: readFileSync(host.pluginState, "utf8").split("\n").filter(Boolean),
535
+ marketplaces: readFileSync(host.mktState, "utf8").split("\n").filter(Boolean),
460
536
  };
461
537
  }
538
+ function runInstall(opts: FakeOpts & { label: string }): FakeRun & { host: FakeHost } {
539
+ const host = makeFakeHost(`install-${opts.label}`, opts);
540
+ return { ...runVerb(host, "install-copilot-bridge"), host };
541
+ }
462
542
 
463
543
  const withStale = runInstall({ installed: [STALE], label: "stale" });
464
544
  ok(
@@ -493,5 +573,602 @@ ok(
493
573
  clean.status === 0 && clean.stdout.includes("nothing to remove"),
494
574
  );
495
575
 
576
+ // ── 12. install ownership state (#86 C3a) ───────────────────────────────────
577
+ // The birth unit gets the same discipline the other three Copilot units have: a
578
+ // package-owned install-state written after the assembly publish and before any
579
+ // vendor mutation, adoption that is explicit and narrow, and vendor-list failures
580
+ // that read as UNKNOWN, never as absence.
581
+ ok(
582
+ "a fresh install writes the ownership state with the exact schema",
583
+ (() => {
584
+ if (!existsSync(clean.host.stateFile)) return false;
585
+ const s = JSON.parse(readFileSync(clean.host.stateFile, "utf8")) as Record<string, unknown>;
586
+ return (
587
+ s.schemaVersion === 1 &&
588
+ s.qualifiedId === OURS &&
589
+ s.marketplaceName === MKT &&
590
+ s.assemblyPath === clean.host.asm &&
591
+ s.pluginVersion === SHIPPED_VERSION &&
592
+ s.ownedMarketplace === true &&
593
+ s.ownedAssembly === true &&
594
+ typeof s.installedAt === "string"
595
+ );
596
+ })(),
597
+ );
598
+ ok(
599
+ "the vendor sequence ran loud and in the bounded order (add before install, no blind uninstall)",
600
+ clean.calls.some((c) => c.startsWith("plugin marketplace add ")) &&
601
+ clean.calls.indexOf(`plugin install ${OURS}`) >
602
+ clean.calls.findIndex((c) => c.startsWith("plugin marketplace add ")),
603
+ );
604
+
605
+ const mktListBroken = runInstall({ installed: [], mktListFails: true, label: "mkt-list-error" });
606
+ ok(
607
+ "a FAILING marketplace list is not read as an empty host — the install refuses with zero writes",
608
+ mktListBroken.status !== 0 && !mktListBroken.installed.includes(OURS) && !existsSync(mktListBroken.host.stateFile),
609
+ );
610
+
611
+ // Reinstall over an existing state: the old blind `|| true` pair is gone, so a failing
612
+ // vendor step is an honest partial failure with the state retained for the rerun. The
613
+ // first install on each host is clean by construction (nothing to uninstall/remove), so
614
+ // the failure mode only fires on the RE-install, which is the path the blind pair hid.
615
+ {
616
+ const host = makeFakeHost("reinstall-uninstall-broken", { uninstallFails: true });
617
+ const first = runVerb(host, "install-copilot-bridge");
618
+ ok("precondition: the first install on the uninstall-broken host is green", first.status === 0);
619
+ const second = runVerb(host, "install-copilot-bridge");
620
+ ok(
621
+ "a reinstall whose exact-id uninstall FAILS stops loud with the state retained for repair",
622
+ second.status !== 0 && existsSync(host.stateFile) && second.installed.includes(OURS),
623
+ );
624
+ }
625
+ {
626
+ const host = makeFakeHost("reinstall-mkt-remove-broken", { mktRemoveFails: true });
627
+ const first = runVerb(host, "install-copilot-bridge");
628
+ ok("precondition: the first install on the remove-broken host is green", first.status === 0);
629
+ const second = runVerb(host, "install-copilot-bridge");
630
+ ok(
631
+ "a reinstall whose marketplace remove FAILS stops loud (never retried with --force) with the state retained",
632
+ second.status !== 0 && existsSync(host.stateFile) && !second.calls.some((c) => c.includes("--force")),
633
+ );
634
+ }
635
+ {
636
+ // No state + marketplace pre-registered at our path + no valid assembly → the
637
+ // legacy-marketplace adoption clause refuses before any write.
638
+ const host = makeFakeHost("mkt-no-assembly", {
639
+ marketplaces: [[MKT, path.join(root, "fake-mkt-no-assembly", "asm")]],
640
+ });
641
+ const refuse = runVerb(host, "install-copilot-bridge");
642
+ ok(
643
+ "a no-state marketplace registration over an INVALID assembly refuses with zero writes",
644
+ refuse.status !== 0 && !existsSync(host.stateFile) && !existsSync(host.asm),
645
+ );
646
+ }
647
+
648
+ // Legacy adoption, both shapes, driven through the honest route: install once, then
649
+ // strip the state and run again.
650
+ {
651
+ const adopt = runInstall({ installed: [], label: "adopt" });
652
+ ok("precondition: the adoption host installed green", adopt.status === 0 && existsSync(adopt.host.stateFile));
653
+ rmSync(adopt.host.stateFile);
654
+ const second = runVerb(adopt.host, "install-copilot-bridge");
655
+ ok(
656
+ "a legacy no-state installation with the exact QUALIFIED, our marketplace path and a valid assembly is ADOPTED and reported",
657
+ second.status === 0 &&
658
+ second.stdout.includes("adopting the legacy no-state installation") &&
659
+ existsSync(adopt.host.stateFile),
660
+ );
661
+ // Now break the assembly and strip the state again: adoption must refuse, and the
662
+ // refusal is zero-write (no state minted, the broken assembly untouched).
663
+ rmSync(adopt.host.stateFile);
664
+ rmSync(path.join(adopt.host.asm, PLUGIN, "hooks", "hooks.json"));
665
+ const invalid = runVerb(adopt.host, "install-copilot-bridge");
666
+ ok(
667
+ "a legacy no-state installation whose assembly FAILS the structural oracle refuses with zero writes",
668
+ invalid.status !== 0 &&
669
+ !existsSync(adopt.host.stateFile) &&
670
+ !existsSync(path.join(adopt.host.asm, PLUGIN, "hooks", "hooks.json")),
671
+ );
672
+ }
673
+
674
+ // ── 13. the package-owned INVERSE (#86 C3a) ─────────────────────────────────
675
+ {
676
+ const host = makeFakeHost("inverse-no-state", {
677
+ installed: [OURS],
678
+ marketplaces: [[MKT, path.join(root, "fake-inverse-no-state", "asm")]],
679
+ });
680
+ const res = runVerb(host, "uninstall-copilot-bridge");
681
+ ok(
682
+ "[QK:COPILOT-BIRTH-INVERSE-NO-STATE] with NO ownership state the inverse refuses and mutates NOTHING — a blind best-effort remove is exactly the defect",
683
+ res.status !== 0 &&
684
+ res.installed.includes(OURS) &&
685
+ res.marketplaces.length === 1 &&
686
+ !res.calls.some((c) => c.startsWith("plugin uninstall") || c.startsWith("plugin marketplace remove")),
687
+ );
688
+ ok("the no-state refusal names the adoption repair", (res.stderr ?? "").includes("install-copilot-bridge"));
689
+ }
690
+ {
691
+ const host = makeFakeHost("inverse-corrupt", { installed: [OURS] });
692
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
693
+ writeFileSync(host.stateFile, "{not json");
694
+ const res = runVerb(host, "uninstall-copilot-bridge");
695
+ ok(
696
+ "a CORRUPT state refuses with zero vendor mutation and the state retained for inspection",
697
+ res.status !== 0 && res.installed.includes(OURS) && existsSync(host.stateFile) && res.calls.length === 0,
698
+ );
699
+ }
700
+ {
701
+ const host = makeFakeHost("inverse-drift", { installed: [OURS] });
702
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
703
+ writeFileSync(
704
+ host.stateFile,
705
+ JSON.stringify({
706
+ schemaVersion: 1,
707
+ qualifiedId: OURS,
708
+ marketplaceName: MKT,
709
+ assemblyPath: "/somewhere/else/.assembled",
710
+ pluginVersion: SHIPPED_VERSION,
711
+ ownedMarketplace: true,
712
+ ownedAssembly: true,
713
+ installedAt: "2026-08-27T00:00:00Z",
714
+ }),
715
+ );
716
+ const res = runVerb(host, "uninstall-copilot-bridge");
717
+ ok(
718
+ "a state whose assemblyPath DRIFTED from this host's effective assembly refuses with zero vendor mutation",
719
+ res.status !== 0 && res.installed.includes(OURS) && res.calls.length === 0,
720
+ );
721
+ }
722
+ function writeBoundState(host: FakeHost): void {
723
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
724
+ writeFileSync(
725
+ host.stateFile,
726
+ JSON.stringify({
727
+ schemaVersion: 1,
728
+ qualifiedId: OURS,
729
+ marketplaceName: MKT,
730
+ assemblyPath: host.asm,
731
+ pluginVersion: SHIPPED_VERSION,
732
+ ownedMarketplace: true,
733
+ ownedAssembly: true,
734
+ installedAt: "2026-08-27T00:00:00Z",
735
+ }),
736
+ );
737
+ }
738
+ {
739
+ const host = makeFakeHost("inverse-foreign-mkt", {
740
+ installed: [OURS],
741
+ marketplaces: [[MKT, "/somebody/elses/marketplace-root"]],
742
+ });
743
+ writeBoundState(host);
744
+ const res = runVerb(host, "uninstall-copilot-bridge");
745
+ ok(
746
+ "[QK:COPILOT-BIRTH-INVERSE-FOREIGN] a marketplace with OUR name at ANOTHER path refuses BEFORE the plugin uninstall — zero vendor writes, everything preserved (that qualified id could be THAT marketplace's plugin)",
747
+ res.status !== 0 &&
748
+ res.installed.includes(OURS) &&
749
+ !res.calls.includes(`plugin uninstall ${OURS}`) &&
750
+ res.marketplaces.some((row) => row.includes("/somebody/elses/marketplace-root")) &&
751
+ !res.calls.some((c) => c.startsWith("plugin marketplace remove")) &&
752
+ existsSync(host.stateFile),
753
+ );
754
+ }
755
+ {
756
+ // ownedMarketplace=false while OUR marketplace IS registered at the recorded path:
757
+ // completing the inverse would delete the backing assembly/state under a
758
+ // registration it must preserve — so the whole operation refuses up front.
759
+ const host = makeFakeHost("inverse-unowned-mkt", { installed: [OURS] });
760
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
761
+ writeFileSync(host.mktState, `${MKT}\t${host.asm}\n`);
762
+ mkdirSync(path.join(host.asm, PLUGIN), { recursive: true });
763
+ writeFileSync(
764
+ host.stateFile,
765
+ JSON.stringify({
766
+ schemaVersion: 1,
767
+ qualifiedId: OURS,
768
+ marketplaceName: MKT,
769
+ assemblyPath: host.asm,
770
+ pluginVersion: SHIPPED_VERSION,
771
+ ownedMarketplace: false,
772
+ ownedAssembly: true,
773
+ installedAt: "2026-08-27T00:00:00Z",
774
+ }),
775
+ );
776
+ const res = runVerb(host, "uninstall-copilot-bridge");
777
+ ok(
778
+ "a REGISTERED marketplace the state does not own refuses the WHOLE inverse before any vendor write — no dangling registration over removed backing",
779
+ res.status !== 0 &&
780
+ res.installed.includes(OURS) &&
781
+ !res.calls.includes(`plugin uninstall ${OURS}`) &&
782
+ res.marketplaces.length === 1 &&
783
+ existsSync(path.join(host.asm, PLUGIN)) &&
784
+ existsSync(host.stateFile),
785
+ );
786
+ }
787
+ {
788
+ // State schema is fail-closed EXACTLY: representative missing-key, wrong-typed
789
+ // bool, and unknown-key states each refuse with zero vendor mutation.
790
+ const badStates: Array<[string, Record<string, unknown>]> = [
791
+ [
792
+ "missing-key",
793
+ {
794
+ schemaVersion: 1,
795
+ qualifiedId: OURS,
796
+ marketplaceName: MKT,
797
+ assemblyPath: "PLACEHOLDER",
798
+ pluginVersion: SHIPPED_VERSION,
799
+ ownedMarketplace: true,
800
+ ownedAssembly: true,
801
+ },
802
+ ],
803
+ [
804
+ "wrong-bool",
805
+ {
806
+ schemaVersion: 1,
807
+ qualifiedId: OURS,
808
+ marketplaceName: MKT,
809
+ assemblyPath: "PLACEHOLDER",
810
+ pluginVersion: SHIPPED_VERSION,
811
+ ownedMarketplace: "true",
812
+ ownedAssembly: true,
813
+ installedAt: "2026-08-27T00:00:00Z",
814
+ },
815
+ ],
816
+ [
817
+ "unknown-key",
818
+ {
819
+ schemaVersion: 1,
820
+ qualifiedId: OURS,
821
+ marketplaceName: MKT,
822
+ assemblyPath: "PLACEHOLDER",
823
+ pluginVersion: SHIPPED_VERSION,
824
+ ownedMarketplace: true,
825
+ ownedAssembly: true,
826
+ installedAt: "2026-08-27T00:00:00Z",
827
+ extra: 1,
828
+ },
829
+ ],
830
+ ];
831
+ for (const [label, state] of badStates) {
832
+ const host = makeFakeHost(`inverse-state-${label}`, { installed: [OURS] });
833
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
834
+ state.assemblyPath = host.asm;
835
+ writeFileSync(host.stateFile, JSON.stringify(state));
836
+ const res = runVerb(host, "uninstall-copilot-bridge");
837
+ ok(
838
+ `a ${label} ownership state refuses fail-closed with zero vendor mutation (flags are never coerced)`,
839
+ res.status !== 0 && res.installed.includes(OURS) && res.calls.length === 0 && existsSync(host.stateFile),
840
+ );
841
+ }
842
+ }
843
+ {
844
+ // EXACT-row adversary: a longer qualified id that CONTAINS ours authorizes nothing —
845
+ // not the inverse's uninstall branch, not install adoption, not the doctor's
846
+ // registration read.
847
+ const ADVERSARY = `${OURS}-extra`;
848
+ const invHost = makeFakeHost("adversary-inverse", { installed: [ADVERSARY] });
849
+ writeBoundState(invHost);
850
+ const inv = runVerb(invHost, "uninstall-copilot-bridge");
851
+ ok(
852
+ "inverse: a longer row containing our qualified id is NOT our plugin — named already-absent, adversary survives, no uninstall argv",
853
+ inv.status === 0 &&
854
+ inv.stdout.includes("already absent") &&
855
+ inv.installed.includes(ADVERSARY) &&
856
+ !inv.calls.some((c) => c.startsWith("plugin uninstall")),
857
+ );
858
+ const adoptHost = makeFakeHost("adversary-adopt", { installed: [ADVERSARY] });
859
+ const adopt = runVerb(adoptHost, "install-copilot-bridge");
860
+ ok(
861
+ "install: the adversary row does not trigger the adoption branch — the host reads as fresh and the adversary survives",
862
+ adopt.status === 0 && !adopt.stdout.includes("adopting") && adopt.installed.includes(ADVERSARY),
863
+ );
864
+ const docHost = makeFakeHost("adversary-doctor", { installed: [ADVERSARY] });
865
+ const doc = runVerb(docHost, "doctor-copilot-bridge");
866
+ ok(
867
+ "doctor: the adversary row does not read as our registration",
868
+ doc.status !== 0 && doc.stdout.includes("NOT installed"),
869
+ );
870
+ }
871
+ {
872
+ // ASM delete-safety representative: a symlinked recorded assembly refuses the whole
873
+ // inverse before any vendor write.
874
+ const host = makeFakeHost("inverse-asm-symlink", { installed: [OURS] });
875
+ const realDir = path.join(host.home, "real-asm");
876
+ mkdirSync(realDir, { recursive: true });
877
+ rmSync(host.asm, { recursive: true, force: true });
878
+ symlinkSync(realDir, host.asm);
879
+ writeBoundState(host);
880
+ const res = runVerb(host, "uninstall-copilot-bridge");
881
+ ok(
882
+ "a SYMLINKED recorded assembly refuses the whole inverse before any vendor write (read-only lists are the only argv)",
883
+ res.status !== 0 &&
884
+ res.installed.includes(OURS) &&
885
+ res.calls.every((c) => c === "plugin list" || c === "plugin marketplace list") &&
886
+ existsSync(realDir) &&
887
+ existsSync(host.stateFile),
888
+ );
889
+ }
890
+ {
891
+ // --assemble-only is gate-only and must never rebuild the LIVE assembly: no
892
+ // override (or the default path) refuses before any assembly mutation.
893
+ const host = makeFakeHost("assemble-only-default", {});
894
+ const env = { ...host.env };
895
+ delete env.ENTWURF_COPILOT_ASM;
896
+ const res = spawnSync("bash", [path.join(REPO, "run.sh"), "install-copilot-bridge", "--assemble-only"], {
897
+ env,
898
+ encoding: "utf8",
899
+ });
900
+ const liveAsm = path.join(env.XDG_DATA_HOME as string, "entwurf", "meta-bridge-copilot", ".assembled");
901
+ ok(
902
+ "--assemble-only without an explicit temp ENTWURF_COPILOT_ASM refuses BEFORE any assembly mutation",
903
+ res.status !== 0 && (res.stderr ?? "").includes("ENTWURF_COPILOT_ASM") && !existsSync(liveAsm),
904
+ );
905
+ }
906
+ {
907
+ const host = makeFakeHost("inverse-happy", { installed: [OURS, FOREIGN] });
908
+ // A real assembly to remove: the honest route again — this is the same host shape a
909
+ // clean install leaves behind, minus the vendor rows the fake seeds directly.
910
+ writeBoundState(host);
911
+ mkdirSync(path.join(host.asm, PLUGIN), { recursive: true });
912
+ writeFileSync(host.mktState, `${MKT}\t${host.asm}\n`);
913
+ const res = runVerb(host, "uninstall-copilot-bridge");
914
+ ok(
915
+ "the full inverse removes the exact plugin, the exact marketplace, the recorded assembly, then the state LAST",
916
+ res.status === 0 &&
917
+ !res.installed.includes(OURS) &&
918
+ res.marketplaces.length === 0 &&
919
+ !existsSync(host.asm) &&
920
+ !existsSync(host.stateFile),
921
+ );
922
+ ok(
923
+ "the inverse used only EXACT qualified/marketplace argv — never a bare id, never --force",
924
+ res.calls.includes(`plugin uninstall ${OURS}`) &&
925
+ res.calls.includes(`plugin marketplace remove ${MKT}`) &&
926
+ !res.calls.includes(`plugin uninstall ${PLUGIN}`) &&
927
+ !res.calls.some((c) => c.includes("--force")),
928
+ );
929
+ ok("a same-named FOREIGN plugin survives the inverse untouched", res.installed.includes(FOREIGN));
930
+ }
931
+ {
932
+ const host = makeFakeHost("inverse-retry", { installed: [], marketplaces: [] });
933
+ writeBoundState(host);
934
+ const res = runVerb(host, "uninstall-copilot-bridge");
935
+ ok(
936
+ "a RETRY over already-absent vendor resources names each absence and still clears the state (rerun-repair)",
937
+ res.status === 0 && res.stdout.includes("already absent") && !existsSync(host.stateFile),
938
+ );
939
+ }
940
+ {
941
+ const host = makeFakeHost("inverse-list-broken", { installed: [OURS], listFails: true });
942
+ writeBoundState(host);
943
+ const res = runVerb(host, "uninstall-copilot-bridge");
944
+ ok(
945
+ "[QK:COPILOT-BIRTH-NO-FALSE-ABSENCE] a FAILING plugin list is UNKNOWN, not an empty host — the inverse refuses with the state retained and no mutation",
946
+ res.status !== 0 &&
947
+ (res.stderr ?? "").includes("UNKNOWN") &&
948
+ existsSync(host.stateFile) &&
949
+ !res.calls.some((c) => c.startsWith("plugin uninstall") || c.startsWith("plugin marketplace remove")),
950
+ );
951
+ }
952
+
953
+ {
954
+ // Version drift (final amendment): the vendor lists our exact id at a version the
955
+ // state did not record — the inverse must refuse fail-closed with everything
956
+ // preserved, and the doctor must name it.
957
+ const invHost = makeFakeHost("inverse-version-drift", { installed: [OURS] });
958
+ mkdirSync(path.dirname(invHost.stateFile), { recursive: true });
959
+ mkdirSync(path.join(invHost.asm, PLUGIN), { recursive: true });
960
+ writeFileSync(invHost.mktState, `${MKT}\t${invHost.asm}\n`);
961
+ writeFileSync(
962
+ invHost.stateFile,
963
+ JSON.stringify({
964
+ schemaVersion: 1,
965
+ qualifiedId: OURS,
966
+ marketplaceName: MKT,
967
+ assemblyPath: invHost.asm,
968
+ pluginVersion: "0.0.9-drift",
969
+ ownedMarketplace: true,
970
+ ownedAssembly: true,
971
+ installedAt: "2026-08-27T00:00:00Z",
972
+ }),
973
+ );
974
+ const inv = runVerb(invHost, "uninstall-copilot-bridge");
975
+ ok(
976
+ "inverse: an exact row at a version the state did not record is VERSION-DRIFT — fail-closed, no vendor mutation, plugin/marketplace/assembly/state all preserved",
977
+ inv.status !== 0 &&
978
+ (inv.stderr ?? "").includes("version-drift") &&
979
+ inv.installed.includes(OURS) &&
980
+ inv.marketplaces.length === 1 &&
981
+ existsSync(path.join(invHost.asm, PLUGIN)) &&
982
+ existsSync(invHost.stateFile) &&
983
+ !inv.calls.some((c) => c.startsWith("plugin uninstall") || c.startsWith("plugin marketplace remove")),
984
+ );
985
+ const docHost = makeFakeHost("doctor-version-drift", { installed: [OURS] });
986
+ const doctorAsm = path.join(docHost.env.XDG_DATA_HOME as string, "entwurf", "meta-bridge-copilot", ".assembled");
987
+ mkdirSync(path.dirname(docHost.stateFile), { recursive: true });
988
+ writeFileSync(docHost.mktState, `${MKT}\t${doctorAsm}\n`);
989
+ writeFileSync(
990
+ docHost.stateFile,
991
+ JSON.stringify({
992
+ schemaVersion: 1,
993
+ qualifiedId: OURS,
994
+ marketplaceName: MKT,
995
+ assemblyPath: doctorAsm,
996
+ pluginVersion: "0.0.9-drift",
997
+ ownedMarketplace: true,
998
+ ownedAssembly: true,
999
+ installedAt: "2026-08-27T00:00:00Z",
1000
+ }),
1001
+ );
1002
+ const doc = runVerb(docHost, "doctor-copilot-bridge");
1003
+ ok(
1004
+ "doctor: the same drift is a named RED (`version drift`)",
1005
+ doc.status !== 0 && doc.stdout.includes("version drift"),
1006
+ );
1007
+ }
1008
+
1009
+ // ── 13b. C3a corrective amendment (B review defects, 2026-08-27) ─────────────
1010
+ {
1011
+ // B defect 2: TWO marketplace rows with our exact name. The retired grep|head -1
1012
+ // grammar silently took the FIRST — here deliberately the one at OUR assembly, so
1013
+ // the old parser would proceed. Every surface must refuse the ambiguity instead.
1014
+ const invHost = makeFakeHost("inverse-dup-mkt", { installed: [OURS] });
1015
+ writeFileSync(invHost.mktState, `${MKT}\t${invHost.asm}\n${MKT}\t/somebody/elses/dup-root\n`);
1016
+ writeBoundState(invHost);
1017
+ const inv = runVerb(invHost, "uninstall-copilot-bridge");
1018
+ ok(
1019
+ "[QK:COPILOT-MKT-DUPLICATE-REFUSED] inverse: duplicate same-named marketplace rows are ambiguity — the whole inverse refuses before any vendor write, never 'the first row'",
1020
+ inv.status !== 0 &&
1021
+ (inv.stderr ?? "").includes("multiple marketplace rows") &&
1022
+ inv.installed.includes(OURS) &&
1023
+ inv.marketplaces.length === 2 &&
1024
+ !inv.calls.some((c) => c.startsWith("plugin uninstall") || c.startsWith("plugin marketplace remove")) &&
1025
+ existsSync(invHost.stateFile),
1026
+ );
1027
+ const instHost = makeFakeHost("install-dup-mkt", {});
1028
+ writeFileSync(instHost.mktState, `${MKT}\t${instHost.asm}\n${MKT}\t/somebody/elses/dup-root\n`);
1029
+ const inst = runVerb(instHost, "install-copilot-bridge");
1030
+ ok(
1031
+ "install: the same duplicate marketplace listing refuses before any write (no state, no assembly)",
1032
+ inst.status !== 0 && !existsSync(instHost.stateFile) && !existsSync(instHost.asm),
1033
+ );
1034
+ const docHost = makeFakeHost("doctor-dup-mkt", { installed: [OURS] });
1035
+ writeFileSync(docHost.mktState, `${MKT}\t/first/root\n${MKT}\t/second/root\n`);
1036
+ const doc = runVerb(docHost, "doctor-copilot-bridge");
1037
+ ok(
1038
+ "doctor: duplicate same-named marketplace rows are a RED ownership fact",
1039
+ doc.status !== 0 && doc.stdout.includes("duplicated") && doc.stdout.includes("ownership axis: FAIL"),
1040
+ );
1041
+ }
1042
+ {
1043
+ // B defect 3: a TRUNCATED exact row (`(v0.1` — no closing paren) used to slip the
1044
+ // startswith/endswith pair and round to ABSENT; absence licenses the inverse to
1045
+ // continue as retry-safe. Malformed must be malformed on every surface.
1046
+ const raw = ` • ${OURS} (v0.1`;
1047
+ const invHost = makeFakeHost("inverse-truncated-row", { pluginListRaw: raw });
1048
+ writeBoundState(invHost);
1049
+ mkdirSync(path.join(invHost.asm, PLUGIN), { recursive: true });
1050
+ writeFileSync(invHost.mktState, `${MKT}\t${invHost.asm}\n`);
1051
+ const inv = runVerb(invHost, "uninstall-copilot-bridge");
1052
+ ok(
1053
+ "[QK:COPILOT-MALFORMED-ROW-NOT-ABSENT] inverse: a truncated exact row is MALFORMED — refuse with zero vendor mutation, never 'already absent'",
1054
+ inv.status !== 0 &&
1055
+ (inv.stderr ?? "").includes("malformed") &&
1056
+ !inv.stdout.includes("already absent") &&
1057
+ !inv.calls.some((c) => c.startsWith("plugin uninstall") || c.startsWith("plugin marketplace remove")) &&
1058
+ existsSync(path.join(invHost.asm, PLUGIN)) &&
1059
+ existsSync(invHost.stateFile),
1060
+ );
1061
+ const instHost = makeFakeHost("install-truncated-row", { pluginListRaw: raw });
1062
+ const inst = runVerb(instHost, "install-copilot-bridge");
1063
+ ok(
1064
+ "install: the same truncated row refuses before any write (no state minted)",
1065
+ inst.status !== 0 && (inst.stderr ?? "").includes("malformed") && !existsSync(instHost.stateFile),
1066
+ );
1067
+ }
1068
+ {
1069
+ // B defect 4: a pluginVersion carrying whitespace would be truncated by the
1070
+ // space-separated fact transport (`cut -d' ' -f3`) into a FABRICATED version and a
1071
+ // misleading downstream reason. The shared state validator refuses it as corrupt.
1072
+ const wsState = (asmPath: string): string =>
1073
+ JSON.stringify({
1074
+ schemaVersion: 1,
1075
+ qualifiedId: OURS,
1076
+ marketplaceName: MKT,
1077
+ assemblyPath: asmPath,
1078
+ pluginVersion: `${SHIPPED_VERSION} extra`,
1079
+ ownedMarketplace: true,
1080
+ ownedAssembly: true,
1081
+ installedAt: "2026-08-27T00:00:00Z",
1082
+ });
1083
+ const invHost = makeFakeHost("inverse-ws-version", { installed: [OURS] });
1084
+ mkdirSync(path.dirname(invHost.stateFile), { recursive: true });
1085
+ writeFileSync(invHost.stateFile, wsState(invHost.asm));
1086
+ const inv = runVerb(invHost, "uninstall-copilot-bridge");
1087
+ ok(
1088
+ "[QK:COPILOT-STATE-VERSION-WHITESPACE] inverse: a whitespace-carrying pluginVersion is a CORRUPT state named for its transport reason — zero vendor mutation, never a fabricated version verdict",
1089
+ inv.status !== 0 &&
1090
+ (inv.stderr ?? "").includes("whitespace") &&
1091
+ !(inv.stderr ?? "").includes("version-drift") &&
1092
+ inv.installed.includes(OURS) &&
1093
+ inv.calls.length === 0 &&
1094
+ existsSync(invHost.stateFile),
1095
+ );
1096
+ const docHost = makeFakeHost("doctor-ws-version", { installed: [OURS] });
1097
+ const doctorAsm = path.join(docHost.env.XDG_DATA_HOME as string, "entwurf", "meta-bridge-copilot", ".assembled");
1098
+ mkdirSync(path.dirname(docHost.stateFile), { recursive: true });
1099
+ writeFileSync(docHost.mktState, `${MKT}\t${doctorAsm}\n`);
1100
+ writeFileSync(docHost.stateFile, wsState(doctorAsm));
1101
+ const doc = runVerb(docHost, "doctor-copilot-bridge");
1102
+ ok(
1103
+ "doctor: the same whitespace version is a corrupt-state RED, not a fabricated 'version drift'",
1104
+ doc.status !== 0 && doc.stdout.includes("whitespace") && !doc.stdout.includes("version drift"),
1105
+ );
1106
+ }
1107
+ {
1108
+ // The doctor's structural verdict IS the shared oracle (B defect 1's doctor half):
1109
+ // a host the real installer just set up reads green THROUGH the oracle, and one
1110
+ // broken oracle fact turns the same host red with the oracle's own reason. The
1111
+ // install here runs WITHOUT the ENTWURF_COPILOT_ASM seam so the assembly lands at
1112
+ // the XDG-derived path the doctor actually reads.
1113
+ const host = makeFakeHost("doctor-oracle", {});
1114
+ const envNoSeam = { ...host.env };
1115
+ delete envNoSeam.ENTWURF_COPILOT_ASM;
1116
+ const inst = spawnSync("bash", [path.join(REPO, "run.sh"), "install-copilot-bridge"], {
1117
+ env: envNoSeam,
1118
+ encoding: "utf8",
1119
+ });
1120
+ ok("precondition: a full install at the doctor's own XDG assembly path is green", inst.status === 0);
1121
+ const doctorAsm = path.join(host.env.XDG_DATA_HOME as string, "entwurf", "meta-bridge-copilot", ".assembled");
1122
+ const green = runVerb(host, "doctor-copilot-bridge");
1123
+ ok(
1124
+ "doctor: the freshly installed host is GREEN through the shared structural oracle",
1125
+ green.status === 0 && green.stdout.includes("shared structural oracle"),
1126
+ );
1127
+ rmSync(path.join(doctorAsm, PLUGIN, "hooks", "hooks.json"));
1128
+ const red = runVerb(host, "doctor-copilot-bridge");
1129
+ ok(
1130
+ "[QK:COPILOT-DOCTOR-ORACLE-CONSUMED] doctor: one broken oracle fact turns the same host RED with the oracle's reason",
1131
+ red.status !== 0 && red.stdout.includes("structural oracle") && red.stdout.includes("hooks.json"),
1132
+ );
1133
+ }
1134
+
1135
+ // ── 14. the doctor's ownership axis (#86 C3a) ───────────────────────────────
1136
+ {
1137
+ const host = makeFakeHost("doctor-list-broken", { installed: [], listFails: true });
1138
+ const res = runVerb(host, "doctor-copilot-bridge");
1139
+ ok(
1140
+ "the doctor reads a FAILING plugin list as UNKNOWN (red), never as an empty host",
1141
+ res.status !== 0 && res.stdout.includes("UNKNOWN"),
1142
+ );
1143
+ }
1144
+ {
1145
+ const host = makeFakeHost("doctor-ownership-drift", {
1146
+ installed: [OURS],
1147
+ marketplaces: [[MKT, "/somebody/elses/marketplace-root"]],
1148
+ });
1149
+ // The doctor derives its assembly path from XDG (no ENTWURF_COPILOT_ASM seam), so
1150
+ // bind the state to that derived path — the drift under test is the MARKETPLACE's.
1151
+ const doctorAsm = path.join(host.env.XDG_DATA_HOME as string, "entwurf", "meta-bridge-copilot", ".assembled");
1152
+ mkdirSync(path.dirname(host.stateFile), { recursive: true });
1153
+ writeFileSync(
1154
+ host.stateFile,
1155
+ JSON.stringify({
1156
+ schemaVersion: 1,
1157
+ qualifiedId: OURS,
1158
+ marketplaceName: MKT,
1159
+ assemblyPath: doctorAsm,
1160
+ pluginVersion: SHIPPED_VERSION,
1161
+ ownedMarketplace: true,
1162
+ ownedAssembly: true,
1163
+ installedAt: "2026-08-27T00:00:00Z",
1164
+ }),
1165
+ );
1166
+ const res = runVerb(host, "doctor-copilot-bridge");
1167
+ ok(
1168
+ "the doctor FAILs a marketplace registered under our name at another path (ownership drift) and prints the axes separately",
1169
+ res.status !== 0 && res.stdout.includes("ownership drift") && res.stdout.includes("ownership axis: FAIL"),
1170
+ );
1171
+ }
1172
+
496
1173
  writeFileSync(path.join(root, "gate.ok"), "");
497
1174
  console.log(`[check-copilot-birth-hook] ${passed} assertions ok`);