333 lines
23 KiB
TypeScript
333 lines
23 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, createElement } from 'react';
|
|
import { readFileSync } from 'node:fs';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { AssessorApiClient } from '../../src/api/assessor-api-client';
|
|
import type { DbStructureTable } from '../../src/domain/db-structure';
|
|
import { DbStructurePanel } from '../../src/features/db-structure/DbStructurePanel';
|
|
import { EngineDataFlow } from '../../src/features/db-structure/EngineDataFlow';
|
|
import { DATA_FLOW_RELATIONSHIPS } from '../../src/features/db-structure/DataFlowRelationships';
|
|
import { buildDbStructureFlow, dbStructureNeighbours, DB_FLOW_STEPS } from '../../src/features/db-structure/db-structure-flow';
|
|
|
|
const table = (name: string, references: string[] = [], rows: number | null = null): DbStructureTable => ({
|
|
name: `assessor_${name}`, layer: 'other', approximateRowCount: rows,
|
|
columns: [{ name: 'id', dataType: 'text', nullable: false }],
|
|
foreignKeys: references.map(target => ({ column: `${target}_id`, referencedTable: `assessor_${target}`, referencedColumn: 'id' }))
|
|
});
|
|
|
|
describe('DB Structure data flow', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it.each(['dark', 'light'])('keeps every engine framed after deselection without highlighting arrows in %s', async theme => {
|
|
document.documentElement.dataset.theme = theme;
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
try {
|
|
await act(async () => root.render(createElement(EngineDataFlow)));
|
|
const engines = [...container.querySelectorAll<HTMLButtonElement>('.db-engine-flow__node.is-engine')];
|
|
expect(engines.map(engine => engine.dataset.node)).toEqual(['core', 'context', 'report', 'remediation', 'compliance']);
|
|
const connectionCount: Record<string, number> = { core: 3, context: 4, report: 3, remediation: 4, compliance: 4 };
|
|
// Remove the initial Core selection; identity frames must remain independently.
|
|
await act(async () => engines[0].click());
|
|
const highlighted = () => container.querySelectorAll('.db-engine-flow__edges > path.is-related');
|
|
expect(highlighted()).toHaveLength(0);
|
|
for (const engine of engines) {
|
|
expect(engine.classList.contains('is-engine')).toBe(true);
|
|
expect(engine.getAttribute('aria-pressed')).toBe('false');
|
|
await act(async () => engine.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })));
|
|
expect(highlighted()).toHaveLength(0);
|
|
await act(async () => engine.click());
|
|
expect(engine.getAttribute('aria-pressed')).toBe('true');
|
|
const selectedPaths = [...highlighted()].map(path => path.getAttribute('d'));
|
|
expect(selectedPaths).toHaveLength(connectionCount[engine.dataset.node!]);
|
|
await act(async () => engine.click());
|
|
expect(highlighted()).toHaveLength(0);
|
|
expect(engine.classList.contains('is-engine')).toBe(true);
|
|
}
|
|
const css = readFileSync('src/styles/datahub.css', 'utf8');
|
|
// Engine identity must contrast with ordinary related-node borders in both themes.
|
|
expect(css).toMatch(/\.db-engine-flow__node\.is-engine\s*\{[^}]*outline:\s*var\(--bw-emphasis\) solid var\(--muted\)/);
|
|
expect(css).toMatch(/\.db-engine-flow__node\.is-engine\s*\{[^}]*border-left:\s*var\(--marker-width\) solid var\(--muted\)/);
|
|
expect(css).toMatch(/\.db-engine-flow__node\[aria-pressed="true"\]\s*\{[^}]*outline:[^;]*var\(--marking-color\)/);
|
|
expect(css).toMatch(/\.db-engine-flow__edges > path\.is-related\s*\{[^}]*stroke:\s*var\(--marking-color\)/);
|
|
} finally { await act(async () => root.unmount()); }
|
|
});
|
|
|
|
it('selects engine modules, highlights only their directed connections and shows the implemented Context publication', async () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
await act(async () => root.render(createElement(EngineDataFlow)));
|
|
expect(container.querySelectorAll('.db-engine-flow__node')).toHaveLength(16);
|
|
expect(container.querySelectorAll('.db-engine-flow__edges > path')).toHaveLength(17);
|
|
expect(container.querySelectorAll('.db-engine-flow__edges > path[marker-start]')).toHaveLength(4);
|
|
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(3);
|
|
expect(container.querySelector('.db-engine-flow__dictionary')?.hasAttribute('open')).toBe(false);
|
|
const context = [...container.querySelectorAll<HTMLButtonElement>('.db-engine-flow__node')]
|
|
.find(button => button.textContent?.startsWith('Context Engine'))!;
|
|
await act(async () => context.click());
|
|
expect(context.getAttribute('aria-pressed')).toBe('true');
|
|
expect(container.querySelector('.db-engine-flow__inspector h4')?.textContent).toBe('Context Engine');
|
|
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(4);
|
|
// The Management Report consumes the published Context results, so no relationship is drawn as planned.
|
|
expect(container.querySelector('.db-engine-flow__edges > path.is-planned')).toBeNull();
|
|
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('Versioned Context publication for the Management Report');
|
|
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).not.toContain('Planned');
|
|
await act(async () => context.click());
|
|
expect(container.querySelector('.db-engine-flow__inspector')).toBeNull();
|
|
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(0);
|
|
// The Compliance Engine reads through the port, owns its tables and reaches the UI only through extension points.
|
|
const compliance = [...container.querySelectorAll<HTMLButtonElement>('.db-engine-flow__node')]
|
|
.find(button => button.textContent?.startsWith('Compliance Engine'))!;
|
|
await act(async () => compliance.click());
|
|
expect(container.querySelector('.db-engine-flow__inspector h4')?.textContent).toBe('Compliance Engine');
|
|
expect([...container.querySelectorAll('.db-engine-flow__inspector li strong')].map(item => item.textContent)).toEqual([
|
|
'Core read-only port → Compliance Engine', 'Compliance Engine ↔ Compliance tables',
|
|
'Compliance Engine ↔ Retrieval & model', 'Compliance Engine → ISO mapping in the UI']);
|
|
await act(async () => compliance.click());
|
|
// The Remediation Engine reads through the port and the published Context, owns its schema and reaches the UI through extension points.
|
|
const remediation = [...container.querySelectorAll<HTMLButtonElement>('.db-engine-flow__node')]
|
|
.find(button => button.textContent?.startsWith('Remediation Engine'))!;
|
|
await act(async () => remediation.click());
|
|
expect(container.querySelector('.db-engine-flow__inspector h4')?.textContent).toBe('Remediation Engine');
|
|
expect([...container.querySelectorAll('.db-engine-flow__inspector li strong')].map(item => item.textContent)).toEqual([
|
|
'Core read-only port → Remediation Engine', 'Context Engine → Remediation Engine',
|
|
'Remediation Engine ↔ Remediation tables', 'Remediation Engine → Work packages in the UI']);
|
|
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('assessor_remediation');
|
|
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).not.toContain('Planned');
|
|
await act(async () => remediation.click());
|
|
expect(container.querySelectorAll('.db-engine-flow__node')).toHaveLength(16);
|
|
const dictionary = container.querySelector<HTMLDetailsElement>('.db-engine-flow__dictionary')!;
|
|
await act(async () => dictionary.querySelector('summary')!.click());
|
|
expect(dictionary.open).toBe(true);
|
|
expect(dictionary.querySelector('summary')?.textContent).toBe('All relationships (17)');
|
|
expect(dictionary.querySelectorAll('tbody tr')).toHaveLength(17);
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it('assigns each table to exactly one step and never hides an unassigned table', () => {
|
|
const assigned = DB_FLOW_STEPS.flatMap(step => step.tables);
|
|
expect(new Set(assigned).size).toBe(assigned.length);
|
|
const flow = buildDbStructureFlow([table('projects'), table('gpo_backup_instances', ['projects']), table('synthetic_new_table')]);
|
|
expect(flow.steps.map(step => step.id)).toEqual(['foundation', 'gpo-inventory']);
|
|
expect(flow.other.map(item => item.name)).toEqual(['assessor_synthetic_new_table']);
|
|
});
|
|
|
|
it('keeps a schema-qualified engine table visible under Other tables', () => {
|
|
// Engine-owned schemas are listed as schema.table; no processing step claims them.
|
|
const flow = buildDbStructureFlow([table('projects'), table('remediation.package_snapshots')]);
|
|
expect(flow.steps.map(step => step.id)).toEqual(['foundation']);
|
|
expect(flow.other.map(item => item.name)).toEqual(['assessor_remediation.package_snapshots']);
|
|
});
|
|
|
|
it('lists outgoing and incoming relations of the selected table', () => {
|
|
const tables = [table('projects'), table('assessments', ['projects']), table('reviews', ['assessments', 'projects'])];
|
|
expect(dbStructureNeighbours(tables, 'assessor_assessments')).toEqual({ outgoing: ['assessor_projects'], incoming: ['assessor_reviews'] });
|
|
});
|
|
|
|
it('keeps every arrow outside tiles after selecting a tile and resizing to a stacked flow', async () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
vi.spyOn(AssessorApiClient.prototype, 'getDatabaseStructure').mockResolvedValue({
|
|
generatedAt: '2026-10-02T10:00:00.000Z', persistence: 'postgresql',
|
|
tables: DB_FLOW_STEPS.flatMap(step => step.tables.map(name => table(name)))
|
|
});
|
|
const desktop: Record<string, [number, number, number, number]> = {
|
|
foundation: [454, 100, 358, 100], upload: [454, 296, 358, 180],
|
|
'scanner-processing': [0, 700, 358, 200], 'scanner-mapping': [0, 1096, 358, 100],
|
|
'scanner-canonical': [0, 1392, 358, 180], 'scanner-findings': [0, 1768, 358, 220],
|
|
'gpo-inventory': [454, 700, 358, 100], 'gpo-identity': [454, 996, 358, 200],
|
|
'gpo-aggregation': [454, 1392, 358, 180], 'gpo-evaluation': [454, 1768, 358, 230],
|
|
'gpo-findings': [454, 2194, 358, 140], 'reference-baselines': [908, 700, 358, 280],
|
|
'result-materialization': [454, 2530, 358, 180], 'result-db': [227, 2906, 358, 460],
|
|
review: [681, 2906, 358, 140], comparison: [454, 3562, 358, 240], other: [454, 3998, 358, 100]
|
|
};
|
|
let positions = desktop;
|
|
let width = 1266;
|
|
const originalBounds = HTMLElement.prototype.getBoundingClientRect;
|
|
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
|
|
if (this.classList.contains('db-flow')) return new DOMRect(0, 0, width, 7000);
|
|
const box = positions[this.dataset.flowId ?? ''];
|
|
return box ? new DOMRect(...box) : originalBounds.call(this);
|
|
});
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(createElement(DbStructurePanel, { backendStatus: 'connected' }));
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
});
|
|
const assertClear = () => {
|
|
const paths = [...container.querySelectorAll('.db-flow__edges > path')];
|
|
expect(paths).toHaveLength(17);
|
|
const visibleBoxes = [...container.querySelectorAll<HTMLElement>('[data-flow-id]')]
|
|
.map(node => positions[node.dataset.flowId!]);
|
|
for (const path of paths) {
|
|
let x = 0, y = 0;
|
|
for (const command of path.getAttribute('d')!.matchAll(/([MHV])(-?[\d.]+)(?:\s+(-?[\d.]+))?/g)) {
|
|
const nextX = command[1] === 'V' ? x : Number(command[2]);
|
|
const nextY = command[1] === 'H' ? y : Number(command[1] === 'V' ? command[2] : command[3]);
|
|
if (command[1] !== 'M') for (const [left, top, boxWidth, height] of visibleBoxes) {
|
|
const intersects = Math.max(x, nextX) > left && Math.min(x, nextX) < left + boxWidth &&
|
|
Math.max(y, nextY) > top && Math.min(y, nextY) < top + height;
|
|
expect(intersects, `${path.getAttribute('data-relationship')} crosses tile at ${left},${top}`).toBe(false);
|
|
}
|
|
x = nextX; y = nextY;
|
|
}
|
|
const relation = DATA_FLOW_RELATIONSHIPS.find(([from, to]) => `${from}-${to}` === path.getAttribute('data-relationship'))!;
|
|
const [left, top, boxWidth, height] = positions[relation[1]];
|
|
const distance = Math.hypot(Math.max(left - x, 0, x - left - boxWidth), Math.max(top - y, 0, y - top - height));
|
|
expect(distance).toBeCloseTo(4);
|
|
}
|
|
};
|
|
assertClear();
|
|
const select = container.querySelector<HTMLButtonElement>('[data-flow-id="scanner-findings"] .db-flow__select')!;
|
|
await act(async () => select.click());
|
|
let y = 100;
|
|
positions = Object.fromEntries(Object.entries(desktop).map(([id, box]) => {
|
|
const stacked: [number, number, number, number] = [164, y, 358, box[3]];
|
|
y += box[3] + 96;
|
|
return [id, stacked];
|
|
}));
|
|
width = 686;
|
|
await act(async () => window.dispatchEvent(new Event('resize')));
|
|
assertClear();
|
|
expect(select.getAttribute('aria-pressed')).toBe('true');
|
|
expect(container.querySelectorAll('.db-flow__edges > path.is-related')).toHaveLength(4);
|
|
expect(document.querySelector('.floating-data-window')).toBeNull();
|
|
act(() => root.unmount());
|
|
});
|
|
|
|
it('opens the selected flow table in a popup and returns to the unchanged flow on close', async () => {
|
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
vi.spyOn(AssessorApiClient.prototype, 'getDatabaseStructure').mockResolvedValue({
|
|
generatedAt: '2026-09-25T10:00:00.000Z', persistence: 'postgresql',
|
|
tables: [table('projects', [], 0), table('import_artifacts', ['projects'], 12), table('scan_runs', ['import_artifacts']),
|
|
table('gpo_backup_instances', ['import_artifacts']), table('microsoft_security_baselines'), table('reviews', ['projects']),
|
|
table('finding_occurrences'), table('policy_finding_occurrences'), table('policy_evaluation_results'),
|
|
table('result_db_rows'), table('comparisons')]
|
|
});
|
|
const container = document.createElement('div');
|
|
const root = createRoot(container);
|
|
await act(async () => {
|
|
root.render(createElement(DbStructurePanel, { backendStatus: 'connected' }));
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
});
|
|
expect([...container.querySelectorAll('.db-flow__lane-title')].map(item => item.textContent)).toEqual(['Scanner reports', 'GPO backups', 'Reference data']);
|
|
expect(container.querySelector('.db-structure-intro-copy')).toBeNull();
|
|
expect(container.querySelector('.db-flow__header p')).toBeNull();
|
|
expect(container.querySelector('.db-flow .db-flow__header h3')?.textContent).toBe('Data flow');
|
|
expect([...container.querySelectorAll('.db-engine-flow__node.is-engine strong')].map(item => item.textContent))
|
|
.toEqual(['Core Engine', 'Context Engine', 'Report Engine', 'Remediation Engine', 'Compliance Engine']);
|
|
const relations = container.querySelector('table[aria-label="Directed engine relationships"]');
|
|
expect(relations?.querySelectorAll('tbody tr')).toHaveLength(17);
|
|
expect(relations?.textContent).toContain('Core read-only port');
|
|
expect(relations?.textContent).not.toContain('Planned');
|
|
expect(relations?.textContent).toContain('Versioned Context publication for the Management Report');
|
|
await act(async () => {
|
|
container.querySelector<HTMLButtonElement>('.db-structure-intro-actions button')!.click();
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
});
|
|
expect(container.querySelectorAll('.db-engine-flow__node.is-engine')).toHaveLength(5);
|
|
expect(container.querySelectorAll('.db-engine-flow tbody tr')).toHaveLength(17);
|
|
expect(container.querySelector('.db-structure-intro-actions button')?.textContent).toBe('Refresh schema');
|
|
expect(container.querySelector('.db-flow__lane.is-reference')?.textContent).toContain('Input to GPO step');
|
|
const chips = [...container.querySelectorAll<HTMLButtonElement>('.db-flow__table')];
|
|
const flowStepCount = container.querySelectorAll('.db-flow__step').length;
|
|
// Permanent acceptance: missing consumer/merge/reference arrows remain visible
|
|
// through table selection, popup navigation and closing the popup.
|
|
const edgeIds = () => [...container.querySelectorAll('.db-flow__edges > path')]
|
|
.map(edge => edge.getAttribute('data-relationship'));
|
|
const requiredEdges = ['scanner-findings-result-materialization', 'gpo-findings-result-materialization',
|
|
'result-materialization-result-db', 'scanner-findings-review', 'gpo-findings-review',
|
|
'scanner-findings-comparison', 'reference-baselines-gpo-evaluation'];
|
|
expect(edgeIds()).not.toContain('gpo-findings-comparison');
|
|
expect(edgeIds()).toEqual(expect.arrayContaining(requiredEdges));
|
|
const originalBounds = HTMLElement.prototype.getBoundingClientRect;
|
|
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
|
|
const id = this.dataset.flowId;
|
|
const positions: Record<string, [number, number, number, number]> = {
|
|
'scanner-findings': [0, 100, 358, 100], 'gpo-findings': [400, 180, 358, 200],
|
|
'result-materialization': [400, 450, 358, 150], 'review': [600, 700, 358, 150],
|
|
};
|
|
const box = positions[id ?? ''];
|
|
return box ? new DOMRect(...box) : originalBounds.call(this);
|
|
});
|
|
await act(async () => window.dispatchEvent(new Event('resize')));
|
|
const mergePath = container.querySelector('[data-relationship="scanner-findings-result-materialization"]')!.getAttribute('d')!;
|
|
// The cross-lane horizontal segment must clear the taller GPO tile.
|
|
const crossLaneY = Number(mergePath.match(/V([\d.]+) H/)![1]);
|
|
expect(crossLaneY).toBeGreaterThan(380);
|
|
expect(crossLaneY).toBeLessThan(450);
|
|
for (const edge of container.querySelectorAll('.db-flow__edges > path')) {
|
|
expect(edge.getAttribute('marker-end')).toMatch(/^url\(#/);
|
|
expect(edge.querySelector('title')?.textContent).toContain('→');
|
|
}
|
|
// A zero estimate is not shown as a row count.
|
|
expect(chips.find(chip => chip.textContent?.startsWith('projects'))?.textContent).toBe('projects');
|
|
expect(chips.find(chip => chip.textContent?.startsWith('import_artifacts'))?.textContent).toBe('import_artifacts~12');
|
|
await act(async () => chips.find(chip => chip.textContent?.startsWith('import_artifacts'))!.click());
|
|
expect(container.querySelectorAll('.db-flow__table[aria-pressed="true"]')).toHaveLength(1);
|
|
expect(container.querySelector('.db-flow__edges [data-relationship="foundation-upload"]')?.classList.contains('is-related')).toBe(true);
|
|
expect([...new Set([...container.querySelectorAll('.db-flow__table[data-related]')]
|
|
.map(chip => `${chip.textContent}:${chip.getAttribute('data-related')}`))])
|
|
.toEqual(['projects:outgoing', 'scan_runs:incoming', 'gpo_backup_instances:incoming']);
|
|
expect(container.querySelector('.db-flow__detail')).toBeNull();
|
|
const popup = document.querySelector<HTMLElement>('.floating-data-window[role="dialog"]');
|
|
expect(popup?.textContent).toContain('assessor_import_artifacts');
|
|
expect(popup?.querySelector('[aria-label="Columns in assessor_import_artifacts"]')).not.toBeNull();
|
|
expect(popup?.textContent).toContain('Referenced by');
|
|
await act(async () => [...popup!.querySelectorAll<HTMLButtonElement>('.db-flow__links button')]
|
|
.find(button => button.textContent === 'scan_runs')!.click());
|
|
expect(popup?.querySelector('h2')?.textContent).toBe('assessor_scan_runs');
|
|
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })));
|
|
expect(document.querySelector('.floating-data-window')).toBeNull();
|
|
expect(container.querySelectorAll('.db-flow__table[aria-pressed="true"]')).toHaveLength(0);
|
|
expect(container.querySelectorAll('.db-flow__step')).toHaveLength(flowStepCount);
|
|
expect(edgeIds()).toEqual(expect.arrayContaining(requiredEdges));
|
|
expect(container.querySelectorAll('.db-flow__edges > path.is-related')).toHaveLength(0);
|
|
const findingTile = container.querySelector<HTMLElement>('[data-flow-id="scanner-findings"]')!;
|
|
const findingSelect = findingTile.querySelector<HTMLButtonElement>('.db-flow__select')!;
|
|
await act(async () => findingTile.querySelector('p')!.click());
|
|
expect(findingTile.dataset.selected).toBe('true');
|
|
expect(findingSelect.getAttribute('aria-pressed')).toBe('true');
|
|
const markedPaths = [...container.querySelectorAll('.db-flow__edges > path.is-related')];
|
|
expect(markedPaths.map(path => path.getAttribute('data-relationship')).sort()).toEqual([
|
|
'scanner-findings-comparison', 'scanner-findings-result-materialization', 'scanner-findings-review'
|
|
]);
|
|
for (const path of markedPaths) {
|
|
const markerId = path.getAttribute('marker-end')!.slice(5, -1);
|
|
expect(container.querySelector(`[id="${markerId}"] path`)?.classList.contains('is-related')).toBe(true);
|
|
}
|
|
// Tile selection does not open a table popup; chips keep their own action.
|
|
expect(document.querySelector('.floating-data-window')).toBeNull();
|
|
await act(async () => findingTile.querySelector<HTMLButtonElement>('.db-flow__table')!.click());
|
|
expect(document.querySelector('.floating-data-window')).not.toBeNull();
|
|
await act(async () => window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })));
|
|
expect(findingTile.dataset.selected).toBe('true');
|
|
await act(async () => findingSelect.click());
|
|
expect(findingTile.dataset.selected).toBe('false');
|
|
expect(container.querySelectorAll('.db-flow__edges > path.is-related')).toHaveLength(0);
|
|
// The layered table dictionary stays available below the flow.
|
|
expect(container.querySelector('details.db-structure-dictionary summary')?.textContent).toBe('Table dictionary by layer');
|
|
act(() => root.unmount());
|
|
});
|
|
});
|
|
|
|
describe('DB Structure read views', () => {
|
|
it('assigns stored disclosed Result DB rows to their Core-owned materialization step', () => {
|
|
const flow = buildDbStructureFlow([table('result_db_rows'), table('result_db_state')]);
|
|
expect(flow.steps.map(step => step.id)).toEqual(['result-materialization']);
|
|
expect(flow.other).toEqual([]);
|
|
});
|
|
it('shows the Result DB as a read view over tables owned by the processing steps', async () => {
|
|
const { buildDbStructureViews } = await import('../../src/features/db-structure/db-structure-flow');
|
|
const views = buildDbStructureViews([table('finding_occurrences'), table('policy_evaluation_results'), table('reviews')]);
|
|
expect(views.map(view => [view.title, view.resolved.map(item => item.name)])).toEqual([
|
|
['Result DB', ['assessor_finding_occurrences', 'assessor_policy_evaluation_results']]
|
|
]);
|
|
// Owning steps stay unique: a read view never claims a table.
|
|
expect(buildDbStructureFlow([table('finding_occurrences')]).other).toEqual([]);
|
|
});
|
|
});
|