接入后台架构标签与端到端验证
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const jobId = "job_architecture";
|
||||
const callId = "llmcall_draft";
|
||||
|
||||
const run = {
|
||||
job_id: jobId,
|
||||
case_id: "case_architecture",
|
||||
status: "completed",
|
||||
current_stage: "final",
|
||||
trace_completeness: "complete",
|
||||
error_stage: null,
|
||||
error_summary: null,
|
||||
started_at: "2026-07-16T00:00:00.000Z",
|
||||
finished_at: "2026-07-16T00:00:03.000Z",
|
||||
updated_at: "2026-07-16T00:00:03.000Z",
|
||||
};
|
||||
|
||||
const call = {
|
||||
call_id: callId,
|
||||
job_id: jobId,
|
||||
sequence: 2,
|
||||
task: "article_optimizer",
|
||||
workflow_stage: "draft",
|
||||
rewrite_round: null,
|
||||
provider: "deepseek",
|
||||
model: "deepseek-chat",
|
||||
status: "validated",
|
||||
token_usage: { prompt_tokens: 120, completion_tokens: 80, total_tokens: 200 },
|
||||
schema_name: "optimizedArticleSchema",
|
||||
schema_valid: true,
|
||||
validation_issues: [],
|
||||
business_status: null,
|
||||
duration_ms: 1600,
|
||||
started_at: "2026-07-16T00:00:01.000Z",
|
||||
responded_at: "2026-07-16T00:00:02.500Z",
|
||||
validated_at: "2026-07-16T00:00:02.600Z",
|
||||
failed_at: null,
|
||||
error_type: null,
|
||||
error_summary: null,
|
||||
request_available: true,
|
||||
response_available: true,
|
||||
};
|
||||
|
||||
const article = {
|
||||
job_id: jobId,
|
||||
revision: 1,
|
||||
title: "可观测的 GEO 优化稿",
|
||||
summary: "展示真实后台调用。",
|
||||
body_markdown: "## 优化结果\n正文内容。",
|
||||
image_suggestions: [],
|
||||
changed_sections: ["title", "body"],
|
||||
requires_user_confirmation: [],
|
||||
};
|
||||
|
||||
const qaReport = {
|
||||
job_id: jobId,
|
||||
revision: 1,
|
||||
overall_status: "pass",
|
||||
checks: [],
|
||||
};
|
||||
|
||||
test("后台架构标签按需展示完整 LLM 请求与响应", async ({ page }) => {
|
||||
const payloadReads = { request: 0, response: 0 };
|
||||
const streamEvents = [
|
||||
{ type: "job_created", job: { id: jobId } },
|
||||
{
|
||||
type: "llm_call_started",
|
||||
job_id: jobId,
|
||||
call_id: callId,
|
||||
sequence: 2,
|
||||
task: "article_optimizer",
|
||||
workflow_stage: "draft",
|
||||
rewrite_round: null,
|
||||
provider: "deepseek",
|
||||
model: "deepseek-chat",
|
||||
started_at: call.started_at,
|
||||
request_available: true,
|
||||
},
|
||||
{
|
||||
type: "llm_call_responded",
|
||||
job_id: jobId,
|
||||
call_id: callId,
|
||||
duration_ms: call.duration_ms,
|
||||
token_usage: call.token_usage,
|
||||
responded_at: call.responded_at,
|
||||
response_available: true,
|
||||
},
|
||||
{
|
||||
type: "llm_call_validated",
|
||||
job_id: jobId,
|
||||
call_id: callId,
|
||||
schema_name: call.schema_name,
|
||||
schema_valid: true,
|
||||
validation_issues: [],
|
||||
validated_at: call.validated_at,
|
||||
},
|
||||
{
|
||||
type: "final_ready",
|
||||
job_id: jobId,
|
||||
optimized_article: article,
|
||||
qa_report: qaReport,
|
||||
export_paths: {
|
||||
markdown: `/api/jobs/${jobId}/exports/optimized.md`,
|
||||
docx: `/api/jobs/${jobId}/exports/optimized.docx`,
|
||||
qa_report: `/api/jobs/${jobId}/exports/qa_report.json`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await page.route("**/api/jobs/optimize-stream", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/x-ndjson; charset=utf-8",
|
||||
body: `${streamEvents.map((event) => JSON.stringify(event)).join("\n")}\n`,
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/jobs/${jobId}/llm-trace`, async (route) => {
|
||||
await route.fulfill({ json: { run, calls: [call] } });
|
||||
});
|
||||
await page.route("**/api/llm-traces/latest", async (route) => {
|
||||
await route.fulfill({ json: { run, calls: [call] } });
|
||||
});
|
||||
await page.route(`**/api/jobs/${jobId}/llm-trace/${callId}/request`, async (route) => {
|
||||
payloadReads.request += 1;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
model: "deepseek-chat",
|
||||
messages: [
|
||||
{ role: "system", content: "你是 GEO 文章优化器。" },
|
||||
{ role: "user", content: "优化这篇原始文章。" },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.2,
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.route(`**/api/jobs/${jobId}/llm-trace/${callId}/response`, async (route) => {
|
||||
payloadReads.response += 1;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "chatcmpl_observer",
|
||||
choices: [{ message: { role: "assistant", content: JSON.stringify(article) } }],
|
||||
usage: call.token_usage,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByLabel("访问密钥").fill("local-dev-key");
|
||||
await page.getByLabel("文章内容").fill("这是一篇需要优化的原始文章。");
|
||||
await page.getByRole("button", { name: "开始优化" }).click();
|
||||
await page.getByRole("button", { name: "后台架构" }).click();
|
||||
|
||||
await expect(page.getByLabel("文章优化后台架构")).toBeVisible();
|
||||
await expect(page.locator(".llm-call-list code", { hasText: "article_optimizer" }))
|
||||
.toBeVisible();
|
||||
await expect(page.locator('[data-node="final"]')).toContainText("已完成");
|
||||
|
||||
await page.getByRole("tab", { name: "请求" }).click();
|
||||
await expect(page.locator(".llm-json-view"))
|
||||
.toContainText("开启技术详情后按需读取完整正文。");
|
||||
expect(payloadReads.request).toBe(0);
|
||||
expect(payloadReads.response).toBe(0);
|
||||
|
||||
await page.getByLabel("技术详情").check();
|
||||
await expect(page.locator(".llm-json-view")).toContainText("messages");
|
||||
await expect(page.locator(".llm-json-view")).toContainText("你是 GEO 文章优化器。");
|
||||
expect(payloadReads.request).toBe(1);
|
||||
|
||||
await page.getByRole("tab", { name: "响应" }).click();
|
||||
await expect(page.locator(".llm-json-view")).toContainText("choices");
|
||||
await expect(page.locator(".llm-json-view")).toContainText("chatcmpl_observer");
|
||||
expect(payloadReads.response).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "GEO 文章优化" }).click();
|
||||
await expect(page.getByText("优化完成。", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible();
|
||||
});
|
||||
+1
-16
@@ -6,29 +6,14 @@ test("中文界面可以生成优化文章和导出链接", async ({ page }) =>
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByLabel("访问密钥").fill("local-dev-key");
|
||||
await page.getByLabel("标题").fill("Example Technology Co., Ltd. GEO 指南");
|
||||
await page
|
||||
.getByLabel("正文")
|
||||
.getByLabel("文章内容")
|
||||
.fill(
|
||||
"Example Technology Co., Ltd. has 8 years of GEO optimization experience. Example GEO 帮助市场团队优化内容结构。",
|
||||
);
|
||||
await page.getByLabel("图片描述或图片链接").fill("产品仪表盘截图");
|
||||
await page.getByLabel("用户要求").fill("保持事实准确,语气自然。");
|
||||
|
||||
await page.getByRole("button", { name: "分析文章" }).click();
|
||||
await expect(page.getByText("候选事实卡已生成")).toBeVisible({
|
||||
timeout: 70_000,
|
||||
});
|
||||
|
||||
const confirmUncertainItemButtons = page.getByRole("button", {
|
||||
name: "采纳为核心事实",
|
||||
});
|
||||
while ((await confirmUncertainItemButtons.count()) > 0) {
|
||||
await confirmUncertainItemButtons.first().click();
|
||||
}
|
||||
await page.getByRole("button", { name: "确认事实卡" }).click();
|
||||
await expect(page.getByText("事实卡已确认。")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "开始优化" }).click();
|
||||
await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible({
|
||||
timeout: 120_000,
|
||||
|
||||
@@ -37,6 +37,16 @@ interface QaReportJson {
|
||||
checks?: Array<{ rule_id?: string; status?: "pass" | "warn" | "fail" }>;
|
||||
}
|
||||
|
||||
interface TraceManifestJson {
|
||||
run?: { job_id?: string; status?: string; trace_completeness?: string };
|
||||
calls?: Array<{
|
||||
task?: string;
|
||||
status?: string;
|
||||
request_available?: boolean;
|
||||
response_available?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function runSamplePageFlow({
|
||||
page,
|
||||
request,
|
||||
@@ -87,17 +97,29 @@ export async function runSamplePageFlow({
|
||||
jobId,
|
||||
exportsDir,
|
||||
});
|
||||
const traceResult = await validateLlmTrace({
|
||||
request,
|
||||
baseURL,
|
||||
apiAccessKey,
|
||||
jobId,
|
||||
});
|
||||
const qa = readQaReport(exportsDir);
|
||||
const finalScreenshot = join(sampleDir, "final.png");
|
||||
await page.screenshot({ path: finalScreenshot, fullPage: true });
|
||||
|
||||
const failedExports = exportResults.filter((result) => result.status === "failed");
|
||||
const failureMessage = [
|
||||
...failedExports.map(
|
||||
(result) => `${result.fileName}: ${result.error ?? result.statusCode}`,
|
||||
),
|
||||
...(traceResult.error ? [`LLM trace: ${traceResult.error}`] : []),
|
||||
].join("; ");
|
||||
|
||||
return {
|
||||
file: sample.filePath,
|
||||
name: sample.name,
|
||||
slug: sample.slug,
|
||||
status: failedExports.length === 0 ? "passed" : "failed",
|
||||
status: failedExports.length === 0 && !traceResult.error ? "passed" : "failed",
|
||||
duration_ms: Date.now() - startedAt,
|
||||
job_id: jobId,
|
||||
qa_status: qa.overall_status,
|
||||
@@ -108,18 +130,61 @@ export async function runSamplePageFlow({
|
||||
exports: Object.fromEntries(
|
||||
exportResults.map((result) => [result.fileName, result.status]),
|
||||
),
|
||||
llm_tasks: [],
|
||||
failure_category: failedExports.length > 0 ? "export_failed" : undefined,
|
||||
failure_message:
|
||||
failedExports
|
||||
.map((result) => `${result.fileName}: ${result.error ?? result.statusCode}`)
|
||||
.join("; ") || undefined,
|
||||
llm_tasks: traceResult.tasks,
|
||||
failure_category: failedExports.length > 0
|
||||
? "export_failed"
|
||||
: traceResult.error
|
||||
? "llm_failed"
|
||||
: undefined,
|
||||
failure_message: failureMessage || undefined,
|
||||
artifacts: {
|
||||
final_screenshot: finalScreenshot,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function validateLlmTrace({
|
||||
request,
|
||||
baseURL,
|
||||
apiAccessKey,
|
||||
jobId,
|
||||
}: {
|
||||
request: APIRequestContext;
|
||||
baseURL: string;
|
||||
apiAccessKey: string;
|
||||
jobId: string;
|
||||
}) {
|
||||
const headers: Record<string, string> | undefined = apiAccessKey
|
||||
? { "x-api-key": apiAccessKey }
|
||||
: undefined;
|
||||
const response = await request.get(
|
||||
`${baseURL}/api/jobs/${jobId}/llm-trace`,
|
||||
{ headers },
|
||||
);
|
||||
if (!response.ok()) {
|
||||
return { tasks: [], error: `metadata endpoint returned ${response.status()}` };
|
||||
}
|
||||
|
||||
const manifest = await response.json() as TraceManifestJson;
|
||||
const calls = manifest.calls ?? [];
|
||||
const tasks = calls
|
||||
.map((call) => call.task)
|
||||
.filter((task): task is string => Boolean(task));
|
||||
if (manifest.run?.job_id !== jobId) {
|
||||
return { tasks, error: "run job_id does not match completed job" };
|
||||
}
|
||||
if (manifest.run.status !== "completed") {
|
||||
return { tasks, error: `run status is ${manifest.run.status ?? "missing"}` };
|
||||
}
|
||||
if (calls.length === 0) {
|
||||
return { tasks, error: "no LLM calls were recorded" };
|
||||
}
|
||||
if (calls.some((call) => !call.status || call.request_available !== true)) {
|
||||
return { tasks, error: "call metadata is incomplete" };
|
||||
}
|
||||
return { tasks, error: undefined };
|
||||
}
|
||||
|
||||
async function fillFirstAvailable(page: Page, labels: string[], value: string) {
|
||||
for (const label of labels) {
|
||||
const locator = page.getByLabel(label);
|
||||
|
||||
Reference in New Issue
Block a user