Files
msd-core/tests/effort-sync-installed-runtime.test.cjs
Tom Boucher 015c3a7fda fix(#2071): extract install-time effort resolvers so effort sync stops requiring the un-shipped bin/install.js
`gsd-tools effort sync` crashed in every installed runtime (e.g. ~/.claude/gsd-core/)
with `Cannot find module '../../../bin/install.js'`: cmdEffortSync (src/commands.cts)
required the package-root bin/install.js for its install-time effort resolvers, but the
installer only copies the gsd-core/ subtree into a runtime home — bin/install.js is never
present there. So `effort` config changes silently never reached installed agents without
a full reinstall (exactly the gap #488 was meant to close). 4th instance of the recurring
"runtime code under gsd-core/ requires a file outside the shipped subtree via ../../../"
anti-pattern (#1223/#1920/#1383 were the prior three, all already mitigated).

Fix (ADR-457 direction — extract, single source): move readGsdEffectiveEffortConfig +
resolveInstallTimeEffort (with their _getGsdEffortCatalog + _readGsdConfigFile helpers)
out of the hand-authored bin/install.js into a new src/install-effort-resolver.cts that
compiles into the shipped gsd-core/bin/lib/install-effort-resolver.cjs. commands.cts now
requires it as a sibling (`./install-effort-resolver.cjs`) — always present in the
installed tree — instead of `../../../bin/install.js`. bin/install.js imports the same four
symbols back from the new module (it still calls them + re-exports them), so there is one
source of truth and no duplication/drift. The lazy manifest read is repointed from the
package-root layout (`.., gsd-core, bin, shared`) to the bin/lib layout (`.., shared`).

Scope note: this is one of four instances of the anti-pattern; the other three are already
shipped/guarded. A build-time guard rejecting new cross-boundary requires whose target isn't
in the installer copy manifest (to prevent instance #5) is recommended on the issue but kept
out of this fix.

Tests: tests/effort-sync-installed-runtime.test.cjs does a real minimal install into a temp
home (the golden-parity helper) and runs the issue's exact repro
(`gsd-tools effort sync --config-dir <temp>`), asserting no MODULE_NOT_FOUND for
bin/install.js. Fail-first verified: against pristine next the same test throws
`Cannot find module '../../../bin/install.js'` at cmdEffortSync; post-fix it syncs cleanly.

New module registered in .gitignore (ADR-457), eslint ignores, docs/INVENTORY.md +
INVENTORY-MANIFEST.json. bin/install.js is not shipped and the new module is under bin/lib
(excluded from golden parity), so no golden fixtures change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:21:45 -04:00

73 lines
3.1 KiB
JavaScript

'use strict';
/**
* #2071 — `gsd-tools effort sync` crashed in an INSTALLED runtime because
* commands.cjs did `require('../../../bin/install.js')`, but the installer only
* copies the `gsd-core/` subtree into a runtime home — the package-root
* `bin/install.js` is never present there, so the require threw MODULE_NOT_FOUND.
*
* This does a real minimal install into a temp home (the same helper the
* golden-parity suite uses) and runs the exact repro from the issue against the
* installed shim: `node <configDir>/gsd-core/bin/gsd-tools.cjs effort sync`. Pre-fix
* this throws `Cannot find module '../../../bin/install.js'`; post-fix the
* install-time resolvers live in the shipped sibling
* `gsd-core/bin/lib/install-effort-resolver.cjs` and the require resolves.
*
* `--config-dir <temp>` keeps it hermetic (targets the temp install, never the
* developer's real ~/.claude); effort sync defaults to dry-run so nothing is written.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
describe('#2071: effort sync runs in an installed runtime (no package-root bin/install.js)', () => {
test('effort sync does not crash reaching for the un-shipped bin/install.js', () => {
if (process.platform === 'win32') return; // install layout is POSIX-path-shaped
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
try {
// Installed layout invariant: the package-root installer is never copied in.
assert.ok(!fs.existsSync(path.join(root, 'bin', 'install.js')), 'installed home must not contain bin/install.js');
assert.ok(!fs.existsSync(path.join(configDir, 'bin', 'install.js')), 'no bin/install.js beside gsd-core');
// A project effort config gives the sync something to resolve.
fs.mkdirSync(path.join(root, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(root, '.planning', 'config.json'),
JSON.stringify({ effort: { default: 'high' } }),
);
const gsdTools = path.join(configDir, 'gsd-core', 'bin', 'gsd-tools.cjs');
let combined = '';
try {
combined = execFileSync(
process.execPath,
[gsdTools, 'effort', 'sync', '--config-dir', configDir],
{ cwd: root, encoding: 'utf-8', env: { ...process.env, HOME: root } },
);
} catch (e) {
combined = `${e.stdout || ''}${e.stderr || ''}${e.message || ''}`;
}
assert.doesNotMatch(
combined,
/Cannot find module[^\n]*install\.js|'\.\.\/\.\.\/\.\.\/bin\/install\.js'/,
`effort sync must not reach for the un-shipped bin/install.js:\n${combined}`,
);
assert.doesNotMatch(
combined,
/MODULE_NOT_FOUND/,
`effort sync must not crash on module resolution in an installed runtime:\n${combined}`,
);
} finally {
cleanup(root);
}
});
});