fix(harness): a non-folder or empty plugin path is never taken as kept edits

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-01 06:38:44 +00:00
1 parent 375d7dd934
commit e265acabcd
2 files changed
+17 -2

No files matched your search

+6 -2
View File
@@ -158,8 +158,12 @@ export function inspectHook(t: HookTarget, profile: string, env: Env, home: stri
}
/** True when a plugin folder holds nothing but the .edited-<stamp> copies gstack kept when it turned the hook off. */
const onlyKeptEdits = (root: string) => fs.readdirSync(root, { recursive: true, withFileTypes: true })
.every(e => e.isDirectory() || (e.isFile() && /\.edited-[0-9T-]+Z$/.test(e.name)));
function onlyKeptEdits(root: string): boolean {
let entries: fs.Dirent[];
try { entries = fs.readdirSync(root, { recursive: true, withFileTypes: true }); } catch { return false; }
const files = entries.filter(e => !e.isDirectory());
return files.length > 0 && files.every(e => e.isFile() && /\.edited-[0-9T-]+Z$/.test(e.name));
}
/** Checks the plugin folder before anything is written: under the profile, no symlink from the profile down, and not someone else's. */
export function assertHookRoot(t: HookTarget, profile: string, root: string, recordedRoot: string | undefined, fail: (code: 'HARNESS_UNSAFE_DESTINATION' | 'HARNESS_HOOK_OWNED_ELSEWHERE', msg: string) => never): void {
+11
View File
@@ -169,6 +169,17 @@ describe('start hook: who decides', () => {
expect(r.calls().concat(s.calls())).toEqual([]);
});
test('a file, an empty folder or a folder with any non-kept file at the plugin path is still owned elsewhere', () => {
for (const make of [(root: string) => fs.writeFileSync(root, 'x'), (root: string) => fs.mkdirSync(path.join(root, 'sub'), { recursive: true }),
(root: string) => { fs.mkdirSync(root, { recursive: true }); fs.writeFileSync(path.join(root, 'note.txt.edited-2026-11-01T06-31-18-911Z'), 'kept'); fs.writeFileSync(path.join(root, 'plugin.yaml'), 'mine'); }]) {
const r = rig('hermes');
fs.mkdirSync(path.dirname(r.root), { recursive: true });
make(r.root);
expect(() => register(r.o({ startHook: true }))).toThrow(expect.objectContaining({ code: 'HARNESS_HOOK_OWNED_ELSEWHERE' }));
expect(register({ ...r.o(), env: { ...r.env, GSTACK_HARNESS_START_HOOK: '1' } }).hook?.state).toBe('off');
}
}, 300_000);
test('a profile path with a trailing slash under a symlinked parent still takes the hook and is saved without the slash', () => {
const r = rig('hermes');
const link = path.join(tmp(), 'ln');