fix(ui): 确认框在 Tauri 下被绕过——改用 await plugin-dialog confirm

tauri-plugin-dialog 把 window.confirm 替换为 async 函数,同步判断拿到恒为真的
Promise,撤销测试账户、删除 Gitea 实例/解绑仓库、删除仓库、准备工具链 5 处
破坏性或改机器状态的操作均未弹框即执行(UI 终验发现)。统一 await
@tauri-apps/plugin-dialog 的 confirm,并加源码扫描回归测试防复发。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lanrtop 2026-09-30 13:48:54 +09:00
parent 9211883a9b
commit f74fe11145
5 changed files with 37 additions and 6 deletions

View File

@ -24,6 +24,7 @@ import {
gitPush,
type GiteaInstance,
} from "../../lib/commands";
import { confirm } from "@tauri-apps/plugin-dialog";
import { useUIStore } from "../../store/ui";
type BindMode = "create" | "migrate" | "link";
@ -98,7 +99,7 @@ export function GiteaInstancesSection() {
};
const handleDelete = async (inst: GiteaInstance) => {
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
if (!(await confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`, { kind: "warning" }))) return;
try {
await giteaDeleteInstance(inst.id);
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
@ -308,7 +309,7 @@ export function GiteaPanel({ projectId, projectName, projectPath }: Props) {
};
const handleUnlink = async () => {
if (!confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)")) return;
if (!(await confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)", { kind: "warning" }))) return;
try {
await giteaUnlinkRepo(projectId);
refresh();

View File

@ -6,6 +6,7 @@ import {
saveProjectTestAccount,
revokeProjectDevVariable,
} from "../../lib/commands";
import { confirm } from "@tauri-apps/plugin-dialog";
import { useUIStore } from "../../store/ui";
import { Dialog } from "../ui/Dialog";
@ -142,8 +143,8 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
{item.configured && (
<button
disabled={revokeMutation.isPending}
onClick={() => {
if (window.confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`)) {
onClick={async () => {
if (await confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`, { kind: "warning" })) {
revokeMutation.mutate(item.name);
}
}}

View File

@ -12,6 +12,7 @@ import {
type RuntimeToolStatus,
type ToolchainStatus,
} from "../../lib/commands";
import { confirm } from "@tauri-apps/plugin-dialog";
import { useUIStore } from "../../store/ui";
// ── 层元信息 ───────────────────────────────────────────────────────────────────
@ -113,8 +114,9 @@ export function ProjectEnvModal({ project, onClose }: Props) {
};
const handlePrepareToolchain = async () => {
const confirmed = window.confirm(
const confirmed = await confirm(
"炼境将通过 mise install 下载并准备项目已声明的 Node/pnpm 版本。不会修改 package.json 或 lockfile,是否继续?",
{ kind: "warning" },
);
if (!confirmed) return;
setPreparing(true);

View File

@ -7,6 +7,7 @@ import {
type GitRepo,
type UpdateGitRepoDeployInput,
} from "../../lib/commands";
import { confirm } from "@tauri-apps/plugin-dialog";
import { useUIStore } from "../../store/ui";
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
@ -127,7 +128,7 @@ export function RegistryTab() {
});
const handleDelete = async (id: string, name: string) => {
if (!confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`)) return;
if (!(await confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`, { kind: "warning" }))) return;
try {
await deleteGitRepo(id);
qc.invalidateQueries({ queryKey: ["git-repos"] });

View File

@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
// tauri-plugin-dialog 把 window.confirm 替换成 async 函数:同步写法拿到的是永远为真的 Promise,
// 确认框形同虚设(2026-09-30 撤销/删除未弹框即执行)。破坏性操作必须 await plugin-dialog 的 confirm。
const sources = import.meta.glob<string>(["../**/*.{ts,tsx}", "!../**/*.test.ts"], {
query: "?raw",
import: "default",
eager: true,
});
describe("confirm usage", () => {
it("scans the frontend sources", () => {
expect(Object.keys(sources).length).toBeGreaterThan(10);
});
it("never calls confirm without await", () => {
const offenders = Object.entries(sources).flatMap(([file, text]) =>
text
.split("\n")
.map((line, index) => ({ line, at: `${file}:${index + 1}` }))
.filter(({ line }) => /(^|[^\w.])(window\.)?confirm\(/.test(line) && !/await\s+(window\.)?confirm\(/.test(line))
.map(({ at }) => at),
);
expect(offenders).toEqual([]);
});
});