diff --git a/dsh/install.sh b/dsh/install.sh index f74cec1..6baab09 100755 --- a/dsh/install.sh +++ b/dsh/install.sh @@ -414,10 +414,12 @@ ensure_pinned_clone() { fi # Online: refresh the pin. Fetching from $DSH_SUPERPOWERS_REPO (not from # the clone's origin) makes a repo change take effect on the next run - # (T3.3); fetching the tag ref itself and checking out FETCH_HEAD handles - # a moved tag, which `git checkout ` (resolving the stale local ref) - # would miss. - if git -C "$dir" fetch -q --depth 1 "$DSH_SUPERPOWERS_REPO" "refs/tags/$ref" 2>/dev/null || + # (T3.3); the force refspec updates the LOCAL tag as well as FETCH_HEAD, so + # a later offline run still sees HEAD on the ref (T3.5), and checking out + # FETCH_HEAD handles a moved tag, which `git checkout ` (resolving the + # stale local ref) would miss. The plain "$ref" fallback covers non-tag + # refs (e.g. a branch pin). + if git -C "$dir" fetch -q --depth 1 "$DSH_SUPERPOWERS_REPO" "+refs/tags/$ref:refs/tags/$ref" 2>/dev/null || git -C "$dir" fetch -q --depth 1 "$DSH_SUPERPOWERS_REPO" "$ref" 2>/dev/null; then if ! git -C "$dir" checkout -q FETCH_HEAD; then echo "FAIL: checkout of the fetched ref failed in $dir" >&2 diff --git a/dsh/tests/install.test.mjs b/dsh/tests/install.test.mjs index c3f8d02..ae6b717 100644 --- a/dsh/tests/install.test.mjs +++ b/dsh/tests/install.test.mjs @@ -240,6 +240,34 @@ test('--skills updates an existing clone from DSH_SUPERPOWERS_REPO, not origin ( } }) +test('after a repo switch, an offline run still reuses the clone on the ref (T3.5)', () => { + const work = tempDir() + try { + const home = join(work, 'home') + const repoA = makePinnedRepo(work, 'from-a', 'upstream-a') + const repoB = makePinnedRepo(work, 'from-b', 'upstream-b') + const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md') + + const first = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoA}` }) + assert.equal(first.status, 0, first.stdout + first.stderr) + + // Online switch: the clone moves to repoB's v7.0.1 (T3.3 behavior). + const second = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${repoB}` }) + assert.equal(second.status, 0, second.stdout + second.stderr) + assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-b/) + + // Offline afterwards: the local tag must have moved with the fetch, so + // the on-ref reuse succeeds with a WARN instead of a hard FAIL. + const third = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: 'file:///nonexistent' }) + const out = third.stdout + third.stderr + assert.equal(third.status, 0, out) + assert.match(out, /offline; reusing the existing clone/) + assert.match(readFileSync(installed, 'utf8'), /pin-marker: from-b/) + } finally { + cleanup(work) + } +}) + test('--skills removes a half-cloned dir after a failed fresh clone (RF5)', () => { const home = tempDir() try {