Files
content-factory/apps/frontend/tests/admin_workflows.model.test.mjs

412 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
const testDir = path.dirname(fileURLToPath(import.meta.url));
const failures = [];
function loadTsModule(sourcePath, requireOverrides = {}) {
const source = readFileSync(sourcePath, "utf8");
const compiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
});
const moduleExports = {};
const module = { exports: moduleExports };
const localRequire = (specifier) => {
if (specifier in requireOverrides) {
return requireOverrides[specifier];
}
throw new Error(`Unexpected runtime import in model test: ${specifier}`);
};
new Function("exports", "module", "require", compiled.outputText)(
moduleExports,
module,
localRequire,
);
return module.exports;
}
function loadExpectedModule(sourcePath, label, expectedExports = []) {
if (!existsSync(sourcePath)) {
const expected = expectedExports.join(", ");
failures.push(
`${label} is missing at ${path.relative(process.cwd(), sourcePath)}; expected exports: ${expected}`,
);
return null;
}
try {
return loadTsModule(sourcePath);
} catch (error) {
failures.push(`${label} failed to load: ${error.message}`);
return null;
}
}
function expectFunction(moduleExports, exportName, label) {
if (!moduleExports) {
return null;
}
const value = moduleExports?.[exportName];
if (typeof value !== "function") {
failures.push(`${label} must export ${exportName}()`);
return null;
}
return value;
}
async function recordAssertion(label, assertion) {
try {
await assertion();
} catch (error) {
failures.push(`${label}: ${error.message}`);
}
}
const workflowModelPath = path.resolve(
testDir,
"../src/features/admin-workflows/model.ts",
);
const apiPath = path.resolve(testDir, "../src/shared/pipeline-api.ts");
const navigationModelPath = path.resolve(
testDir,
"../src/widgets/role-navigation/model.ts",
);
const expectedWorkflowModelExports = [
"defaultWorkflowDraft",
"buildCreateWorkflowPayload",
"defaultWorkflowStageDraft",
"buildCreateWorkflowStagePayload",
"moveWorkflowStage",
"buildReorderWorkflowStagesPayload",
];
const workflowDraft = {
name: "Article production workflow",
slug: "article-production-v1",
description: "Reusable editorial workflow for longform article production.",
};
const workflowId = "11111111-1111-1111-1111-111111111111";
const intakeStageId = "22222222-2222-2222-2222-222222222222";
const draftStageId = "33333333-3333-3333-3333-333333333333";
const workflowPart = {
key: "required-context-checklist",
type: "checklist",
title: "Required context checklist",
payload: {
prompt: "Identify missing audience, keyword, and source constraints.",
config: {
required_fields: ["audience", "primary_keyword", "source_rules"],
block_on_missing: true,
},
},
acceptance_criteria: [
"Missing required context is listed explicitly.",
"No free-text parsing is needed to read the checklist.",
],
};
const intakeStageDraft = {
stableKey: "intake-boundary-questions",
displayName: "Boundary question intake",
description: "Collect required editorial context before planning.",
position: 1,
ownerRole: "EDITOR",
runnerProfileKey: "boundary-question-agent-v1",
requiredInputs: ["article_brief", "target_site"],
expectedOutputs: ["answered_boundary_questions"],
acceptanceCriteria: ["All required boundary questions are answered."],
requiresHumanApproval: true,
retryPolicy: { max_attempts: 1, backoff_seconds: 0 },
parts: [workflowPart],
};
const expectedIntakeStagePayload = {
stable_key: "intake-boundary-questions",
display_name: "Boundary question intake",
description: "Collect required editorial context before planning.",
position: 1,
owner_role: "EDITOR",
runner_profile_key: "boundary-question-agent-v1",
required_inputs: ["article_brief", "target_site"],
expected_outputs: ["answered_boundary_questions"],
acceptance_criteria: ["All required boundary questions are answered."],
requires_human_approval: true,
retry_policy: { max_attempts: 1, backoff_seconds: 0 },
parts: [workflowPart],
};
const workflowModelExports = loadExpectedModule(
workflowModelPath,
"features/admin-workflows/model",
expectedWorkflowModelExports,
);
const apiExports = loadExpectedModule(apiPath, "shared/pipeline-api");
const navigationExports = loadExpectedModule(
navigationModelPath,
"widgets/role-navigation/model",
);
const defaultWorkflowDraft = expectFunction(
workflowModelExports,
"defaultWorkflowDraft",
"features/admin-workflows/model",
);
const buildCreateWorkflowPayload = expectFunction(
workflowModelExports,
"buildCreateWorkflowPayload",
"features/admin-workflows/model",
);
const defaultWorkflowStageDraft = expectFunction(
workflowModelExports,
"defaultWorkflowStageDraft",
"features/admin-workflows/model",
);
const buildCreateWorkflowStagePayload = expectFunction(
workflowModelExports,
"buildCreateWorkflowStagePayload",
"features/admin-workflows/model",
);
const moveWorkflowStage = expectFunction(
workflowModelExports,
"moveWorkflowStage",
"features/admin-workflows/model",
);
const buildReorderWorkflowStagesPayload = expectFunction(
workflowModelExports,
"buildReorderWorkflowStagesPayload",
"features/admin-workflows/model",
);
const createWorkflowTemplate = expectFunction(
apiExports,
"createWorkflowTemplate",
"shared/pipeline-api",
);
const addWorkflowStage = expectFunction(
apiExports,
"addWorkflowStage",
"shared/pipeline-api",
);
const reorderWorkflowStages = expectFunction(
apiExports,
"reorderWorkflowStages",
"shared/pipeline-api",
);
const getRoleNavigationItems = expectFunction(
navigationExports,
"getRoleNavigationItems",
"widgets/role-navigation/model",
);
if (
defaultWorkflowDraft &&
buildCreateWorkflowPayload &&
defaultWorkflowStageDraft &&
buildCreateWorkflowStagePayload &&
moveWorkflowStage &&
buildReorderWorkflowStagesPayload
) {
await recordAssertion("workflow draft payload maps model state to API request", () => {
const createPayload = buildCreateWorkflowPayload({
...defaultWorkflowDraft(),
...workflowDraft,
});
assert.deepEqual(createPayload, workflowDraft);
});
await recordAssertion("workflow stage payload preserves structured parts", () => {
const stagePayload = buildCreateWorkflowStagePayload({
...defaultWorkflowStageDraft(),
...intakeStageDraft,
});
assert.deepEqual(stagePayload, expectedIntakeStagePayload);
});
await recordAssertion("workflow stage reorder returns ordered IDs for API", () => {
const stages = [
{ id: intakeStageId, stableKey: "intake-boundary-questions", position: 1 },
{ id: draftStageId, stableKey: "draft-assembly", position: 2 },
];
const reorderedStages = moveWorkflowStage(stages, draftStageId, 0);
assert.deepEqual(
reorderedStages.map((stage) => ({ id: stage.id, position: stage.position })),
[
{ id: draftStageId, position: 1 },
{ id: intakeStageId, position: 2 },
],
);
assert.deepEqual(buildReorderWorkflowStagesPayload(reorderedStages), {
stage_ids: [draftStageId, intakeStageId],
});
});
}
if (createWorkflowTemplate && addWorkflowStage && reorderWorkflowStages) {
await recordAssertion("workflow API client posts create/add/reorder requests", async () => {
const originalFetch = globalThis.fetch;
const requests = [];
const responses = [
{
workflow: {
id: workflowId,
...workflowDraft,
version: 1,
status: "DRAFT",
stages: [],
created_at: "2026-05-22T00:00:00Z",
updated_at: "2026-05-22T00:00:00Z",
},
},
{
stage: {
id: intakeStageId,
workflow_id: workflowId,
...expectedIntakeStagePayload,
},
},
{
stage: {
id: draftStageId,
workflow_id: workflowId,
...expectedIntakeStagePayload,
stable_key: "draft-assembly",
display_name: "Draft assembly",
position: 2,
},
},
{
workflow: {
id: workflowId,
...workflowDraft,
version: 1,
status: "DRAFT",
stages: [
{ id: draftStageId, stable_key: "draft-assembly", position: 1 },
{
id: intakeStageId,
stable_key: "intake-boundary-questions",
position: 2,
},
],
created_at: "2026-05-22T00:00:00Z",
updated_at: "2026-05-22T00:00:00Z",
},
},
];
globalThis.fetch = async (url, options) => {
requests.push({
path: new URL(String(url)).pathname,
method: options.method,
userEmail: options.headers["X-Demo-User-Email"],
body: options.body ? JSON.parse(options.body) : undefined,
});
return {
ok: true,
status: 200,
statusText: "OK",
text: async () => JSON.stringify(responses.shift()),
};
};
try {
const created = await createWorkflowTemplate(workflowDraft);
const intakeStage = await addWorkflowStage(
created.workflow.id,
expectedIntakeStagePayload,
);
const draftStage = await addWorkflowStage(created.workflow.id, {
...expectedIntakeStagePayload,
stable_key: "draft-assembly",
display_name: "Draft assembly",
position: 2,
});
const reordered = await reorderWorkflowStages(created.workflow.id, {
stage_ids: [draftStage.stage.id, intakeStage.stage.id],
});
assert.equal(created.workflow.status, "DRAFT");
assert.deepEqual(
reordered.workflow.stages.map((stage) => stage.id),
[draftStageId, intakeStageId],
);
} finally {
globalThis.fetch = originalFetch;
}
assert.deepEqual(requests, [
{
path: "/api/admin/workflows",
method: "POST",
userEmail: "admin@example.com",
body: workflowDraft,
},
{
path: `/api/admin/workflows/${workflowId}/stages`,
method: "POST",
userEmail: "admin@example.com",
body: expectedIntakeStagePayload,
},
{
path: `/api/admin/workflows/${workflowId}/stages`,
method: "POST",
userEmail: "admin@example.com",
body: {
...expectedIntakeStagePayload,
stable_key: "draft-assembly",
display_name: "Draft assembly",
position: 2,
},
},
{
path: `/api/admin/workflows/${workflowId}/stages/reorder`,
method: "POST",
userEmail: "admin@example.com",
body: { stage_ids: [draftStageId, intakeStageId] },
},
]);
});
}
if (getRoleNavigationItems) {
await recordAssertion("Admin navigation exposes Workflows builder route", () => {
const adminItems = getRoleNavigationItems("ADMIN");
assert.deepEqual(
adminItems.find((item) => item.id === "workflows"),
{
id: "workflows",
label: "Workflows",
href: "/admin/workflows",
roles: ["ADMIN"],
},
);
});
await recordAssertion("Editor navigation does not expose Workflows", () => {
const editorItems = getRoleNavigationItems("EDITOR");
assert.equal(
editorItems.some(
(item) =>
item.id === "workflows" ||
item.href === "/admin/workflows" ||
item.label === "Workflows",
),
false,
);
});
}
if (failures.length > 0) {
assert.fail(
`Admin workflow builder frontend pre-requirements missing:\n- ${failures.join("\n- ")}`,
);
}