diff --git a/apps/loopover-miner-ui/src/chat-action-plugins-preview.test.ts b/apps/loopover-miner-ui/src/chat-action-plugins-preview.test.ts new file mode 100644 index 0000000000..4e951d6697 --- /dev/null +++ b/apps/loopover-miner-ui/src/chat-action-plugins-preview.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { chatGovernorActionsPlugin } from "../vite-chat-governor-actions"; +import { chatDiscoverAttemptActionsPlugin } from "../vite-chat-discover-attempt-actions"; + +// The two plugins register their chat actions via a dynamic `import("./src/lib/...")` on server start; mock those +// lib modules so the registration functions are spies. vi.hoisted keeps the spies referenceable from the hoisted +// vi.mock factories. The mock paths resolve to the SAME files the plugins import (both resolve to +// apps/loopover-miner-ui/src/lib/chat-*-actions), so a single mock intercepts the plugin's own dynamic import. +const { registerGovernorChatActions, registerDiscoverAttemptChatActions } = vi.hoisted(() => ({ + registerGovernorChatActions: vi.fn(), + registerDiscoverAttemptChatActions: vi.fn(), +})); +vi.mock("./lib/chat-governor-actions", () => ({ registerGovernorChatActions })); +vi.mock("./lib/chat-discover-attempt-actions", () => ({ registerDiscoverAttemptChatActions })); + +type HookFn = () => void; + +beforeEach(() => { + registerGovernorChatActions.mockClear(); + registerDiscoverAttemptChatActions.mockClear(); +}); + +describe("chatGovernorActionsPlugin (#7228)", () => { + it("registers the governor chat actions on configureServer", async () => { + const plugin = chatGovernorActionsPlugin(); + (plugin.configureServer as HookFn)(); + await vi.waitFor(() => expect(registerGovernorChatActions).toHaveBeenCalledTimes(1)); + }); + + it("REGRESSION (#7228): also registers under configurePreviewServer (the `vite preview` deployment path)", async () => { + const plugin = chatGovernorActionsPlugin(); + expect(plugin.configurePreviewServer).toBeTypeOf("function"); + (plugin.configurePreviewServer as HookFn)(); + await vi.waitFor(() => expect(registerGovernorChatActions).toHaveBeenCalledTimes(1)); + }); +}); + +describe("chatDiscoverAttemptActionsPlugin (#7228)", () => { + it("registers the discover/attempt chat actions on configureServer", async () => { + const plugin = chatDiscoverAttemptActionsPlugin(); + (plugin.configureServer as HookFn)(); + await vi.waitFor(() => expect(registerDiscoverAttemptChatActions).toHaveBeenCalledTimes(1)); + }); + + it("REGRESSION (#7228): also registers under configurePreviewServer (the `vite preview` deployment path)", async () => { + const plugin = chatDiscoverAttemptActionsPlugin(); + expect(plugin.configurePreviewServer).toBeTypeOf("function"); + (plugin.configurePreviewServer as HookFn)(); + await vi.waitFor(() => expect(registerDiscoverAttemptChatActions).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/apps/loopover-miner-ui/vite-chat-discover-attempt-actions.ts b/apps/loopover-miner-ui/vite-chat-discover-attempt-actions.ts index a9f3a08f7e..b0bbded27f 100644 --- a/apps/loopover-miner-ui/vite-chat-discover-attempt-actions.ts +++ b/apps/loopover-miner-ui/vite-chat-discover-attempt-actions.ts @@ -1,17 +1,28 @@ import type { Plugin } from "vite"; -// Registers discover/attempt chat actions into the shared registry on dev-server start (#6837). Handlers call -// the existing miner-ui `requestDiscover` / `requestAttempt` clients — the same POST `/api/discover` and +// Registers discover/attempt chat actions into the shared registry on server start (#6837). Handlers call the +// existing miner-ui `requestDiscover` / `requestAttempt` clients — the same POST `/api/discover` and // `/api/attempt` path the routes already serve. No new /api/* route is added here (mirrors // vite-chat-governor-actions.ts). - +// +// Registered from BOTH configureServer AND configurePreviewServer (#7228): `vite preview` — the mode the +// README's "persistent service" path and systemd/loopover-miner-ui.service.example actually run — only fires +// configurePreviewServer, so registering on configureServer alone left the chat-action registry empty under +// preview and every discover/attempt chat command dispatched as "unknown_action". +// registerDiscoverAttemptChatActions is already idempotent, so calling it from both hooks is safe. export function chatDiscoverAttemptActionsPlugin(): Plugin { + const register = () => { + void import("./src/lib/chat-discover-attempt-actions").then((mod) => { + mod.registerDiscoverAttemptChatActions(); + }); + }; return { name: "loopover-miner-chat-discover-attempt-actions", configureServer() { - void import("./src/lib/chat-discover-attempt-actions").then((mod) => { - mod.registerDiscoverAttemptChatActions(); - }); + register(); + }, + configurePreviewServer() { + register(); }, }; } diff --git a/apps/loopover-miner-ui/vite-chat-governor-actions.ts b/apps/loopover-miner-ui/vite-chat-governor-actions.ts index 6b2e3cf8ab..269135693b 100644 --- a/apps/loopover-miner-ui/vite-chat-governor-actions.ts +++ b/apps/loopover-miner-ui/vite-chat-governor-actions.ts @@ -1,16 +1,27 @@ import type { Plugin } from "vite"; -// Registers governor pause/resume chat actions into the shared registry on dev-server start (#6521). -// Handlers call the existing miner-ui `pauseGovernor` / `resumeGovernor` clients — same path as the Ledgers -// buttons. No new /api/governor/* route is added here. - +// Registers governor pause/resume chat actions into the shared registry on server start (#6521). Handlers call +// the existing miner-ui `pauseGovernor` / `resumeGovernor` clients — same path as the Ledgers buttons. No new +// /api/governor/* route is added here. +// +// Registered from BOTH configureServer AND configurePreviewServer (#7228): `vite preview` — the mode the +// README's "persistent service" path and systemd/loopover-miner-ui.service.example actually run — only fires +// configurePreviewServer, so registering on configureServer alone left the chat-action registry empty under +// preview and every governor chat command dispatched as "unknown_action". registerGovernorChatActions is already +// idempotent, so calling it from both hooks is safe. export function chatGovernorActionsPlugin(): Plugin { + const register = () => { + void import("./src/lib/chat-governor-actions").then((mod) => { + mod.registerGovernorChatActions(); + }); + }; return { name: "loopover-miner-chat-governor-actions", configureServer() { - void import("./src/lib/chat-governor-actions").then((mod) => { - mod.registerGovernorChatActions(); - }); + register(); + }, + configurePreviewServer() { + register(); }, }; }