407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
articleInputSchema,
|
|
confirmedFactCardSchema,
|
|
candidateFactCardSchema,
|
|
copyOptimizationRequestSchema,
|
|
copyOptimizationResultSchema,
|
|
optimizationFactCardSchema,
|
|
optimizedArticleSchema,
|
|
qaReportSchema,
|
|
} from "../validation";
|
|
|
|
describe("domain validation", () => {
|
|
it("accepts article input with title, body, images, platform, and instructions", () => {
|
|
const parsed = articleInputSchema.parse({
|
|
title: "How GEO Optimization Improves Brand Visibility",
|
|
body: "A practical overview of GEO optimization for marketing teams.",
|
|
images: [
|
|
{ type: "description", content: "Dashboard screenshot" },
|
|
{ type: "link", content: "https://example.com/image.png" },
|
|
],
|
|
platform: "official_site",
|
|
user_instructions: "Keep the article factual and concise.",
|
|
});
|
|
|
|
expect(parsed.images).toHaveLength(2);
|
|
expect(parsed.platform).toBe("official_site");
|
|
});
|
|
|
|
it("accepts article input with an empty optional title", () => {
|
|
const parsed = articleInputSchema.parse({
|
|
title: " ",
|
|
body: "完整文章正文可以直接粘贴在这里。",
|
|
images: [],
|
|
platform: "official_site",
|
|
user_instructions: "",
|
|
});
|
|
|
|
expect(parsed.title).toBe("");
|
|
expect(parsed.body).toBe("完整文章正文可以直接粘贴在这里。");
|
|
});
|
|
|
|
it("still rejects article input with an empty body", () => {
|
|
expect(() =>
|
|
articleInputSchema.parse({
|
|
title: "",
|
|
body: " ",
|
|
images: [],
|
|
platform: "official_site",
|
|
user_instructions: "",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("accepts a trimmed copy optimization request", () => {
|
|
const parsed = copyOptimizationRequestSchema.parse({
|
|
source_text: " 我写了一段有点卡的文案 ",
|
|
goal: "",
|
|
intensity: "light",
|
|
user_instructions: " 保留口语感 ",
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
source_text: "我写了一段有点卡的文案",
|
|
goal: "保留原意,减少 AI 味",
|
|
intensity: "light",
|
|
user_instructions: "保留口语感",
|
|
publish_target: "未指定",
|
|
});
|
|
});
|
|
|
|
it("validates human-copy publish target", () => {
|
|
expect(
|
|
copyOptimizationRequestSchema.parse({
|
|
source_text: "这是一段普通文案。",
|
|
goal: "",
|
|
intensity: "light",
|
|
user_instructions: "",
|
|
publish_target: "朋友圈",
|
|
}),
|
|
).toMatchObject({
|
|
goal: "保留原意,减少 AI 味",
|
|
publish_target: "朋友圈",
|
|
});
|
|
});
|
|
|
|
it("rejects empty copy optimization source text", () => {
|
|
expect(() =>
|
|
copyOptimizationRequestSchema.parse({
|
|
source_text: " ",
|
|
intensity: "light",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("accepts structured copy optimization results", () => {
|
|
const parsed = copyOptimizationResultSchema.parse({
|
|
optimized_text: "我把句子顺了一下。",
|
|
change_notes: [
|
|
{
|
|
original: "我把句子顺顺。",
|
|
revised: "我把句子顺了一下。",
|
|
reason: "修正口语里不顺的重复。",
|
|
confidence: "confident",
|
|
revertible: false,
|
|
},
|
|
],
|
|
ai_taste_checks: [
|
|
{
|
|
rule_id: "promotion_tone",
|
|
status: "pass",
|
|
evidence: "没有新增宣传词。",
|
|
suggestion: "",
|
|
},
|
|
],
|
|
warnings: [],
|
|
});
|
|
|
|
expect(parsed.optimized_text).toBe("我把句子顺了一下。");
|
|
expect(parsed.change_notes[0]?.confidence).toBe("confident");
|
|
expect(parsed.ai_taste_checks[0]?.rule_id).toBe("promotion_tone");
|
|
});
|
|
|
|
it("normalizes empty AI taste check evidence from LLM copy results", () => {
|
|
const parsed = copyOptimizationResultSchema.parse({
|
|
optimized_text: "我把句子顺了一下。",
|
|
change_notes: [],
|
|
ai_taste_checks: [
|
|
{
|
|
rule_id: "promotion_tone",
|
|
status: "pass",
|
|
evidence: "",
|
|
suggestion: "",
|
|
},
|
|
],
|
|
warnings: [],
|
|
});
|
|
|
|
expect(parsed.ai_taste_checks[0]?.evidence).toBe("未提供具体证据。");
|
|
});
|
|
|
|
it("accepts an unconfirmed optimization fact card with unresolved items", () => {
|
|
const parsed = optimizationFactCardSchema.parse({
|
|
company_full_name: "",
|
|
company_short_names: ["示例科技"],
|
|
brand_names: [],
|
|
product_names: ["GEO内容优化平台"],
|
|
target_industry: "",
|
|
target_audience: "市场团队",
|
|
experience_years: "",
|
|
core_claims: ["提供GEO内容优化服务"],
|
|
forbidden_claims: [],
|
|
image_topics: [],
|
|
uncertain_items: ["客户案例需要确认"],
|
|
confirmed_by_user: false,
|
|
});
|
|
|
|
expect(parsed.company_full_name).toBe("");
|
|
expect(parsed.experience_years).toBeNull();
|
|
expect(parsed.confirmed_by_user).toBe(false);
|
|
expect(parsed.is_ready_for_optimization).toBe(false);
|
|
expect(parsed.uncertain_items).toEqual(["客户案例需要确认"]);
|
|
});
|
|
|
|
it("marks a fact card with unresolved uncertain items as not ready for optimization", () => {
|
|
const parsed = candidateFactCardSchema.parse({
|
|
company_full_name: "Example Technology Co., Ltd.",
|
|
company_short_names: ["Example Tech"],
|
|
brand_names: ["Example"],
|
|
product_names: ["Example GEO"],
|
|
target_industry: "GEO optimization",
|
|
target_audience: "Marketing teams",
|
|
experience_years: 8,
|
|
core_claims: ["Eight years of GEO optimization experience"],
|
|
forbidden_claims: [],
|
|
image_topics: ["Product dashboard"],
|
|
uncertain_items: ["Conflicting product names found"],
|
|
});
|
|
|
|
expect(parsed.is_ready_for_optimization).toBe(false);
|
|
});
|
|
|
|
it("normalizes near-valid LLM fact card field types", () => {
|
|
const emptyExperience = candidateFactCardSchema.parse({
|
|
company_full_name: "Example Technology Co., Ltd.",
|
|
company_short_names: ["Example Tech"],
|
|
brand_names: ["Example"],
|
|
product_names: ["Example GEO"],
|
|
target_industry: "GEO optimization",
|
|
target_audience: ["Marketing teams", "Brand teams"],
|
|
experience_years: "",
|
|
core_claims: ["GEO optimization experience"],
|
|
forbidden_claims: [],
|
|
image_topics: [],
|
|
uncertain_items: [],
|
|
});
|
|
|
|
const numericExperience = candidateFactCardSchema.parse({
|
|
company_full_name: "Example Technology Co., Ltd.",
|
|
company_short_names: ["Example Tech"],
|
|
brand_names: ["Example"],
|
|
product_names: ["Example GEO"],
|
|
target_industry: "GEO optimization",
|
|
target_audience: "Marketing teams",
|
|
experience_years: "8年",
|
|
core_claims: ["GEO optimization experience"],
|
|
forbidden_claims: [],
|
|
image_topics: [],
|
|
uncertain_items: [],
|
|
});
|
|
|
|
expect(emptyExperience.target_audience).toBe("Marketing teams、Brand teams");
|
|
expect(emptyExperience.experience_years).toBeNull();
|
|
expect(numericExperience.experience_years).toBe(8);
|
|
});
|
|
|
|
it("normalizes object-shaped and single-string LLM fact card fields", () => {
|
|
const parsed = candidateFactCardSchema.parse({
|
|
company_full_name: { name: "示例科技有限公司" },
|
|
company_short_names: "示例科技",
|
|
brand_names: [{ name: "示例品牌" }],
|
|
product_names: [{ product: "GEO内容优化平台" }],
|
|
target_industry: { industry: "GEO内容优化" },
|
|
target_audience: { audience: "市场团队" },
|
|
experience_years: { years: "8年" },
|
|
core_claims: [
|
|
{ claim: "提供GEO内容优化服务", source: "原文明确出现" },
|
|
],
|
|
forbidden_claims: [
|
|
{ claim: "行业第一", reason: "缺少第三方依据" },
|
|
],
|
|
image_topics: [{ topic: "产品后台截图" }],
|
|
uncertain_items: [
|
|
{ item: "客户案例", reason: "原文没有给出客户名称" },
|
|
{ claim: "出海能力", evidence: "只出现营销表述" },
|
|
],
|
|
});
|
|
|
|
expect(parsed.company_full_name).toBe("示例科技有限公司");
|
|
expect(parsed.company_short_names).toEqual(["示例科技"]);
|
|
expect(parsed.brand_names).toEqual(["示例品牌"]);
|
|
expect(parsed.product_names).toEqual(["GEO内容优化平台"]);
|
|
expect(parsed.target_industry).toBe("GEO内容优化");
|
|
expect(parsed.target_audience).toBe("市场团队");
|
|
expect(parsed.experience_years).toBe(8);
|
|
expect(parsed.core_claims).toEqual(["提供GEO内容优化服务"]);
|
|
expect(parsed.forbidden_claims).toEqual(["行业第一"]);
|
|
expect(parsed.image_topics).toEqual(["产品后台截图"]);
|
|
expect(parsed.uncertain_items).toEqual(["客户案例", "出海能力"]);
|
|
expect(parsed.is_ready_for_optimization).toBe(false);
|
|
});
|
|
|
|
it("keeps incomplete candidate fact cards editable and ready", () => {
|
|
const parsed = candidateFactCardSchema.parse({
|
|
company_full_name: "",
|
|
company_short_names: [],
|
|
brand_names: [],
|
|
product_names: [],
|
|
target_industry: "",
|
|
target_audience: "",
|
|
experience_years: "",
|
|
core_claims: [],
|
|
forbidden_claims: [],
|
|
image_topics: [],
|
|
uncertain_items: [],
|
|
});
|
|
|
|
expect(parsed.target_industry).toBe("");
|
|
expect(parsed.target_audience).toBe("");
|
|
expect(parsed.experience_years).toBeNull();
|
|
expect(parsed.uncertain_items).toEqual([]);
|
|
expect(parsed.is_ready_for_optimization).toBe(true);
|
|
});
|
|
|
|
it("rejects a confirmed fact card with an empty company full name", () => {
|
|
expect(() =>
|
|
confirmedFactCardSchema.parse({
|
|
company_full_name: "",
|
|
company_short_names: ["Example"],
|
|
brand_names: ["Example"],
|
|
product_names: ["Example GEO"],
|
|
target_industry: "GEO optimization",
|
|
target_audience: "Marketing teams",
|
|
experience_years: 8,
|
|
core_claims: ["Eight years of GEO optimization experience"],
|
|
forbidden_claims: [],
|
|
image_topics: [],
|
|
uncertain_items: [],
|
|
confirmed_by_user: true,
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("accepts QA reports only with pass, warn, or fail statuses", () => {
|
|
const valid = qaReportSchema.parse({
|
|
job_id: "job_123",
|
|
revision: 1,
|
|
overall_status: "warn",
|
|
checks: [
|
|
{
|
|
rule_id: "title_quality",
|
|
status: "pass",
|
|
evidence: "Title reads naturally.",
|
|
reason: "No grammar issue detected.",
|
|
suggested_fix: "",
|
|
target_agent: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(valid.checks[0]?.status).toBe("pass");
|
|
expect(() =>
|
|
qaReportSchema.parse({
|
|
job_id: "job_123",
|
|
revision: 1,
|
|
overall_status: "blocked",
|
|
checks: [],
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("normalizes near-valid LLM QA report values", () => {
|
|
const parsed = qaReportSchema.parse({
|
|
overall_status: "警告",
|
|
checks: [
|
|
{
|
|
rule_id: "标题质量",
|
|
status: "警告",
|
|
evidence: { detail: "标题仍然偏营销化" },
|
|
reason: { reason: "官网标题需要更克制" },
|
|
suggested_fix: null,
|
|
target_agent: "",
|
|
},
|
|
{
|
|
rule_id: "company_name_integrity",
|
|
status: "通过",
|
|
evidence: "公司全称一致",
|
|
reason: "正文保留了事实卡中的公司全称",
|
|
target_agent: "无",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(parsed.overall_status).toBe("warn");
|
|
expect(parsed.checks[0]).toEqual({
|
|
rule_id: "title_quality",
|
|
status: "warn",
|
|
evidence: "标题仍然偏营销化",
|
|
reason: "官网标题需要更克制",
|
|
suggested_fix: "",
|
|
target_agent: null,
|
|
});
|
|
expect(parsed.checks[1]?.target_agent).toBeNull();
|
|
});
|
|
|
|
it("normalizes object-shaped changed sections from LLM output", () => {
|
|
const parsed = optimizedArticleSchema.parse({
|
|
title: "Optimized article",
|
|
summary: "Summary constrained by the confirmed fact card.",
|
|
body_markdown: "## Body\nUpdated body.",
|
|
image_suggestions: [],
|
|
changed_sections: [
|
|
{ section: "title", change: "Improved keyword clarity." },
|
|
{ name: "body", reason: "Fixed sentence flow." },
|
|
],
|
|
requires_user_confirmation: [],
|
|
});
|
|
|
|
expect(parsed.changed_sections).toEqual([
|
|
"title: Improved keyword clarity.",
|
|
"body: Fixed sentence flow.",
|
|
]);
|
|
});
|
|
|
|
it("normalizes near-valid optimized article LLM optional fields", () => {
|
|
const parsed = optimizedArticleSchema.parse({
|
|
title: { text: "示例科技 GEO 内容优化方案" },
|
|
summary: ["围绕事实卡重写官网文章摘要"],
|
|
body_markdown: {
|
|
markdown: "## 服务能力\n示例科技有限公司提供GEO内容优化服务。",
|
|
},
|
|
image_suggestions: [
|
|
"当前版本不生成图片建议",
|
|
{ source: "image_1" },
|
|
{ suggestion: "使用产品后台截图" },
|
|
{ source: "image_2", suggestion: "保留原文截图说明" },
|
|
],
|
|
changed_sections: [
|
|
{ section: "title", change: "改成中文官网标题" },
|
|
],
|
|
requires_user_confirmation: [
|
|
{ claim: "客户案例", reason: "原文没有给出客户名称" },
|
|
],
|
|
});
|
|
|
|
expect(parsed.title).toBe("示例科技 GEO 内容优化方案");
|
|
expect(parsed.summary).toBe("围绕事实卡重写官网文章摘要");
|
|
expect(parsed.body_markdown).toContain("## 服务能力");
|
|
expect(parsed.image_suggestions).toEqual([
|
|
{ source: "image_2", suggestion: "保留原文截图说明" },
|
|
]);
|
|
expect(parsed.changed_sections).toEqual(["title: 改成中文官网标题"]);
|
|
expect(parsed.requires_user_confirmation).toEqual(["客户案例"]);
|
|
});
|
|
});
|