Extract mouse interop runtime lifecycle seam

This commit is contained in:
OpenPets Dev 2026-06-19 20:24:40 +00:00
parent 19a4a8988c
commit 5ab4c5cd0f
5 changed files with 174 additions and 112 deletions

View file

@ -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.");

View file

@ -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

View file

@ -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<BrowserWindow, (reason: string) => void>;
readonly dragging: WeakMap<BrowserWindow, boolean>;
readonly scaling: WeakMap<BrowserWindow, boolean>;
}
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);
}

View file

@ -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<BrowserWindow, (reason: string) => void>;
readonly dragging: WeakMap<BrowserWindow, boolean>;
readonly scaling: WeakMap<BrowserWindow, boolean>;
}
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,
});
}

View file

@ -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.");