diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 01:13:28 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 01:13:31 +0900 |
| commit | 2fa03f8d7410c2b8d6be8e10ad088863e83d7177 (patch) | |
| tree | 94e1923180ae38d571d34b578afecb0a18913c24 /src/features/heartbeat/ui | |
| parent | 80f99665034a0e510300793205c162fc7a46769f (diff) | |
| parent | 08b12478636f4a5c86a1f3c40a843f2906b7c82f (diff) | |
| download | dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.tar.gz dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.zip | |
Merge branch 'dev' into feature/heartbeat
# Conflicts:
# src/app/App.svelte
# src/app/store.svelte.ts
# src/app/store.test.ts
# src/features/workspaces/ui/WorkspaceCard.test.ts
Diffstat (limited to 'src/features/heartbeat/ui')
| -rw-r--r-- | src/features/heartbeat/ui/PromptEditor.test.ts | 284 |
1 files changed, 142 insertions, 142 deletions
diff --git a/src/features/heartbeat/ui/PromptEditor.test.ts b/src/features/heartbeat/ui/PromptEditor.test.ts index 4b5c960..284b319 100644 --- a/src/features/heartbeat/ui/PromptEditor.test.ts +++ b/src/features/heartbeat/ui/PromptEditor.test.ts @@ -2,166 +2,166 @@ import { render, screen } from "@testing-library/svelte"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import type { - HeartbeatConfigPatch, - HeartbeatConfigResult, - SaveHeartbeatConfig, + HeartbeatConfigPatch, + HeartbeatConfigResult, + SaveHeartbeatConfig, } from "../logic/types"; import PromptEditor from "./PromptEditor.svelte"; // Fakes for the injected ports. function fakeLoadVariables() { - return vi.fn(async () => ({ ok: true, variables: [] }) as const); + return vi.fn(async () => ({ ok: true, variables: [] }) as const); } function fakeLoadDefaultPrompt(template = "You are a helpful assistant.") { - return vi.fn(async () => ({ ok: true, template }) as const); + return vi.fn(async () => ({ ok: true, template }) as const); } /** A capturing saveConfig that resolves ok, echoing the merged config shape. */ function fakeSaveConfig(): { - calls: HeartbeatConfigPatch[]; - impl: SaveHeartbeatConfig; + calls: HeartbeatConfigPatch[]; + impl: SaveHeartbeatConfig; } { - const calls: HeartbeatConfigPatch[] = []; - const impl: SaveHeartbeatConfig = async (patch) => { - calls.push(patch); - // Echo a config that reflects the persisted patch (so onSaved sync is realistic). - const config = { - enabled: false, - systemPrompt: patch.systemPrompt ?? "", - taskPrompt: patch.taskPrompt ?? "", - intervalMinutes: 30, - model: "openai/gpt-4o", - reasoningEffort: null, - }; - return { ok: true, config } satisfies HeartbeatConfigResult; - }; - return { calls, impl }; + const calls: HeartbeatConfigPatch[] = []; + const impl: SaveHeartbeatConfig = async (patch) => { + calls.push(patch); + // Echo a config that reflects the persisted patch (so onSaved sync is realistic). + const config = { + enabled: false, + systemPrompt: patch.systemPrompt ?? "", + taskPrompt: patch.taskPrompt ?? "", + intervalMinutes: 30, + model: "openai/gpt-4o", + reasoningEffort: null, + }; + return { ok: true, config } satisfies HeartbeatConfigResult; + }; + return { calls, impl }; } const baseProps = (overrides: Record<string, unknown> = {}) => ({ - systemPrompt: "", - taskPrompt: "", - loadVariables: fakeLoadVariables(), - loadDefaultPrompt: fakeLoadDefaultPrompt(), - saveConfig: fakeSaveConfig().impl, - onSaved: vi.fn(), - onClose: vi.fn(), - ...overrides, + systemPrompt: "", + taskPrompt: "", + loadVariables: fakeLoadVariables(), + loadDefaultPrompt: fakeLoadDefaultPrompt(), + saveConfig: fakeSaveConfig().impl, + onSaved: vi.fn(), + onClose: vi.fn(), + ...overrides, }); describe("PromptEditor save flow", () => { - it("persists an edited system prompt and clears the unsaved state (regression: save flickered + reverted)", async () => { - const user = userEvent.setup(); - const save = fakeSaveConfig(); - const onSaved = vi.fn(); - render(PromptEditor, { - props: baseProps({ - // Start inheriting (empty override); the default pre-fills. - systemPrompt: "", - saveConfig: save.impl, - onSaved, - }), - }); - - // Wait for the default to load + pre-fill the system textarea. - const systemBox = await screen.findByLabelText("Heartbeat system prompt"); - expect(systemBox).toHaveValue("You are a helpful assistant."); - - // Save is disabled while it matches the default (no explicit edit). - expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); - - // Edit the system prompt → an override. - await user.clear(systemBox); - await user.type(systemBox, "custom override"); - - // Save is now enabled. - const saveBtn = screen.getByRole("button", { name: "Save" }); - expect(saveBtn).toBeEnabled(); - await user.click(saveBtn); - - // The save port was called with the override persisted verbatim. - expect(save.calls).toHaveLength(1); - expect(save.calls[0]?.systemPrompt).toBe("custom override"); - expect(onSaved).toHaveBeenCalledWith("custom override", ""); - - // THE REGRESSION: after save, hasChanges must clear (Save disabled again) - // and the "Saved." confirmation shows — NOT "Unsaved changes". - await vi.waitFor(() => { - expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); - }); - expect(screen.getByText("Saved.")).toBeInTheDocument(); - expect(screen.queryByText(/Unsaved changes/i)).not.toBeInTheDocument(); - }); - - it("persisting text that matches the default sends '' (inherit) and clears unsaved state", async () => { - const user = userEvent.setup(); - const save = fakeSaveConfig(); - render(PromptEditor, { - props: baseProps({ - // Start with an override. - systemPrompt: "old override", - saveConfig: save.impl, - }), - }); - - const systemBox = await screen.findByLabelText("Heartbeat system prompt"); - expect(systemBox).toHaveValue("old override"); - - // Reset to default → text matches the default → saving inherits (""). - await user.click(screen.getByRole("button", { name: "Reset to default" })); - expect(systemBox).toHaveValue("You are a helpful assistant."); - - const saveBtn = screen.getByRole("button", { name: "Save" }); - expect(saveBtn).toBeEnabled(); - await user.click(saveBtn); - - expect(save.calls).toHaveLength(1); - expect(save.calls[0]?.systemPrompt).toBe(""); // inherit - await vi.waitFor(() => { - expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); - }); - expect(screen.getByText("Saved.")).toBeInTheDocument(); - }); - - it("editing the task prompt saves + clears unsaved state", async () => { - const user = userEvent.setup(); - const save = fakeSaveConfig(); - render(PromptEditor, { - props: baseProps({ saveConfig: save.impl }), - }); - - const taskBox = await screen.findByLabelText("Heartbeat task prompt"); - await user.type(taskBox, "do the thing"); - - const saveBtn = screen.getByRole("button", { name: "Save" }); - expect(saveBtn).toBeEnabled(); - await user.click(saveBtn); - - expect(save.calls[0]?.taskPrompt).toBe("do the thing"); - await vi.waitFor(() => { - expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); - }); - expect(screen.getByText("Saved.")).toBeInTheDocument(); - }); - - it("a failed save surfaces the error and keeps the edit unsaved", async () => { - const user = userEvent.setup(); - const failingSave: SaveHeartbeatConfig = async () => ({ ok: false, error: "boom" }); - render(PromptEditor, { - props: baseProps({ saveConfig: failingSave }), - }); - - const systemBox = await screen.findByLabelText("Heartbeat system prompt"); - await user.clear(systemBox); - await user.type(systemBox, "custom"); - - await user.click(screen.getByRole("button", { name: "Save" })); - - expect(screen.getByText("boom")).toBeInTheDocument(); - // Still unsaved (Save stays enabled), no success badge. - expect(screen.getByRole("button", { name: "Save" })).toBeEnabled(); - expect(screen.queryByText("Saved.")).not.toBeInTheDocument(); - }); + it("persists an edited system prompt and clears the unsaved state (regression: save flickered + reverted)", async () => { + const user = userEvent.setup(); + const save = fakeSaveConfig(); + const onSaved = vi.fn(); + render(PromptEditor, { + props: baseProps({ + // Start inheriting (empty override); the default pre-fills. + systemPrompt: "", + saveConfig: save.impl, + onSaved, + }), + }); + + // Wait for the default to load + pre-fill the system textarea. + const systemBox = await screen.findByLabelText("Heartbeat system prompt"); + expect(systemBox).toHaveValue("You are a helpful assistant."); + + // Save is disabled while it matches the default (no explicit edit). + expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + + // Edit the system prompt → an override. + await user.clear(systemBox); + await user.type(systemBox, "custom override"); + + // Save is now enabled. + const saveBtn = screen.getByRole("button", { name: "Save" }); + expect(saveBtn).toBeEnabled(); + await user.click(saveBtn); + + // The save port was called with the override persisted verbatim. + expect(save.calls).toHaveLength(1); + expect(save.calls[0]?.systemPrompt).toBe("custom override"); + expect(onSaved).toHaveBeenCalledWith("custom override", ""); + + // THE REGRESSION: after save, hasChanges must clear (Save disabled again) + // and the "Saved." confirmation shows — NOT "Unsaved changes". + await vi.waitFor(() => { + expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + }); + expect(screen.getByText("Saved.")).toBeInTheDocument(); + expect(screen.queryByText(/Unsaved changes/i)).not.toBeInTheDocument(); + }); + + it("persisting text that matches the default sends '' (inherit) and clears unsaved state", async () => { + const user = userEvent.setup(); + const save = fakeSaveConfig(); + render(PromptEditor, { + props: baseProps({ + // Start with an override. + systemPrompt: "old override", + saveConfig: save.impl, + }), + }); + + const systemBox = await screen.findByLabelText("Heartbeat system prompt"); + expect(systemBox).toHaveValue("old override"); + + // Reset to default → text matches the default → saving inherits (""). + await user.click(screen.getByRole("button", { name: "Reset to default" })); + expect(systemBox).toHaveValue("You are a helpful assistant."); + + const saveBtn = screen.getByRole("button", { name: "Save" }); + expect(saveBtn).toBeEnabled(); + await user.click(saveBtn); + + expect(save.calls).toHaveLength(1); + expect(save.calls[0]?.systemPrompt).toBe(""); // inherit + await vi.waitFor(() => { + expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + }); + expect(screen.getByText("Saved.")).toBeInTheDocument(); + }); + + it("editing the task prompt saves + clears unsaved state", async () => { + const user = userEvent.setup(); + const save = fakeSaveConfig(); + render(PromptEditor, { + props: baseProps({ saveConfig: save.impl }), + }); + + const taskBox = await screen.findByLabelText("Heartbeat task prompt"); + await user.type(taskBox, "do the thing"); + + const saveBtn = screen.getByRole("button", { name: "Save" }); + expect(saveBtn).toBeEnabled(); + await user.click(saveBtn); + + expect(save.calls[0]?.taskPrompt).toBe("do the thing"); + await vi.waitFor(() => { + expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + }); + expect(screen.getByText("Saved.")).toBeInTheDocument(); + }); + + it("a failed save surfaces the error and keeps the edit unsaved", async () => { + const user = userEvent.setup(); + const failingSave: SaveHeartbeatConfig = async () => ({ ok: false, error: "boom" }); + render(PromptEditor, { + props: baseProps({ saveConfig: failingSave }), + }); + + const systemBox = await screen.findByLabelText("Heartbeat system prompt"); + await user.clear(systemBox); + await user.type(systemBox, "custom"); + + await user.click(screen.getByRole("button", { name: "Save" })); + + expect(screen.getByText("boom")).toBeInTheDocument(); + // Still unsaved (Save stays enabled), no success badge. + expect(screen.getByRole("button", { name: "Save" })).toBeEnabled(); + expect(screen.queryByText("Saved.")).not.toBeInTheDocument(); + }); }); |
