|
| 1 | +// @vitest-environment jsdom |
| 2 | +/** |
| 3 | + * RefineLaunchDialog.vision.test.tsx — 画面理解开关入精修弹层(REQ-284,v0.19.7)。 |
| 4 | + * |
| 5 | + * @ai-context: 契约——会话级模式显示开关且初值=全局默认;勾选变化=本次覆写 |
| 6 | + * (confirm 携带 visionRefine);「设为默认」显式写回全局(单向); |
| 7 | + * 未覆写时 payload 不带 visionRefine(null=后端跟随全局);笔记级 |
| 8 | + * 模式(纯文本语境)整行隐藏。 |
| 9 | + */ |
| 10 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 11 | +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; |
| 12 | +import RefineLaunchDialog from "./RefineLaunchDialog"; |
| 13 | + |
| 14 | +const invokeMock = vi.fn(); |
| 15 | + |
| 16 | +vi.mock("@tauri-apps/api/core", () => ({ |
| 17 | + invoke: (...args: unknown[]) => invokeMock(...args), |
| 18 | +})); |
| 19 | + |
| 20 | +const meta = { |
| 21 | + strategyDims: [], |
| 22 | + ladderPresets: [ |
| 23 | + { id: "standard", name: "标准精修", desc: "", instruction: "", dimValues: {} }, |
| 24 | + ], |
| 25 | + intents: [], |
| 26 | +}; |
| 27 | + |
| 28 | +function settingsOf(visionRefineEnabled: boolean) { |
| 29 | + return { |
| 30 | + enabled: true, authorized: true, baseUrl: "https://x", model: "m", lowBalanceThreshold: 1, |
| 31 | + rememberCostChoice: false, visionRefineEnabled, |
| 32 | + refineStrategy: { defaultLadder: "", dimOverrides: {} }, hasKey: true, keySource: "credential", |
| 33 | + }; |
| 34 | +} |
| 35 | + |
| 36 | +beforeEach(() => { |
| 37 | + invokeMock.mockReset(); |
| 38 | + invokeMock.mockImplementation((cmd: string) => { |
| 39 | + switch (cmd) { |
| 40 | + case "ai_get_settings": return Promise.resolve(settingsOf(false)); |
| 41 | + case "ai_refine_strategy_meta": return Promise.resolve(meta); |
| 42 | + case "ai_refine_estimate": return Promise.resolve({ estimate: { estTokens: 100, estCostYuan: 0, pricePer1m: 0, priceKnown: true }, rememberCostChoice: false }); |
| 43 | + case "ai_get_balance": return Promise.resolve({ balance: { totalBalance: 10, grantsBalance: 10, toppedUpBalance: 0, currency: "CNY" }, lowBalanceWarning: null }); |
| 44 | + case "ai_refine_prompt_preview": return Promise.resolve("preview"); |
| 45 | + case "ai_refine_start": return Promise.resolve({ taskId: 9, state: "Pending" }); |
| 46 | + case "ai_set_vision_refine": return Promise.resolve(null); |
| 47 | + default: return Promise.resolve(null); |
| 48 | + } |
| 49 | + }); |
| 50 | +}); |
| 51 | + |
| 52 | +afterEach(cleanup); |
| 53 | + |
| 54 | +async function findStartCall(): Promise<Record<string, unknown>> { |
| 55 | + return new Promise((resolve, reject) => { |
| 56 | + const started = Date.now(); |
| 57 | + const poll = () => { |
| 58 | + const call = invokeMock.mock.calls.find((c) => c[0] === "ai_refine_start"); |
| 59 | + if (call) { resolve(call[1] as Record<string, unknown>); return; } |
| 60 | + if (Date.now() - started > 2000) { reject(new Error("未捕获 ai_refine_start 调用")); return; } |
| 61 | + setTimeout(poll, 20); |
| 62 | + }; |
| 63 | + poll(); |
| 64 | + }); |
| 65 | +} |
| 66 | + |
| 67 | +describe("画面理解开关(REQ-284)", () => { |
| 68 | + it("会话级:初值=全局关;勾选=本次覆写,confirm 携带 visionRefine=true", async () => { |
| 69 | + render(<RefineLaunchDialog sessionId={5} onClose={vi.fn()} onStarted={vi.fn()} />); |
| 70 | + const toggle = (await screen.findByTestId("vision-refine-toggle")) as HTMLInputElement; |
| 71 | + expect(toggle.checked).toBe(false); |
| 72 | + expect(screen.getByText(/跟随全局默认(关)——勾选即本次覆写/)).toBeTruthy(); |
| 73 | + fireEvent.click(toggle); |
| 74 | + expect(screen.getByText(/本次:开(全局默认:关)/)?.textContent).toBeTruthy(); |
| 75 | + // 开启时的耗时提示与「设为默认」写回全局 |
| 76 | + expect(screen.getByText(/⚠ 开启会显著增加耗时与费用/)).toBeTruthy(); |
| 77 | + fireEvent.click(screen.getByTestId("vision-set-default")); |
| 78 | + await waitFor(() => { |
| 79 | + expect(invokeMock).toHaveBeenCalledWith("ai_set_vision_refine", { refineEnabled: true }); |
| 80 | + }); |
| 81 | + // 确认启动 → 任务级覆写随请求下发 |
| 82 | + fireEvent.click(screen.getByText("确认并精修 →")); |
| 83 | + const args = await findStartCall(); |
| 84 | + expect(args).toMatchObject({ sessionId: 5, visionRefine: true }); |
| 85 | + }); |
| 86 | + |
| 87 | + it("会话级:未覆写(跟随全局)时 payload 不带 visionRefine 键", async () => { |
| 88 | + render(<RefineLaunchDialog sessionId={5} onClose={vi.fn()} onStarted={vi.fn()} />); |
| 89 | + await screen.findByTestId("vision-refine-toggle"); |
| 90 | + fireEvent.click(screen.getByText("确认并精修 →")); |
| 91 | + const args = await findStartCall(); |
| 92 | + expect((args as { visionRefine?: unknown }).visionRefine).toBeUndefined(); |
| 93 | + }); |
| 94 | + |
| 95 | + it("笔记级(纯文本语境):开关整行不显示", async () => { |
| 96 | + render( |
| 97 | + <RefineLaunchDialog |
| 98 | + noteId={6} |
| 99 | + noteContent="手写笔记内容" |
| 100 | + onClose={vi.fn()} |
| 101 | + onStarted={vi.fn()} |
| 102 | + />, |
| 103 | + ); |
| 104 | + await waitFor(() => { |
| 105 | + expect(screen.queryByTestId("vision-refine-toggle")).toBeNull(); |
| 106 | + }); |
| 107 | + }); |
| 108 | +}); |
0 commit comments