- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import { afterEach, expect, it, vi } from "vitest";
|
||
import {
|
||
候选浏览器缓冲,
|
||
候选缓冲键,
|
||
旧候选缓冲键,
|
||
type 候选范围,
|
||
type 改稿缓冲,
|
||
} from "../../src/功能/正文写作/候选缓冲";
|
||
const scope: 候选范围 = {
|
||
author_id: "author",
|
||
work_id: "work",
|
||
chapter_id: "chapter",
|
||
branch_id: "A",
|
||
candidate_id: "new",
|
||
};
|
||
const draft: 改稿缓冲 = {
|
||
document: {
|
||
format_version: 1,
|
||
paragraphs: [
|
||
{
|
||
paragraph_id: "p",
|
||
content: [{ type: "text", text: "A稿", marks: [] }],
|
||
},
|
||
],
|
||
},
|
||
base_revision: 7,
|
||
base_hash: "hash",
|
||
mode: "edit",
|
||
selected: [],
|
||
};
|
||
afterEach(() => {
|
||
sessionStorage.clear();
|
||
vi.restoreAllMocks();
|
||
});
|
||
// @muse-case {"case_id": "O01-candidate-branch-scope", "environment": "离线前端单元", "when": "同版本的两分支草稿互不读取或覆盖,分隔符无歧义", "contract": "docs/系统架构/新版设计/工作台设计.md"}
|
||
it("O01-candidate-branch-scope:同版本的两分支草稿互不读取或覆盖,分隔符无歧义", () => {
|
||
const a = 候选浏览器缓冲(scope),
|
||
bScope = { ...scope, branch_id: "B" },
|
||
b = 候选浏览器缓冲(bScope);
|
||
a.read();
|
||
b.read();
|
||
a.write(draft);
|
||
expect(b.read()).toBeUndefined();
|
||
b.write({ ...draft, base_hash: "B" });
|
||
expect(a.read()).toEqual(draft);
|
||
expect(候选缓冲键({ ...scope, author_id: "a:b", work_id: "c" })).not.toEqual(
|
||
候选缓冲键({ ...scope, author_id: "a", work_id: "b:c" }),
|
||
);
|
||
});
|
||
// @muse-case {"case_id": "O01-candidate-original-pending", "environment": "离线前端单元", "when": "刷新保持原请求,跨分支pending拒绝且字节不变", "contract": "docs/系统架构/新版设计/工作台设计.md"}
|
||
it("O01-candidate-original-pending:刷新保持原请求,跨分支pending拒绝且字节不变", () => {
|
||
const pending = {
|
||
kind: "create" as const,
|
||
body: {
|
||
command_id: "original",
|
||
chapter_id: "chapter",
|
||
branch_id: "A",
|
||
expected_revision: 7,
|
||
document: draft.document,
|
||
},
|
||
};
|
||
const a = 候选浏览器缓冲(scope);
|
||
a.read();
|
||
a.write({ ...draft, pending });
|
||
expect(候选浏览器缓冲(scope).read()?.pending).toEqual(pending);
|
||
const other = { ...scope, branch_id: "B" },
|
||
key = 候选缓冲键(other);
|
||
const raw = JSON.stringify({
|
||
schema_version: 2,
|
||
scope: other,
|
||
draft: { ...draft, pending },
|
||
});
|
||
sessionStorage.setItem(key, raw);
|
||
expect(() => 候选浏览器缓冲(other).read()).toThrow("分支 A");
|
||
expect(sessionStorage.getItem(key)).toBe(raw);
|
||
});
|
||
// @muse-case {"case_id": "O01-candidate-owner-quota", "environment": "离线前端单元", "when": "迟到clear不删新owner,配额失败保留旧字节", "contract": "docs/系统架构/新版设计/工作台设计.md"}
|
||
it("O01-candidate-owner-quota:迟到clear不删新owner,配额失败保留旧字节", () => {
|
||
const a = 候选浏览器缓冲(scope);
|
||
a.read();
|
||
a.write(draft);
|
||
const b = 候选浏览器缓冲(scope);
|
||
b.read();
|
||
b.write({ ...draft, base_hash: "new" });
|
||
const raw = sessionStorage.getItem(候选缓冲键(scope));
|
||
expect(() => a.write(draft)).toThrow("其他会话");
|
||
a.clear();
|
||
expect(sessionStorage.getItem(候选缓冲键(scope))).toBe(raw);
|
||
vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
||
throw new Error("quota");
|
||
});
|
||
expect(() => b.write(draft)).toThrow("quota");
|
||
expect(sessionStorage.getItem(候选缓冲键(scope))).toBe(raw);
|
||
});
|
||
// @muse-case {"case_id": "O01-candidate-legacy-corrupt", "environment": "离线前端单元", "when": "旧键只读保留且损坏新记录不允许覆盖", "contract": "docs/系统架构/新版设计/工作台设计.md"}
|
||
it("O01-candidate-legacy-corrupt:旧键只读保留且损坏新记录不允许覆盖", () => {
|
||
const key = 旧候选缓冲键(scope),
|
||
raw = JSON.stringify(draft);
|
||
sessionStorage.setItem(key, raw);
|
||
const a = 候选浏览器缓冲(scope);
|
||
expect(a.legacy()?.value).toEqual(draft);
|
||
a.read();
|
||
a.write(draft);
|
||
a.clear();
|
||
expect(sessionStorage.getItem(key)).toBe(raw);
|
||
sessionStorage.setItem(候选缓冲键(scope), "{broken");
|
||
expect(() => a.read()).toThrow();
|
||
expect(() => a.write(draft)).toThrow("不能覆盖");
|
||
expect(sessionStorage.getItem(候选缓冲键(scope))).toBe("{broken");
|
||
});
|