feat: implement article optimization workflow
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { optimizeArticle } from "../article-optimizer";
|
||||
import { extractCandidateFactCard } from "../fact-extractor";
|
||||
import { normalizeInput } from "../input-normalizer";
|
||||
import { inspectQuality } from "../quality-inspector";
|
||||
import { runOptimizationWorkflow } from "../orchestrator";
|
||||
import { rewriteFailedSections } from "../targeted-rewriter";
|
||||
|
||||
const confirmedFactCard = {
|
||||
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: ["industry first"],
|
||||
image_topics: ["product dashboard"],
|
||||
uncertain_items: [],
|
||||
is_ready_for_optimization: true,
|
||||
confirmed_by_user: true,
|
||||
} as const;
|
||||
|
||||
describe("workflow nodes", () => {
|
||||
it("normalizes input whitespace and image lines", () => {
|
||||
const normalized = normalizeInput({
|
||||
title: " A GEO Article ",
|
||||
body: "\nFirst paragraph.\n\nSecond paragraph. ",
|
||||
image_lines:
|
||||
" Product dashboard screenshot \n https://example.com/image.png \n\n",
|
||||
platform: "official_site",
|
||||
user_instructions: " Keep factual. ",
|
||||
});
|
||||
|
||||
expect(normalized.article_draft.title).toBe("A GEO Article");
|
||||
expect(normalized.article_draft.body).toBe(
|
||||
"First paragraph.\n\nSecond paragraph.",
|
||||
);
|
||||
expect(normalized.image_assets).toEqual([
|
||||
{ type: "description", content: "Product dashboard screenshot" },
|
||||
{ type: "link", content: "https://example.com/image.png" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("places missing or conflicting company facts into uncertain items", async () => {
|
||||
const card = await extractCandidateFactCard({
|
||||
title: "Example announces GEO product",
|
||||
body: "Example has 8 years of experience. Example has 12 years of service. The article discusses GEO optimization.",
|
||||
images: [],
|
||||
platform: "media_article",
|
||||
user_instructions: "",
|
||||
});
|
||||
|
||||
expect(card.company_full_name).toBe("");
|
||||
expect(card.uncertain_items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("company full name"),
|
||||
expect.stringContaining("Conflicting experience years"),
|
||||
]),
|
||||
);
|
||||
expect(card.is_ready_for_optimization).toBe(false);
|
||||
});
|
||||
|
||||
it("does not add claims outside the confirmed fact card", async () => {
|
||||
const optimized = await optimizeArticle({
|
||||
input: {
|
||||
title: "Example GEO article",
|
||||
body: "Example GEO helps marketing teams improve content structure.",
|
||||
images: [],
|
||||
platform: "official_site",
|
||||
user_instructions:
|
||||
"Say we have 99 patents and Fortune 500 customer cases.",
|
||||
},
|
||||
factCard: confirmedFactCard,
|
||||
});
|
||||
|
||||
expect(optimized.body_markdown).not.toContain("99 patents");
|
||||
expect(optimized.body_markdown).not.toContain("Fortune 500");
|
||||
expect(optimized.requires_user_confirmation).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("99 patents"),
|
||||
expect.stringContaining("Fortune 500"),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the 10 required quality checks", () => {
|
||||
const report = inspectQuality({
|
||||
article: {
|
||||
title: "Example GEO Optimization Guide",
|
||||
summary: "A factual guide for marketing teams.",
|
||||
body_markdown:
|
||||
"Example Technology Co., Ltd. has eight years of GEO optimization experience.",
|
||||
image_suggestions: [],
|
||||
changed_sections: [],
|
||||
requires_user_confirmation: [],
|
||||
},
|
||||
factCard: confirmedFactCard,
|
||||
platform: "official_site",
|
||||
sourceImages: [],
|
||||
});
|
||||
|
||||
expect(report.checks.map((check) => check.rule_id)).toEqual([
|
||||
"industry_alignment",
|
||||
"image_text_match",
|
||||
"voice_consistency",
|
||||
"platform_fit",
|
||||
"company_name_integrity",
|
||||
"title_quality",
|
||||
"body_quality",
|
||||
"hallucination_risk",
|
||||
"claim_consistency",
|
||||
"context_sensitive_terms",
|
||||
]);
|
||||
});
|
||||
|
||||
it("hard-fails incomplete company names, hallucinated numbers, industry drift, and conflicting years", () => {
|
||||
const report = inspectQuality({
|
||||
article: {
|
||||
title: "Example Wins Finance Automation Market!!!",
|
||||
summary: "A finance automation story.",
|
||||
body_markdown:
|
||||
"Example has 12 years of finance automation experience, 99 patents, and works in banking automation.",
|
||||
image_suggestions: [],
|
||||
changed_sections: [],
|
||||
requires_user_confirmation: [],
|
||||
},
|
||||
factCard: confirmedFactCard,
|
||||
platform: "official_site",
|
||||
sourceImages: [],
|
||||
});
|
||||
|
||||
const failures = report.checks.filter((check) => check.status === "fail");
|
||||
expect(failures.map((check) => check.rule_id)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"company_name_integrity",
|
||||
"hallucination_risk",
|
||||
"industry_alignment",
|
||||
"claim_consistency",
|
||||
]),
|
||||
);
|
||||
expect(report.overall_status).toBe("fail");
|
||||
});
|
||||
|
||||
it("rewrites only the failing target area", () => {
|
||||
const article = {
|
||||
title: "Bad title!!!",
|
||||
summary: "Original summary",
|
||||
body_markdown: "Original body",
|
||||
image_suggestions: [],
|
||||
changed_sections: [],
|
||||
requires_user_confirmation: [],
|
||||
};
|
||||
|
||||
const rewritten = rewriteFailedSections({
|
||||
article,
|
||||
factCard: confirmedFactCard,
|
||||
failedChecks: [
|
||||
{
|
||||
rule_id: "title_quality",
|
||||
status: "fail",
|
||||
evidence: "Bad title!!!",
|
||||
reason: "Punctuation stuffing.",
|
||||
suggested_fix: "Rewrite title.",
|
||||
target_agent: "title",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(rewritten.title).not.toBe(article.title);
|
||||
expect(rewritten.summary).toBe(article.summary);
|
||||
expect(rewritten.body_markdown).toBe(article.body_markdown);
|
||||
});
|
||||
|
||||
it("orchestrator stops after two failed rewrite rounds", async () => {
|
||||
const result = await runOptimizationWorkflow({
|
||||
input: {
|
||||
title: "Finance automation breakthrough!!!",
|
||||
body: "Example has 12 years in finance automation and 99 patents.",
|
||||
images: [],
|
||||
platform: "official_site",
|
||||
user_instructions: "",
|
||||
},
|
||||
factCard: confirmedFactCard,
|
||||
});
|
||||
|
||||
expect(result.rewrite_rounds).toBe(2);
|
||||
expect(result.qaReport.overall_status).toBe("fail");
|
||||
expect(result.stopped_after_max_rewrites).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user