EmboFlow/apps/api/test/domain-contracts.spec.ts
eust-w 7d7cd14233
Some checks failed
Guardrails / repository-guardrails (push) Has been cancelled
feat: add dataset-aware workflow inputs
2026-03-30 14:18:57 +08:00

150 lines
4.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
ASSET_TYPES,
WORKFLOW_RUN_STATUSES,
WORKSPACE_TYPES,
} from "../../../packages/contracts/src/domain.ts";
import {
buildCustomNodeEnvelopePreview,
formatCustomNodeValidationIssue,
validateCustomNodeDefinition,
} from "../../../packages/contracts/src/custom-node.ts";
import {
normalizeWorkflowInputBindings,
splitWorkflowInputBindings,
} from "../../../packages/contracts/src/workflow-input.ts";
import { createMongoConnectionUri } from "../src/common/mongo/mongo.module.ts";
import {
ASSET_COLLECTION_NAME,
PROJECT_COLLECTION_NAME,
WORKFLOW_DEFINITION_COLLECTION_NAME,
WORKSPACE_COLLECTION_NAME,
} from "../src/common/mongo/schemas/index.ts";
test("workspace types include personal and team", () => {
assert.deepEqual(WORKSPACE_TYPES, ["personal", "team"]);
});
test("asset types include raw and dataset-oriented sources", () => {
assert.equal(ASSET_TYPES.includes("raw_file"), true);
assert.equal(ASSET_TYPES.includes("standard_dataset"), true);
assert.equal(ASSET_TYPES.includes("rosbag"), true);
});
test("workflow run statuses include the documented execution states", () => {
assert.equal(WORKFLOW_RUN_STATUSES.includes("pending"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("running"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("success"), true);
assert.equal(WORKFLOW_RUN_STATUSES.includes("failed"), true);
});
test("mongo connection uri builder uses environment-style config", () => {
assert.equal(
createMongoConnectionUri({
username: "emboflow",
password: "emboflow",
host: "localhost",
port: 27017,
database: "emboflow",
}),
"mongodb://emboflow:emboflow@localhost:27017/emboflow?authSource=admin",
);
});
test("schema collection names match the core domain objects", () => {
assert.equal(WORKSPACE_COLLECTION_NAME, "workspaces");
assert.equal(PROJECT_COLLECTION_NAME, "projects");
assert.equal(ASSET_COLLECTION_NAME, "assets");
assert.equal(WORKFLOW_DEFINITION_COLLECTION_NAME, "workflow_definitions");
});
test("custom node validation accepts a valid docker image utility node", () => {
const issues = validateCustomNodeDefinition({
name: "Merge Assets",
category: "Utility",
source: {
kind: "image",
image: "python:3.11-alpine",
command: ["python3", "-c", "print('merge')"],
},
contract: {
inputMode: "multi_asset_set",
outputMode: "asset_set",
artifactType: "json",
},
});
assert.deepEqual(issues, []);
});
test("custom node validation rejects invalid dockerfile and impossible source contract combinations", () => {
const issues = validateCustomNodeDefinition({
name: "Bad Source",
category: "Source",
source: {
kind: "dockerfile",
dockerfileContent: "CMD [\"python3\"]",
},
contract: {
inputMode: "multi_asset_set",
outputMode: "report",
artifactType: "json",
},
});
assert.deepEqual(issues, ["source_cannot_be_multi_input", "dockerfile_missing_from"]);
assert.equal(
formatCustomNodeValidationIssue("dockerfile_missing_from"),
"custom node dockerfile must include a FROM instruction",
);
});
test("custom node envelope preview reflects the declared input and output contract", () => {
const preview = buildCustomNodeEnvelopePreview({
inputMode: "multi_asset_set",
outputMode: "asset_set_with_report",
artifactType: "json",
});
assert.deepEqual(preview.input.context.assetIds, ["asset-123"]);
assert.deepEqual(preview.input.context.upstreamResults[0]?.result?.assetIds, ["asset-123"]);
assert.deepEqual(preview.output.result.assetIds, ["asset-123"]);
assert.equal(preview.output.result.artifactType, "json");
assert.equal(typeof preview.output.result.report, "object");
});
test("workflow input bindings normalize legacy asset and dataset inputs into one contract", () => {
const bindings = normalizeWorkflowInputBindings({
assetIds: ["asset-1", "asset-1", ""],
datasetIds: ["dataset-1", "dataset-2", "dataset-1"],
});
assert.deepEqual(bindings, [
{ kind: "asset", id: "asset-1" },
{ kind: "dataset", id: "dataset-1" },
{ kind: "dataset", id: "dataset-2" },
]);
assert.deepEqual(splitWorkflowInputBindings(bindings), {
assetIds: ["asset-1"],
datasetIds: ["dataset-1", "dataset-2"],
});
});
test("workflow input bindings prefer explicit bindings over legacy fallback arrays", () => {
const bindings = normalizeWorkflowInputBindings({
inputBindings: [
{ kind: "dataset", id: "dataset-9" },
{ kind: "dataset", id: "dataset-9" },
{ kind: "asset", id: "asset-2" },
],
assetIds: ["asset-legacy"],
});
assert.deepEqual(bindings, [
{ kind: "dataset", id: "dataset-9" },
{ kind: "asset", id: "asset-2" },
]);
});