| … | … | @@ -0,0 +1,108 @@ |
| 1 | /** |
| 2 | * Default NameRegistry subscription — a fresh machine must resolve bare |
| 3 | * app names without a manual `pgh names subscribe`. |
| 4 | * |
| 5 | * Bug this pins: name resolution is subscription-based (`_resolveNameAcross` |
| 6 | * = own registry, then each subscribed replica). Nothing seeded either source |
| 7 | * on a fresh machine — not `pgh init`, not install.sh, not any package.json |
| 8 | * default — so a new peer booted its OWN empty NameRegistry, had zero |
| 9 | * replicas, and every documented `pgh app install <name>` failed with |
| 10 | * "name not found in pgh-sidecar's NameRegistry". Only a pgh:// URL worked. |
| 11 | * |
| 12 | * Contract: configuration WINS OUTRIGHT over the default (no merging), so a |
| 13 | * peer subscribed to its own namespace registries never has ours forced on it. |
| 14 | * |
| 15 | * Constructor-only — no boot, no swarm. The subscription decision happens in |
| 16 | * the Sidecar constructor; opening the replicas is gated behind enableSeed. |
| 17 | */ |
| 18 | |
| 19 | import test from 'brittle' |
| 20 | import fs from '#node:fs' |
| 21 | import path from '#node:path' |
| 22 | import os from '#node:os' |
| 23 | import process from '#node:process' |
| 24 | |
| 25 | import { DEFAULT_NAME_REGISTRIES } from 'pgh/bootstrap' |
| 26 | import { Sidecar } from '../lib/index.js' |
| 27 | |
| 28 | const OTHER_KEY = 'a'.repeat(64) |
| 29 | const THIRD_KEY = 'b'.repeat(64) |
| 30 | |
| 31 | // Point subscriptions.json at a scratch file so the test never reads (or |
| 32 | // writes) the real ~/.config/pgh/subscriptions.json. |
| 33 | function withSubscriptions (contents, fn) { |
| 34 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgh-subs-')) |
| 35 | const file = path.join(dir, 'subscriptions.json') |
| 36 | if (contents !== null) fs.writeFileSync(file, JSON.stringify(contents, null, 2)) |
| 37 | // NB: under Bare `process.env` is a Proxy whose deleteProperty trap returns |
| 38 | // false — `delete process.env.X` THROWS. Both readers treat '' as unset |
| 39 | // (plain falsy checks), so clear by assignment instead. |
| 40 | const prevPath = process.env.PGH_SUBSCRIPTIONS_PATH || '' |
| 41 | const prevEnv = process.env.PGH_NAME_REGISTRIES || '' |
| 42 | process.env.PGH_SUBSCRIPTIONS_PATH = file |
| 43 | process.env.PGH_NAME_REGISTRIES = '' |
| 44 | try { |
| 45 | return fn() |
| 46 | } finally { |
| 47 | process.env.PGH_SUBSCRIPTIONS_PATH = prevPath |
| 48 | process.env.PGH_NAME_REGISTRIES = prevEnv |
| 49 | fs.rmSync(dir, { recursive: true, force: true }) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // Constructing a Sidecar touches no disk and starts nothing — `_open` does. |
| 54 | function subscriptionsFor (opts = {}) { |
| 55 | return new Sidecar({ enableSeed: false, ...opts }).nameRegistrySubscriptions |
| 56 | } |
| 57 | |
| 58 | test('default is non-empty and well-formed (64-hex keys)', (t) => { |
| 59 | t.ok(DEFAULT_NAME_REGISTRIES.length > 0, 'ships at least one default registry') |
| 60 | for (const k of DEFAULT_NAME_REGISTRIES) { |
| 61 | t.ok(/^[0-9a-f]{64}$/.test(k), k.slice(0, 12) + '… is a 64-char hex key') |
| 62 | } |
| 63 | }) |
| 64 | |
| 65 | test('fresh machine (no subscriptions.json) gets the default', (t) => { |
| 66 | withSubscriptions(null, () => { |
| 67 | const subs = subscriptionsFor() |
| 68 | for (const k of DEFAULT_NAME_REGISTRIES) { |
| 69 | t.ok(subs.includes(k), 'subscribed to default ' + k.slice(0, 12) + '…') |
| 70 | } |
| 71 | }) |
| 72 | }) |
| 73 | |
| 74 | test('subscriptions.json without a nameRegistries key gets the default', (t) => { |
| 75 | // The real-world fresh shape: `pgh app install pgh://…` writes `registries` |
| 76 | // (release) and never touches `nameRegistries`. |
| 77 | withSubscriptions({ registries: [OTHER_KEY] }, () => { |
| 78 | const subs = subscriptionsFor() |
| 79 | t.alike(subs, [...DEFAULT_NAME_REGISTRIES], 'release subs do not count as name subs') |
| 80 | }) |
| 81 | }) |
| 82 | |
| 83 | test('configured nameRegistries win outright — default is NOT merged in', (t) => { |
| 84 | withSubscriptions({ nameRegistries: [OTHER_KEY] }, () => { |
| 85 | const subs = subscriptionsFor() |
| 86 | t.alike(subs, [OTHER_KEY], 'exactly what the user configured') |
| 87 | for (const k of DEFAULT_NAME_REGISTRIES) { |
| 88 | t.absent(subs.includes(k), 'default not forced on a configured peer') |
| 89 | } |
| 90 | }) |
| 91 | }) |
| 92 | |
| 93 | test('PGH_NAME_REGISTRIES wins outright over the default', (t) => { |
| 94 | withSubscriptions(null, () => { |
| 95 | process.env.PGH_NAME_REGISTRIES = THIRD_KEY |
| 96 | const subs = subscriptionsFor() |
| 97 | t.alike(subs, [THIRD_KEY], 'env-configured set replaces the default') |
| 98 | }) |
| 99 | }) |
| 100 | |
| 101 | test('explicit opts win over everything', (t) => { |
| 102 | withSubscriptions(null, () => { |
| 103 | t.alike(subscriptionsFor({ nameRegistrySubscriptions: [OTHER_KEY] }), [OTHER_KEY]) |
| 104 | // An explicit empty list is a real choice (subscribe to nothing), not |
| 105 | // "unset" — it must not fall through to the default. |
| 106 | t.alike(subscriptionsFor({ nameRegistrySubscriptions: [] }), [], 'explicit [] opts out') |
| 107 | }) |
| 108 | }) |