diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts index 55a2f14ab8a..deff26ad578 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts @@ -6,7 +6,11 @@ import { BIN_DIR } from "./paths"; export const WRAPPER_MARKER = "# Superset agent-wrapper v1"; export { SUPERSET_MANAGED_BINARIES }; -const SUPERSET_MANAGED_HOOK_PATH_PATTERN = /\/\.superset(?:-[^/'"\s\\]+)?\//; +// Dev setup (.superset/lib/setup/steps.sh) points SUPERSET_HOME_DIR at +// $PWD/superset-dev-data — without a leading dot — so we must recognize that +// variant to reap stale notify.sh paths from deleted worktrees. +const SUPERSET_MANAGED_HOOK_PATH_PATTERN = + /\/(?:\.superset(?:-[^/'"\s\\]+)?|superset-dev-data)\//; export function writeFileIfChanged( filePath: string, diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts index 9e96a3e4912..0d97253eb97 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts @@ -1193,6 +1193,71 @@ describe("agent-wrappers codex hooks.json", () => { ).toBe(true); }); + it("reaps stale notify.sh paths from in-repo dev worktrees", () => { + const codexHooksPath = path.join(mockedHomeDir, ".codex", "hooks.json"); + // Real-world layout: a dev worktree lives under /.worktrees/ + // and its dev setup writes SUPERSET_HOME_DIR=/superset-dev-data. + // There is no /.superset/ segment anywhere in the path. + const staleHookPath = + "/Users/test/code/superset/.worktrees/old-branch/superset-dev-data/hooks/notify.sh"; + const currentHookPath = "/tmp/.superset-new/hooks/notify.sh"; + + mkdirSync(path.dirname(codexHooksPath), { recursive: true }); + writeFileSync( + codexHooksPath, + JSON.stringify( + { + hooks: { + SessionStart: [ + { hooks: [{ type: "command", command: staleHookPath }] }, + ], + UserPromptSubmit: [ + { hooks: [{ type: "command", command: staleHookPath }] }, + ], + Stop: [ + { hooks: [{ type: "command", command: staleHookPath }] }, + ], + }, + }, + null, + 2, + ), + ); + + const content = getCodexGlobalHooksJsonContent(currentHookPath); + expect(content).not.toBeNull(); + if (content === null) throw new Error("Expected content"); + + const parsed = JSON.parse(content) as { + hooks: Record< + string, + Array<{ + matcher?: string; + hooks: Array<{ type: string; command: string }>; + }> + >; + }; + + for (const eventName of [ + "SessionStart", + "UserPromptSubmit", + "Stop", + ] as const) { + const hooks = parsed.hooks[eventName]; + expect(Array.isArray(hooks)).toBe(true); + expect( + hooks.some((def) => + def.hooks.some((hook) => hook.command === currentHookPath), + ), + ).toBe(true); + expect( + hooks.some((def) => + def.hooks.some((hook) => hook.command === staleHookPath), + ), + ).toBe(false); + } + }); + it("skips Codex hooks writes when existing JSON is invalid", () => { const codexHooksPath = path.join(mockedHomeDir, ".codex", "hooks.json"); const invalidJson = "{not-json";