diff --git a/apps/desktop/src/check-packaging-contract.ts b/apps/desktop/src/check-packaging-contract.ts index 98dc7799..952e1ed2 100644 --- a/apps/desktop/src/check-packaging-contract.ts +++ b/apps/desktop/src/check-packaging-contract.ts @@ -76,6 +76,7 @@ const petWindowContentSource = readFileSync(join(appDir, "src", "familiar-window const petWindowInteractionSource = readFileSync(join(appDir, "src", "familiar-window-interactions.ts"), "utf8"); const petWindowMouseInteropSource = readFileSync(join(appDir, "src", "familiar-window-mouse-interop.ts"), "utf8"); const petWindowMouseInteropRuntimeSource = readFileSync(join(appDir, "src", "familiar-window-mouse-interop-runtime.ts"), "utf8"); +const petWindowMouseInteropRuntimeLifecycleSource = readFileSync(join(appDir, "src", "familiar-window-mouse-interop-runtime-lifecycle.ts"), "utf8"); const petWindowMouseInteropSupportSource = readFileSync(join(appDir, "src", "familiar-window-mouse-interop-support.ts"), "utf8"); const petWindowPluginMenuSource = readFileSync(join(appDir, "src", "familiar-window-plugin-menu.ts"), "utf8"); const petWindowRendererBridgeSource = readFileSync(join(appDir, "src", "familiar-window-renderer-bridge.ts"), "utf8"); @@ -271,7 +272,7 @@ const controlCenterSettingsCombinedSource = [ ].join("\n"); const petWindowRenderCombinedSource = `${petWindowSource}\n${petWindowRenderSource}\n${petWindowRenderBubblesSource}`; const petWindowContentCombinedSource = `${petWindowSource}\n${petWindowContentSource}`; -const petWindowRuntimeCombinedSource = `${petWindowSource}\n${petWindowHostSource}\n${petWindowInteractionSource}\n${petWindowMouseInteropSource}\n${petWindowMouseInteropRuntimeSource}\n${petWindowMouseInteropSupportSource}`; +const petWindowRuntimeCombinedSource = `${petWindowSource}\n${petWindowHostSource}\n${petWindowInteractionSource}\n${petWindowMouseInteropSource}\n${petWindowMouseInteropRuntimeSource}\n${petWindowMouseInteropRuntimeLifecycleSource}\n${petWindowMouseInteropSupportSource}`; assert.match(loggerSource, /familiaros\.log/, "desktop logger must write a user-sendable familiaros.log file."); assert.match(loggerSource, /familiaros\.previous\.log/, "desktop logger must retain a previous log file for bug reports."); assert.match(loggerSource, /FAMILIAROS_LOG_LEVEL/, "desktop logger must support verbose dev logging via environment."); @@ -324,8 +325,8 @@ assert.match(petWindowSource, /function clearTransientReaction/, "finite reactio assert.match(petWindowRendererBridgeSource, /sendWindowChannel\(window, "familiaros:familiar-reaction-state"/, "finite reaction animations must clear sprite state without reloading the bubble."); assert.match(petPreloadSource, /familiaros:familiar-reaction-state/, "familiar preload must accept in-place reaction state updates."); assert.match(petWindowMouseInteropSupportSource, /webContents:\s*window\.webContents/, "mouse interop support must capture webContents once so cleanup can avoid touching a destroyed BrowserWindow."); -assert.match(petWindowMouseInteropRuntimeSource, /const removeListeners = \(\): void => \{[\s\S]*?if \(!context\.webContents\.isDestroyed\(\)\)/, "mouse interop cleanup must avoid touching destroyed webContents objects."); -assert.match(petWindowMouseInteropRuntimeSource, /context\.window\.on\("close", removeListeners\);\s*context\.window\.once\("closed", removeListeners\);/, "familiar window cleanup must run before and after close so agent lease release and Cmd/Ctrl+W are idempotent."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /const removeListeners = \(\): void => \{[\s\S]*?if \(!context\.webContents\.isDestroyed\(\)\)/, "mouse interop cleanup must avoid touching destroyed webContents objects."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /context\.window\.on\("close", removeListeners\);\s*context\.window\.once\("closed", removeListeners\);/, "familiar window cleanup must run before and after close so agent lease release and Cmd\/Ctrl\+W are idempotent."); assert.match(displaySource, /width:\s*220/, "familiar windows must stay tightly bounded around familiar and bubble."); assert.match(displaySource, /height:\s*320/, "familiar windows must be tall enough for adaptive long message bubbles at large familiar scale without becoming a huge click shield."); assert.match(petWindowRenderCombinedSource, /function getBubbleClassName|export function getBubbleClassName/, "familiar bubbles must classify explicit messages by length."); @@ -361,6 +362,8 @@ assert.match(petWindowInteractionSource, /from "\.\/familiar-window-mouse-intero assert.match(petWindowMouseInteropSource, /export function installMousePassthroughAndDrag/, "mouse interop runtime must export the mouse passthrough seam."); assert.match(petWindowMouseInteropSource, /from "\.\/familiar-window-mouse-interop-runtime(?:\.js)?"/, "mouse interop shell must compose the extracted runtime seam."); assert.match(petWindowMouseInteropRuntimeSource, /export function attachPetWindowMouseInteropRuntime/, "mouse interop runtime seam must export listener and IPC attachment."); +assert.match(petWindowMouseInteropRuntimeSource, /from "\.\/familiar-window-mouse-interop-runtime-lifecycle(?:\.js)?"/, "mouse interop runtime seam must compose the extracted lifecycle seam."); +assert.match(petWindowMouseInteropRuntimeSource, /attachPetWindowMouseInteropLifecycle\(context, maps, \{/, "mouse interop runtime seam must delegate listener lifecycle attachment."); assert.match(petWindowRuntimeCombinedSource, /setIgnoreMouseEvents\(true, \{ forward: true \}\)/, "transparent familiar window background must use OS-level mouse passthrough."); assert.match(petWindowRuntimeCombinedSource, /setIgnoreMouseEvents\(false\)/, "visible familiar and bubble hit targets must re-enable mouse handling."); assert.match(petWindowRuntimeCombinedSource, /familiaros:familiar-ready/, "familiar windows must resync passthrough after each renderer reload."); @@ -368,11 +371,12 @@ assert.match(petWindowMouseInteropSupportSource, /export function rearmPassthrou assert.match(petWindowMouseInteropSupportSource, /scheduleWindowsMouseForwardingRearm\(context, `\$\{reason\}\+75ms`, 75\);[\s\S]*?scheduleWindowsMouseForwardingRearm\(context, `\$\{reason\}\+175ms`, 175\);/, "Windows familiar reloads must retry mouse forwarding rearm after load settles."); assert.match(petWindowRuntimeCombinedSource, /familiaros:familiar-probe-hit-test/, "Windows familiar reloads must probe current cursor hit target when mousemove forwarding is stale."); assert.match(petWindowMouseInteropSource, /export function recoverPetMouseInterop/, "familiar windows must expose a controlled mouse interop recovery hook for OS display and resume events."); -assert.match(petWindowMouseInteropRuntimeSource, /recovery\.set\(context\.window, \(reason\) =>[\s\S]*?scheduleMouseInteropRecovery\(context, reason\)/, "familiar windows must register their mouse interop recovery callback."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /export function attachPetWindowMouseInteropLifecycle/, "mouse interop lifecycle seam must export listener lifecycle attachment."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /recovery\.set\(context\.window, \(reason\) =>[\s\S]*?scheduleMouseInteropRecovery\(context, reason\)/, "familiar windows must register their mouse interop recovery callback."); assert.match(petWindowMouseInteropSupportSource, /export function scheduleWindowsForwardingWatch[\s\S]*?rearmWindowsMouseForwarding\(context, reason, false\)[\s\S]*?scheduleWindowsForwardingWatch\(context, reason\)/, "Windows familiar passthrough must keep rearming while idle so hover and drag recover after familiar reloads without noisy logs."); assert.match(petPreloadSource, /familiaros:familiar-probe-hit-test[\s\S]*?elementFromPoint\(clientX, clientY\)[\s\S]*?reportInteractiveHit/, "familiar preload must answer main-process cursor hit-test probes."); -assert.match(petWindowMouseInteropRuntimeSource, /did-finish-load", rearmAfterLoad/, "familiar windows must re-arm mouse passthrough after every content load."); -assert.match(petWindowMouseInteropRuntimeSource, /did-fail-load", handleLoadFailure/, "familiar windows must restore passthrough after failed content loads."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /did-finish-load", rearmAfterLoad/, "familiar windows must re-arm mouse passthrough after every content load."); +assert.match(petWindowMouseInteropRuntimeLifecycleSource, /did-fail-load", handleLoadFailure/, "familiar windows must restore passthrough after failed content loads."); assert.match(petWindowHostSource, /window\.setIgnoreMouseEvents\(false\);[\s\S]*?await window\.loadFile/, "familiar reloads must reset OS mouse passthrough before navigation."); assert.match(petWindowHostSource, /export function allocateWindowLoadSequence/, "familiar content reloads must allocate request sequence before async rendering."); assert.match(petWindowSource, /tryUpdateLoadedPetContent\(window, render, "default", sequence\)/, "default familiar transient updates must avoid BrowserWindow reloads when the familiar document is already loaded."); diff --git a/apps/desktop/src/codemap.md b/apps/desktop/src/codemap.md index 2c683800..039c68db 100644 --- a/apps/desktop/src/codemap.md +++ b/apps/desktop/src/codemap.md @@ -50,7 +50,8 @@ familiar-window.ts ├── familiar-window-host.ts (Electron BrowserWindow shell, navigation allowlist, and serialized HTML load queue) ├── familiar-window-interactions.ts (public interaction facade plus motion-state publishing) ├── familiar-window-mouse-interop.ts (public mouse passthrough/drag facade plus recovery accessors) -├── familiar-window-mouse-interop-runtime.ts (IPC handler and listener attachment for familiar window mouse interop) +├── familiar-window-mouse-interop-runtime.ts (IPC handler attachment and lifecycle seam delegation for familiar window mouse interop) +├── familiar-window-mouse-interop-runtime-lifecycle.ts (listener registration, cleanup, and load/reset recovery for familiar window mouse interop) ├── familiar-window-mouse-interop-support.ts (context creation, platform passthrough helpers, and hit-test probes) ├── familiar-window-plugin-menu.ts (pet context menu, default-pet plugin commands, plugin command forms) ├── familiar-window-renderer-bridge.ts (bubble layout, reaction, sprite, audio, and TTS renderer IPC helpers) @@ -248,7 +249,8 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo - `familiar-window-layout.ts`: Extracted familiar bubble sizing, scroll-cap selection, and base window layout calculations - `familiar-window-interactions.ts`: Public familiar-window interaction facade that re-exports mouse interop and owns motion-state publishing - `familiar-window-mouse-interop.ts`: Public familiar mouse passthrough, drag/scale state, and recovery facade over the extracted runtime seam -- `familiar-window-mouse-interop-runtime.ts`: Extracted familiar mouse IPC handlers, window/webContents listener attachment, cleanup, and recovery registration +- `familiar-window-mouse-interop-runtime.ts`: Extracted familiar mouse IPC handlers and delegation into the lifecycle attachment seam +- `familiar-window-mouse-interop-runtime-lifecycle.ts`: Extracted familiar mouse window/webContents listener attachment, cleanup, navigation/load reset, and recovery registration - `familiar-window-mouse-interop-support.ts`: Extracted familiar mouse interop context, platform-specific passthrough rearm helpers, cursor hit-test probes, and payload guards - `familiar-window-plugin-menu.ts`: Extracted pet context-menu construction, default familiar plugin command menus, and plugin command form hosting - `familiar-window-renderer-bridge.ts`: Extracted renderer IPC helpers for bubble layout updates, reaction state, sprite overrides, audio playback, and TTS diff --git a/apps/desktop/src/familiar-window-mouse-interop-runtime-lifecycle.ts b/apps/desktop/src/familiar-window-mouse-interop-runtime-lifecycle.ts new file mode 100644 index 00000000..ec4f2c0d --- /dev/null +++ b/apps/desktop/src/familiar-window-mouse-interop-runtime-lifecycle.ts @@ -0,0 +1,131 @@ +import { ipcMain, type BrowserWindow, type IpcMainEvent } from "electron"; + +import { + clearRearmTimers, + clearWindowsForwardingWatch, + rearmPassthrough, + scheduleMouseInteropRecovery, + setPassthrough, + type PetWindowMouseInteropContext, +} from "./familiar-window-mouse-interop-support.js"; +import { debug } from "./logger.js"; + +export interface PetWindowMouseInteropRuntimeMaps { + readonly recovery: WeakMap void>; + readonly dragging: WeakMap; + readonly scaling: WeakMap; +} + +export type PetWindowMouseInteropRuntimeHandlers = { + readonly handleReady: (event: IpcMainEvent) => void; + readonly handleHitTest: (event: IpcMainEvent, interactive: unknown, source: unknown) => void; + readonly handleDragStart: (event: IpcMainEvent, point: unknown) => void; + readonly handleDragMove: (event: IpcMainEvent, point: unknown) => void; + readonly handleDragEnd: (event: IpcMainEvent) => void; + readonly handleBubbleDismissed: (event: IpcMainEvent, dismissToken: unknown) => void; + readonly handlePromptRequested: (event: IpcMainEvent) => void; + readonly handleScaleStart: (event: IpcMainEvent) => void; + readonly handleScalePreview: (event: IpcMainEvent, payload: unknown) => void; + readonly handleScaleEnd: (event: IpcMainEvent, payload: unknown) => void; + readonly handleBubbleAction: (event: IpcMainEvent, dismissToken: unknown, actionId: unknown) => void; + readonly handleBubbleSubmit: (event: IpcMainEvent, dismissToken: unknown, values: unknown) => void; + readonly handlePetEvent: (event: IpcMainEvent, name: unknown, payload: unknown) => void; +}; + +export function attachPetWindowMouseInteropLifecycle( + context: PetWindowMouseInteropContext, + maps: PetWindowMouseInteropRuntimeMaps, + handlers: PetWindowMouseInteropRuntimeHandlers, +): void { + const resetForNavigation = (): void => { + context.state.dragging = null; + maps.dragging.set(context.window, false); + context.state.rendererReady = false; + context.state.lastInteractive = false; + clearRearmTimers(context); + debug("familiar.window", "navigation reset passthrough", { + windowId: context.windowId, + }); + setPassthrough(context, false); + }; + + const rearmAfterLoad = (): void => { + context.state.dragging = null; + maps.dragging.set(context.window, false); + context.state.lastInteractive = false; + debug("familiar.window", "load rearm passthrough", { + windowId: context.windowId, + }); + rearmPassthrough(context, "did-finish-load"); + }; + + const handleDomReady = (): void => { + if (!context.state.rendererReady) { + setPassthrough(context, true); + } + }; + + const handleLoadFailure = (): void => { + context.state.dragging = null; + context.state.lastInteractive = false; + debug("familiar.window", "load failure rearm passthrough", { + windowId: context.windowId, + }); + setPassthrough(context, true); + }; + + const removeListeners = (): void => { + if (context.state.listenersRemoved) return; + context.state.listenersRemoved = true; + ipcMain.off("familiaros:familiar-ready", handlers.handleReady); + ipcMain.off("familiaros:familiar-hit-test", handlers.handleHitTest); + ipcMain.off("familiaros:familiar-drag-start", handlers.handleDragStart); + ipcMain.off("familiaros:familiar-drag-move", handlers.handleDragMove); + ipcMain.off("familiaros:familiar-drag-end", handlers.handleDragEnd); + ipcMain.off("familiaros:bubble-dismissed", handlers.handleBubbleDismissed); + ipcMain.off("familiaros:familiar-open-prompt", handlers.handlePromptRequested); + ipcMain.off("familiaros:familiar-scale-start", handlers.handleScaleStart); + ipcMain.off("familiaros:familiar-scale-preview", handlers.handleScalePreview); + ipcMain.off("familiaros:familiar-scale-end", handlers.handleScaleEnd); + ipcMain.off("familiaros:bubble-action", handlers.handleBubbleAction); + ipcMain.off("familiaros:bubble-submit", handlers.handleBubbleSubmit); + ipcMain.off("familiaros:familiar-event", handlers.handlePetEvent); + clearRearmTimers(context); + clearWindowsForwardingWatch(context); + maps.recovery.delete(context.window); + maps.dragging.delete(context.window); + maps.scaling.delete(context.window); + if (!context.webContents.isDestroyed()) { + context.webContents.off("did-start-navigation", resetForNavigation); + context.webContents.off("did-start-loading", resetForNavigation); + context.webContents.off("did-finish-load", rearmAfterLoad); + context.webContents.off("dom-ready", handleDomReady); + context.webContents.off("did-fail-load", handleLoadFailure); + } + }; + + maps.recovery.set(context.window, (reason) => + scheduleMouseInteropRecovery(context, reason), + ); + + ipcMain.on("familiaros:familiar-ready", handlers.handleReady); + ipcMain.on("familiaros:familiar-hit-test", handlers.handleHitTest); + ipcMain.on("familiaros:familiar-drag-start", handlers.handleDragStart); + ipcMain.on("familiaros:familiar-drag-move", handlers.handleDragMove); + ipcMain.on("familiaros:familiar-drag-end", handlers.handleDragEnd); + ipcMain.on("familiaros:bubble-dismissed", handlers.handleBubbleDismissed); + ipcMain.on("familiaros:familiar-open-prompt", handlers.handlePromptRequested); + ipcMain.on("familiaros:familiar-scale-start", handlers.handleScaleStart); + ipcMain.on("familiaros:familiar-scale-preview", handlers.handleScalePreview); + ipcMain.on("familiaros:familiar-scale-end", handlers.handleScaleEnd); + ipcMain.on("familiaros:bubble-action", handlers.handleBubbleAction); + ipcMain.on("familiaros:bubble-submit", handlers.handleBubbleSubmit); + ipcMain.on("familiaros:familiar-event", handlers.handlePetEvent); + context.webContents.on("did-start-navigation", resetForNavigation); + context.webContents.on("did-start-loading", resetForNavigation); + context.webContents.on("did-finish-load", rearmAfterLoad); + context.webContents.on("dom-ready", handleDomReady); + context.webContents.on("did-fail-load", handleLoadFailure); + context.window.on("close", removeListeners); + context.window.once("closed", removeListeners); +} diff --git a/apps/desktop/src/familiar-window-mouse-interop-runtime.ts b/apps/desktop/src/familiar-window-mouse-interop-runtime.ts index 83b57eda..00c0a4bf 100644 --- a/apps/desktop/src/familiar-window-mouse-interop-runtime.ts +++ b/apps/desktop/src/familiar-window-mouse-interop-runtime.ts @@ -1,26 +1,23 @@ -import { ipcMain, type BrowserWindow, type IpcMainEvent } from "electron"; +import type { IpcMainEvent } from "electron"; import type { PetScaleValue } from "./app-state.js"; +import { + attachPetWindowMouseInteropLifecycle, + type PetWindowMouseInteropRuntimeMaps, +} from "./familiar-window-mouse-interop-runtime-lifecycle.js"; import { allowedPetEventNames, - clearRearmTimers, clearWindowsForwardingWatch, isFromWindow, isRecord, isScreenPoint, - rearmPassthrough, - scheduleMouseInteropRecovery, scheduleWindowsForwardingWatch, setPassthrough, type PetWindowMouseInteropContext, } from "./familiar-window-mouse-interop-support.js"; import { debug } from "./logger.js"; -export interface PetWindowMouseInteropRuntimeMaps { - readonly recovery: WeakMap void>; - readonly dragging: WeakMap; - readonly scaling: WeakMap; -} +export type { PetWindowMouseInteropRuntimeMaps } from "./familiar-window-mouse-interop-runtime-lifecycle.js"; export function attachPetWindowMouseInteropRuntime( context: PetWindowMouseInteropContext, @@ -264,95 +261,19 @@ export function attachPetWindowMouseInteropRuntime( context.hooks.onPetEvent?.(name, data); }; - const resetForNavigation = (): void => { - context.state.dragging = null; - maps.dragging.set(context.window, false); - context.state.rendererReady = false; - context.state.lastInteractive = false; - clearRearmTimers(context); - debug("familiar.window", "navigation reset passthrough", { - windowId: context.windowId, - }); - setPassthrough(context, false); - }; - - const rearmAfterLoad = (): void => { - context.state.dragging = null; - maps.dragging.set(context.window, false); - context.state.lastInteractive = false; - debug("familiar.window", "load rearm passthrough", { - windowId: context.windowId, - }); - rearmPassthrough(context, "did-finish-load"); - }; - - const handleDomReady = (): void => { - if (!context.state.rendererReady) { - setPassthrough(context, true); - } - }; - - const handleLoadFailure = (): void => { - context.state.dragging = null; - context.state.lastInteractive = false; - debug("familiar.window", "load failure rearm passthrough", { - windowId: context.windowId, - }); - setPassthrough(context, true); - }; - - const removeListeners = (): void => { - if (context.state.listenersRemoved) return; - context.state.listenersRemoved = true; - ipcMain.off("familiaros:familiar-ready", handleReady); - ipcMain.off("familiaros:familiar-hit-test", handleHitTest); - ipcMain.off("familiaros:familiar-drag-start", handleDragStart); - ipcMain.off("familiaros:familiar-drag-move", handleDragMove); - ipcMain.off("familiaros:familiar-drag-end", handleDragEnd); - ipcMain.off("familiaros:bubble-dismissed", handleBubbleDismissed); - ipcMain.off("familiaros:familiar-open-prompt", handlePromptRequested); - ipcMain.off("familiaros:familiar-scale-start", handleScaleStart); - ipcMain.off("familiaros:familiar-scale-preview", handleScalePreview); - ipcMain.off("familiaros:familiar-scale-end", handleScaleEnd); - ipcMain.off("familiaros:bubble-action", handleBubbleAction); - ipcMain.off("familiaros:bubble-submit", handleBubbleSubmit); - ipcMain.off("familiaros:familiar-event", handlePetEvent); - clearRearmTimers(context); - clearWindowsForwardingWatch(context); - maps.recovery.delete(context.window); - maps.dragging.delete(context.window); - maps.scaling.delete(context.window); - if (!context.webContents.isDestroyed()) { - context.webContents.off("did-start-navigation", resetForNavigation); - context.webContents.off("did-start-loading", resetForNavigation); - context.webContents.off("did-finish-load", rearmAfterLoad); - context.webContents.off("dom-ready", handleDomReady); - context.webContents.off("did-fail-load", handleLoadFailure); - } - }; - - maps.recovery.set(context.window, (reason) => - scheduleMouseInteropRecovery(context, reason), - ); - - ipcMain.on("familiaros:familiar-ready", handleReady); - ipcMain.on("familiaros:familiar-hit-test", handleHitTest); - ipcMain.on("familiaros:familiar-drag-start", handleDragStart); - ipcMain.on("familiaros:familiar-drag-move", handleDragMove); - ipcMain.on("familiaros:familiar-drag-end", handleDragEnd); - ipcMain.on("familiaros:bubble-dismissed", handleBubbleDismissed); - ipcMain.on("familiaros:familiar-open-prompt", handlePromptRequested); - ipcMain.on("familiaros:familiar-scale-start", handleScaleStart); - ipcMain.on("familiaros:familiar-scale-preview", handleScalePreview); - ipcMain.on("familiaros:familiar-scale-end", handleScaleEnd); - ipcMain.on("familiaros:bubble-action", handleBubbleAction); - ipcMain.on("familiaros:bubble-submit", handleBubbleSubmit); - ipcMain.on("familiaros:familiar-event", handlePetEvent); - context.webContents.on("did-start-navigation", resetForNavigation); - context.webContents.on("did-start-loading", resetForNavigation); - context.webContents.on("did-finish-load", rearmAfterLoad); - context.webContents.on("dom-ready", handleDomReady); - context.webContents.on("did-fail-load", handleLoadFailure); - context.window.on("close", removeListeners); - context.window.once("closed", removeListeners); + attachPetWindowMouseInteropLifecycle(context, maps, { + handleReady, + handleHitTest, + handleDragStart, + handleDragMove, + handleDragEnd, + handleBubbleDismissed, + handlePromptRequested, + handleScaleStart, + handleScalePreview, + handleScaleEnd, + handleBubbleAction, + handleBubbleSubmit, + handlePetEvent, + }); } diff --git a/apps/desktop/tests/familiar-window-mouse-interop-runtime.test.ts b/apps/desktop/tests/familiar-window-mouse-interop-runtime.test.ts index 57dd15bb..5bce27fa 100644 --- a/apps/desktop/tests/familiar-window-mouse-interop-runtime.test.ts +++ b/apps/desktop/tests/familiar-window-mouse-interop-runtime.test.ts @@ -6,12 +6,16 @@ import { fileURLToPath } from "node:url"; const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), ".."); const familiarWindowMouseInteropSource = readFileSync(resolve(desktopRoot, "src/familiar-window-mouse-interop.ts"), "utf8"); const familiarWindowMouseInteropRuntimeSource = readFileSync(resolve(desktopRoot, "src/familiar-window-mouse-interop-runtime.ts"), "utf8"); +const familiarWindowMouseInteropRuntimeLifecycleSource = readFileSync(resolve(desktopRoot, "src/familiar-window-mouse-interop-runtime-lifecycle.ts"), "utf8"); assert.match(familiarWindowMouseInteropSource, /from "\.\/familiar-window-mouse-interop-runtime(?:\.js)?"/, "mouse interop shell must compose the extracted runtime seam."); assert.match(familiarWindowMouseInteropRuntimeSource, /export function attachPetWindowMouseInteropRuntime/, "mouse interop runtime seam must export listener and IPC attachment."); -assert.match(familiarWindowMouseInteropRuntimeSource, /recovery\.set\(context\.window, \(reason\) =>[\s\S]*?scheduleMouseInteropRecovery\(context, reason\)/, "mouse interop runtime seam must register the recovery callback against the public recovery facade."); -assert.match(familiarWindowMouseInteropRuntimeSource, /ipcMain\.on\("familiaros:familiar-drag-start", handleDragStart\)/, "mouse interop runtime seam must register drag-start IPC handling."); -assert.match(familiarWindowMouseInteropRuntimeSource, /context\.webContents\.on\("did-finish-load", rearmAfterLoad\)/, "mouse interop runtime seam must re-arm passthrough after reload."); -assert.match(familiarWindowMouseInteropRuntimeSource, /context\.window\.once\("closed", removeListeners\)/, "mouse interop runtime seam must detach listeners when the familiar window closes."); +assert.match(familiarWindowMouseInteropRuntimeSource, /from "\.\/familiar-window-mouse-interop-runtime-lifecycle(?:\.js)?"/, "mouse interop runtime seam must compose the extracted lifecycle seam."); +assert.match(familiarWindowMouseInteropRuntimeSource, /attachPetWindowMouseInteropLifecycle\(context, maps, \{/, "mouse interop runtime seam must delegate listener lifecycle attachment."); +assert.match(familiarWindowMouseInteropRuntimeLifecycleSource, /export function attachPetWindowMouseInteropLifecycle/, "mouse interop lifecycle seam must export listener lifecycle attachment."); +assert.match(familiarWindowMouseInteropRuntimeLifecycleSource, /recovery\.set\(context\.window, \(reason\) =>[\s\S]*?scheduleMouseInteropRecovery\(context, reason\)/, "mouse interop lifecycle seam must register the recovery callback against the public recovery facade."); +assert.match(familiarWindowMouseInteropRuntimeLifecycleSource, /ipcMain\.on\("familiaros:familiar-drag-start", handlers\.handleDragStart\)/, "mouse interop lifecycle seam must register drag-start IPC handling."); +assert.match(familiarWindowMouseInteropRuntimeLifecycleSource, /context\.webContents\.on\("did-finish-load", rearmAfterLoad\)/, "mouse interop lifecycle seam must re-arm passthrough after reload."); +assert.match(familiarWindowMouseInteropRuntimeLifecycleSource, /context\.window\.once\("closed", removeListeners\)/, "mouse interop lifecycle seam must detach listeners when the familiar window closes."); console.error("Familiar window mouse interop runtime seam validation passed.");