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