`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>
73 lines
3.1 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
});
|