From ae9179c032ee43748894a494e576ff496093ffb6 Mon Sep 17 00:00:00 2001 From: "extro.rook.host" Date: Mon, 27 Jul 2026 17:49:09 -0600 Subject: [PATCH] fix(beacon): distinguish invalid remote config from an unlit beacon vit beacon previously reported an existing remote .vit/config.json with an unnormalizable beacon as unlit, contradicting VOCAB.md's definition. Report a distinct invalid state without displaying the raw remote value. Document why init deliberately uses tolerant normalization for a stored secondary beacon during repair: an invalid secondary must be droppable rather than block repair of the primary. Remove the ineffective mock.restore() cleanup from the subprocess-isolated remote suite. --- src/cmd/beacon.js | 19 ++++++++++++------- src/cmd/init.js | 2 ++ ...nonical-beacon-remote-suite-260727.test.js | 19 ++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/cmd/beacon.js b/src/cmd/beacon.js index fd28277..5690a92 100644 --- a/src/cmd/beacon.js +++ b/src/cmd/beacon.js @@ -6,7 +6,7 @@ import http from 'isomorphic-git/http/node'; import { memfs } from 'memfs'; import { beaconToHttps, normalizeBeacon, tryNormalizeBeacon } from '../lib/beacon.js'; import { mark } from '../lib/brand.js'; -import { errorMessage, formatError } from '../lib/error-format.js'; +import { formatError } from '../lib/error-format.js'; async function readTreeFile(fs, dir, treeOid, pathParts) { for (let i = 0; i < pathParts.length; i++) { @@ -44,20 +44,25 @@ export default function register(program) { if (verbose) console.log(`[verbose] HEAD resolved: ${head}`); const commit = await git.readObject({ fs, dir, oid: head, format: 'parsed' }); const content = await readTreeFile(fs, dir, commit.object.tree, ['.vit', 'config.json']); - if (verbose) console.log(`[verbose] Read .vit/config.json: ${content ? 'found' : 'not found'}`); + const configPresent = content !== null; + if (verbose) console.log(`[verbose] Read .vit/config.json: ${configPresent ? 'found' : 'not found'}`); let beacon; - try { - beacon = tryNormalizeBeacon(content && JSON.parse(content).beacon); - } catch (err) { - console.warn(`warning: failed to parse .vit/config.json from ${url}: ${errorMessage(err)}`); + if (configPresent) { + try { + beacon = tryNormalizeBeacon(JSON.parse(content).beacon); + } catch { + beacon = null; + } } if (beacon) { console.log(`${mark} beacon: lit ${beacon}`); - } else { + } else if (!configPresent) { console.log(`${mark} beacon: unlit — this repo hasn't initialized vit yet.`); console.log("the maintainer can light the beacon by running 'vit init' inside the repo."); + } else { + console.log(`${mark} beacon: invalid — remote .vit/config.json does not contain a valid project identity`); } } catch (err) { console.error(formatError(err, { verbose: opts.verbose })); diff --git a/src/cmd/init.js b/src/cmd/init.js index 33807c9..c6d8d11 100644 --- a/src/cmd/init.js +++ b/src/cmd/init.js @@ -232,6 +232,8 @@ export default function register(program) { if (opts.secondary) { merged.secondaryBeacon = normalizeBeacon(opts.secondary, '--secondary'); } else if (Object.hasOwn(existing, 'secondaryBeacon')) { + // Tolerant normalization is deliberate on this repair path: an invalid secondary + // must be dropped with a warning, not block repair of the primary beacon. const secondary = tryNormalizeBeacon(existing.secondaryBeacon); if (secondary) { merged.secondaryBeacon = secondary; diff --git a/test/.fixtures/canonical-beacon-remote-suite-260727.test.js b/test/.fixtures/canonical-beacon-remote-suite-260727.test.js index e3a17b9..5538a73 100644 --- a/test/.fixtures/canonical-beacon-remote-suite-260727.test.js +++ b/test/.fixtures/canonical-beacon-remote-suite-260727.test.js @@ -4,7 +4,6 @@ // This lives under test/.fixtures/ so top-level `bun test` does not auto-discover // it in the shared process; its sibling wrapper executes it explicitly. import { - afterAll, afterEach, beforeEach, describe, @@ -184,10 +183,6 @@ describe('canonical remote beacon behavior', () => { rmSync(testDir, { recursive: true, force: true }); }); - afterAll(() => { - mock.restore(); - }); - function logs() { return logSpy.mock.calls.map(call => call.join(' ')).join('\n'); } @@ -391,7 +386,7 @@ describe('canonical remote beacon behavior', () => { expect(output.publishers[0].beacons).toEqual(['vit:github.com/solpbc/thermals']); }); - test('remote beacon display canonicalizes aliases and uses existing unlit wording for garbage', async () => { + test('remote beacon display canonicalizes aliases and distinguishes invalid config', async () => { remoteRepoConfig = { beacon: 'https://tangled.org/solpbc.org/rookery' }; await runRegistered(registerBeacon, ['beacon', 'vit:github.com/solpbc/vit']); expect(logs()).toContain('beacon: lit vit:tangled.org/solpbc.org/rookery'); @@ -399,7 +394,17 @@ describe('canonical remote beacon behavior', () => { logSpy.mockClear(); remoteRepoConfig = { beacon: 'not a url' }; await runRegistered(registerBeacon, ['beacon', 'vit:github.com/solpbc/vit']); - expect(logs()).toContain('beacon: unlit'); + expect(logs()).toContain( + 'beacon: invalid — remote .vit/config.json does not contain a valid project identity', + ); + expect(logs()).not.toContain('beacon: unlit'); expect(logs()).not.toContain('not a url'); + + logSpy.mockClear(); + remoteRepoConfig = {}; + await runRegistered(registerBeacon, ['beacon', 'vit:github.com/solpbc/vit']); + expect(logs()).toContain( + 'beacon: invalid — remote .vit/config.json does not contain a valid project identity', + ); }); }); -- 2.51.2