feat(agent): implement phase 2 runtime and inline clarification

This commit is contained in:
2026-04-09 14:28:27 +02:00
parent b08e448be0
commit 29c93eeb35
18 changed files with 2376 additions and 5 deletions

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import {
AGENT_MODELS,
DEFAULT_AGENT_MODEL_ID,
getAgentModel,
getAvailableAgentModels,
isAgentModelAvailableForTier,
} from "@/lib/agent-models";
import { NODE_DEFAULTS } from "@/lib/canvas-utils";
describe("agent models registry", () => {
it("contains approved models in stable order", () => {
expect(Object.keys(AGENT_MODELS)).toEqual([
"openai/gpt-5.4-nano",
"openai/gpt-5.4-mini",
"openai/gpt-5.4",
"openai/gpt-5.4-pro",
]);
expect(DEFAULT_AGENT_MODEL_ID).toBe("openai/gpt-5.4-mini");
});
it("resolves model lookup and pricing", () => {
expect(getAgentModel("openai/gpt-5.4-nano")?.creditCost).toBe(6);
expect(getAgentModel("openai/gpt-5.4-mini")?.creditCost).toBe(15);
expect(getAgentModel("openai/gpt-5.4")?.creditCost).toBe(38);
expect(getAgentModel("openai/gpt-5.4-pro")?.creditCost).toBe(180);
expect(getAgentModel("unknown-model")).toBeUndefined();
});
it("filters models by tier", () => {
expect(getAvailableAgentModels("free").map((model) => model.id)).toEqual([]);
expect(getAvailableAgentModels("starter").map((model) => model.id)).toEqual([
"openai/gpt-5.4-nano",
"openai/gpt-5.4-mini",
"openai/gpt-5.4",
]);
expect(getAvailableAgentModels("max").map((model) => model.id)).toEqual([
"openai/gpt-5.4-nano",
"openai/gpt-5.4-mini",
"openai/gpt-5.4",
"openai/gpt-5.4-pro",
]);
});
it("guards access by tier", () => {
expect(isAgentModelAvailableForTier("starter", "openai/gpt-5.4")).toBe(true);
expect(isAgentModelAvailableForTier("starter", "openai/gpt-5.4-pro")).toBe(false);
expect(isAgentModelAvailableForTier("max", "openai/gpt-5.4-pro")).toBe(true);
});
it("uses the registry default in agent node defaults", () => {
expect(NODE_DEFAULTS.agent?.data).toMatchObject({
templateId: "campaign-distributor",
modelId: DEFAULT_AGENT_MODEL_ID,
clarificationQuestions: [],
clarificationAnswers: {},
outputNodeIds: [],
});
});
});

View File

@@ -0,0 +1,96 @@
import { describe, expect, it } from "vitest";
import {
areClarificationAnswersComplete,
normalizeAgentOutputDraft,
type AgentClarificationAnswerMap,
type AgentClarificationQuestion,
} 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("");
});
});
});