import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { describe, expect, it, vi } from 'vitest' import { PET_WINDOW_HEIGHT, PET_WINDOW_WIDTH, PetWindowController, clampPetWindowPosition, getPetWindowBounds, petWindowStatePath, petWindowOptions, readPetWindowPosition, writePetWindowPosition, } from './petWindow' const desktopRoot = existsSync(path.resolve(process.cwd(), 'electron', 'main.ts')) ? process.cwd() : path.resolve(process.cwd(), 'desktop') const mainSource = readFileSync(path.join(desktopRoot, 'electron', 'main.ts'), 'utf8') function createFakeWindow(initialBounds = { x: 100, y: 100, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) { const handlers = new Map void>() let visible = false let destroyed = false let bounds = { ...initialBounds } return { handlers, isDestroyed: vi.fn(() => destroyed), isVisible: vi.fn(() => visible), showInactive: vi.fn(() => { visible = true }), hide: vi.fn(() => { visible = false }), destroy: vi.fn(() => { destroyed = true }), setAlwaysOnTop: vi.fn(), setVisibleOnAllWorkspaces: vi.fn(), setIgnoreMouseEvents: vi.fn(), setShape: vi.fn(), getBounds: vi.fn(() => ({ ...bounds })), setPosition: vi.fn((x: number, y: number) => { bounds = { ...bounds, x, y } }), on: vi.fn((event: string, handler: () => void) => { handlers.set(event, handler) }), } } describe('Electron pet window service', () => { it('places fixed companion bounds inside the current display work area', () => { expect(getPetWindowBounds({ x: 1440, y: 25, width: 1920, height: 1055 })).toEqual({ x: 1440 + 1920 - PET_WINDOW_WIDTH - 24, y: 25 + 1055 - PET_WINDOW_HEIGHT - 24, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) }) it('restores and clamps a saved position into its visible display work area', () => { const workArea = { x: -900, y: 25, width: 900, height: 700 } expect(clampPetWindowPosition({ x: -1_200, y: 900 }, workArea)).toEqual({ x: -900, y: 325, }) expect(getPetWindowBounds(workArea, { x: -1_200, y: 900 })).toEqual({ x: -900, y: 325, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) }) it('persists position only in the app-owned cc-haha config root', () => { const root = mkdtempSync(path.join(tmpdir(), 'cc-haha-pet-position-')) const configDir = path.join(root, 'portable') const env = { CLAUDE_CONFIG_DIR: configDir, CODEX_HOME: path.join(root, 'codex-must-not-be-used'), } try { expect(petWindowStatePath(env, root)).toBe(path.join( configDir, 'cc-haha', 'pet-window.json', )) writePetWindowPosition({ x: -420.4, y: 85.7 }, env, root) expect(readPetWindowPosition(env, root)).toEqual({ x: -420, y: 86 }) expect(existsSync(path.join(root, 'codex-must-not-be-used'))).toBe(false) } finally { rmSync(root, { recursive: true, force: true }) } }) it('creates a transparent frameless sandboxed always-on-top window', () => { const macOptions = petWindowOptions( { x: 20, y: 30, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT }, '/app/electron-dist/preload.cjs', 'darwin', ) expect(macOptions).toMatchObject({ x: 20, y: 30, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, alwaysOnTop: true, backgroundColor: '#00000000', frame: false, fullscreenable: false, hasShadow: false, resizable: false, show: false, transparent: true, type: 'panel', webPreferences: { contextIsolation: true, nodeIntegration: false, partition: 'cc-haha-pet', preload: '/app/electron-dist/preload.cjs', sandbox: true, }, }) expect(macOptions).not.toHaveProperty('skipTaskbar') const windowsOptions = petWindowOptions( { x: 20, y: 30, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT }, '/app/electron-dist/preload.cjs', 'win32', ) expect(windowsOptions.type).toBeUndefined() expect(windowsOptions.skipTaskbar).toBe(true) }) it('loads the dedicated renderer mode with pet-scoped server auth configured', () => { expect(mainSource).toContain("loadRendererEntry(window, { petWindow: '1' })") expect(mainSource).toContain('window.webContents.session.webRequest') expect(mainSource).toContain('resolvePetServerAccess') expect(mainSource).toContain('token: runtime.getPetAccessToken()') }) it('validates the exact bounded spritesheet bytes without reopening a path', () => { expect(mainSource).toContain('nativeImage.createFromBuffer(data).getSize()') expect(mainSource).not.toContain('nativeImage.createFromPath') }) it('focuses the main app before emitting the named session navigation event', () => { expect(mainSource).toContain('showMainWindow(mainWindow, app)') expect(mainSource).toContain( 'mainWindow?.webContents.send(ELECTRON_EVENT_CHANNELS.petNavigateSession, sessionId)', ) }) it('routes the native context menu through the sender-owned pet controller', () => { expect(mainSource).toContain( 'registerHandler(ELECTRON_IPC_CHANNELS.petsShowContextMenu, (event, payload)', ) expect(mainSource).toContain( 'getPetWindowController().showContextMenu(\n currentWindow(event),', ) expect(mainSource).toContain('closeLabel.trim(),\n Menu,') }) it('routes drag coordinates through the sender-owned pet controller', () => { expect(mainSource).toContain( 'getCursorScreenPoint: () => screen.getCursorScreenPoint()', ) expect(mainSource).toContain( 'registerHandler(ELECTRON_IPC_CHANNELS.petsDragWindow, (event, payload)', ) expect(mainSource).toContain( 'getPetWindowController().dragWindow(\n currentWindow(event),', ) }) it('notifies the main renderer whenever native pet visibility changes', () => { expect(mainSource).toContain( 'mainWindow?.webContents.send(ELECTRON_EVENT_CHANNELS.petVisibilityChanged, true)', ) expect(mainSource).toContain( 'mainWindow?.webContents.send(ELECTRON_EVENT_CHANNELS.petVisibilityChanged, false)', ) }) it('creates and loads only one window across concurrent show calls', async () => { const window = createFakeWindow() const createWindow = vi.fn(() => window) let finishLoad: (() => void) | undefined const load = vi.fn(() => new Promise((resolve) => { finishLoad = resolve })) const controller = new PetWindowController({ createWindow: createWindow as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load, platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', }) const firstShow = controller.show() const secondShow = controller.show() finishLoad?.() await Promise.all([firstShow, secondShow]) expect(createWindow).toHaveBeenCalledTimes(1) expect(load).toHaveBeenCalledTimes(1) expect(window.showInactive).toHaveBeenCalledTimes(1) expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'floating') expect(window.setVisibleOnAllWorkspaces).toHaveBeenCalledWith(true, { skipTransformProcessType: true, visibleOnFullScreen: true, }) expect(window.setIgnoreMouseEvents).toHaveBeenCalledWith(true, { forward: true }) }) it('restores the saved companion position before creating the window', async () => { const window = createFakeWindow() const createWindow = vi.fn(() => window) const getWorkAreaForPoint = vi.fn(() => ({ x: -900, y: 25, width: 900, height: 700 })) const controller = new PetWindowController({ createWindow: createWindow as never, getCurrentWorkArea: () => ({ x: 0, y: 25, width: 1440, height: 875 }), getWorkAreaForPoint, load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', readPosition: () => ({ x: -600, y: 80 }), }) await controller.show() expect(getWorkAreaForPoint).toHaveBeenCalledWith({ x: -600 + Math.floor(PET_WINDOW_WIDTH / 2), y: 80 + Math.floor(PET_WINDOW_HEIGHT / 2), }) expect(createWindow).toHaveBeenCalledWith(expect.objectContaining({ x: -600, y: 80, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, })) }) it('keeps the pointer anchor, clamps dragging to the pointer display, and saves on release', async () => { const petWindow = createFakeWindow({ x: 100, y: 120, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) const writePosition = vi.fn() const getWorkAreaForPoint = vi.fn(() => ({ x: 0, y: 25, width: 800, height: 575 })) const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 25, width: 800, height: 575 }), getWorkAreaForPoint, load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', writePosition, }) await controller.show() controller.dragWindow(petWindow as never, { phase: 'start', x: 150, y: 180 }) controller.dragWindow(petWindow as never, { phase: 'move', x: 190, y: 220 }) expect(petWindow.setPosition).toHaveBeenLastCalledWith(140, 160, false) controller.dragWindow(petWindow as never, { phase: 'end', x: 1_000, y: 900 }) expect(getWorkAreaForPoint).toHaveBeenLastCalledWith({ x: 1_000, y: 900 }) expect(petWindow.setPosition).toHaveBeenLastCalledWith( 800 - PET_WINDOW_WIDTH, 25 + 575 - PET_WINDOW_HEIGHT, false, ) expect(writePosition).toHaveBeenCalledOnce() expect(writePosition).toHaveBeenCalledWith({ x: 800 - PET_WINDOW_WIDTH, y: 25 + 575 - PET_WINDOW_HEIGHT, }) }) it('tracks the native cursor at 60 Hz without renderer move payloads', async () => { vi.useFakeTimers() try { const petWindow = createFakeWindow({ x: 100, y: 120, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) let cursor = { x: 150, y: 180 } const writePosition = vi.fn() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCursorScreenPoint: () => cursor, getCurrentWorkArea: () => ({ x: 0, y: 25, width: 1_200, height: 775 }), getWorkAreaForPoint: () => ({ x: 0, y: 25, width: 1_200, height: 775 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', writePosition, }) await controller.show() controller.dragWindow(petWindow as never, { phase: 'start', x: 150, y: 180 }) cursor = { x: 203, y: 227 } vi.advanceTimersByTime(16) expect(petWindow.setPosition).toHaveBeenLastCalledWith(153, 167, false) expect(writePosition).not.toHaveBeenCalled() controller.dragWindow(petWindow as never, { phase: 'end', x: 203, y: 227 }) expect(writePosition).toHaveBeenCalledOnce() expect(writePosition).toHaveBeenCalledWith({ x: 153, y: 167 }) expect(vi.getTimerCount()).toBe(0) const setPositionCalls = petWindow.setPosition.mock.calls.length cursor = { x: 260, y: 280 } vi.advanceTimersByTime(32) expect(petWindow.setPosition).toHaveBeenCalledTimes(setPositionCalls) } finally { vi.useRealTimers() } }) it.each(['hide', 'closed', 'dispose'] as const)( 'stops native cursor tracking and persists the final position on %s', async (action) => { vi.useFakeTimers() try { const petWindow = createFakeWindow({ x: 100, y: 120, width: PET_WINDOW_WIDTH, height: PET_WINDOW_HEIGHT, }) let cursor = { x: 150, y: 180 } const writePosition = vi.fn() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCursorScreenPoint: () => cursor, getCurrentWorkArea: () => ({ x: 0, y: 25, width: 1_200, height: 775 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', writePosition, }) await controller.show() controller.dragWindow(petWindow as never, { phase: 'start', x: 150, y: 180 }) expect(vi.getTimerCount()).toBe(1) cursor = { x: 180, y: 200 } vi.advanceTimersByTime(16) expect(petWindow.setPosition).toHaveBeenLastCalledWith(130, 140, false) if (action === 'hide') controller.hide() if (action === 'closed') petWindow.handlers.get('closed')?.() if (action === 'dispose') controller.dispose() expect(vi.getTimerCount()).toBe(0) expect(writePosition).toHaveBeenCalledOnce() expect(writePosition).toHaveBeenCalledWith({ x: 130, y: 140 }) const setPositionCalls = petWindow.setPosition.mock.calls.length cursor = { x: 260, y: 280 } vi.advanceTimersByTime(32) expect(petWindow.setPosition).toHaveBeenCalledTimes(setPositionCalls) } finally { vi.useRealTimers() } }, ) it('rejects drag coordinates and drag senders outside the owned pet window', async () => { const petWindow = createFakeWindow() const otherWindow = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() expect(() => controller.dragWindow(otherWindow as never, { phase: 'start', x: 10, y: 10, })).toThrow('does not own') expect(() => controller.dragWindow(petWindow as never, { phase: 'start', x: Number.POSITIVE_INFINITY, y: 10, })).toThrow('finite screen coordinates') expect(() => controller.dragWindow(petWindow as never, { phase: 'move', x: 10, y: 10, })).toThrow('has not started') }) it('destroys a hidden companion so its renderer releases observers', async () => { const firstWindow = createFakeWindow() const secondWindow = createFakeWindow() const createWindow = vi.fn() .mockReturnValueOnce(firstWindow) .mockReturnValueOnce(secondWindow) const controller = new PetWindowController({ createWindow: createWindow as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'linux', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() controller.hide() await controller.show() expect(createWindow).toHaveBeenCalledTimes(2) expect(firstWindow.destroy).toHaveBeenCalledTimes(1) expect(secondWindow.showInactive).toHaveBeenCalledTimes(1) }) it('uses native shapes off macOS and rejects IPC from another window', async () => { const petWindow = createFakeWindow() const otherWindow = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'linux', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() controller.setInteractiveRegions(petWindow as never, [ { x: 100, y: 220, width: 144, height: 170 }, ]) controller.setIgnoreMouseEvents(petWindow as never, true) expect(petWindow.setShape).toHaveBeenLastCalledWith([ { x: 100, y: 220, width: 144, height: 170 }, ]) expect(petWindow.setIgnoreMouseEvents).toHaveBeenCalledTimes(1) expect(() => controller.setInteractiveRegions(otherWindow as never, [ { x: 0, y: 0, width: 10, height: 10 }, ])).toThrow('does not own') }) it('returns whether the native pet context menu close item was selected', async () => { const petWindow = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() let clickClose: (() => void) | undefined let dismissMenu: (() => void) | undefined const popup = vi.fn((options: { callback: () => void }) => { dismissMenu = options.callback }) const menuFactory = { buildFromTemplate: vi.fn((template: Array<{ click?: () => void }>) => { clickClose = template[0]?.click return { popup } }), } const selection = controller.showContextMenu( petWindow as never, '关闭宠物', menuFactory as never, ) clickClose?.() dismissMenu?.() await expect(selection).resolves.toBe(true) expect(menuFactory.buildFromTemplate).toHaveBeenCalledWith([{ label: '关闭宠物', click: expect.any(Function), }]) expect(popup).toHaveBeenCalledWith({ window: petWindow, callback: expect.any(Function), }) }) it('returns false when the native pet context menu is dismissed', async () => { const petWindow = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() const menuFactory = { buildFromTemplate: vi.fn(() => ({ popup: ({ callback }: { callback: () => void }) => callback(), })), } await expect(controller.showContextMenu( petWindow as never, 'Close pet', menuFactory as never, )).resolves.toBe(false) }) it('rejects native context menu requests from any non-pet window', async () => { const petWindow = createFakeWindow() const otherWindow = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => petWindow) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockResolvedValue(undefined), platform: 'darwin', preloadPath: '/app/electron-dist/preload.cjs', }) await controller.show() await expect(controller.showContextMenu( otherWindow as never, 'Close pet', { buildFromTemplate: vi.fn() } as never, )).rejects.toThrow('does not own') }) it('destroys a partially created window when renderer loading fails', async () => { const window = createFakeWindow() const controller = new PetWindowController({ createWindow: vi.fn(() => window) as never, getCurrentWorkArea: () => ({ x: 0, y: 0, width: 1440, height: 900 }), load: vi.fn().mockRejectedValue(new Error('load failed')), platform: 'win32', preloadPath: '/app/electron-dist/preload.cjs', }) await expect(controller.show()).rejects.toThrow('load failed') expect(window.destroy).toHaveBeenCalledTimes(1) }) })