feat(appliance): initial release of MERICA standalone appliance v1.0.0
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user