import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { parseMetricFlowStagedDir } from '../../../../../src/context/ingest/adapters/metricflow/parse.js'; async function writeFixture(stagedDir: string, relPath: string, body: string): Promise { const abs = join(stagedDir, relPath); await mkdir(join(abs, '..'), { recursive: true }); await writeFile(abs, body, 'utf-8'); } describe('parseMetricFlowStagedDir', () => { let stagedDir: string; beforeEach(async () => { stagedDir = await mkdtemp(join(tmpdir(), 'mf-parse-')); }); afterEach(async () => { await rm(stagedDir, { recursive: true, force: true }); }); it('extracts one semantic_model with its measures + dimensions + entities', async () => { await writeFixture( stagedDir, 'models/orders.yml', [ 'semantic_models:', ' - name: orders', ' description: Order fact table.', " model: ref('orders')", ' entities:', ' - name: order_id', ' type: primary', ' - name: customer_id', ' type: foreign', ' dimensions:', ' - name: ordered_at', ' type: time', ' type_params:', ' time_granularity: day', ' measures:', ' - name: order_count', ' agg: count', ' expr: order_id', ' - name: gross_amount', ' agg: sum', ' expr: amount', '', ].join('\n'), ); const project = await parseMetricFlowStagedDir(stagedDir); expect(project.semanticModels).toHaveLength(1); const sm = project.semanticModels[0]; expect(sm.path).toBe('models/orders.yml'); expect(sm.name).toBe('orders'); expect(sm.modelRef).toBe('orders'); expect(sm.measureNames).toEqual(['gross_amount', 'order_count']); expect(sm.dimensionNames).toEqual(['ordered_at']); expect(sm.entityNames).toEqual(['customer_id', 'order_id']); expect(sm.primaryEntities).toEqual(['order_id']); expect(sm.foreignEntities).toEqual(['customer_id']); expect(sm.extendsFrom).toEqual([]); expect(project.files).toEqual([ { path: 'models/orders.yml', content: expect.stringContaining('semantic_models:'), }, ]); }); it('captures `extends:` as a string OR a list', async () => { await writeFixture( stagedDir, 'models/orders.yml', [ 'semantic_models:', ' - name: orders', " model: ref('orders')", ' measures:', ' - {name: order_count, agg: count, expr: order_id}', '', ].join('\n'), ); await writeFixture( stagedDir, 'models/orders_ext_list.yml', [ 'semantic_models:', ' - name: orders_ext_list', " model: ref('orders_ext')", ' extends: [orders]', ' measures:', ' - {name: refund_amount, agg: sum, expr: refund_amt}', '', ].join('\n'), ); await writeFixture( stagedDir, 'models/orders_ext_str.yml', [ 'semantic_models:', ' - name: orders_ext_str', " model: ref('orders_ext')", ' extends: orders', ' measures:', ' - {name: refund_amount2, agg: sum, expr: refund_amt2}', '', ].join('\n'), ); const project = await parseMetricFlowStagedDir(stagedDir); const list = project.semanticModels.find((sm) => sm.name === 'orders_ext_list'); const str = project.semanticModels.find((sm) => sm.name === 'orders_ext_str'); expect(list?.extendsFrom).toEqual(['orders']); expect(str?.extendsFrom).toEqual(['orders']); }); it('extracts metrics with referenced measures for simple + derived + ratio + cumulative', async () => { await writeFixture( stagedDir, 'metrics/core.yml', [ 'metrics:', ' - name: total_orders', ' type: simple', ' type_params:', ' measure: order_count', ' - name: revenue', ' type: derived', ' type_params:', ' expr: gross_amount - refund_amount', ' metrics:', ' - name: gross_amount', ' - name: refund_amount', ' - name: refund_rate', ' type: ratio', ' type_params:', ' numerator: refund_amount', ' denominator: gross_amount', ' - name: cum_revenue', ' type: cumulative', ' type_params:', ' measure: gross_amount', ' window: 7 days', '', ].join('\n'), ); const project = await parseMetricFlowStagedDir(stagedDir); expect(project.metrics).toHaveLength(4); const byName = new Map(project.metrics.map((m) => [m.name, m])); expect(byName.get('total_orders')?.type).toBe('simple'); expect(byName.get('total_orders')?.measureRef).toBe('order_count'); expect(byName.get('revenue')?.type).toBe('derived'); expect(byName.get('revenue')?.dependsOn.sort()).toEqual(['gross_amount', 'refund_amount']); expect(byName.get('refund_rate')?.type).toBe('ratio'); expect(byName.get('refund_rate')?.dependsOn.sort()).toEqual(['gross_amount', 'refund_amount']); expect(byName.get('cum_revenue')?.type).toBe('cumulative'); expect(byName.get('cum_revenue')?.measureRef).toBe('gross_amount'); }); it('returns empty arrays for a non-MetricFlow YAML (e.g. dbt_project.yml)', async () => { await writeFixture(stagedDir, 'dbt_project.yml', 'name: my_proj\nversion: "1.0.0"\n'); const project = await parseMetricFlowStagedDir(stagedDir); expect(project.semanticModels).toEqual([]); expect(project.metrics).toEqual([]); expect(project.allPaths).toEqual(['dbt_project.yml']); }); it('skips files that are not YAML (or fail to parse) without throwing', async () => { await writeFixture(stagedDir, 'broken.yml', '{ this is: not valid YAML :::'); await writeFixture(stagedDir, 'other.txt', 'ignore me'); const project = await parseMetricFlowStagedDir(stagedDir); expect(project.semanticModels).toEqual([]); expect(project.metrics).toEqual([]); // allPaths includes `.yml` / `.yaml` only, even when unparseable: expect(project.allPaths).toEqual(['broken.yml']); }); it('allPaths is sorted deterministically', async () => { await writeFixture(stagedDir, 'models/z.yml', 'semantic_models: []\n'); await writeFixture(stagedDir, 'models/a.yml', 'semantic_models: []\n'); await writeFixture(stagedDir, 'metrics/b.yaml', 'metrics: []\n'); const project = await parseMetricFlowStagedDir(stagedDir); expect(project.allPaths).toEqual(['metrics/b.yaml', 'models/a.yml', 'models/z.yml']); }); it("extracts modelRef from ref('name') and source('src','table') and literal strings", async () => { await writeFixture( stagedDir, 'models/a.yml', [ 'semantic_models:', ' - {name: a, model: "ref(\'orders\')", measures: [{name: c, agg: count, expr: id}]}', " - {name: b, model: \"source('raw','orders_raw')\", measures: [{name: c, agg: count, expr: id}]}", ' - {name: c, model: plain_table, measures: [{name: c, agg: count, expr: id}]}', '', ].join('\n'), ); const project = await parseMetricFlowStagedDir(stagedDir); const byName = new Map(project.semanticModels.map((s) => [s.name, s])); expect(byName.get('a')?.modelRef).toBe('orders'); expect(byName.get('b')?.modelRef).toBe('orders_raw'); expect(byName.get('c')?.modelRef).toBe('plain_table'); }); });