feat(agent): implement phase 2 runtime and inline clarification
This commit is contained in:
61
tests/lib/agent-models.test.ts
Normal file
61
tests/lib/agent-models.test.ts
Normal 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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
96
tests/lib/agent-run-contract.test.ts
Normal file
96
tests/lib/agent-run-contract.test.ts
Normal 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("");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user