Files
msd-core/tests/bug-3668-workflow-runtime-resolution.test.cjs
Tom Boucher 4fbb61c915 fix(#372): harden bug-3668 resolver test for Windows/CI (non-login shell + CRLF) (#374)
* fix(#372): use non-login shell in bug-3668 resolver test for hermetic PATH

bash -lc re-sourced profile files (e.g. Homebrew shellenv) that prepended
real bin dirs ahead of the test's injected PATH, so the installed-gsd-tools
fallback subtest resolved a host-global gsd-tools instead of the injected
fake — failing on dev machines and CI-adjacent benches while passing on
clean CI. Use bash -c (non-login) so the injected PATH is authoritative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#372): split workflow snippet on CRLF in bug-3668 resolver test

extractResolverSnippet split on '\n' and exact-matched the closing \`fi\`
line, so on a Windows checkout (CRLF) the line was \`fi\r\`, the end marker
was never found, and the test failed with "SDK resolution snippet must end
with fi". Split on /\r?\n/ so extraction works on LF and CRLF checkouts and
the snippet handed to bash is carriage-return-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#372): assert resolver bin by normalized suffix, not exact OS path

On Windows the snippet runs under Git bash, so GSD_TOOLS is reported POSIX-
style / mixed-separator while the test built its expected regex from Node
path.join (backslashes) — the resolver was correct (installed:/runtime: output
proves the right bin ran) but the exact-path assertions failed. Normalize
separators and assert the GSD_TOOLS path suffix, keeping the behavioral
installed:/runtime: assertions as the primary checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:33:49 -04:00

97 lines
3.8 KiB
JavaScript

/**
* Bug #3668: workflow resolver snippets must run from installed user projects.
*
* A user project normally does not contain get-shit-done/bin/gsd-tools.cjs.
* The snippets should still prefer RUNTIME_DIR for local/dev installs, then
* fall back to the installed gsd-tools binary on PATH.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const WORKFLOW_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'next.md');
function extractResolverSnippet() {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const lines = content.split(/\r?\n/);
const start = lines.findIndex((line) => line.includes('SDK resolution: prefer local gsd-tools.cjs'));
assert.notEqual(start, -1, 'next.md must contain the SDK resolution snippet');
const end = lines.findIndex((line, index) => index > start && line === 'fi');
assert.notEqual(end, -1, 'SDK resolution snippet must end with fi');
return lines.slice(start, end + 1).join('\n');
}
function makeTempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-runtime-resolution-'));
}
function runResolver({ cwd, runtimeDir, pathDir }) {
const script = [
'set -e',
extractResolverSnippet(),
'printf "GSD_TOOLS=%s\\n" "$GSD_TOOLS"',
'$GSD_SDK query state.json',
].join('\n');
return execFileSync('bash', ['-c', script], {
cwd,
env: {
...process.env,
PATH: `${pathDir}${path.delimiter}${process.env.PATH || ''}`,
RUNTIME_DIR: runtimeDir || '',
},
encoding: 'utf8',
});
}
function writeExecutable(file, content) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, content, { mode: 0o755 });
}
describe('bug-3668: workflow SDK resolver supports installed user projects', () => {
test('falls back to installed gsd-tools when project-local runtime copy is absent', () => {
const tmp = makeTempDir();
const project = path.join(tmp, 'user-project');
const bin = path.join(tmp, 'bin');
fs.mkdirSync(project, { recursive: true });
writeExecutable(path.join(bin, 'gsd-tools'), '#!/bin/sh\nprintf "installed:%s %s\\n" "$1" "$2"\n');
const output = runResolver({ cwd: project, pathDir: bin });
// Normalize separators so the assertion works on Windows (Git bash emits POSIX paths)
const norm = output.replace(/\\/g, '/');
// subtest 1: the resolved bin is the PATH-fallback gsd-tools (suffix /bin/gsd-tools, no .cjs)
assert.match(norm, /GSD_TOOLS=\S*\/bin\/gsd-tools(?:\s|$)/m);
assert.match(output, /installed:query state\.json/);
});
test('preserves RUNTIME_DIR local gsd-tools.cjs preference over PATH fallback', () => {
const tmp = makeTempDir();
const project = path.join(tmp, 'user-project');
const runtime = path.join(tmp, 'runtime');
const pathBin = path.join(tmp, 'bin');
fs.mkdirSync(project, { recursive: true });
writeExecutable(path.join(pathBin, 'gsd-tools'), '#!/bin/sh\nprintf "installed:%s %s\\n" "$1" "$2"\n');
writeExecutable(
path.join(runtime, 'get-shit-done', 'bin', 'gsd-tools.cjs'),
'#!/usr/bin/env node\nconsole.log(`runtime:${process.argv[2]} ${process.argv[3]}`);\n',
);
const output = runResolver({ cwd: project, runtimeDir: runtime, pathDir: pathBin });
// Normalize separators so the assertion works on Windows (Git bash emits POSIX paths)
const norm = output.replace(/\\/g, '/');
// subtest 2: the resolved bin is the RUNTIME_DIR local runtime (suffix /get-shit-done/bin/gsd-tools.cjs)
assert.match(norm, /GSD_TOOLS=\S*\/get-shit-done\/bin\/gsd-tools\.cjs(?:\s|$)/m);
assert.match(output, /runtime:query state\.json/);
assert.doesNotMatch(output, /installed:query state\.json/);
});
});