diff --git a/packages/happy-app/sources/sync/ops.test.ts b/packages/happy-app/sources/sync/ops.test.ts new file mode 100644 index 000000000..33b77b834 --- /dev/null +++ b/packages/happy-app/sources/sync/ops.test.ts @@ -0,0 +1,61 @@ +/** + * Tests for session operations (ops.ts) + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { mockSessionRPC } = vi.hoisted(() => ({ + mockSessionRPC: vi.fn(), +})); + +vi.mock('./apiSocket', () => ({ + apiSocket: { + sessionRPC: mockSessionRPC, + } +})); + +vi.mock('./sync', () => ({ + sync: {} +})); + +import { sessionKill } from './ops'; + +describe('sessionKill', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return success when RPC succeeds', async () => { + mockSessionRPC.mockResolvedValue({ + success: true, + message: 'Killing happy-cli process' + }); + + const result = await sessionKill('test-session-123'); + + expect(result.success).toBe(true); + expect(mockSessionRPC).toHaveBeenCalledWith( + 'test-session-123', + 'killSession', + {} + ); + }); + + it('should return success when RPC fails because process already exited (regression #687)', async () => { + mockSessionRPC.mockRejectedValue(new Error('RPC call failed')); + + const result = await sessionKill('dead-session-456'); + + expect(result.success).toBe(true); + expect(result.message).toBe('Session already stopped'); + }); + + it('should return success even on non-Error RPC failures', async () => { + mockSessionRPC.mockRejectedValue('timeout'); + + const result = await sessionKill('dead-session-789'); + + expect(result.success).toBe(true); + expect(result.message).toBe('Session already stopped'); + }); +}); diff --git a/packages/happy-app/sources/sync/ops.ts b/packages/happy-app/sources/sync/ops.ts index 07f70e694..37bf03787 100644 --- a/packages/happy-app/sources/sync/ops.ts +++ b/packages/happy-app/sources/sync/ops.ts @@ -473,7 +473,9 @@ export async function sessionRipgrep( } /** - * Kill the session process immediately + * Kill the session process immediately. + * Idempotent: if the process already exited, treat it as success + * since the desired outcome (session stopped) is already achieved. */ export async function sessionKill(sessionId: string): Promise { try { @@ -484,9 +486,12 @@ export async function sessionKill(sessionId: string): Promise