// https://code.claude.com/docs/en/hooks#cwdchanged const { readStdin } = vi.hoisted(() => ({ readStdin: vi.fn<() => Promise>(), })); vi.mock('./utils/stdin.ts', () => ({ readStdin })); const { getRepoRoot } = vi.hoisted(() => ({ getRepoRoot: vi.fn<(dir?: string) => Promise>(), })); vi.mock('./utils/git.ts', () => ({ getRepoRoot, getChangedFiles: vi.fn() })); const { provision } = vi.hoisted(() => ({ provision: vi.fn() })); vi.mock('./utils/provision.ts', () => ({ provision })); const { access } = vi.hoisted(() => ({ access: vi.fn<(path: string) => Promise>(), })); vi.mock('node:fs/promises', () => ({ access })); const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); beforeEach(() => { vi.resetModules(); }); it('exits with code 1 when stdin is not valid JSON', async () => { readStdin.mockResolvedValue('not json'); await expect(import('./cwdchanged-check.ts')).rejects.toThrow('process.exit'); expect(exitSpy).toHaveBeenCalledWith(1); expect(provision).not.toHaveBeenCalled(); }); it('exits with code 1 when hook_event_name is wrong', async () => { readStdin.mockResolvedValue( JSON.stringify({ session_id: 'x', transcript_path: 'x', cwd: '/some/dir', hook_event_name: 'SessionStart', }), ); await expect(import('./cwdchanged-check.ts')).rejects.toThrow('process.exit'); expect(exitSpy).toHaveBeenCalledWith(1); expect(provision).not.toHaveBeenCalled(); }); it('exits silently when directory is not a git repo (getRepoRoot returns null)', async () => { readStdin.mockResolvedValue( JSON.stringify({ session_id: 'x', transcript_path: 'x', cwd: '/not/a/repo', hook_event_name: 'CwdChanged', }), ); getRepoRoot.mockResolvedValue(null); await expect(import('./cwdchanged-check.ts')).rejects.toThrow('process.exit'); expect(exitSpy).toHaveBeenCalledWith(0); expect(provision).not.toHaveBeenCalled(); }); it('exits silently when node_modules already exists (already installed checkout)', async () => { readStdin.mockResolvedValue( JSON.stringify({ session_id: 'x', transcript_path: 'x', cwd: '/repo/sub', hook_event_name: 'CwdChanged', }), ); getRepoRoot.mockResolvedValue('/repo'); access.mockResolvedValue(undefined); await expect(import('./cwdchanged-check.ts')).rejects.toThrow('process.exit'); expect(exitSpy).toHaveBeenCalledWith(0); expect(access).toHaveBeenCalledWith('/repo/node_modules'); expect(provision).not.toHaveBeenCalled(); }); it('provisions the root when it is a fresh worktree (no node_modules)', async () => { readStdin.mockResolvedValue( JSON.stringify({ session_id: 'x', transcript_path: 'x', cwd: '/worktrees/my-feature', hook_event_name: 'CwdChanged', }), ); getRepoRoot.mockResolvedValue('/worktrees/my-feature'); access.mockRejectedValue(new Error('ENOENT')); provision.mockResolvedValue(undefined); await import('./cwdchanged-check.ts'); expect(getRepoRoot).toHaveBeenCalledWith('/worktrees/my-feature'); expect(access).toHaveBeenCalledWith('/worktrees/my-feature/node_modules'); expect(provision).toHaveBeenCalledWith('/worktrees/my-feature'); expect(exitSpy).not.toHaveBeenCalled(); });