Merge remote-tracking branch 'upstream/main' into main (保留常驻通知与顺延迁移版本)
This commit is contained in:
@@ -366,3 +366,42 @@ test("banner announcement editor keeps title styles through edit, save and statu
|
||||
expect(apiSource).toContain("export type { BannerTitleRun }");
|
||||
});
|
||||
|
||||
test("admin console tokens and shell stay isolated from the user workspace", async () => {
|
||||
const [tokens, shell, chrome, globals] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/theme/admin-tokens.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/theme/admin-chrome.css", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(),
|
||||
]);
|
||||
|
||||
expect(tokens).toContain("--admin-canvas: #f5f5f5;");
|
||||
expect(tokens).toContain("--admin-canvas: #0f0f0f;");
|
||||
expect(tokens).not.toContain("--admin-layer-0: var(--workspace-");
|
||||
expect(tokens).not.toContain("--admin-layer-0: var(--skin-admin-");
|
||||
expect(shell).toContain("data-admin-root");
|
||||
expect(shell).toContain("getIsolatedAdminAntTheme");
|
||||
expect(shell).not.toContain("WorkspacePage");
|
||||
expect(shell).not.toContain("getAdminAntThemeConfig");
|
||||
expect(shell).not.toContain("app-workspace-nav-link");
|
||||
expect(chrome).toContain("[data-admin-root] .admin-nav-link");
|
||||
expect(chrome).toContain("border-left: 0 !important");
|
||||
expect(chrome).not.toContain("left: -8px");
|
||||
expect(chrome).toContain(".admin-drawer .ant-drawer-content");
|
||||
expect(globals).not.toContain("/* 管理端专用视觉收口:不覆盖创作端 workspace 的导航、状态和图表样式。 */");
|
||||
|
||||
const [overlays, userDetail, prompts, payments, modelEditor] = await Promise.all([
|
||||
Bun.file(new URL("../src/pages/admin/ui/overlays.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/admin-user-detail-drawer.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/storyboard-prompts/storyboard-prompts-page.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/payments/payments-page.tsx", import.meta.url)).text(),
|
||||
Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(),
|
||||
]);
|
||||
expect(overlays).toContain('rootClassName={cn("admin-drawer"');
|
||||
expect(overlays).toContain('rootClassName={cn("admin-modal-root"');
|
||||
expect(overlays).not.toContain("@/components/ui/product");
|
||||
for (const source of [userDetail, prompts, payments, modelEditor]) {
|
||||
expect(source).not.toContain("@/components/ui/product");
|
||||
expect(source).not.toContain("AppDrawer");
|
||||
expect(source).not.toContain("AppModal");
|
||||
}
|
||||
});
|
||||
|
||||
50
web/test/agent-interjection.test.ts
Normal file
50
web/test/agent-interjection.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
// 自研(2026-09-17):Agent 运行中插话。
|
||||
//
|
||||
// 这条功能的核心是**取消一个此前刻意加上的限制**:运行时输入框本来是禁用的
|
||||
// (旧代码 `disabled={busy || running || …}`),策略提示词也据此写着「运行中用户无法打字」。
|
||||
// 所以这里的断言要盯住两件事:入口真的打开了,以及旧前提没有残留在代码里。
|
||||
test("运行中不再锁死输入框,发送走插话而不是新建轮次", async () => {
|
||||
const panel = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-panel.tsx", import.meta.url)).text();
|
||||
|
||||
// 运行中走插话分支(不建轮次、不走那套扣费幂等记账)
|
||||
expect(panel).toContain("await interject(value)");
|
||||
expect(panel).toContain("sendAgentInterjection(activeRun.id, { text: value, messageId })");
|
||||
// 乐观落进时间线时带插话标记,与「开新一轮」的消息在界面上区分开
|
||||
expect(panel).toContain('interjection: "sent"');
|
||||
|
||||
// 输入框的禁用条件里不能再有 running —— 只保留「连不上事件流」「历史/待确认未水合」这类真不能发的状态
|
||||
expect(panel).toContain('disabled={Boolean(run && connectionStatus !== "connected")');
|
||||
expect(panel).not.toContain("disabled={busy || running ||");
|
||||
// 停止按钮由 running 驱动,发送按钮由我们自己的请求驱动,两者不再互斥
|
||||
expect(panel).toContain("sending={busy}");
|
||||
expect(panel).toContain("running={running}");
|
||||
|
||||
// 两个 SSE 事件都要处理:回显(多标签页)与未送达退回
|
||||
expect(panel).toContain('event.type === "user_interjection"');
|
||||
expect(panel).toContain('event.type === "user_interjection_dropped"');
|
||||
// 退回时要让文案能被再次发送
|
||||
expect(panel).toContain("setPrompt?.((current) => (current.trim() ? current : text))");
|
||||
});
|
||||
|
||||
test("输入框运行中同时给出「发送(插话)」与「停止」", async () => {
|
||||
const composer = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent-chat-ui.tsx", import.meta.url)).text();
|
||||
|
||||
// 关键改动:canStop 不再等于 sending —— 否则运行中两个动作只能二选一
|
||||
expect(composer).toContain("const canStop = Boolean(running && onStop);");
|
||||
expect(composer).toContain("const canSubmit = !disabled && !sending && Boolean(prompt.trim() || attachments.length);");
|
||||
expect(composer).not.toContain("const canStop = Boolean(sending && onStop);");
|
||||
expect(composer).toContain('aria-label="停止本轮"');
|
||||
// 插话/未送达要有可见标记
|
||||
expect(composer).toContain('interjection?: "sent" | "undelivered";');
|
||||
expect(composer).toContain('item.interjection === "undelivered" ? "插话未送达" : "插话"');
|
||||
});
|
||||
|
||||
test("插话走独立接口,不碰轮次接口", async () => {
|
||||
const api = await Bun.file(new URL("../src/services/api/agent.ts", import.meta.url)).text();
|
||||
|
||||
expect(api).toContain("export function sendAgentInterjection(");
|
||||
expect(api).toContain("/interjections");
|
||||
expect(api).toContain("messageId: string");
|
||||
});
|
||||
@@ -57,3 +57,35 @@ describe("parseAssetRecord", () => {
|
||||
expect(() => parseAssetRecordList([{ ...completeImage, id: "bad", tags: undefined }])).toThrow(/素材 bad/);
|
||||
});
|
||||
});
|
||||
|
||||
const completeVideo = {
|
||||
id: "video-1",
|
||||
kind: "video",
|
||||
title: "生成视频",
|
||||
coverUrl: "https://example.com/a.mp4",
|
||||
tags: ["生成"],
|
||||
createdAt: "2026-08-29T00:00:00.000Z",
|
||||
updatedAt: "2026-08-29T00:00:00.000Z",
|
||||
data: { url: "https://example.com/a.mp4", width: 720, height: 1280, bytes: 1, mimeType: "video/mp4" },
|
||||
};
|
||||
|
||||
describe("parseAssetRecord 视频尺寸", () => {
|
||||
test("宽高未知(0)放行,视频照常入库", () => {
|
||||
const parsed = parseAssetRecord({ ...completeVideo, data: { ...completeVideo.data, width: 0, height: 0 } });
|
||||
expect(parsed.kind).toBe("video");
|
||||
if (parsed.kind === "video") {
|
||||
expect(parsed.data.width).toBe(0);
|
||||
expect(parsed.data.height).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test("负数与非法值仍然拒", () => {
|
||||
expect(() => parseAssetRecord({ ...completeVideo, data: { ...completeVideo.data, width: -1 } })).toThrow(/width/);
|
||||
expect(() => parseAssetRecord({ ...completeVideo, data: { ...completeVideo.data, height: Number.NaN } })).toThrow(/height/);
|
||||
expect(() => parseAssetRecord({ ...completeVideo, data: { ...completeVideo.data, width: "720" } })).toThrow(/width/);
|
||||
});
|
||||
|
||||
test("图片不受影响:零尺寸仍然拒", () => {
|
||||
expect(() => parseAssetRecord({ ...completeImage, data: { ...completeImage.data, width: 0, height: 1 } })).toThrow(/width/);
|
||||
});
|
||||
});
|
||||
|
||||
82
web/test/cloud-agent-plan.test.ts
Normal file
82
web/test/cloud-agent-plan.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { agentPlanVisible, latestAgentPlanItems, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
|
||||
|
||||
const items = (...statuses: Array<"pending" | "doing" | "done">) => statuses.map((status, index) => ({ id: `${index + 1}`, title: `第 ${index + 1} 项`, status }));
|
||||
|
||||
/**
|
||||
* 回归(2026-09-16):一个对话会跨多轮运行,每轮各有一条 `plan-${runId}` 清单消息,
|
||||
* 都在同一个 messages 数组里。原来取的是**第一条**,于是清单永远钉在第 1 轮的初始状态 ——
|
||||
* 后面怎么推进都不刷新、全部做完也不消失(实测反馈:plan 完成了不更新、全完成也不关闭)。
|
||||
*/
|
||||
describe("latestAgentPlanItems", () => {
|
||||
test("跨轮取最新那份,而不是第一条", () => {
|
||||
const messages = [
|
||||
{ id: "plan-run-1", planItems: items("pending", "pending") },
|
||||
{ id: "user-1", text: "..." },
|
||||
{ id: "plan-run-2", planItems: items("done", "doing") },
|
||||
];
|
||||
expect(latestAgentPlanItems(messages).map((entry) => entry.status)).toEqual(["done", "doing"]);
|
||||
});
|
||||
|
||||
test("最新那份全部完成 → 清单收起", () => {
|
||||
const messages = [
|
||||
{ id: "plan-run-1", planItems: items("pending") },
|
||||
{ id: "plan-run-2", planItems: items("done", "done") },
|
||||
];
|
||||
const plan = latestAgentPlanItems(messages);
|
||||
expect(plan.every((entry) => entry.status === "done")).toBe(true);
|
||||
expect(agentPlanVisible(plan)).toBe(false);
|
||||
});
|
||||
|
||||
test("最新那份还没做完 → 继续显示", () => {
|
||||
expect(agentPlanVisible(latestAgentPlanItems([{ id: "plan-run-1", planItems: items("done", "doing") }]))).toBe(true);
|
||||
});
|
||||
|
||||
test("没有任何清单(包括带空数组的)→ 空且不显示", () => {
|
||||
expect(latestAgentPlanItems([])).toEqual([]);
|
||||
expect(latestAgentPlanItems([{ id: "user-1" }, { id: "plan-run-1", planItems: [] }])).toEqual([]);
|
||||
expect(agentPlanVisible([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 回归(2026-09-16):运行中输入框是禁用的,模型在正文里提问用户根本答不了
|
||||
* (生产实测它连问 4 轮、白烧十几步)。`ask_user` 让本轮收尾并把问题结构化,
|
||||
* 这个选择器决定「现在该不该把选项面板亮给用户」。
|
||||
*/
|
||||
describe("pendingAgentQuestion", () => {
|
||||
const question = { question: "选哪个模型?", options: [{ label: "H3文生视频" }, { label: "Seedance 2.5" }] };
|
||||
|
||||
test("有未作答的提问 → 亮面板", () => {
|
||||
const messages = [
|
||||
{ role: "user" },
|
||||
{ role: "assistant", question },
|
||||
];
|
||||
expect(pendingAgentQuestion(messages)?.question).toBe("选哪个模型?");
|
||||
});
|
||||
|
||||
test("提问之后用户已回复 → 收起面板", () => {
|
||||
const messages = [
|
||||
{ role: "assistant", question },
|
||||
{ role: "user" },
|
||||
];
|
||||
expect(pendingAgentQuestion(messages)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("多轮提问取最新那道;旧的那道不算数", () => {
|
||||
const later = { question: "确认开始吗?", options: [{ label: "开始" }, { label: "再改改" }] };
|
||||
const messages = [
|
||||
{ role: "assistant", question },
|
||||
{ role: "user" },
|
||||
{ role: "assistant", question },
|
||||
{ role: "assistant", question: later },
|
||||
];
|
||||
expect(pendingAgentQuestion(messages)?.question).toBe("确认开始吗?");
|
||||
});
|
||||
|
||||
test("没有提问 → 不亮面板", () => {
|
||||
expect(pendingAgentQuestion([])).toBeUndefined();
|
||||
expect(pendingAgentQuestion([{ role: "assistant" }, { role: "user" }])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,7 @@ test("welcome contributors parse the README HTML table", () => {
|
||||
});
|
||||
expect(people[0]?.signature).toContain("没有任何黑魔法");
|
||||
expect(people.some((person) => person.name === "爱笑的毛毛虫")).toBe(true);
|
||||
expect(people.some((person) => person.name === "morrow" && person.email === "3201190041@qq.com" && person.signature === "最难不过坚持")).toBe(true);
|
||||
expect(people.filter((person) => person.isFounder)).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user