- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
164 lines
6.5 KiB
TypeScript
164 lines
6.5 KiB
TypeScript
/** 组件传输替身只验证渲染与取数路径;真实后端旅程由 Playwright 承担。 */
|
||
import { afterEach, expect, it, vi } from "vitest";
|
||
import {
|
||
cleanup,
|
||
render,
|
||
screen,
|
||
fireEvent,
|
||
waitFor,
|
||
} from "@testing-library/react";
|
||
import { Ui依赖装配 } from "../../src/应用/依赖装配";
|
||
import { Ui任务上下文 } from "../../src/功能/上下文";
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
const 有效快照 = {
|
||
snapshot_id: "snap-1",
|
||
task_id: "task-1",
|
||
work_id: "work-1",
|
||
input_hash: "a".repeat(64),
|
||
state: "有效",
|
||
invalidation: null,
|
||
changes: [] as string[],
|
||
assembly: {
|
||
included_sources: ["目标章细纲", "历史:ch-1"],
|
||
text_hash: "b".repeat(64),
|
||
byte_size: 328,
|
||
omissions: [{ source: "参考:超长素材", reason: "BUDGET_EXCEEDED" }],
|
||
},
|
||
sources: [
|
||
{
|
||
source_id: "doc-1",
|
||
data_version: "1",
|
||
schema_hash: "c".repeat(64),
|
||
projection_version: "visible-text-codepoints/v1:production:generation",
|
||
current_state: "有效",
|
||
},
|
||
],
|
||
};
|
||
|
||
function 桩快照(快照: unknown) {
|
||
return vi.fn(async (request: Request) => {
|
||
expect(new URL(request.url).pathname).toBe(
|
||
"/api/v1/tasks/task-1/context-snapshots/page",
|
||
);
|
||
return new Response(
|
||
JSON.stringify({ items: 快照, has_more: false, next_cursor: null }),
|
||
{
|
||
headers: { "Content-Type": "application/json" },
|
||
},
|
||
);
|
||
});
|
||
}
|
||
|
||
// @muse-case {"case_id":"NC-ui-context-snapshot-list","environment":"离线组件,传输替身","given":"含省略条目的有效快照","when":"渲染任务上下文组件","then":["材料清单、字节数与省略原因渲染;取数走 /tasks/{id}/context-snapshots"],"contract":"docs/系统架构/新版设计/模块设计/B09-上下文.md"}
|
||
it("NC-ui-context-snapshot-list:材料清单与省略原因渲染", async () => {
|
||
vi.stubGlobal("fetch", 桩快照([有效快照]));
|
||
render(
|
||
<Ui依赖装配>
|
||
<Ui任务上下文 taskId="task-1" scope="work-1" />
|
||
</Ui依赖装配>,
|
||
);
|
||
expect(
|
||
await screen.findByText("目标章细纲", { selector: ".step-list strong" }),
|
||
).toBeTruthy();
|
||
expect(screen.getByText("历史:ch-1")).toBeTruthy();
|
||
expect(screen.getByText("省略原因:超出材料预算,整项省略")).toBeTruthy();
|
||
expect(screen.getByText(/材料 328 字节/)).toBeTruthy();
|
||
expect(screen.getByText(new RegExp("b".repeat(64)))).toBeTruthy();
|
||
expect(screen.getByText("doc-1")).toBeTruthy();
|
||
expect(screen.getByText("版本 1")).toBeTruthy();
|
||
expect(
|
||
screen.getByText(/visible-text-codepoints\/v1:production:generation/),
|
||
).toBeTruthy();
|
||
expect(screen.getByText("仍有效")).toBeTruthy();
|
||
expect(screen.queryByRole("status")).toBeNull();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-context-stale-evidence","environment":"离线组件,传输替身","given":"漂移后的快照","when":"渲染任务上下文组件","then":["失效标注与具体变化项可见;来源证据仍显示版本与哈希"],"contract":"docs/系统架构/新版设计/模块设计/B09-上下文.md"}
|
||
it("NC-ui-context-stale-evidence:来源漂移后失效标注与具体变化可见", async () => {
|
||
const 漂移快照 = {
|
||
...有效快照,
|
||
state: "SOURCE_STALE",
|
||
invalidation:
|
||
"文稿 ch-1 已漂移(当前版本 2,冻结版本 1);冻结范围当前失效,需重新授权",
|
||
changes: ["文稿 ch-1 已从版本 1 前进到 2;来源锁定失效"],
|
||
sources: [
|
||
{
|
||
...有效快照.sources[0],
|
||
data_version: "1",
|
||
current_state: "SOURCE_STALE",
|
||
},
|
||
],
|
||
};
|
||
vi.stubGlobal("fetch", 桩快照([漂移快照]));
|
||
render(
|
||
<Ui依赖装配>
|
||
<Ui任务上下文 taskId="task-1" scope="work-1" />
|
||
</Ui依赖装配>,
|
||
);
|
||
expect(await screen.findByText("来源已漂移,需重新授权")).toBeTruthy();
|
||
expect(screen.getByText(/文稿 ch-1 已漂移/)).toBeTruthy();
|
||
expect(
|
||
screen.getByText("文稿 ch-1 已从版本 1 前进到 2;来源锁定失效"),
|
||
).toBeTruthy();
|
||
expect(screen.getByText("已漂移")).toBeTruthy();
|
||
// 失效不改写历史装配:清单与省略原因仍按冻结时原样显示。
|
||
expect(screen.getByText("历史:ch-1")).toBeTruthy();
|
||
expect(screen.getByText("省略原因:超出材料预算,整项省略")).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-context-empty-state","environment":"离线组件,传输替身","given":"没有快照的任务","when":"渲染任务上下文组件","then":["如实显示空态,不伪造材料"],"contract":"docs/系统架构/新版设计/模块设计/B09-上下文.md"}
|
||
it("NC-ui-context-empty-state:无快照任务如实显示空态", async () => {
|
||
vi.stubGlobal("fetch", 桩快照([]));
|
||
render(
|
||
<Ui依赖装配>
|
||
<Ui任务上下文 taskId="task-1" scope={null} />
|
||
</Ui依赖装配>,
|
||
);
|
||
expect(await screen.findByText("这个任务还没有冻结材料快照")).toBeTruthy();
|
||
expect(screen.getByText("0 份冻结快照")).toBeTruthy();
|
||
});
|
||
|
||
// @muse-case {"case_id":"O08-context-explicit-page","then":"材料分页初始只读20份,只有点击才读下一页,连续加载最多保留3页"}
|
||
it("O08-context-explicit-page:材料快照按页读取且缓存有界", async () => {
|
||
const reads: string[] = [];
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const url = new URL(request.url);
|
||
reads.push(url.searchParams.get("cursor") ?? "first");
|
||
expect(url.pathname).toBe("/api/v1/tasks/task-1/context-snapshots/page");
|
||
expect(url.searchParams.get("limit")).toBe("20");
|
||
const page = Number(url.searchParams.get("cursor") ?? 0);
|
||
return new Response(
|
||
JSON.stringify({
|
||
items: Array.from({ length: 20 }, (_, index) => ({
|
||
...有效快照,
|
||
snapshot_id: `s-${page}-${index}`,
|
||
})),
|
||
has_more: page < 3,
|
||
next_cursor: page < 3 ? String(page + 1) : null,
|
||
}),
|
||
{ headers: { "Content-Type": "application/json" } },
|
||
);
|
||
}),
|
||
);
|
||
render(
|
||
<Ui依赖装配>
|
||
<Ui任务上下文 taskId="task-1" scope="work-1" />
|
||
</Ui依赖装配>,
|
||
);
|
||
await screen.findByText("20 份冻结快照");
|
||
expect(reads).toEqual(["first"]);
|
||
for (let page = 1; page <= 3; page++) {
|
||
fireEvent.click(screen.getByRole("button", { name: "加载更多材料快照" }));
|
||
await waitFor(() => expect(reads).toHaveLength(page + 1));
|
||
await screen.findByText(`${Math.min(page + 1, 3) * 20} 份冻结快照`);
|
||
}
|
||
expect(screen.queryByRole("button", { name: "加载更多材料快照" })).toBeNull();
|
||
});
|