/** * tests/analytics/dashboard-model.test.ts * ======================================== * BI-P060 — ANALYTICS DASHBOARD MODEL. * * The DoD is "a native SIAX dashboard can be reconstructed without Superset", * so the central test takes a document that CAME from Superset (it still * carries the slice ids in `vendor_ref`), plans it, and then greps the whole * plan for any trace of the vendor. A plan that still needs Superset to be * understood fails here. * * The counterfactual matters as much: `vendor-only-widget-is-refused` builds * the document that CANNOT be reconstructed — a widget whose only content is a * Superset id — and requires the model to refuse it. Without that test the * vendor-free assertion would pass trivially for documents nobody could * render. */ import { describe, expect, it } from 'vitest'; import { SiaxError } from '@/src/platform/errors'; import { dashboardDocumentHash, parseDashboardDocument, planDashboard, VISUALIZATION_TYPES, type DashboardDocument, } from '@/src/analytics/dashboards/dashboard-document'; /** A dashboard exported from Superset and rewritten into the canonical model. */ function supersetImportedDocument(): unknown { return { id: 'dash_sales_overview', title: 'Sales overview', description: 'Migrated from Superset dashboard 41.', version: 1, layout: { grid_columns: 12, row_height_px: 40 }, parameters: [ { name: 'region', type: 'enum', required: false, default_value: 'SE', allowed_values: ['SE', 'NO', 'DK'], }, { name: 'since', type: 'date', required: true }, ], filters: [ { filter_id: 'f_region', label: 'Region', dimension: 'region', operator: 'eq', default_values: ['{{region}}'], }, { filter_id: 'f_channel', label: 'Channel', dimension: 'channel', operator: 'in', default_values: ['web'], }, { filter_id: 'f_tenant_scope', label: 'Business unit', dimension: 'business_unit', operator: 'eq', default_values: ['bu_north'], locked: true, }, ], widgets: [ { widget_id: 'w_revenue_total', title: 'Revenue', source: { kind: 'metric', dataset: 'ds_sales', metric: 'revenue' }, visualization: { type: 'big_number', encoding: { value: 'revenue' } }, layout: { x: 0, y: 0, w: 3, h: 2 }, vendor_ref: { system: 'superset', id: 'slice-8812' }, }, { widget_id: 'w_revenue_by_day', title: 'Revenue by day', source: { kind: 'metric', dataset: 'ds_sales', metric: 'revenue', dimensions: ['order_date'], grain: 'day', }, visualization: { type: 'line', encoding: { x: 'order_date', y: ['revenue'] }, options: { stacked: false }, }, layout: { x: 3, y: 0, w: 9, h: 4 }, filter_bindings: ['f_region', 'f_tenant_scope'], vendor_ref: { system: 'superset', id: 'slice-8813' }, }, { widget_id: 'w_top_products', title: 'Top products', source: { kind: 'query', request: { dataset: 'ds_sales', metrics: ['revenue', 'orders'], dimensions: ['product'], filters: [{ dimension: 'status', operator: 'eq', values: ['completed'] }], orderBy: [{ ref: 'revenue', direction: 'desc' }], limit: 20, }, }, visualization: { type: 'table', encoding: { x: 'product', y: ['revenue', 'orders'] } }, layout: { x: 0, y: 4, w: 12, h: 6 }, }, ], refresh: { mode: 'interval', interval_seconds: 300, max_staleness_seconds: 900 }, permissions: { visibility: 'roles', roles: ['analyst', 'sales_lead'], required_entitlement: 'bi_dashboards', }, vendor_ref: { system: 'superset', id: 'dashboard-41' }, }; } describe('BI-P060 dashboard model — reconstruction without Superset', () => { it('plans every widget into a runnable analytics request', () => { const doc = parseDashboardDocument(supersetImportedDocument()); const plan = planDashboard(doc, { parameters: { since: '2026-01-01', region: 'NO' } }); expect(plan.widgets).toHaveLength(3); for (const w of plan.widgets) { expect(w.request.dataset).toBe('ds_sales'); expect(w.request.metrics.length).toBeGreaterThan(0); expect(VISUALIZATION_TYPES).toContain(w.visualization.type); } // The parameter reference was BOUND, not left as a template string. const byDay = plan.widgets.find((w) => w.widget_id === 'w_revenue_by_day'); expect(byDay?.request.filters.find((f) => f.dimension === 'region')?.values).toEqual(['NO']); expect(plan.bound_parameters).toEqual({ region: 'NO', since: '2026-01-01' }); }); it('the plan contains no vendor identifier anywhere', () => { const doc = parseDashboardDocument(supersetImportedDocument()); const plan = planDashboard(doc, { parameters: { since: '2026-01-01' } }); const serialized = JSON.stringify(plan).toLowerCase(); // The detector must be capable of finding one: the document DOES carry // these ids, so a grep that finds nothing in the plan means the plan is // clean, not that the grep is broken. expect(JSON.stringify(doc).toLowerCase()).toContain('slice-8812'); for (const trace of ['superset', 'slice-8812', 'slice-8813', 'dashboard-41', 'viz_type', 'form_data']) { expect(serialized).not.toContain(trace); } }); it('vendor-only widget is refused — it is the document that cannot be reconstructed', () => { const raw = supersetImportedDocument() as { widgets: unknown[] }; raw.widgets = [ { widget_id: 'w_mystery', title: 'Whatever Superset had', source: { kind: 'vendor', system: 'superset', id: 'slice-9001' }, visualization: { type: 'line', encoding: {} }, layout: { x: 0, y: 0, w: 4, h: 3 }, }, ]; expect(() => parseDashboardDocument(raw)).toThrow(SiaxError); expect(() => parseDashboardDocument(raw)).toThrow(/reconstruct/i); }); it('vendor-native fields are refused at the top level and inside a widget', () => { const top = { ...(supersetImportedDocument() as object), position_json: '{}' }; expect(() => parseDashboardDocument(top)).toThrow(/vendor-native/); const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[0].viz_type = 'echarts_timeseries'; expect(() => parseDashboardDocument(raw)).toThrow(/vendor-native/); }); it('a Superset viz_type value is not a SIAX visualization type', () => { const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[0].visualization = { type: 'echarts_timeseries', encoding: {} }; expect(() => parseDashboardDocument(raw)).toThrow(/not a SIAX visualization type/); }); }); describe('BI-P060 dashboard model — no SQL surface', () => { it('a widget cannot carry SQL, however it is spelled', () => { for (const key of ['sql', 'query_text', 'statement', 'where', 'expression']) { const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[0][key] = 'SELECT 1'; expect(() => parseDashboardDocument(raw), key).toThrow(SiaxError); } }); it('a query widget goes through the BI-P016 contract, so tenant selection is impossible', () => { const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[2].source = { kind: 'query', request: { dataset: 'ds_sales', metrics: ['revenue'], dimensions: [], filters: [], tenant_id: 't_victim', }, }; expect(() => parseDashboardDocument(raw)).toThrow(/does not accept SQL, catalogs, or tenant selection/); }); it('the document itself cannot name a tenant', () => { const raw = { ...(supersetImportedDocument() as object), tenant_id: 't_victim' }; expect(() => parseDashboardDocument(raw)).toThrow(/dashboards do not carry SQL|unknown field/); }); }); describe('BI-P060 dashboard model — validation that would otherwise bite at render time', () => { it('a widget binding an undeclared filter is refused', () => { const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[1].filter_bindings = ['f_nonexistent']; expect(() => parseDashboardDocument(raw)).toThrow(/undeclared filter/); }); it('a filter defaulting to an undeclared parameter is refused', () => { const raw = supersetImportedDocument() as { filters: Record[] }; raw.filters[0].default_values = ['{{not_declared}}']; expect(() => parseDashboardDocument(raw)).toThrow(/undeclared parameter/); }); it('a widget overflowing the grid is refused', () => { const raw = supersetImportedDocument() as { widgets: Record[] }; raw.widgets[1].layout = { x: 9, y: 0, w: 6, h: 3 }; expect(() => parseDashboardDocument(raw)).toThrow(/overflows the 12-column grid/); }); it('an interval refresh with a cron, and a manual refresh with a schedule, are both refused', () => { const a = supersetImportedDocument() as Record; a.refresh = { mode: 'interval', interval_seconds: 300, cron: '0 * * * *', max_staleness_seconds: 900 }; expect(() => parseDashboardDocument(a)).toThrow(/meaningless/); const b = supersetImportedDocument() as Record; b.refresh = { mode: 'manual', interval_seconds: 300, max_staleness_seconds: 900 }; expect(() => parseDashboardDocument(b)).toThrow(/manual/); }); it('visibility "roles" with no roles is refused rather than guessed', () => { const raw = supersetImportedDocument() as Record; raw.permissions = { visibility: 'roles', roles: [] }; expect(() => parseDashboardDocument(raw)).toThrow(/deny all/); }); it('a required parameter that was not supplied fails the plan, not the render', () => { const doc = parseDashboardDocument(supersetImportedDocument()); expect(() => planDashboard(doc, { parameters: {} })).toThrow(/required parameter "since"/); }); it('a locked filter cannot be switched off by a viewer', () => { const doc = parseDashboardDocument(supersetImportedDocument()); expect(() => planDashboard(doc, { parameters: { since: '2026-01-01' }, disabled_filter_ids: ['f_tenant_scope'] }), ).toThrow(/locked/); // An unlocked one can be, and disappears from the compiled request. const plan = planDashboard(doc, { parameters: { since: '2026-01-01' }, disabled_filter_ids: ['f_channel'], }); const table = plan.widgets.find((w) => w.widget_id === 'w_top_products'); expect(table?.request.filters.map((f) => f.dimension)).not.toContain('channel'); }); it('an enum parameter without allowed_values is refused', () => { const raw = supersetImportedDocument() as { parameters: Record[] }; raw.parameters[0] = { name: 'region', type: 'enum', required: false, default_value: 'SE' }; expect(() => parseDashboardDocument(raw)).toThrow(/unbounded enum/); }); }); /** Rebuild an object graph with every object's keys in reverse order. */ function shuffleKeys(value: unknown): unknown { if (Array.isArray(value)) return value.map(shuffleKeys); if (value !== null && typeof value === 'object') { const out: Record = {}; for (const k of Object.keys(value as object).reverse()) { out[k] = shuffleKeys((value as Record)[k]); } return out; } return value; } describe('BI-P060 dashboard model — canonical hash', () => { function parsed(): DashboardDocument { return parseDashboardDocument(supersetImportedDocument()); } it('is stable across key order and vendor re-export', () => { const a = parsed(); const b = parsed(); b.vendor_ref = { system: 'superset', id: 'dashboard-999-after-migration' }; b.widgets[0].vendor_ref = { system: 'metabase', id: 'card-3' }; expect(dashboardDocumentHash(b)).toBe(dashboardDocumentHash(a)); // Key order in the source JSON must not change identity either. expect(dashboardDocumentHash(parseDashboardDocument(shuffleKeys(supersetImportedDocument())))).toBe( dashboardDocumentHash(a), ); }); it('changes when meaning changes', () => { const base = dashboardDocumentHash(parsed()); const retitled = parsed(); retitled.widgets[0].title = 'Net revenue'; expect(dashboardDocumentHash(retitled)).not.toBe(base); const remetric = parsed(); (remetric.widgets[0].source as { metric: string }).metric = 'revenue_net'; expect(dashboardDocumentHash(remetric)).not.toBe(base); const moved = parsed(); moved.widgets[1].layout = { x: 3, y: 1, w: 9, h: 4 }; expect(dashboardDocumentHash(moved)).not.toBe(base); const refiltered = parsed(); refiltered.filters[1].default_values = ['store']; expect(dashboardDocumentHash(refiltered)).not.toBe(base); }); });