Files
get-shit-done/tests/bug-2525-sdk-executable-bit.test.cjs
Tom Boucher e972f598f1 fix(install): chmod dist/cli.js 0o755 after tsc build to fix missing executable bit (closes #2525)
tsc emits .js files as 644; npm install -g creates the bin symlink
without chmod-ing the target, so the kernel refuses to exec the file on
macOS with a Homebrew npm prefix. Add a chmodSync(cliPath, 0o755) call
between the build step and the global install step, matching the pattern
already used for hook files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 16:45:44 -04:00

83 lines
3.3 KiB
JavaScript

/**
* Regression test for bug #2525
*
* After running the GSD installer on macOS with a Homebrew npm prefix,
* `gsd-sdk` is installed but `command -v gsd-sdk` returns nothing because
* `dist/cli.js` is installed with mode 644 (no executable bit). tsc emits
* .js files as 644, and `npm install -g .` creates the bin symlink without
* chmod-ing the target. The kernel then refuses to exec the file.
*
* Fix: between the `npm run build` step and `npm install -g .`, chmod
* dist/cli.js to 0o755. This mirrors the pattern already used for hook
* files at lines 5838, 5846, 5959, and 5965.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
describe('bug #2525: dist/cli.js chmod 0o755 after tsc build', () => {
const content = fs.readFileSync(INSTALL_PATH, 'utf-8');
test('install.js exists', () => {
assert.ok(fs.existsSync(INSTALL_PATH), 'bin/install.js should exist');
});
test('chmodSync is called for dist/cli.js after the build step', () => {
// Find the installSdkIfNeeded function body
const fnStart = content.indexOf('function installSdkIfNeeded()');
assert.ok(fnStart !== -1, 'installSdkIfNeeded function must exist in bin/install.js');
// Find the closing brace of the function (next top-level function definition)
const fnEnd = content.indexOf('\nfunction ', fnStart + 1);
assert.ok(fnEnd !== -1, 'installSdkIfNeeded function must have a closing boundary');
const fnBody = content.slice(fnStart, fnEnd);
// Locate the build step
const buildStep = fnBody.indexOf("'run', 'build'");
assert.ok(buildStep !== -1, "installSdkIfNeeded must contain the 'run', 'build' spawn call");
// Locate the global install step
const globalStep = fnBody.indexOf("'install', '-g', '.'");
assert.ok(globalStep !== -1, "installSdkIfNeeded must contain the 'install', '-g', '.' spawn call");
// Locate chmodSync for dist/cli.js
const chmodIdx = fnBody.indexOf("chmodSync");
assert.ok(chmodIdx !== -1, "installSdkIfNeeded must call chmodSync to set the executable bit on dist/cli.js");
// The path may be assembled via path.join(sdkDir, 'dist', 'cli.js') so check
// for the component strings rather than the joined slash form.
const cliPathIdx = fnBody.indexOf("'cli.js'");
assert.ok(cliPathIdx !== -1, "installSdkIfNeeded must reference 'cli.js' (via path.join or literal) for the chmod call");
// chmodSync must appear AFTER the build step
assert.ok(
chmodIdx > buildStep,
'chmodSync for dist/cli.js must appear AFTER the npm run build step'
);
// chmodSync must appear BEFORE the global install step
assert.ok(
chmodIdx < globalStep,
'chmodSync for dist/cli.js must appear BEFORE the npm install -g . step'
);
});
test('chmod mode is 0o755', () => {
const fnStart = content.indexOf('function installSdkIfNeeded()');
const fnEnd = content.indexOf('\nfunction ', fnStart + 1);
const fnBody = content.slice(fnStart, fnEnd);
assert.ok(
fnBody.includes('0o755'),
"chmodSync call in installSdkIfNeeded must use mode 0o755 (not 0o644 or a bare number)"
);
});
});