Files
GEOAgentArticleOptimizer/src/lib/domain/__tests__/validation.test.ts
T

373 lines
12 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: "保留口语感",
});
});
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("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(["客户案例"]);
});
});