diff --git a/bin/gstack-harness-ledger.sh b/bin/gstack-harness-ledger.sh index 115dd551c..85a6d6c47 100644 --- a/bin/gstack-harness-ledger.sh +++ b/bin/gstack-harness-ledger.sh @@ -26,6 +26,7 @@ _gstack_harness_sha() { # True when $1 is below $2 and no path from $2 down to $1 is a symlink. _gstack_harness_owned() { local p="$1" dest="$2" + [ -n "$dest" ] || return 1 case "$p" in "$dest"/*) ;; *) return 1 ;; esac while [ "$p" != "$dest" ]; do [ -L "$p" ] && return 1; p="$(dirname "$p")"; done [ ! -L "$dest" ] diff --git a/test/harness-register.test.ts b/test/harness-register.test.ts index 9a0df7250..faebb0492 100644 --- a/test/harness-register.test.ts +++ b/test/harness-register.test.ts @@ -233,6 +233,18 @@ describe('bin/gstack-harness-ledger.sh (the bun-free reader gstack-uninstall sou expect(fs.existsSync(path.join(d, 'skills'))).toBe(false); expect(listLedgers(path.join(d, 'state'))).toEqual([]); }, 60_000); + + test('a ledger with no destination row touches nothing and still finishes', () => { + const d = tmp(); + const victim = path.join(d, 'mine.md'); + fs.writeFileSync(victim, 'x'); + const ledger = path.join(d, 'state', 'harness', 'broken.ledger'); + fs.mkdirSync(path.dirname(ledger), { recursive: true }); + fs.writeFileSync(ledger, 'meta\ttarget\tdots\nfile\t' + require('node:crypto').createHash('sha256').update('x').digest('hex') + '\t' + victim + '\n'); + const r = spawnSync('bash', ['-c', '. "$0" && gstack_harness_unregister_ledger "$1"', ledgerSh, ledger], { env: { PATH: '/usr/bin:/bin', GSTACK_STATE_ROOT: path.join(d, 'state') }, encoding: 'utf8', timeout: 10_000 }); + expect(r.status, r.stderr).toBe(0); + expect(fs.readFileSync(victim, 'utf8')).toBe('x'); + }, 30_000); }); describe('gstack-harness QM (org skill pack, against a fake admin API)', () => {