Files
merica/frontend/src/utils/yamlCodec.test.js
T

140 lines
4.4 KiB
JavaScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import * as yaml from 'js-yaml';
/**
* Helper to serialize canvas topology to Airflow YAML specification.
* Directly mirrors StudioPage.jsx generateCanvasYaml implementation.
*/
function generateCanvasYaml({ nodes, edges, pipelineId, schedule, bundleName, description }) {
const taskNodes = nodes.filter((n) => n.type !== 'noteNode');
if (taskNodes.length === 0) {
return 'dag_id: ' + (pipelineId || 'example') + '\ntasks: []';
}
const tasks = taskNodes.map((n) => {
const nodeType = n.data?.type || n.type || 'source_db';
const config = n.data?.params || n.data?.config || {};
const targetEdges = edges.filter((e) => (e.target || e[1]) === n.id);
const dependencies = targetEdges.map((e) => e.source || e[0]);
const isDisabled = n.data?.disabled === true || n.data?.enabled === false;
return {
task_id: n.id,
type: isDisabled ? 'empty' : (n.data?.yamlType || nodeType),
label: n.data?.label || n.data?.title || n.id,
enabled: !isDisabled,
...(dependencies.length > 0 ? { dependencies } : {}),
config: isDisabled ? {} : config,
};
});
const pipelineSpec = {
dag_id: pipelineId || 'example_pipeline',
schedule: schedule || '@hourly',
bundle: bundleName || 'dlh-core-ingestion',
description: description || '',
tasks,
};
return yaml.dump(pipelineSpec);
}
describe('yamlCodec.test.js - Airflow YAML Generator and Codec', () => {
it('1. Generates clean YAML with dag_id, schedule, and tasks from canvas graph', () => {
const nodes = [
{
id: 'node-db',
type: 'customNode',
data: {
label: 'Source Database',
type: 'source_db',
yamlType: 'source_db',
params: { conn: 'Postgres', table: 'permits' },
},
},
{
id: 'node-dbt',
type: 'customNode',
data: {
label: 'dbt Transform',
type: 'dbt_transform',
yamlType: 'dbt_transform',
params: { sel: 'gold', tgt: 'postgres' },
},
},
];
const edges = [{ source: 'node-db', target: 'node-dbt' }];
const yamlOutput = generateCanvasYaml({
nodes,
edges,
pipelineId: 'permits_pipeline',
schedule: '0 */2 * * *',
});
expect(yamlOutput).toContain('dag_id: permits_pipeline');
expect(yamlOutput).toMatch(/schedule:\s*"?0 \*\/2 \* \* \*"?/);
expect(yamlOutput).toContain('task_id: node-db');
expect(yamlOutput).toContain('type: source_db');
expect(yamlOutput).toContain('task_id: node-dbt');
expect(yamlOutput).toContain('type: dbt_transform');
expect(yamlOutput).toContain('dependencies:');
expect(yamlOutput).toContain('- node-db');
// Parse back to confirm valid YAML syntax
const parsed = yaml.load(yamlOutput);
expect(parsed.dag_id).toBe('permits_pipeline');
expect(parsed.tasks).toHaveLength(2);
expect(parsed.tasks[1].dependencies).toContain('node-db');
expect(parsed.tasks[0].config.table).toBe('permits');
});
it('2. Maps disabled task to type: empty and enabled: false per Decision #1', () => {
const nodes = [
{
id: 'node-disabled',
type: 'customNode',
data: {
label: 'Disabled Task',
type: 'ge_quality',
yamlType: 'ge_quality',
disabled: true,
enabled: false,
params: { suite: 'bronze_suite' },
},
},
];
const yamlOutput = generateCanvasYaml({ nodes, edges: [], pipelineId: 'disabled_test' });
const parsed = yaml.load(yamlOutput);
expect(parsed.tasks[0].type).toBe('empty');
expect(parsed.tasks[0].enabled).toBe(false);
expect(parsed.tasks[0].config).toEqual({});
});
it('3. Strictly omits note nodes from YAML output per data-model.md', () => {
const nodes = [
{
id: 'note-1',
type: 'noteNode',
data: { text: 'Architecture notes\nInternal details' },
},
{
id: 'node-task',
type: 'customNode',
data: { label: 'Real Task', type: 'source_file', params: { path: 'data/' } },
},
];
const yamlOutput = generateCanvasYaml({ nodes, edges: [], pipelineId: 'notes_test' });
const parsed = yaml.load(yamlOutput);
expect(parsed.tasks).toHaveLength(1);
expect(parsed.tasks[0].task_id).toBe('node-task');
expect(yamlOutput).not.toContain('note-1');
expect(yamlOutput).not.toContain('Architecture notes');
});
});