test(02-01): 增加线程与 skills 合同回归测试
- 新增 node:test 覆盖线程路由与 bootstrap 合同归一 - 更新 e2e 路由辅助与用例,移除 isnew 依赖
This commit is contained in:
parent
034e35c880
commit
c01ac7b8de
|
|
@ -0,0 +1,34 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { normalizeBootstrapRemoteSkillRequest } = await import(
|
||||
new URL("./normalize-bootstrap.ts", import.meta.url).href
|
||||
);
|
||||
|
||||
void test("keeps content_ids as primary contract", () => {
|
||||
const normalized = normalizeBootstrapRemoteSkillRequest({
|
||||
thread_id: "t1",
|
||||
content_ids: [11, 22],
|
||||
});
|
||||
|
||||
assert.deepEqual(normalized.content_ids, [11, 22]);
|
||||
});
|
||||
|
||||
void test("maps legacy content_id to content_ids for compatibility", () => {
|
||||
const normalized = normalizeBootstrapRemoteSkillRequest({
|
||||
thread_id: "t1",
|
||||
content_id: 7,
|
||||
});
|
||||
|
||||
assert.deepEqual(normalized.content_ids, [7]);
|
||||
});
|
||||
|
||||
void test("throws when neither content_ids nor content_id is provided", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeBootstrapRemoteSkillRequest({
|
||||
thread_id: "t1",
|
||||
}),
|
||||
/content_ids is required/,
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const { resolveThreadQueryIntent } = await import(
|
||||
new URL("./utils.ts", import.meta.url).href
|
||||
);
|
||||
|
||||
void test("uses /chats/new route as the only new-session signal", () => {
|
||||
const intent = resolveThreadQueryIntent({
|
||||
pathThreadId: "new",
|
||||
queryThreadId: "thread-from-query",
|
||||
isNewRoute: true,
|
||||
});
|
||||
|
||||
assert.equal(intent.isNewThread, true);
|
||||
assert.equal(intent.showWelcomeStyle, true);
|
||||
assert.equal(intent.threadId, "thread-from-query");
|
||||
});
|
||||
|
||||
void test("prefers path thread id over query thread id when not on /new", () => {
|
||||
const intent = resolveThreadQueryIntent({
|
||||
pathThreadId: "thread-from-path",
|
||||
queryThreadId: "thread-from-query",
|
||||
isNewRoute: false,
|
||||
});
|
||||
|
||||
assert.equal(intent.isNewThread, false);
|
||||
assert.equal(intent.threadId, "thread-from-path");
|
||||
assert.equal(intent.invalidNewRoute, false);
|
||||
});
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import { expect, type Page, type TestInfo } from "@playwright/test";
|
||||
|
||||
const rawPrimaryThreadId = process.env.FRONTEND_E2E_THREAD_ID?.trim();
|
||||
|
||||
function envThread(name: string) {
|
||||
return process.env[name]?.trim() ?? undefined;
|
||||
}
|
||||
|
||||
export const PRIMARY_THREAD_ID = rawPrimaryThreadId ?? undefined;
|
||||
export const THREAD_FOR_WELCOME = PRIMARY_THREAD_ID;
|
||||
export const THREAD_WITH_HISTORY = PRIMARY_THREAD_ID;
|
||||
export const THREAD_WITH_MARKDOWN = envThread("FRONTEND_E2E_MARKDOWN_THREAD_ID");
|
||||
export const THREAD_WITH_TODOS = envThread("FRONTEND_E2E_TODOS_THREAD_ID");
|
||||
export const THREAD_WITH_ARTIFACTS = envThread("FRONTEND_E2E_ARTIFACTS_THREAD_ID");
|
||||
export const THREAD_WITH_IMAGE_ARTIFACT = envThread(
|
||||
"FRONTEND_E2E_IMAGE_ARTIFACT_THREAD_ID",
|
||||
);
|
||||
export const THREAD_WITH_HTML_ARTIFACT = envThread(
|
||||
"FRONTEND_E2E_HTML_ARTIFACT_THREAD_ID",
|
||||
);
|
||||
|
||||
export function skipIfMissingThread(
|
||||
testInfo: TestInfo,
|
||||
threadId: string | undefined,
|
||||
label: string,
|
||||
) {
|
||||
testInfo.skip(!threadId, `未配置 ${label}。`);
|
||||
}
|
||||
|
||||
export function buildChatUrl({
|
||||
pathThreadId,
|
||||
xclawUsed,
|
||||
threadId,
|
||||
}: {
|
||||
pathThreadId?: string;
|
||||
xclawUsed: boolean;
|
||||
threadId?: string;
|
||||
}) {
|
||||
const resolvedThreadId = threadId ?? pathThreadId;
|
||||
if (!pathThreadId && !resolvedThreadId) {
|
||||
throw new Error("threadId is required for /workspace/chats/new routes.");
|
||||
}
|
||||
|
||||
const query = new URLSearchParams();
|
||||
query.set("xclaw_used", String(xclawUsed));
|
||||
if (resolvedThreadId) {
|
||||
query.set("thread_id", resolvedThreadId);
|
||||
}
|
||||
|
||||
const basePath = pathThreadId
|
||||
? `/workspace/chats/${pathThreadId}`
|
||||
: "/workspace/chats/new";
|
||||
return `${basePath}?${query.toString()}`;
|
||||
}
|
||||
|
||||
export function invalidNewChatUrl() {
|
||||
const query = new URLSearchParams();
|
||||
query.set("xclaw_used", "false");
|
||||
return `/workspace/chats/new?${query.toString()}`;
|
||||
}
|
||||
|
||||
export function newChatEntry(threadId: string) {
|
||||
return buildChatUrl({
|
||||
xclawUsed: false,
|
||||
threadId,
|
||||
});
|
||||
}
|
||||
|
||||
export function reuseThreadWelcomeEntry(threadId: string) {
|
||||
return buildChatUrl({
|
||||
xclawUsed: false,
|
||||
threadId,
|
||||
});
|
||||
}
|
||||
|
||||
export function reuseThreadChatEntry(threadId: string) {
|
||||
return buildChatUrl({
|
||||
pathThreadId: threadId,
|
||||
xclawUsed: true,
|
||||
threadId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function openChat(
|
||||
page: Page,
|
||||
url: string,
|
||||
options?: { expectInput?: boolean },
|
||||
) {
|
||||
await page.goto(url);
|
||||
if (options?.expectInput ?? true) {
|
||||
await expect(page.locator("textarea[name='message']")).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendMessage(page: Page, text: string) {
|
||||
const textarea = page.locator("textarea[name='message']");
|
||||
const submit = page.locator("button[aria-label='Submit']");
|
||||
await textarea.click();
|
||||
await textarea.fill(text);
|
||||
await submit.click();
|
||||
}
|
||||
|
||||
export async function waitForMessageListReady(
|
||||
page: Page,
|
||||
options?: { requireMessages?: boolean; minMessages?: number },
|
||||
) {
|
||||
const { requireMessages = false, minMessages = 1 } = options ?? {};
|
||||
await expect(page.getByRole("main").first()).toBeVisible();
|
||||
if (requireMessages) {
|
||||
await expect
|
||||
.poll(
|
||||
async () => await page.locator(".is-user, .is-assistant").count(),
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBeGreaterThan(minMessages - 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
THREAD_FOR_WELCOME,
|
||||
newChatEntry,
|
||||
openChat,
|
||||
reuseThreadChatEntry,
|
||||
skipIfMissingThread,
|
||||
waitForMessageListReady,
|
||||
} from "./support/chat-helpers";
|
||||
|
||||
test.describe("线程路由(无 isnew)", () => {
|
||||
test("/new 始终走欢迎态,发送后进入具体 thread 路由", async ({ page }, testInfo) => {
|
||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
||||
|
||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||
});
|
||||
|
||||
test("/chats/:thread_id 直接复用并渲染历史", async ({ page }, testInfo) => {
|
||||
skipIfMissingThread(testInfo, THREAD_FOR_WELCOME, "FRONTEND_E2E_THREAD_ID");
|
||||
|
||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||
await waitForMessageListReady(page, { requireMessages: true });
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/workspace/chats/${THREAD_FOR_WELCOME!}`));
|
||||
await expect(page.locator(".is-user, .is-assistant").first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
THREAD_FOR_WELCOME,
|
||||
invalidNewChatUrl,
|
||||
newChatEntry,
|
||||
openChat,
|
||||
reuseThreadChatEntry,
|
||||
reuseThreadWelcomeEntry,
|
||||
skipIfMissingThread,
|
||||
waitForMessageListReady,
|
||||
} from "./support/chat-helpers";
|
||||
|
||||
test.describe("聊天工作台 / 路由与欢迎态", () => {
|
||||
test("DF-ROUTE-001 /new 带 thread_id 时展示欢迎态与建议词", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_FOR_WELCOME,
|
||||
"FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await openChat(page, newChatEntry(THREAD_FOR_WELCOME!));
|
||||
|
||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||
await expect(page.getByText(/Webpage|网页/)).toBeVisible();
|
||||
await expect(page.locator(".is-user, .is-assistant")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("DF-ROUTE-002 /new 复用模式保留欢迎态且不直接渲染历史", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_FOR_WELCOME,
|
||||
"FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await openChat(page, reuseThreadWelcomeEntry(THREAD_FOR_WELCOME!));
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/workspace/chats/new\\?.*thread_id=${THREAD_FOR_WELCOME!}`),
|
||||
);
|
||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||
await expect(page.locator(".is-user, .is-assistant")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("DF-ROUTE-003 /new 缺少 thread_id 时进入欢迎态", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(invalidNewChatUrl());
|
||||
|
||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||
await expect(page.locator("textarea[name='message']")).toBeVisible();
|
||||
});
|
||||
|
||||
test("DF-ROUTE-004 线程页直接展示历史消息与标题栏", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_FOR_WELCOME,
|
||||
"FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||
await waitForMessageListReady(page, { requireMessages: true });
|
||||
|
||||
await expect(page.locator(".is-user, .is-assistant").first()).toBeVisible();
|
||||
await expect(page.locator("header button").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("DF-ROUTE-005 退出确认取消后保持当前线程页面", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_FOR_WELCOME,
|
||||
"FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||
await waitForMessageListReady(page, { requireMessages: true });
|
||||
|
||||
await page.locator("header button").first().click();
|
||||
await expect(page.getByText("退出后,当前会话结束并销毁")).toBeVisible();
|
||||
await page.getByRole("button", { name: "取消" }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/workspace/chats/${THREAD_FOR_WELCOME!}`),
|
||||
);
|
||||
});
|
||||
|
||||
test("DF-ROUTE-006 退出确认确认后返回带 thread_id 的 /new", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
skipIfMissingThread(
|
||||
testInfo,
|
||||
THREAD_FOR_WELCOME,
|
||||
"FRONTEND_E2E_THREAD_ID",
|
||||
);
|
||||
await openChat(page, reuseThreadChatEntry(THREAD_FOR_WELCOME!));
|
||||
await waitForMessageListReady(page, { requireMessages: true });
|
||||
|
||||
await page.locator("header button").first().click();
|
||||
await page.getByRole("button", { name: "确定" }).click();
|
||||
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(
|
||||
`/workspace/chats/new\\?.*xclaw_used=false.*thread_id=${THREAD_FOR_WELCOME!}`,
|
||||
),
|
||||
);
|
||||
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue