Files
0pt/tests/analytics/dashboard-model.test.ts
Claude Opus 5 2af126dfc4
CI (SIAX Cloud) / quality (push) Successful in 1m22s
CI (SIAX Cloud) / integration (push) Successful in 1m7s
CI (SIAX Cloud) / security (push) Successful in 34s
Security Scan (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m8s
CI (SIAX Cloud) / integration (pull_request) Successful in 45s
CI (SIAX Cloud) / security (pull_request) Successful in 24s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 28s
feat(analytics): BI-P060 — engine-neutral dashboard model that plans without Superset
BI-P002 named DashboardDefinition/DashboardVersion and sql/008 stored them,
but `layout_json` was a string, and a string holds anything — including a
Superset export only Superset can read back. This gives the document a body
and makes the DoD checkable rather than asserted.

planDashboard() is the DoD: a document in, runnable AnalyticsQueryRequest
objects (BI-P016) plus visualization specs out. The test greps the whole
serialized plan for vendor traces after first proving the detector CAN find
one in the source document — a clean grep over a broken detector is the usual
way this test passes for the wrong reason.

The counterfactual is a test too: a widget whose only content is a Superset
slice id is REFUSED, because that is exactly the document that cannot be
reconstructed. vendor_ref survives for round-tripping but is excluded from
the canonical hash, so re-exporting to a new Superset instance does not
fabricate a new dashboard version.

No SQL surface: query widgets are parsed by parseAnalyticsQueryRequest, so
`sql`, `where` and `tenant_id` are refused structurally rather than by
review. 18 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 03:43:06 +02:00

331 lines
13 KiB
TypeScript

/**
* 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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>[] };
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<string, unknown>;
a.refresh = { mode: 'interval', interval_seconds: 300, cron: '0 * * * *', max_staleness_seconds: 900 };
expect(() => parseDashboardDocument(a)).toThrow(/meaningless/);
const b = supersetImportedDocument() as Record<string, unknown>;
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<string, unknown>;
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<string, unknown>[] };
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<string, unknown> = {};
for (const k of Object.keys(value as object).reverse()) {
out[k] = shuffleKeys((value as Record<string, unknown>)[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);
});
});