mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
fix(context): record the save commit and flag work that landed after it
/context-save recorded the branch, a timestamp and the modified files, but not the commit, so /context-restore could not tell that work landed after the save. A checkpoint saved before a step was committed came back with that step still listed as the plan. - gstack-slug --stamp-checkpoint also writes head:, the commit the save describes, next to remote:/project_root:/worktree:. - context-restore compares head: with HEAD before presenting. When commits landed after the save it prints how many and lists them, and moves Remaining Work under Verify first; a saved commit that left the branch history is reported the same way. Checkpoints saved before this keep their old sorting with a one-line note. - test/context-restore-head.test.ts runs the real save-path, stamp and head-check blocks in fixture repos: unchanged, moved, diverged after a reset, pre-tracking, and a malformed head: that must never execute.
This commit is contained in:
1 parent
00100f1598
commit
d540b102ac
6 files changed
+266
-10
No files matched your search
+7
-4
@@ -8,7 +8,7 @@
|
||||
# Code sessions refuse eval/source)
|
||||
# Modes (not on the hot path; see `gstack-slug --help`):
|
||||
# --identity also print PROJECT_REMOTE, PROJECT_ROOT, LEGACY_SLUG
|
||||
# --stamp-checkpoint <file> write remote:/project_root: into a checkpoint's frontmatter
|
||||
# --stamp-checkpoint <file> write remote:/project_root:/head: into a checkpoint's frontmatter
|
||||
# --classify-checkpoints read checkpoint paths on stdin, print "<status>\t<path>"
|
||||
# --checkpoints-below list other buckets' checkpoints saved below the cwd, plus pointers
|
||||
# --point-from <dir> <file> leave a pointer to <file> in <dir>'s bucket (nested-repo saves)
|
||||
@@ -138,7 +138,7 @@ gstack-slug — print SLUG=<project slug> and BRANCH=<branch> for the current di
|
||||
field); no eval needed
|
||||
gstack-slug --identity also print PROJECT_REMOTE (canonical, credential-free),
|
||||
PROJECT_ROOT and LEGACY_SLUG (pre-#3003 bucket, if different)
|
||||
gstack-slug --stamp-checkpoint FILE write remote:/project_root: into FILE's frontmatter
|
||||
gstack-slug --stamp-checkpoint FILE write remote:/project_root:/head: into FILE's frontmatter
|
||||
gstack-slug --classify-checkpoints read checkpoint paths on stdin; print "<status><TAB><path>",
|
||||
status = match | match-root | foreign | unstamped
|
||||
gstack-slug --checkpoints-below outside git: other buckets' checkpoints whose project_root
|
||||
@@ -682,15 +682,18 @@ case "$MODE" in
|
||||
STAMP_TMP=$(mktemp "${MODE_ARG}.stamp-XXXXXX") || exit 1
|
||||
STAMP_WORKTREE=$(_git_toplevel "$PROJECT_DIR")
|
||||
[[ "$STAMP_WORKTREE" != "$ID_ROOT" ]] || STAMP_WORKTREE=""
|
||||
if GRI_REMOTE="$ID_REMOTE" GRI_ROOT="$ID_ROOT" GRI_WORKTREE="$STAMP_WORKTREE" awk '
|
||||
# The commit the save describes, so /context-restore can tell when work landed after it.
|
||||
STAMP_HEAD=$(git -C "$PROJECT_DIR" rev-parse --verify -q HEAD 2>/dev/null) || STAMP_HEAD=""
|
||||
if GRI_REMOTE="$ID_REMOTE" GRI_ROOT="$ID_ROOT" GRI_WORKTREE="$STAMP_WORKTREE" GRI_HEAD="$STAMP_HEAD" awk '
|
||||
function ids() {
|
||||
if (ENVIRON["GRI_REMOTE"] != "") print "remote: " ENVIRON["GRI_REMOTE"]
|
||||
print "project_root: " ENVIRON["GRI_ROOT"]
|
||||
if (ENVIRON["GRI_WORKTREE"] != "") print "worktree: " ENVIRON["GRI_WORKTREE"]
|
||||
if (ENVIRON["GRI_HEAD"] != "") print "head: " ENVIRON["GRI_HEAD"]
|
||||
}
|
||||
NR == 1 { if ($0 == "---") { infm = 1; print; next } print "---"; ids(); print "---" }
|
||||
infm && $0 == "---" { ids(); print; infm = 0; next }
|
||||
infm && (/^remote:/ || /^project_root:/ || /^worktree:/) { next }
|
||||
infm && (/^remote:/ || /^project_root:/ || /^worktree:/ || /^head:/) { next }
|
||||
{ print }
|
||||
END { if (NR == 0) { print "---"; ids(); print "---" } }' "$MODE_ARG" > "$STAMP_TMP"; then
|
||||
mv "$STAMP_TMP" "$MODE_ARG"
|
||||
|
||||
@@ -527,6 +527,44 @@ checkpoint(s) from another project; only this project's are listed." If it print
|
||||
`LEGACY_BUCKET`, relay that line. If the chosen file is `ROOT_DIFFERS`, add
|
||||
"Saved from another checkout of this repository at `{project_root}`." as info.
|
||||
|
||||
**Check whether the branch moved since the save.** The stamp records the commit
|
||||
the save was made at in `head:`. Before presenting, compare it with this
|
||||
checkout:
|
||||
|
||||
```bash
|
||||
FILE="<the chosen file>"
|
||||
SAVED_HEAD=$(grep -m1 '^head:' "$FILE" 2>/dev/null | sed 's/^head:[[:space:]]*//' | tr -d '\r')
|
||||
REPO_DIR=.
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(grep -m1 '^project_root:' "$FILE" 2>/dev/null | sed 's/^project_root:[[:space:]]*//' | tr -d '\r')
|
||||
CURRENT_HEAD=$(git -C "${REPO_DIR:-.}" rev-parse --verify -q HEAD 2>/dev/null)
|
||||
if [ -z "$SAVED_HEAD" ]; then
|
||||
echo "HEAD_UNTRACKED"
|
||||
elif [ -z "$CURRENT_HEAD" ]; then
|
||||
echo "HEAD_NOT_CHECKED"
|
||||
elif [ "$SAVED_HEAD" = "$CURRENT_HEAD" ]; then
|
||||
echo "HEAD_UNCHANGED"
|
||||
elif ! printf '%s\n' "$SAVED_HEAD" | grep -Eqx '[0-9a-f]{40}([0-9a-f]{24})?' \
|
||||
|| ! git -C "$REPO_DIR" cat-file -e "$SAVED_HEAD^{commit}" 2>/dev/null; then
|
||||
echo "HEAD_UNKNOWN $SAVED_HEAD"
|
||||
elif git -C "$REPO_DIR" merge-base --is-ancestor "$SAVED_HEAD" "$CURRENT_HEAD"; then
|
||||
echo "HEAD_MOVED $(git -C "$REPO_DIR" rev-list --count "$SAVED_HEAD..$CURRENT_HEAD")"
|
||||
git -C "$REPO_DIR" log --oneline -10 "$SAVED_HEAD..$CURRENT_HEAD"
|
||||
else
|
||||
echo "HEAD_DIVERGED $SAVED_HEAD"
|
||||
fi
|
||||
```
|
||||
|
||||
- `HEAD_UNCHANGED` or `HEAD_NOT_CHECKED`: nothing to add.
|
||||
- `HEAD_MOVED <n>`: print `<n> commit(s) landed since this save:` and the commit
|
||||
lines above the Remaining Work groups, and list **every** Remaining Work item
|
||||
under **Verify first**: the saving session could not know about those commits,
|
||||
so any step may already be done.
|
||||
- `HEAD_DIVERGED` or `HEAD_UNKNOWN`: print `This checkpoint was saved at {first 7
|
||||
characters of head}, which is not in this checkout's history (rebased, reset, or
|
||||
not fetched).` and list every item under Verify first.
|
||||
- `HEAD_UNTRACKED`: print `This checkpoint predates commit tracking; restore cannot tell whether work landed after it.`
|
||||
and sort as usual.
|
||||
|
||||
**Sort Remaining Work by provenance.** Keep every item's original text and saved
|
||||
order, and drop nothing. Put an item under **Verify first** when it:
|
||||
- ends in `(path assumed)` or `(code read)`;
|
||||
@@ -563,6 +601,7 @@ Status: {status}
|
||||
{summary from saved file}
|
||||
|
||||
### Remaining Work
|
||||
{commit-tracking lines, if any}
|
||||
{legacy banner line, if it applies}
|
||||
|
||||
Next steps
|
||||
@@ -616,6 +655,8 @@ in the old bucket until the user copies them with `gstack-slug --adopt-legacy`.
|
||||
- **Never present another project's checkpoint as "latest".** Identity is the
|
||||
canonical remote (the root only when there is no remote); a mismatch is
|
||||
reported before any summary.
|
||||
- **A checkpoint describes the commit it was saved at.** When commits landed
|
||||
after the save, every Remaining Work item is verified before it is run.
|
||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||
operations; mtime is not.
|
||||
|
||||
@@ -203,6 +203,44 @@ checkpoint(s) from another project; only this project's are listed." If it print
|
||||
`LEGACY_BUCKET`, relay that line. If the chosen file is `ROOT_DIFFERS`, add
|
||||
"Saved from another checkout of this repository at `{project_root}`." as info.
|
||||
|
||||
**Check whether the branch moved since the save.** The stamp records the commit
|
||||
the save was made at in `head:`. Before presenting, compare it with this
|
||||
checkout:
|
||||
|
||||
```bash
|
||||
FILE="<the chosen file>"
|
||||
SAVED_HEAD=$(grep -m1 '^head:' "$FILE" 2>/dev/null | sed 's/^head:[[:space:]]*//' | tr -d '\r')
|
||||
REPO_DIR=.
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || REPO_DIR=$(grep -m1 '^project_root:' "$FILE" 2>/dev/null | sed 's/^project_root:[[:space:]]*//' | tr -d '\r')
|
||||
CURRENT_HEAD=$(git -C "${REPO_DIR:-.}" rev-parse --verify -q HEAD 2>/dev/null)
|
||||
if [ -z "$SAVED_HEAD" ]; then
|
||||
echo "HEAD_UNTRACKED"
|
||||
elif [ -z "$CURRENT_HEAD" ]; then
|
||||
echo "HEAD_NOT_CHECKED"
|
||||
elif [ "$SAVED_HEAD" = "$CURRENT_HEAD" ]; then
|
||||
echo "HEAD_UNCHANGED"
|
||||
elif ! printf '%s\n' "$SAVED_HEAD" | grep -Eqx '[0-9a-f]{40}([0-9a-f]{24})?' \
|
||||
|| ! git -C "$REPO_DIR" cat-file -e "$SAVED_HEAD^{commit}" 2>/dev/null; then
|
||||
echo "HEAD_UNKNOWN $SAVED_HEAD"
|
||||
elif git -C "$REPO_DIR" merge-base --is-ancestor "$SAVED_HEAD" "$CURRENT_HEAD"; then
|
||||
echo "HEAD_MOVED $(git -C "$REPO_DIR" rev-list --count "$SAVED_HEAD..$CURRENT_HEAD")"
|
||||
git -C "$REPO_DIR" log --oneline -10 "$SAVED_HEAD..$CURRENT_HEAD"
|
||||
else
|
||||
echo "HEAD_DIVERGED $SAVED_HEAD"
|
||||
fi
|
||||
```
|
||||
|
||||
- `HEAD_UNCHANGED` or `HEAD_NOT_CHECKED`: nothing to add.
|
||||
- `HEAD_MOVED <n>`: print `<n> commit(s) landed since this save:` and the commit
|
||||
lines above the Remaining Work groups, and list **every** Remaining Work item
|
||||
under **Verify first**: the saving session could not know about those commits,
|
||||
so any step may already be done.
|
||||
- `HEAD_DIVERGED` or `HEAD_UNKNOWN`: print `This checkpoint was saved at {first 7
|
||||
characters of head}, which is not in this checkout's history (rebased, reset, or
|
||||
not fetched).` and list every item under Verify first.
|
||||
- `HEAD_UNTRACKED`: print `This checkpoint predates commit tracking; restore cannot tell whether work landed after it.`
|
||||
and sort as usual.
|
||||
|
||||
**Sort Remaining Work by provenance.** Keep every item's original text and saved
|
||||
order, and drop nothing. Put an item under **Verify first** when it:
|
||||
- ends in `(path assumed)` or `(code read)`;
|
||||
@@ -239,6 +277,7 @@ Status: {status}
|
||||
{summary from saved file}
|
||||
|
||||
### Remaining Work
|
||||
{commit-tracking lines, if any}
|
||||
{legacy banner line, if it applies}
|
||||
|
||||
Next steps
|
||||
@@ -292,6 +331,8 @@ in the old bucket until the user copies them with `gstack-slug --adopt-legacy`.
|
||||
- **Never present another project's checkpoint as "latest".** Identity is the
|
||||
canonical remote (the root only when there is no remote); a mismatch is
|
||||
reported before any summary.
|
||||
- **A checkpoint describes the commit it was saved at.** When commits landed
|
||||
after the save, every Remaining Work item is verified before it is run.
|
||||
- **"Most recent" means the filename `YYYYMMDD-HHMMSS` prefix**, not
|
||||
`ls -1t` (filesystem mtime). Filenames are stable across file-system
|
||||
operations; mtime is not.
|
||||
|
||||
@@ -542,12 +542,13 @@ files_modified:
|
||||
|
||||
The `files_modified` list comes from `git status --short` (both staged and unstaged
|
||||
modified files). Use relative paths from the repo root. Do not write `remote:`,
|
||||
`project_root:` or `worktree:` yourself; the stamp step below adds them.
|
||||
`project_root:`, `worktree:` or `head:` yourself; the stamp step below adds them.
|
||||
|
||||
Then stamp the project identity. Several projects can share one checkpoints
|
||||
directory, so `/context-restore` needs to know which project wrote each file. The helper adds `remote:` (canonical origin:
|
||||
lowercased host, no credentials, port or `.git`), `project_root:` and, in a
|
||||
linked worktree, `worktree:` to the frontmatter. When the session started in
|
||||
lowercased host, no credentials, port or `.git`), `project_root:`, in a linked
|
||||
worktree `worktree:`, and `head:` (the commit this save describes, so
|
||||
`/context-restore` can tell when work landed after it) to the frontmatter. When the session started in
|
||||
another directory that files elsewhere (a non-git parent of this repo), it also
|
||||
leaves a pointer there so `/context-restore` in that directory finds this save:
|
||||
|
||||
|
||||
@@ -217,12 +217,13 @@ files_modified:
|
||||
|
||||
The `files_modified` list comes from `git status --short` (both staged and unstaged
|
||||
modified files). Use relative paths from the repo root. Do not write `remote:`,
|
||||
`project_root:` or `worktree:` yourself; the stamp step below adds them.
|
||||
`project_root:`, `worktree:` or `head:` yourself; the stamp step below adds them.
|
||||
|
||||
Then stamp the project identity. Several projects can share one checkpoints
|
||||
directory, so `/context-restore` needs to know which project wrote each file. The helper adds `remote:` (canonical origin:
|
||||
lowercased host, no credentials, port or `.git`), `project_root:` and, in a
|
||||
linked worktree, `worktree:` to the frontmatter. When the session started in
|
||||
lowercased host, no credentials, port or `.git`), `project_root:`, in a linked
|
||||
worktree `worktree:`, and `head:` (the commit this save describes, so
|
||||
`/context-restore` can tell when work landed after it) to the frontmatter. When the session started in
|
||||
another directory that files elsewhere (a non-git parent of this repo), it also
|
||||
leaves a pointer there so `/context-restore` in that directory finds this save:
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* /context-save records the commit a checkpoint describes, and /context-restore
|
||||
* notices when commits landed after it, end to end.
|
||||
*
|
||||
* Runs the REAL save-path, stamp and head-check bash blocks extracted from the
|
||||
* generated context-save and context-restore SKILL.md files (with
|
||||
* ~/.claude/skills/gstack pointed at this checkout) inside fixture repos. The bug
|
||||
* these pin: a checkpoint saved before work landed came back as the current plan,
|
||||
* with nothing saying HEAD had moved, so restore offered to redo finished steps.
|
||||
*
|
||||
* Value: protects=save stamps head: and restore reports unchanged, moved, diverged, untracked and unknown commits; fails_when=the stamp drops head: or restore stops comparing it with HEAD; why_new=no test compares a checkpoint with commits made after it; seam=none; no_claim=fixture repos and a hand-written checkpoint body stand in for a live /context-save, so green does not prove the agent follows the prompt
|
||||
*/
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
// Forward slashes keep the path intact inside bash on Windows (Git Bash); a no-op elsewhere.
|
||||
const BASH_ROOT = ROOT.split(path.sep).join('/');
|
||||
|
||||
function bashBlockAfter(skillFile: string, heading: string, contains?: string): string {
|
||||
const md = fs.readFileSync(path.join(ROOT, skillFile), 'utf-8');
|
||||
const start = md.indexOf(heading);
|
||||
if (start < 0) throw new Error(`${skillFile}: heading not found: ${heading}`);
|
||||
const re = /```bash\n([\s\S]*?)```/g;
|
||||
re.lastIndex = start;
|
||||
for (let m = re.exec(md); m; m = re.exec(md)) {
|
||||
if (!contains || m[1].includes(contains)) return m[1].replaceAll('~/.claude/skills/gstack', BASH_ROOT);
|
||||
}
|
||||
throw new Error(`${skillFile}: no bash block after ${heading}`);
|
||||
}
|
||||
|
||||
const SAVE_PATH_BLOCK = bashBlockAfter('context-save/SKILL.md', '### Step 4: Write saved-context file').replace(
|
||||
'TIMESTAMP=$(date +%Y%m%d-%H%M%S)',
|
||||
'TIMESTAMP="$TEST_TIMESTAMP"',
|
||||
);
|
||||
const STAMP_BLOCK = bashBlockAfter('context-save/SKILL.md', '### Step 4: Write saved-context file', '--stamp-checkpoint');
|
||||
const HEAD_BLOCK = bashBlockAfter('context-restore/SKILL.md', '**Check whether the branch moved since the save.**', 'SAVED_HEAD');
|
||||
|
||||
let tmp: string;
|
||||
let home: string;
|
||||
|
||||
function env(extra: Record<string, string> = {}): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) out[k] = v;
|
||||
delete out.GSTACK_PROJECT_SLUG;
|
||||
delete out.GSTACK_STATE_ROOT;
|
||||
delete out.GSTACK_STATE_DIR;
|
||||
delete out.CURRENT_BRANCH;
|
||||
delete out.CLAUDE_PROJECT_DIR;
|
||||
return { ...out, HOME: tmp, GSTACK_HOME: home, ...extra };
|
||||
}
|
||||
|
||||
function sh(script: string, cwd: string, extra: Record<string, string> = {}) {
|
||||
const r = spawnSync('bash', ['-c', script], { cwd, env: env(extra), encoding: 'utf-8', timeout: 30_000 });
|
||||
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', status: r.status ?? -1 };
|
||||
}
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 });
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
|
||||
return r.stdout.trim();
|
||||
}
|
||||
|
||||
function commit(cwd: string, subject: string) {
|
||||
fs.appendFileSync(path.join(cwd, 'app.txt'), `${subject}\n`);
|
||||
git(cwd, 'add', 'app.txt');
|
||||
git(cwd, 'commit', '-q', '-m', subject);
|
||||
}
|
||||
|
||||
function repo(): string {
|
||||
const dir = path.join(tmp, 'app');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
git(dir, 'init', '-q', '-b', 'main');
|
||||
git(dir, 'remote', 'add', 'origin', 'https://github.com/example/head-check.git');
|
||||
git(dir, 'config', 'user.name', 'gstack test');
|
||||
git(dir, 'config', 'user.email', 'gstack-test@example.invalid');
|
||||
commit(dir, 'baseline');
|
||||
return dir;
|
||||
}
|
||||
|
||||
/** The context-save flow: path block, the agent's Write, then the stamp block. */
|
||||
function save(cwd: string): string {
|
||||
const paths = sh(SAVE_PATH_BLOCK, cwd, { TITLE_RAW: 'login flow', TEST_TIMESTAMP: '20261020-090000' });
|
||||
expect(paths.status).toBe(0);
|
||||
const file = paths.stdout.match(/^FILE=(.+)$/m)![1];
|
||||
fs.writeFileSync(file, '---\nstatus: in-progress\nbranch: main\n---\n\n## Working on: login flow\n');
|
||||
const stamp = sh(STAMP_BLOCK.replace('FILE="<the FILE path printed above>"', `FILE=${JSON.stringify(file)}`), cwd);
|
||||
expect(stamp.stdout).toContain('STAMPED');
|
||||
return file;
|
||||
}
|
||||
|
||||
function headCheck(cwd: string, file: string) {
|
||||
const r = sh(HEAD_BLOCK.replace('FILE="<the chosen file>"', `FILE=${JSON.stringify(file)}`), cwd);
|
||||
expect(r.status).toBe(0);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-head-')));
|
||||
home = path.join(tmp, '.gstack');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('context-save records the commit a checkpoint describes', () => {
|
||||
test('the stamp writes head: once, and re-stamping does not duplicate it', () => {
|
||||
const app = repo();
|
||||
const file = save(app);
|
||||
const fm = fs.readFileSync(file, 'utf-8').split('---')[1];
|
||||
expect(fm).toContain(`\nhead: ${git(app, 'rev-parse', 'HEAD')}\n`);
|
||||
spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-slug'), '--stamp-checkpoint', file], { cwd: app, env: env(), timeout: 30_000 });
|
||||
expect(fs.readFileSync(file, 'utf-8').split('---')[1].match(/^head:/gm)?.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context-restore compares the checkpoint with HEAD', () => {
|
||||
test('nothing to add when no commit landed after the save', () => {
|
||||
const app = repo();
|
||||
expect(headCheck(app, save(app)).trim()).toBe('HEAD_UNCHANGED');
|
||||
});
|
||||
|
||||
test('commits that landed after the save are counted and listed', () => {
|
||||
const app = repo();
|
||||
const file = save(app);
|
||||
commit(app, 'add the login route');
|
||||
commit(app, 'add login tests');
|
||||
const out = headCheck(app, file);
|
||||
expect(out).toStartWith('HEAD_MOVED 2\n');
|
||||
expect(out).toContain('add the login route');
|
||||
expect(out).toContain('add login tests');
|
||||
});
|
||||
|
||||
test('a checkpoint whose commit left the branch history is reported, not trusted', () => {
|
||||
const app = repo();
|
||||
commit(app, 'work in progress');
|
||||
const file = save(app);
|
||||
git(app, 'reset', '-q', '--hard', 'HEAD~1');
|
||||
commit(app, 'rewritten work');
|
||||
expect(headCheck(app, file)).toStartWith('HEAD_DIVERGED ');
|
||||
});
|
||||
|
||||
test('checkpoints saved before head tracking, or with a malformed head, are never treated as current', () => {
|
||||
const app = repo();
|
||||
const legacy = path.join(tmp, 'legacy.md');
|
||||
fs.writeFileSync(legacy, '---\nstatus: in-progress\nbranch: main\n---\n');
|
||||
expect(headCheck(app, legacy).trim()).toBe('HEAD_UNTRACKED');
|
||||
const bogus = path.join(tmp, 'bogus.md');
|
||||
fs.writeFileSync(bogus, '---\nbranch: main\nhead: $(touch pwned)\n---\n');
|
||||
expect(headCheck(app, bogus)).toStartWith('HEAD_UNKNOWN ');
|
||||
expect(fs.existsSync(path.join(app, 'pwned'))).toBe(false);
|
||||
});
|
||||
|
||||
test('the prompt maps every status to what the user sees', () => {
|
||||
const restore = fs.readFileSync(path.join(ROOT, 'context-restore/SKILL.md'), 'utf-8');
|
||||
for (const status of ['HEAD_UNCHANGED', 'HEAD_NOT_CHECKED', 'HEAD_MOVED <n>', 'HEAD_DIVERGED', 'HEAD_UNKNOWN', 'HEAD_UNTRACKED']) {
|
||||
expect(restore).toContain(`\`${status}\``);
|
||||
}
|
||||
expect(restore).toContain('`<n> commit(s) landed since this save:`');
|
||||
expect(restore).toContain('under **Verify first**');
|
||||
expect(restore).toContain('{commit-tracking lines, if any}');
|
||||
const save = fs.readFileSync(path.join(ROOT, 'context-save/SKILL.md'), 'utf-8');
|
||||
expect(save).toContain('`project_root:`, `worktree:` or `head:` yourself');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user