mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 17:05:28 +02:00
fix: pre-landing review round — 8 auto-fixes + 8 accepted findings hardened
The ship review army (4 specialists + red-team + checklist, 29 findings)
produced 8 mechanical auto-fixes and 11 decisions; the accepted set:
- win32 slug parity completed: lib/bin-context.ts gains the remote-first
outermost walk + degraded-cache self-heal the bash side got this wave —
the two implementations now agree on the stray-marker live-bug shape,
pinned by shared fixtures (multi-specialist 9/10 finding).
- probe honors the plan's bounded-read decision: 256KB prefix, extraction
semantics mirrored from parseTranscriptJsonl so probe/prepare can never
diverge on the same file (>1MB transcript test).
- policy normalize parity: bash normalize() now matches canonicalizeRemote
on .git/-trailing and uppercase-.GIT shapes (7-shape corpus pinned two
ways) — a deny for those shapes could previously slip the transcript gate.
- session-update reclaim is TOCTOU-safe (atomic mv-aside on both branches).
- settings-hook: unparseable settings.json errors instead of being replaced
with {}; ensure-event keys on (event, source) so matcher changes update
in place — never zero or two registrations.
- dot-only slug guard at both parse sites (hostile 'url = ..' can't escape
projects/); enqueue tmp-file janitor (1h TTL, inside the drain lock);
brain-sync .migrating never clobbered; drop-queue/status count .migrating;
snapshot -o warning correct + surfaced in diff mode; version-bump test
order-dependence removed; uninstall clears the advance stamp.
Deferred with record: slug heal-probe cost sentinel (P3 TODO), FF_OK
conflation (noted, misdiagnosis-only).
270 pass / 0 fail across the 10 touched suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9fecf0f16f
commit
b7d44c45b4
@@ -276,6 +276,85 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => {
|
||||
expectBoth(inner, "acme-outer");
|
||||
});
|
||||
|
||||
test("LIVE BUG SHAPE: a stray empty .git ancestor no longer degrades the slug (remote-first)", () => {
|
||||
// The exact 2026-08-17 reproduction: an ancestor dir with an empty .git
|
||||
// (not a valid repo, no origin) above a canonical-remote repo. The
|
||||
// pre-remote-first native path resolved PROJECT_ROOT to the stray marker
|
||||
// ancestor, found no origin THERE, and degraded to its basename — filing
|
||||
// every repo under it into one shared ~/.gstack/projects/<basename>/.
|
||||
const strayHome = path.join(tmp, "strayhome");
|
||||
fs.mkdirSync(path.join(strayHome, ".git"), { recursive: true }); // empty — invalid repo
|
||||
const repo = path.join(strayHome, "work", "repo");
|
||||
fs.mkdirSync(repo, { recursive: true });
|
||||
spawnSync("git", ["init", "-q", repo]);
|
||||
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"]);
|
||||
expectBoth(repo, "garrytan-gstack");
|
||||
expect(slugFromEnvironment(nativeHome(), repo)).not.toBe("strayhome");
|
||||
});
|
||||
|
||||
test("nested repos under a stray marker: a no-remote outer cannot shadow an inner remote", () => {
|
||||
// Outermost REMOTE-bearing repo wins — an outer repo whose origin does
|
||||
// not resolve is skipped by the remote walk, so the inner remote-bearing
|
||||
// repo carries identity (parity with bin/gstack-slug's _outermost_remote_repo).
|
||||
const outer = path.join(tmp, "outer-plain");
|
||||
const inner = path.join(outer, "vendor", "inner-lib");
|
||||
fs.mkdirSync(inner, { recursive: true });
|
||||
spawnSync("git", ["init", "-q", outer]); // no origin — marker-only repo
|
||||
spawnSync("git", ["init", "-q", inner]);
|
||||
spawnSync("git", ["-C", inner, "remote", "add", "origin", "git@github.com:vendor/inner.git"]);
|
||||
expectBoth(inner, "vendor-inner");
|
||||
});
|
||||
|
||||
test("degraded-ancestor cache self-heals: the pre-remote-first cached value is rewritten", () => {
|
||||
// Pre-fix, the resolver cached basename(PROJECT_ROOT) for the stray
|
||||
// marker ancestor. cached == the marker root's basename while a
|
||||
// remote-bearing repo BELOW it exists → recompute + heal the cache.
|
||||
const strayHome = path.join(tmp, "strayhome");
|
||||
fs.mkdirSync(path.join(strayHome, ".git"), { recursive: true });
|
||||
const repo = path.join(strayHome, "git", "proj");
|
||||
fs.mkdirSync(repo, { recursive: true });
|
||||
spawnSync("git", ["init", "-q", repo]);
|
||||
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"]);
|
||||
|
||||
const cacheDir = path.join(nativeHome(), "slug-cache");
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const cacheFile = path.join(cacheDir, toMsysPath(repo).replace(/\//g, "_"));
|
||||
fs.writeFileSync(cacheFile, "strayhome"); // the degraded value the old resolver cached
|
||||
|
||||
expect(slugFromEnvironment(nativeHome(), repo)).toBe("garrytan-gstack");
|
||||
// The cache file itself must have been overwritten (self-healing).
|
||||
expect(fs.readFileSync(cacheFile, "utf-8")).toBe("garrytan-gstack");
|
||||
});
|
||||
|
||||
test("sticky identity preserved (#2212): a remote adopted AT the marker root is NOT healed", () => {
|
||||
// Legit sticky shape: the repo that adopted the remote IS the marker root
|
||||
// (remote root == project root), so the degraded-ancestor heal must not
|
||||
// fire even though cached == basename(project root).
|
||||
const repo = path.join(tmp, "stickyproj");
|
||||
fs.mkdirSync(repo, { recursive: true });
|
||||
spawnSync("git", ["init", "-q", repo]);
|
||||
spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/x/y.git"]);
|
||||
|
||||
const cacheDir = path.join(nativeHome(), "slug-cache");
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const cacheFile = path.join(cacheDir, toMsysPath(repo).replace(/\//g, "_"));
|
||||
fs.writeFileSync(cacheFile, "stickyproj"); // pre-origin basename identity
|
||||
|
||||
expect(slugFromEnvironment(nativeHome(), repo)).toBe("stickyproj");
|
||||
expect(fs.readFileSync(cacheFile, "utf-8")).toBe("stickyproj");
|
||||
});
|
||||
|
||||
test('hostile origin `url = ..` never becomes a dot slug — basename fallback (dot-only guard)', () => {
|
||||
// git accepts `..` as a remote URL. Unchecked, the derived slug would be
|
||||
// ".." — path traversal one level above ~/.gstack/projects/. Both
|
||||
// implementations must reject it and fall through to the basename.
|
||||
const repo = path.join(tmp, "dotty");
|
||||
fs.mkdirSync(repo, { recursive: true });
|
||||
spawnSync("git", ["init", "-q", repo]);
|
||||
spawnSync("git", ["-C", repo, "remote", "add", "origin", ".."]);
|
||||
expectBoth(repo, "dotty");
|
||||
});
|
||||
|
||||
test("GSTACK_PROJECT_SLUG env override beats every other resolution path, never cached", () => {
|
||||
const projectRoot = path.join(tmp, "loadout");
|
||||
const siteSubdir = path.join(projectRoot, "site");
|
||||
|
||||
Reference in New Issue
Block a user