fix(slug): terminate the marker walk-up on dirname's fixed point — hung every bin on Windows

Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> .
forever: dirname's fixed point there is never "/", so the walk-up loop spun
and every bin that evals gstack-slug (learnings-log first among them) hung
until spawn timeout. Caught by windows-free-tests CI on the wave PR. Break
on the fixed point itself with a depth cap for exotic forms; regression
tests drive the extracted function with hostile path shapes under a hard
timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-08-14 21:00:46 -07:00
parent 4fc3731a50
commit daeec2c017
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 30 additions and 2 deletions

View File

@ -68,7 +68,12 @@ _outermost_project_root() {
local dir="$1"
local outermost_strong=""
local outermost_weak=""
while [[ -n "$dir" && "$dir" != "/" ]]; do
local parent="" depth=0
# Terminate on dirname's FIXED POINT, not on a literal "/": under git-bash
# on Windows a mixed-form path walks C:/Users -> C: -> . -> . forever, which
# hung every bin that evals gstack-slug (caught by windows-free-tests CI).
# The depth cap is belt-and-braces for exotic path forms (UNC, //server).
while [[ -n "$dir" && "$dir" != "/" && $depth -lt 64 ]]; do
if [[ -e "$dir/.git" \
|| -f "$dir/.project.yaml" \
|| -f "$dir/package.json" \
@ -84,7 +89,10 @@ _outermost_project_root() {
|| -f "$dir/LICENSE.md" ]]; then
outermost_weak="$dir"
fi
dir=$(dirname "$dir")
parent=$(dirname "$dir")
[[ "$parent" == "$dir" ]] && break # dirname fixed point (C:/, ., //srv)
dir="$parent"
depth=$((depth + 1))
done
# Strong markers win over weak; either wins over nothing.
if [[ -n "$outermost_strong" ]]; then

View File

@ -289,3 +289,23 @@ describe('gstack-slug — outermost project-root resolution', () => {
expect(slug).toBe('custom-override');
});
});
describe('_outermost_project_root termination (windows-free-tests regression)', () => {
// Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> .
// forever: dirname's fixed point there is never "/". The loop must break on
// the fixed point itself. Extract the function and drive it with hostile
// path forms under a hard timeout — a hang fails the spawn, not the suite.
const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-slug'), 'utf-8');
const fnMatch = script.match(/_outermost_project_root\(\) \{[\s\S]*?\n\}/);
test.each(['C:/Users/nobody/project', '.', '//server/share/dir'])(
'terminates on hostile path form: %s',
(hostile) => {
expect(fnMatch).not.toBeNull();
const r = Bun.spawnSync(['bash', '-c', `${fnMatch![0]}\n_outermost_project_root "$1"; echo TERMINATED`, '_', hostile], {
timeout: 5000,
});
expect(r.stdout.toString()).toContain('TERMINATED');
},
);
});