343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
areClarificationAnswersComplete,
|
|
buildPreflightClarificationQuestions,
|
|
normalizeAgentLocale,
|
|
normalizeAgentExecutionPlan,
|
|
normalizeAgentBriefConstraints,
|
|
normalizeAgentOutputDraft,
|
|
type AgentClarificationAnswerMap,
|
|
type AgentClarificationQuestion,
|
|
type AgentBriefConstraints,
|
|
type AgentExecutionPlan,
|
|
} from "@/lib/agent-run-contract";
|
|
|
|
describe("agent run contract helpers", () => {
|
|
describe("areClarificationAnswersComplete", () => {
|
|
it("returns true when every required question has a non-empty answer", () => {
|
|
const questions: AgentClarificationQuestion[] = [
|
|
{ id: "goal", prompt: "What is the goal?", required: true },
|
|
{ id: "tone", prompt: "Preferred tone?", required: false },
|
|
{ id: "audience", prompt: "Who is the audience?", required: true },
|
|
];
|
|
|
|
const answers: AgentClarificationAnswerMap = {
|
|
goal: "Generate launch captions",
|
|
audience: "SaaS founders",
|
|
};
|
|
|
|
expect(areClarificationAnswersComplete(questions, answers)).toBe(true);
|
|
});
|
|
|
|
it("returns false when a required question is missing", () => {
|
|
const questions: AgentClarificationQuestion[] = [
|
|
{ id: "goal", prompt: "What is the goal?", required: true },
|
|
{ id: "audience", prompt: "Who is the audience?", required: true },
|
|
];
|
|
|
|
const answers: AgentClarificationAnswerMap = {
|
|
goal: "Generate launch captions",
|
|
};
|
|
|
|
expect(areClarificationAnswersComplete(questions, answers)).toBe(false);
|
|
});
|
|
|
|
it("returns false when required answers are blank after trimming", () => {
|
|
const questions: AgentClarificationQuestion[] = [
|
|
{ id: "goal", prompt: "What is the goal?", required: true },
|
|
];
|
|
|
|
const answers: AgentClarificationAnswerMap = {
|
|
goal: " ",
|
|
};
|
|
|
|
expect(areClarificationAnswersComplete(questions, answers)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("normalizeAgentOutputDraft", () => {
|
|
it("trims draft fields and keeps non-empty values", () => {
|
|
const normalized = normalizeAgentOutputDraft({
|
|
title: " Launch Caption Pack ",
|
|
channel: " Instagram Feed ",
|
|
outputType: " caption-package ",
|
|
body: " 3 variants with hook-first copy. ",
|
|
});
|
|
|
|
expect(normalized).toEqual({
|
|
title: "Launch Caption Pack",
|
|
channel: "Instagram Feed",
|
|
outputType: "caption-package",
|
|
body: "3 variants with hook-first copy.",
|
|
});
|
|
});
|
|
|
|
it("uses safe fallback values and guarantees body string", () => {
|
|
const normalized = normalizeAgentOutputDraft({
|
|
title: " ",
|
|
channel: "",
|
|
outputType: " ",
|
|
});
|
|
|
|
expect(normalized).toEqual({
|
|
title: "Untitled",
|
|
channel: "general",
|
|
outputType: "text",
|
|
body: "",
|
|
});
|
|
});
|
|
|
|
it("coerces non-string body values to empty string", () => {
|
|
const normalized = normalizeAgentOutputDraft({
|
|
title: "Recap",
|
|
channel: "Email",
|
|
outputType: "summary",
|
|
body: null as unknown as string,
|
|
});
|
|
|
|
expect(normalized.body).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("normalizeAgentExecutionPlan", () => {
|
|
it("trims summary and step metadata while preserving valid values", () => {
|
|
const normalized = normalizeAgentExecutionPlan({
|
|
summary: " Ship a launch kit ",
|
|
steps: [
|
|
{
|
|
id: " STEP-1 ",
|
|
title: " Instagram captions ",
|
|
channel: " Instagram ",
|
|
outputType: " caption-pack ",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized).toEqual<AgentExecutionPlan>({
|
|
summary: "Ship a launch kit",
|
|
steps: [
|
|
{
|
|
id: "step-1",
|
|
title: "Instagram captions",
|
|
channel: "Instagram",
|
|
outputType: "caption-pack",
|
|
artifactType: "caption-pack",
|
|
goal: "Deliver channel-ready output.",
|
|
requiredSections: [],
|
|
qualityChecks: [],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("falls back to safe defaults for invalid payloads", () => {
|
|
const normalized = normalizeAgentExecutionPlan({
|
|
summary: null,
|
|
steps: [
|
|
{
|
|
id: "",
|
|
title: "",
|
|
channel: " ",
|
|
outputType: undefined,
|
|
},
|
|
null,
|
|
],
|
|
});
|
|
|
|
expect(normalized).toEqual<AgentExecutionPlan>({
|
|
summary: "",
|
|
steps: [
|
|
{
|
|
id: "step-1",
|
|
title: "Untitled",
|
|
channel: "general",
|
|
outputType: "text",
|
|
artifactType: "text",
|
|
goal: "Deliver channel-ready output.",
|
|
requiredSections: [],
|
|
qualityChecks: [],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("deduplicates step ids and creates deterministic fallback ids", () => {
|
|
const normalized = normalizeAgentExecutionPlan({
|
|
summary: "ready",
|
|
steps: [
|
|
{
|
|
id: "step",
|
|
title: "One",
|
|
channel: "email",
|
|
outputType: "copy",
|
|
},
|
|
{
|
|
id: "step",
|
|
title: "Two",
|
|
channel: "x",
|
|
outputType: "thread",
|
|
},
|
|
{
|
|
id: "",
|
|
title: "Three",
|
|
channel: "linkedin",
|
|
outputType: "post",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized.steps.map((step) => step.id)).toEqual(["step", "step-2", "step-3"]);
|
|
});
|
|
|
|
it("normalizes enriched execution-step fields with deterministic array handling", () => {
|
|
const normalized = normalizeAgentExecutionPlan({
|
|
summary: "ready",
|
|
steps: [
|
|
{
|
|
id: "main",
|
|
title: "Deliver",
|
|
channel: "linkedin",
|
|
outputType: "post",
|
|
artifactType: " social-caption-pack ",
|
|
goal: " Explain launch value ",
|
|
requiredSections: ["Hook", "CTA", "Hook", " "],
|
|
qualityChecks: ["fits_tone", "fits_tone", "references_context", ""],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized.steps[0]).toEqual({
|
|
id: "main",
|
|
title: "Deliver",
|
|
channel: "linkedin",
|
|
outputType: "post",
|
|
artifactType: "social-caption-pack",
|
|
goal: "Explain launch value",
|
|
requiredSections: ["Hook", "CTA"],
|
|
qualityChecks: ["fits_tone", "references_context"],
|
|
});
|
|
});
|
|
|
|
it("keeps compatibility by falling back artifactType to outputType", () => {
|
|
const normalized = normalizeAgentExecutionPlan({
|
|
summary: "ready",
|
|
steps: [
|
|
{
|
|
id: "legacy",
|
|
title: "Legacy step",
|
|
channel: "email",
|
|
outputType: "newsletter-copy",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized.steps[0]?.artifactType).toBe("newsletter-copy");
|
|
});
|
|
});
|
|
|
|
describe("normalizeAgentBriefConstraints", () => {
|
|
it("trims fields and normalizes target channels", () => {
|
|
const normalized = normalizeAgentBriefConstraints({
|
|
briefing: " Build a launch sequence ",
|
|
audience: " SaaS founders ",
|
|
tone: " concise ",
|
|
targetChannels: [" Email ", "", "LinkedIn", "email", " "],
|
|
hardConstraints: [
|
|
" Do not mention pricing ",
|
|
"",
|
|
"Keep under 120 words",
|
|
"Keep under 120 words",
|
|
],
|
|
});
|
|
|
|
expect(normalized).toEqual<AgentBriefConstraints>({
|
|
briefing: "Build a launch sequence",
|
|
audience: "SaaS founders",
|
|
tone: "concise",
|
|
targetChannels: ["email", "linkedin"],
|
|
hardConstraints: ["Do not mention pricing", "Keep under 120 words"],
|
|
});
|
|
});
|
|
|
|
it("falls back to safe empty values for invalid payloads", () => {
|
|
const normalized = normalizeAgentBriefConstraints({
|
|
briefing: null,
|
|
audience: undefined,
|
|
tone: 3,
|
|
targetChannels: [" ", null, 1],
|
|
hardConstraints: "must be array",
|
|
});
|
|
|
|
expect(normalized).toEqual<AgentBriefConstraints>({
|
|
briefing: "",
|
|
audience: "",
|
|
tone: "",
|
|
targetChannels: [],
|
|
hardConstraints: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("buildPreflightClarificationQuestions", () => {
|
|
it("builds deterministic required questions for missing preflight requirements", () => {
|
|
const questions = buildPreflightClarificationQuestions({
|
|
briefConstraints: {
|
|
briefing: "",
|
|
audience: "Founders",
|
|
tone: "confident",
|
|
targetChannels: [],
|
|
hardConstraints: [],
|
|
},
|
|
incomingContextCount: 0,
|
|
});
|
|
|
|
expect(questions).toEqual<AgentClarificationQuestion[]>([
|
|
{
|
|
id: "briefing",
|
|
prompt: "What should the agent produce? Provide the brief in one or two sentences.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "target-channels",
|
|
prompt: "Which channels should this run target? List at least one channel.",
|
|
required: true,
|
|
},
|
|
{
|
|
id: "incoming-context",
|
|
prompt: "No context was provided. What source context should the agent use?",
|
|
required: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("returns an empty list when all preflight requirements are satisfied", () => {
|
|
const questions = buildPreflightClarificationQuestions({
|
|
briefConstraints: {
|
|
briefing: "Create 3 posts",
|
|
audience: "Marketers",
|
|
tone: "friendly",
|
|
targetChannels: ["x"],
|
|
hardConstraints: ["No emojis"],
|
|
},
|
|
incomingContextCount: 2,
|
|
});
|
|
|
|
expect(questions).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("normalizeAgentLocale", () => {
|
|
it("returns supported locale values", () => {
|
|
expect(normalizeAgentLocale("de")).toBe("de");
|
|
expect(normalizeAgentLocale("en")).toBe("en");
|
|
});
|
|
|
|
it("falls back to de for unsupported values", () => {
|
|
expect(normalizeAgentLocale("fr")).toBe("de");
|
|
expect(normalizeAgentLocale(undefined)).toBe("de");
|
|
expect(normalizeAgentLocale(null)).toBe("de");
|
|
});
|
|
});
|
|
});
|