mirror of https://github.com/garrytan/gstack.git
fix(context-restore): prefer the current branch's own checkpoint (#2052)
All worktrees of a repo share one origin-derived slug, so they share one `~/.gstack/projects/<slug>/checkpoints/` dir. `/context-restore` loaded the newest checkpoint across the whole dir, so in one worktree it could silently restore a *sibling worktree's* newer checkpoint. Step 1 now orders candidates current-branch-first (read from each file's `branch:` frontmatter), keeping other branches as a fallback. A branch is checked out in at most one worktree, so this stops cross-worktree contamination while preserving Conductor cross-branch handoff: when the current branch has no checkpoint of its own, the full newest-first set is still used. - scan the 200 newest before partitioning so a current-branch checkpoint sitting below a burst of sibling saves is still found; output still capped at 20 - non-git / detached HEAD / branchless legacy saves fall back to the old newest-first behavior (back-compat) - +5 regression tests in context-save-hardening.test.ts (the #2052 bug case fails on the old pipeline); regenerated SKILL.md + proactive-suggestions.json Fixes #2052 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d23ccab56
commit
9adb8ad4ea
|
|
@ -23,8 +23,8 @@ triggers:
|
||||||
## When to invoke this skill
|
## When to invoke this skill
|
||||||
|
|
||||||
Loads the most recent
|
Loads the most recent
|
||||||
saved state (across all branches by default) so you can pick up where you
|
saved state (preferring the current branch, falling back across branches) so
|
||||||
left off — even across Conductor workspace handoffs.
|
you can pick up where you left off — even across Conductor workspace handoffs.
|
||||||
Use when asked to "resume", "restore context", "where was I", or
|
Use when asked to "resume", "restore context", "where was I", or
|
||||||
"pick up where I left off". Pair with /context-save.
|
"pick up where I left off". Pair with /context-save.
|
||||||
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
||||||
|
|
@ -791,13 +791,19 @@ context and present it clearly so the user can resume work without losing a beat
|
||||||
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
||||||
context files and presents the summary.
|
context files and presents the summary.
|
||||||
|
|
||||||
**Default: load the most recent saved context across ALL branches.** This is
|
**Default: prefer the most recent checkpoint saved on the CURRENT branch; if
|
||||||
intentionally different from `/context-save list`, which defaults to the current
|
this branch has none, fall back to the most recent across ALL branches.** The
|
||||||
branch. `/context-restore` is for Conductor workspace handoff — a context saved
|
fallback is for Conductor workspace handoff — a context saved on one branch can
|
||||||
on one branch can be resumed from another.
|
be resumed from another. The current-branch preference exists because every
|
||||||
|
worktree of a repo shares one checkpoints directory (same origin-derived slug),
|
||||||
|
so without it `/context-restore` in one worktree could silently load a sibling
|
||||||
|
worktree's newer checkpoint.
|
||||||
|
|
||||||
**Do NOT filter the candidate set by current branch.** The `list` flow does
|
**Do NOT hard-filter the candidate set to the current branch** — other-branch
|
||||||
that; `/context-restore` does not.
|
checkpoints stay in the set as a fallback. They are just ordered *after* the
|
||||||
|
current branch's own, so a current-branch save is never shadowed by a newer
|
||||||
|
sibling-worktree save. (`/context-save list` is the flow that hard-scopes to one
|
||||||
|
branch.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -828,27 +834,53 @@ else
|
||||||
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
||||||
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
||||||
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
||||||
# Cap at 20 most recent: a user with 10k saved files shouldn't blow the
|
# Scan the 200 newest so a current-branch checkpoint sitting below a burst of
|
||||||
# context window just listing them. /context-save list handles pagination.
|
# sibling-worktree saves can still be found; the result is capped at 20 below.
|
||||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||||
if [ -z "$FILES" ]; then
|
if [ -z "$ALL" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
|
# Order current-branch checkpoints first, other branches after. A git branch
|
||||||
|
# is checked out in at most one worktree, and all worktrees of a repo share
|
||||||
|
# one checkpoints dir (same origin-derived slug), so without this preference
|
||||||
|
# `/context-restore` in worktree A could load worktree B's newer checkpoint.
|
||||||
|
# Cross-branch resume (Conductor handoff) is preserved as the fallback: when
|
||||||
|
# the current branch has no checkpoint, the full newest-first set is used.
|
||||||
|
# CURRENT_BRANCH may be pre-set (tests); otherwise resolve it from git.
|
||||||
|
: "${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||||
|
SAME=""; OTHER=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||||
|
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||||
|
SAME="${SAME}${f}
|
||||||
|
"
|
||||||
|
else
|
||||||
|
OTHER="${OTHER}${f}
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$ALL
|
||||||
|
EOF
|
||||||
|
# Cap at 20: a user with 10k saved files shouldn't blow the context window.
|
||||||
|
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||||
echo "$FILES"
|
echo "$FILES"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
**Candidates include every `.md` file in the directory, regardless of branch**
|
**Candidates include every `.md` file in the directory**, but they are ordered
|
||||||
(the branch is recorded in frontmatter, not used for filtering here). This
|
**current-branch-first** (the branch is read from each file's `branch:`
|
||||||
enables Conductor workspace handoff.
|
frontmatter). Other-branch files stay in the set as a fallback, which preserves
|
||||||
|
Conductor workspace handoff when the current branch has no checkpoint of its own.
|
||||||
|
|
||||||
### Step 2: Load the right file
|
### Step 2: Load the right file
|
||||||
|
|
||||||
- If the user specified a title fragment or number: find the matching file among
|
- If the user specified a title fragment or number: find the matching file among
|
||||||
the candidates.
|
the candidates.
|
||||||
- Otherwise: load the **first file returned by the `sort -r` above** — that is
|
- Otherwise: load the **first file returned by Step 1 above** — that is the
|
||||||
the newest `YYYYMMDD-HHMMSS` prefix, which is the canonical "most recent."
|
newest `YYYYMMDD-HHMMSS` checkpoint for the current branch, or, if the current
|
||||||
|
branch has none, the newest across all branches.
|
||||||
|
|
||||||
Read the chosen file and present a summary:
|
Read the chosen file and present a summary:
|
||||||
|
|
||||||
|
|
@ -900,9 +932,10 @@ state, then `/context-restore` will find it."
|
||||||
## Important Rules
|
## Important Rules
|
||||||
|
|
||||||
- **Never modify code.** This skill only reads saved files and presents them.
|
- **Never modify code.** This skill only reads saved files and presents them.
|
||||||
- **Always search across all branches by default.** Cross-branch resume is the
|
- **Prefer the current branch's own checkpoint, but keep all branches in the
|
||||||
whole point. Only filter by branch if the user explicitly asks via a
|
fallback set.** Cross-branch resume (Conductor handoff) still works when the
|
||||||
title-fragment match that happens to be branch-specific.
|
current branch has no checkpoint; it just no longer lets a sibling worktree's
|
||||||
|
newer save shadow this branch's own.
|
||||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||||
operations; mtime is not.
|
operations; mtime is not.
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ preamble-tier: 2
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
Restore working context saved earlier by /context-save. Loads the most recent
|
Restore working context saved earlier by /context-save. Loads the most recent
|
||||||
saved state (across all branches by default) so you can pick up where you
|
saved state (preferring the current branch, falling back across branches) so
|
||||||
left off — even across Conductor workspace handoffs.
|
you can pick up where you left off — even across Conductor workspace handoffs.
|
||||||
Use when asked to "resume", "restore context", "where was I", or
|
Use when asked to "resume", "restore context", "where was I", or
|
||||||
"pick up where I left off". Pair with /context-save.
|
"pick up where I left off". Pair with /context-save.
|
||||||
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
Formerly /checkpoint resume — renamed because Claude Code treats /checkpoint
|
||||||
|
|
@ -35,13 +35,19 @@ context and present it clearly so the user can resume work without losing a beat
|
||||||
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
**HARD GATE:** Do NOT implement code changes. This skill only reads saved
|
||||||
context files and presents the summary.
|
context files and presents the summary.
|
||||||
|
|
||||||
**Default: load the most recent saved context across ALL branches.** This is
|
**Default: prefer the most recent checkpoint saved on the CURRENT branch; if
|
||||||
intentionally different from `/context-save list`, which defaults to the current
|
this branch has none, fall back to the most recent across ALL branches.** The
|
||||||
branch. `/context-restore` is for Conductor workspace handoff — a context saved
|
fallback is for Conductor workspace handoff — a context saved on one branch can
|
||||||
on one branch can be resumed from another.
|
be resumed from another. The current-branch preference exists because every
|
||||||
|
worktree of a repo shares one checkpoints directory (same origin-derived slug),
|
||||||
|
so without it `/context-restore` in one worktree could silently load a sibling
|
||||||
|
worktree's newer checkpoint.
|
||||||
|
|
||||||
**Do NOT filter the candidate set by current branch.** The `list` flow does
|
**Do NOT hard-filter the candidate set to the current branch** — other-branch
|
||||||
that; `/context-restore` does not.
|
checkpoints stay in the set as a fallback. They are just ordered *after* the
|
||||||
|
current branch's own, so a current-branch save is never shadowed by a newer
|
||||||
|
sibling-worktree save. (`/context-save list` is the flow that hard-scopes to one
|
||||||
|
branch.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -72,27 +78,53 @@ else
|
||||||
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
# copies/rsync). Filesystem mtime drifts and is not authoritative.
|
||||||
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
# 2. On macOS, `find ... | xargs ls -1t` with zero results falls back to
|
||||||
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
# listing cwd. `sort -r` on empty input cleanly returns nothing.
|
||||||
# Cap at 20 most recent: a user with 10k saved files shouldn't blow the
|
# Scan the 200 newest so a current-branch checkpoint sitting below a burst of
|
||||||
# context window just listing them. /context-save list handles pagination.
|
# sibling-worktree saves can still be found; the result is capped at 20 below.
|
||||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||||
if [ -z "$FILES" ]; then
|
if [ -z "$ALL" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
|
# Order current-branch checkpoints first, other branches after. A git branch
|
||||||
|
# is checked out in at most one worktree, and all worktrees of a repo share
|
||||||
|
# one checkpoints dir (same origin-derived slug), so without this preference
|
||||||
|
# `/context-restore` in worktree A could load worktree B's newer checkpoint.
|
||||||
|
# Cross-branch resume (Conductor handoff) is preserved as the fallback: when
|
||||||
|
# the current branch has no checkpoint, the full newest-first set is used.
|
||||||
|
# CURRENT_BRANCH may be pre-set (tests); otherwise resolve it from git.
|
||||||
|
: "${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||||
|
SAME=""; OTHER=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||||
|
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||||
|
SAME="${SAME}${f}
|
||||||
|
"
|
||||||
|
else
|
||||||
|
OTHER="${OTHER}${f}
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$ALL
|
||||||
|
EOF
|
||||||
|
# Cap at 20: a user with 10k saved files shouldn't blow the context window.
|
||||||
|
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||||
echo "$FILES"
|
echo "$FILES"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
**Candidates include every `.md` file in the directory, regardless of branch**
|
**Candidates include every `.md` file in the directory**, but they are ordered
|
||||||
(the branch is recorded in frontmatter, not used for filtering here). This
|
**current-branch-first** (the branch is read from each file's `branch:`
|
||||||
enables Conductor workspace handoff.
|
frontmatter). Other-branch files stay in the set as a fallback, which preserves
|
||||||
|
Conductor workspace handoff when the current branch has no checkpoint of its own.
|
||||||
|
|
||||||
### Step 2: Load the right file
|
### Step 2: Load the right file
|
||||||
|
|
||||||
- If the user specified a title fragment or number: find the matching file among
|
- If the user specified a title fragment or number: find the matching file among
|
||||||
the candidates.
|
the candidates.
|
||||||
- Otherwise: load the **first file returned by the `sort -r` above** — that is
|
- Otherwise: load the **first file returned by Step 1 above** — that is the
|
||||||
the newest `YYYYMMDD-HHMMSS` prefix, which is the canonical "most recent."
|
newest `YYYYMMDD-HHMMSS` checkpoint for the current branch, or, if the current
|
||||||
|
branch has none, the newest across all branches.
|
||||||
|
|
||||||
Read the chosen file and present a summary:
|
Read the chosen file and present a summary:
|
||||||
|
|
||||||
|
|
@ -144,9 +176,10 @@ state, then `/context-restore` will find it."
|
||||||
## Important Rules
|
## Important Rules
|
||||||
|
|
||||||
- **Never modify code.** This skill only reads saved files and presents them.
|
- **Never modify code.** This skill only reads saved files and presents them.
|
||||||
- **Always search across all branches by default.** Cross-branch resume is the
|
- **Prefer the current branch's own checkpoint, but keep all branches in the
|
||||||
whole point. Only filter by branch if the user explicitly asks via a
|
fallback set.** Cross-branch resume (Conductor handoff) still works when the
|
||||||
title-fragment match that happens to be branch-specific.
|
current branch has no checkpoint; it just no longer lets a sibling worktree's
|
||||||
|
newer save shadow this branch's own.
|
||||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||||
operations; mtime is not.
|
operations; mtime is not.
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
},
|
},
|
||||||
"context-restore": {
|
"context-restore": {
|
||||||
"lead": "Restore working context saved earlier by /context-save.",
|
"lead": "Restore working context saved earlier by /context-save.",
|
||||||
"routing": "Loads the most recent\nsaved state (across all branches by default) so you can pick up where you\nleft off — even across Conductor workspace handoffs.\nUse when asked to \"resume\", \"restore context\", \"where was I\", or\n\"pick up where I left off\". Pair with /context-save.\nFormerly /checkpoint resume — renamed because Claude Code treats /checkpoint\nas a native rewind alias in current environments.",
|
"routing": "Loads the most recent\nsaved state (preferring the current branch, falling back across branches) so\nyou can pick up where you left off — even across Conductor workspace handoffs.\nUse when asked to \"resume\", \"restore context\", \"where was I\", or\n\"pick up where I left off\". Pair with /context-save.\nFormerly /checkpoint resume — renamed because Claude Code treats /checkpoint\nas a native rewind alias in current environments.",
|
||||||
"voice_line": null
|
"voice_line": null
|
||||||
},
|
},
|
||||||
"context-save": {
|
"context-save": {
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,33 @@ echo "TITLE_SLUG=$TITLE_SLUG"
|
||||||
echo "FILE=$FILE"
|
echo "FILE=$FILE"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The exact find + sort + head used by context-restore/SKILL.md Step 1.
|
// The exact selection used by context-restore/SKILL.md Step 1: scan newest 200,
|
||||||
|
// order current-branch checkpoints first (fallback: all branches), cap at 20.
|
||||||
|
// CURRENT_BRANCH is injected via env in tests; the skill resolves it from git.
|
||||||
const RESTORE_FIND_BASH = `
|
const RESTORE_FIND_BASH = `
|
||||||
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
if [ ! -d "$CHECKPOINT_DIR" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
FILES=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -20)
|
ALL=$(find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | sort -r | head -200)
|
||||||
if [ -z "$FILES" ]; then
|
if [ -z "$ALL" ]; then
|
||||||
echo "NO_CHECKPOINTS"
|
echo "NO_CHECKPOINTS"
|
||||||
else
|
else
|
||||||
|
: "\${CURRENT_BRANCH:=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
|
||||||
|
SAME=""; OTHER=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
b=$(grep -m1 '^branch:' "$f" 2>/dev/null | sed 's/^branch:[[:space:]]*//')
|
||||||
|
if [ -n "$CURRENT_BRANCH" ] && [ "$b" = "$CURRENT_BRANCH" ]; then
|
||||||
|
SAME="\${SAME}\${f}
|
||||||
|
"
|
||||||
|
else
|
||||||
|
OTHER="\${OTHER}\${f}
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
$ALL
|
||||||
|
EOF
|
||||||
|
FILES=$(printf '%s%s' "$SAME" "$OTHER" | grep -v '^[[:space:]]*$' | head -20)
|
||||||
echo "$FILES"
|
echo "$FILES"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -312,6 +330,73 @@ describe('context-restore: find + sort + head cap', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Current-branch preference (#2052) ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// All worktrees of a repo share one origin-derived slug → one checkpoints dir.
|
||||||
|
// Restore must prefer the CURRENT branch's own checkpoint so a sibling
|
||||||
|
// worktree's newer save can't shadow it, while still falling back across
|
||||||
|
// branches (Conductor handoff) when the current branch has none.
|
||||||
|
|
||||||
|
describe('context-restore: current-branch preference (#2052)', () => {
|
||||||
|
let tmp: string;
|
||||||
|
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-branch-')); });
|
||||||
|
afterEach(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} });
|
||||||
|
|
||||||
|
function writeCheckpoint(ts: string, branch: string | null): string {
|
||||||
|
const file = `${tmp}/${ts}-work.md`;
|
||||||
|
const fm = branch === null
|
||||||
|
? `---\nstatus: in-progress\n---\n`
|
||||||
|
: `---\nstatus: in-progress\nbranch: ${branch}\n---\n`;
|
||||||
|
fs.writeFileSync(file, fm);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstCandidate(currentBranch?: string): string {
|
||||||
|
const env: Record<string, string> = { CHECKPOINT_DIR: tmp };
|
||||||
|
if (currentBranch !== undefined) env.CURRENT_BRANCH = currentBranch;
|
||||||
|
const out = runBash(RESTORE_FIND_BASH, env).stdout;
|
||||||
|
return out.trim().split('\n').filter(Boolean)[0] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the bug: current-branch save is NOT shadowed by a newer sibling-worktree save', () => {
|
||||||
|
const mine = writeCheckpoint('20260101-120000', 'feature-a'); // older, my branch
|
||||||
|
writeCheckpoint('20260619-120000', 'feature-b'); // newer, sibling worktree
|
||||||
|
// On feature-a, restore must load feature-a's own (older) checkpoint.
|
||||||
|
expect(firstCandidate('feature-a')).toBe(mine);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fallback: current branch has no checkpoint → newest across all branches (Conductor handoff)', () => {
|
||||||
|
writeCheckpoint('20260101-120000', 'feature-a');
|
||||||
|
const newest = writeCheckpoint('20260619-120000', 'feature-b');
|
||||||
|
// On feature-c (no own checkpoint), cross-branch resume still works.
|
||||||
|
expect(firstCandidate('feature-c')).toBe(newest);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('back-compat: empty current branch (non-git) → newest across all', () => {
|
||||||
|
writeCheckpoint('20260101-120000', 'feature-a');
|
||||||
|
const newest = writeCheckpoint('20260619-120000', 'feature-b');
|
||||||
|
expect(firstCandidate('')).toBe(newest);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checkpoints without a branch frontmatter still rank as fallback, never lost', () => {
|
||||||
|
const mine = writeCheckpoint('20260101-120000', 'feature-a');
|
||||||
|
writeCheckpoint('20260301-120000', null); // legacy save, no branch field
|
||||||
|
const out = runBash(RESTORE_FIND_BASH, { CHECKPOINT_DIR: tmp, CURRENT_BRANCH: 'feature-a' }).stdout;
|
||||||
|
const lines = out.trim().split('\n').filter(Boolean);
|
||||||
|
expect(lines[0]).toBe(mine); // current branch first
|
||||||
|
expect(lines.length).toBe(2); // legacy file is still present
|
||||||
|
});
|
||||||
|
|
||||||
|
test('within the current branch, ordering stays newest-first', () => {
|
||||||
|
const older = writeCheckpoint('20260101-120000', 'feature-a');
|
||||||
|
const newer = writeCheckpoint('20260619-120000', 'feature-a');
|
||||||
|
const out = runBash(RESTORE_FIND_BASH, { CHECKPOINT_DIR: tmp, CURRENT_BRANCH: 'feature-a' }).stdout;
|
||||||
|
const lines = out.trim().split('\n').filter(Boolean);
|
||||||
|
expect(lines[0]).toBe(newer);
|
||||||
|
expect(lines[1]).toBe(older);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Migration HOME guard ──────────────────────────────────────────────────
|
// ─── Migration HOME guard ──────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('migration v1.1.3.0: HOME guard', () => {
|
describe('migration v1.1.3.0: HOME guard', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue