mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(studio): hold restore choices while awaiting receipts
This commit is contained in:
1 parent
65aef1e228
commit
b924372004
2 files changed
+170
-3
No files matched your search
@@ -0,0 +1,167 @@
|
||||
// @vitest-environment jsdom
|
||||
import { StrictMode } from "react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "./testkit";
|
||||
import { BackupRestore } from "./BackupRestore";
|
||||
import { SsePort } from "../port/sse";
|
||||
import { boot, STORAGE } from "../i18n";
|
||||
import type { BackupEntry, BackupPlan } from "../port/port";
|
||||
|
||||
const entry: BackupEntry = {
|
||||
id: "saved/backup", label: "laptop", format: 1, appVersion: "2.29.0", platform: "darwin/arm64",
|
||||
categories: ["settings", "automation", "memory"], ciphertextBytes: 2048, createdAt: "2026-10-01T00:00:00Z",
|
||||
};
|
||||
const plan: BackupPlan = {
|
||||
planId: "restore-ticket", createdAt: entry.createdAt, platform: entry.platform, samePlatform: true,
|
||||
categories: entry.categories, items: [
|
||||
{ id: "interface", category: "settings", kind: "interface", name: "Interface", status: "changed", recommended: true },
|
||||
{ id: "hook:PreToolUse#1", category: "automation", kind: "hook", name: "PreToolUse#1", status: "new", consent: "executes", summary: "npx prettier --check .", recommended: false },
|
||||
{ id: "memory:docs/REASONIX.md", category: "memory", kind: "memory", name: "REASONIX.md", status: "new", recommended: true },
|
||||
],
|
||||
};
|
||||
|
||||
function reply(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((yes) => { resolve = yes; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function draw(strict: boolean, held: "preview" | "apply" = "preview") {
|
||||
localStorage.setItem(STORAGE, "zh");
|
||||
boot();
|
||||
const pending = deferred<Response>();
|
||||
const previews: { passphrase: string }[] = [];
|
||||
const applies: { planId: string; items: string[]; consented: string[] }[] = [];
|
||||
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const path = new URL(String(input)).pathname;
|
||||
if (init?.method === "POST" && path === "/backups/saved%2Fbackup/preview") {
|
||||
previews.push(JSON.parse(String(init.body)) as { passphrase: string });
|
||||
return held === "preview" ? pending.promise : Promise.resolve(reply(plan));
|
||||
}
|
||||
if (init?.method === "POST" && path === "/backups/apply") {
|
||||
applies.push(JSON.parse(String(init.body)) as typeof applies[number]);
|
||||
return pending.promise;
|
||||
}
|
||||
throw new Error(`Unexpected ${init?.method} ${path}`);
|
||||
}));
|
||||
const onClose = vi.fn();
|
||||
const host = <BackupRestore port={new SsePort("http://kernel.example")} backup={entry} onClose={onClose} />;
|
||||
return { ...render(strict ? <StrictMode>{host}</StrictMode> : host), pending, previews, applies, onClose };
|
||||
}
|
||||
|
||||
function pick(id: string) {
|
||||
return document.querySelector<HTMLInputElement>(`[data-action="backup.pick"][data-target="${id}"]`)!;
|
||||
}
|
||||
|
||||
function choice() {
|
||||
return {
|
||||
picked: Array.from(document.querySelectorAll<HTMLInputElement>('[data-action="backup.pick"]')).filter((input) => input.checked).map((input) => input.dataset.target),
|
||||
consented: Array.from(document.querySelectorAll<HTMLInputElement>('[data-action="backup.allow"]')).filter((input) => input.checked).map((input) => input.dataset.target),
|
||||
};
|
||||
}
|
||||
|
||||
async function preview() {
|
||||
await userEvent.type(screen.getByLabelText("加密口令"), "a long passphrase");
|
||||
await userEvent.click(screen.getByRole("button", { name: "预览差异" }));
|
||||
}
|
||||
|
||||
async function prepareApply() {
|
||||
await preview();
|
||||
await screen.findByText("PreToolUse#1");
|
||||
await userEvent.click(pick("hook:PreToolUse#1"));
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: /恢复所选/ }).disabled).toBe(true);
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: /允许它在本机运行/ }));
|
||||
return choice();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
localStorage.setItem(STORAGE, "zh");
|
||||
boot();
|
||||
});
|
||||
|
||||
it.each([false, true])("holds the passphrase until preview succeeds (StrictMode=%s)", async (strict) => {
|
||||
const { pending, previews } = draw(strict);
|
||||
await preview();
|
||||
expect(previews).toEqual([{ passphrase: "a long passphrase" }]);
|
||||
await userEvent.type(screen.getByLabelText("加密口令"), " not captured");
|
||||
const during = screen.getByLabelText<HTMLInputElement>("加密口令").value;
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: "取消" }).disabled).toBe(false);
|
||||
await act(async () => pending.resolve(reply(plan)));
|
||||
await screen.findByText("PreToolUse#1");
|
||||
expect(screen.queryByLabelText("加密口令")).toBeNull();
|
||||
expect(during).toBe("a long passphrase");
|
||||
await userEvent.click(pick("interface"));
|
||||
expect(pick("interface").checked).toBe(false);
|
||||
expect(previews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([false, true])("preserves and unlocks the passphrase after preview refusal (StrictMode=%s)", async (strict) => {
|
||||
const { pending, previews } = draw(strict);
|
||||
await preview();
|
||||
expect(previews).toEqual([{ passphrase: "a long passphrase" }]);
|
||||
await userEvent.type(screen.getByLabelText("加密口令"), " not captured");
|
||||
await act(async () => pending.resolve(reply({ code: "backup.cloud_unavailable", error: "controlled cloud failure", params: { detail: "controlled cloud failure" } }, 502)));
|
||||
await screen.findByText("暂时连不上备份服务,稍后再试:controlled cloud failure");
|
||||
expect(screen.getByLabelText<HTMLInputElement>("加密口令").value).toBe("a long passphrase");
|
||||
await userEvent.clear(screen.getByLabelText("加密口令"));
|
||||
await userEvent.type(screen.getByLabelText("加密口令"), "retry passphrase");
|
||||
expect(screen.getByLabelText<HTMLInputElement>("加密口令").value).toBe("retry passphrase");
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: "预览差异" }).disabled).toBe(false);
|
||||
expect(previews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([false, true])("holds selected items and consents until apply succeeds (StrictMode=%s)", async (strict) => {
|
||||
const { pending, applies } = draw(strict, "apply");
|
||||
const initial = await prepareApply();
|
||||
await userEvent.click(screen.getByRole("button", { name: /恢复所选/ }));
|
||||
expect(applies).toEqual([{ planId: plan.planId, items: initial.picked, consented: initial.consented }]);
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: /允许它在本机运行/ }));
|
||||
await userEvent.click(pick("interface"));
|
||||
await userEvent.click(pick("memory:docs/REASONIX.md"));
|
||||
const during = choice();
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: "取消" }).disabled).toBe(false);
|
||||
await act(async () => pending.resolve(reply({ applied: initial.picked })));
|
||||
await screen.findByText("已恢复 3 项。");
|
||||
expect(during).toEqual(initial);
|
||||
expect(applies).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([false, true])("preserves and unlocks item choices after apply refusal (StrictMode=%s)", async (strict) => {
|
||||
const { pending, applies } = draw(strict, "apply");
|
||||
const initial = await prepareApply();
|
||||
await userEvent.click(screen.getByRole("button", { name: /恢复所选/ }));
|
||||
expect(applies).toEqual([{ planId: plan.planId, items: initial.picked, consented: initial.consented }]);
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: /允许它在本机运行/ }));
|
||||
await userEvent.click(pick("interface"));
|
||||
await act(async () => pending.resolve(reply({ code: "backup.plan_expired", error: "restore preview expired" }, 410)));
|
||||
await screen.findByText("预览已失效,请重新打开这份备份");
|
||||
expect(choice()).toEqual(initial);
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: /允许它在本机运行/ }));
|
||||
expect(choice().consented).toEqual([]);
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: /恢复所选/ }).disabled).toBe(true);
|
||||
await userEvent.click(pick("hook:PreToolUse#1"));
|
||||
expect(pick("hook:PreToolUse#1").checked).toBe(false);
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: /恢复所选/ }).disabled).toBe(false);
|
||||
expect(applies).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([false, true])("keeps normal selection and consent editing available (StrictMode=%s)", async (strict) => {
|
||||
const { applies } = draw(strict, "apply");
|
||||
const initial = await prepareApply();
|
||||
expect(initial.consented).toEqual(["hook:PreToolUse#1"]);
|
||||
await userEvent.click(pick("interface"));
|
||||
expect(pick("interface").checked).toBe(false);
|
||||
await userEvent.click(screen.getByRole("checkbox", { name: /允许它在本机运行/ }));
|
||||
expect(choice().consented).toEqual([]);
|
||||
expect(screen.getByRole<HTMLButtonElement>("button", { name: /恢复所选/ }).disabled).toBe(true);
|
||||
expect(applies).toHaveLength(0);
|
||||
});
|
||||
@@ -124,7 +124,7 @@ export function BackupRestore({ port, backup, onClose }: { port: AgentPort; back
|
||||
<div className="bk-fields">
|
||||
<label className="grow">
|
||||
<span>{t("加密口令")}</span>
|
||||
<input data-action="backup.passphrase" data-target="open" type="password" autoComplete="current-password" value={pass} onChange={(e) => setPass(e.target.value)} />
|
||||
<input data-action="backup.passphrase" data-target="open" type="password" autoComplete="current-password" disabled={busy} value={pass} onChange={(e) => setPass(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="bk-acts">
|
||||
@@ -150,7 +150,7 @@ export function BackupRestore({ port, backup, onClose }: { port: AgentPort; back
|
||||
{plan.items.map((it) => (
|
||||
<li key={it.id} className="bk-item" data-status={it.status} data-consent={it.consent ? "" : undefined}>
|
||||
<label className="take">
|
||||
<input type="checkbox" checked={picked.has(it.id)} onChange={() => setPicked((s) => flip(s, it.id))} data-action="backup.pick" data-target={it.id} />
|
||||
<input type="checkbox" disabled={busy} checked={picked.has(it.id)} onChange={() => setPicked((s) => flip(s, it.id))} data-action="backup.pick" data-target={it.id} />
|
||||
<span className="cat">{t(CATEGORY_LABEL[it.category])}</span>
|
||||
<span className="nm">{it.name}</span>
|
||||
<span className="st">{t(STATUS_LABEL[it.status])}</span>
|
||||
@@ -168,7 +168,7 @@ export function BackupRestore({ port, backup, onClose }: { port: AgentPort; back
|
||||
{it.crossPlatform && <span className="warn">{t("来自另一种系统,可能无法运行")}</span>}
|
||||
{it.consent && picked.has(it.id) && (
|
||||
<label className="allow">
|
||||
<input type="checkbox" checked={allowed.has(it.id)} onChange={() => setAllowed((s) => flip(s, it.id))} data-action="backup.allow" data-target={it.id} />
|
||||
<input type="checkbox" disabled={busy} checked={allowed.has(it.id)} onChange={() => setAllowed((s) => flip(s, it.id))} data-action="backup.allow" data-target={it.id} />
|
||||
<span>{t(CONSENT_LABEL[it.consent])}</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user