From d234f9431576f07860e2b340bb7aaa7c02f67942 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 2 Oct 2026 08:23:25 +0200 Subject: [PATCH] Test Merge Fokus Agent.md --- docs/IMPLEMENTATION_PLAN.md | 4 ++ package-lock.json | 4 +- package.json | 2 +- src/app/App.tsx | 4 +- .../db-structure/DbStructurePanel.tsx | 3 + src/features/db-structure/EngineDataFlow.tsx | 62 +++++++++++++++++++ .../db-structure/db-structure-flow.ts | 8 ++- src/styles/datahub.css | 51 +++++++++++++++ tests/app/app-version.test.ts | 5 +- tests/features/db-structure-flow.test.tsx | 50 ++++++++++++++- tests/server/assessor-http-server.test.ts | 2 +- 11 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 src/features/db-structure/EngineDataFlow.tsx diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 199cdd7..5f7b0ac 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,5 +1,9 @@ # ASSESSOR – Umsetzungsplan +2026-10-02 - Phase 7 / interactive Data Flow diagram (follow-up to the preceding overview): on renewed Presentation/Core Tests/Platform release, replaced the always-open engine table with an interactive architecture diagram. Nine selectable modules include all three engines, source inputs, canonical PostgreSQL, read-only port, UI consumers and report outputs. Nine labelled relationships show directional delivery, bidirectional Core database read/write and the dashed, explicitly Planned Context-to-Report target. Selection highlights connected edges and opens an inspector; native buttons support keyboard activation and deselection. The complete relationship table remains in a closed disclosure. Narrow views scroll the diagram without shrinking 14px information text; inspector stacks. Coordinates live in CSS classes, using current tokens and flat tiles, with no engine imports, requests or data writes. Existing schema flow, table popups and dictionary retained. Version decision: same unreleased, related 2.11.2 refinement batch; no additional version increment. Permanent synthetic tests cover module selection, deselection, directed/bidirectional markers, planned output and disclosure; 20 focused tests passed before the final CSS correction, and 14 final diagram/layout/import tests passed afterward. Full suite: 1783 passed, 15 skipped, one unrelated 5-second timeout in node-import-engine; all five tests in that unchanged suite passed in the separate rerun (format test 3504ms). Security/design checks passed. Both production builds passed; final client rebuild after the CSS-class and read/write-marker corrections also passed. Existing bundle-size warning remains. Live mouse/keyboard selection, both themes and horizontal overflow checked. Phase status unchanged; planned report Context integration remains outside this UI change. + +2026-10-02 - Phase 7 / Data Flow engine overview (2.11.2 patch): renamed DB Structure to Data Flow in navigation and header. Added Core, Context and Report with nine directed data-delivery relationships, canonical PostgreSQL, read-only Core port, UI consumers and separate report archive. Context-to-Report is explicitly Planned: current report generation does not consume published Context results. Architecture includes disabled engines. Corrected obsolete Result DB wording and assigned result_db_rows/result_db_state to Core-owned disclosure materialization before its consumer. Existing table details preserved. No API, persistence or engine behavior changes. Verified: 1783 tests passed, 15 skipped; UI/API version contracts, architecture and engine invariance passed; security/design checks and both production builds passed, client build repeated after final order correction. Live inspection in both themes; backend healthy at 2.11.2 with PostgreSQL. Existing bundle warning remains. Version decision: backward-compatible refinement, patch 2.11.1 -> 2.11.2, package/lock synchronized. Phase status unchanged. Follow-up request for a more futuristic diagram awaits renewed Core consent under the per-message rule. + 2026-10-01 - Phase 7/13 / Maester description table rendering: shared Markdown presentation recovers flattened pipe tables with retained line breaks inside prose cells, without crossing blank lines, headings, code fences, lists or another delimiter. The reader also handles tables mixed with supported inert inline HTML (links, emphasis and br); original retained fields are untouched. Shared rich-text tables use the agreed 14px data typography. Permanent synthetic reader regressions cover both cell-break representations, full cell text, headers, links and unchanged originals; the shared parser regression preserves a following list. Synthetic browser checks and visual inspection passed in dark and light. Verification: 1782 tests passed, 15 skipped; security/design checks passed; client and server production builds passed, including a refreshed client build after the typography correction. Version decision: bundled into the concurrently established 2.11.1 patch batch; package/lock and UI/API version checks already agree and were not overwritten by this change. No importer, persistence, verdict or score change; no reimport required. Ambiguous table structures remain source text. No phase completion claimed. 2026-10-01 - Phase 13E / client Context loading and All count (2.11.1): Client opening now prepares the enabled Context Engine after the canonical workspace is ready and before publishing the loaded client. A Core API request cache, weakly owned by the dataset and keyed by the exact scope, shares that attempt with every reader; no engine implementation is imported into Core. The Context hook makes no request for an empty dataset and never returns a previous scope's result while a new scope loads. Failed attempts remain visible instead of silently restarting on reader mount; Core results remain accessible, with an English notice to reopen the client to retry. Disabled Context makes no request. Work and Pathfinder's shared Context Cluster selector shows All (N), counting each matching record once, including records without cluster membership, while respecting other filters and excluding its own selection. Overlapping clusters do not inflate the total. Patch version 2.11.1 records this backward-compatible correction; package/lock and UI/API contracts agree. Verified: 37 focused tests passed, including the actual Open/client-switch interactions, deferred Context response, failure/reopen, disabled engine, scope changes, filtered and zero-hit All counts, and version contracts. Synthetic browser checks and visual inspection passed in dark and light. Both production builds passed; the final client build also includes the synchronous stale-scope guard. Local-only and design checks passed. Full suite: 1781 passed, 15 skipped, one unrelated failure in tests/shared/markdown.test.ts:40 (wrapped flattened table followed by a list); the Markdown source and test were not changed by this task. Running backend reports 2.11.1 and unchanged repository revision 535; frontend HTTP 200. Limits: the screenshot did not expose the original failed HTTP response, so its specific backend error was not independently reproduced. This change removes the verified premature/duplicate request path; it does not speed up Context computation itself. No persistence changes, no phase completion claimed. diff --git a/package-lock.json b/package-lock.json index b551b85..162f2d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "assessor", - "version": "2.11.1", + "version": "2.11.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "assessor", - "version": "2.11.1", + "version": "2.11.2", "dependencies": { "@fontsource-variable/inter": "^5.3.0", "jsdom": "^29.1.1", diff --git a/package.json b/package.json index a5a586a..9ac5704 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assessor", - "version": "2.11.1", + "version": "2.11.2", "private": true, "type": "module", "scripts": { diff --git a/src/app/App.tsx b/src/app/App.tsx index 9a74cca..8742ced 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -43,7 +43,7 @@ function AreaIcon({ area }: { area: AppArea }) { } const AREA_TOPBAR_TITLE: Record = { - 'db-structure': 'DB Structure', + 'db-structure': 'Data Flow', 'data-hub': 'Data Hub', dashboard: 'Client Info', graph: 'Pathfinder', @@ -56,7 +56,7 @@ const AREA_TOPBAR_TITLE: Record = { das Panel nicht gerundet sein. Die Liste steht deshalb an einer Stelle, damit die Ecke der Reihenfolge folgt und nicht einem Bereichsnamen im CSS. */ const NAV_AREAS = [ - ['db-structure', 'DB Structure'], + ['db-structure', 'Data Flow'], ['data-hub', 'Datahub'], ['dashboard', 'Client Info'], ['work', 'Work'], diff --git a/src/features/db-structure/DbStructurePanel.tsx b/src/features/db-structure/DbStructurePanel.tsx index a1d42da..24af9e4 100644 --- a/src/features/db-structure/DbStructurePanel.tsx +++ b/src/features/db-structure/DbStructurePanel.tsx @@ -5,6 +5,7 @@ import { TriangleIcon } from '../../components/TriangleIcon'; import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState'; import { BackendConnectionDialog } from '../../components/BackendConnectionDialog/BackendConnectionDialog'; import { FloatingDataWindow } from '../../components/FloatingDataWindow/FloatingDataWindow'; +import { EngineDataFlow } from './EngineDataFlow'; import { buildDbStructureFlow, buildDbStructureViews, dbStructureNeighbours, dbStructureShortName, type DbFlowStep } from './db-structure-flow'; const assessorApi = new AssessorApiClient(); @@ -146,6 +147,7 @@ function DbStructureFlow({ tables }: { tables: DbStructureTable[] }) { + {shared('result-materialization', false)} {/* Both paths end in two consumers: the Result DB read view and Work with its reviews. */}
{views.map(view => Relationships{metrics.relationships.toLocaleString('en-US')}Declared foreign keys
Approximate rows{metrics.countedTables > 0 ? metrics.approximateRows.toLocaleString('en-US') : '—'}{metrics.countedTables > 0 ? `Reported by ${metrics.countedTables.toLocaleString('en-US')} tables` : 'No estimates reported'}
} + {structure.tables.length > 0 && }
Table dictionary by layer diff --git a/src/features/db-structure/EngineDataFlow.tsx b/src/features/db-structure/EngineDataFlow.tsx new file mode 100644 index 0000000..5b4b971 --- /dev/null +++ b/src/features/db-structure/EngineDataFlow.tsx @@ -0,0 +1,62 @@ +import { useId, useState } from 'react'; + +/** Presentation-only architecture map; never imports engine implementations. */ +const nodes = [ + { id: 'input', name: 'Source inputs', caption: 'Scanner reports · GPO backups · catalogs', description: 'Original scanner reports, GPO backups and reference catalogs enter the Core import and evaluation pipeline.' }, + { id: 'core', name: 'Core Engine', caption: 'Authoritative results · always active', description: 'All existing ASSESSOR functions: import, normalization, evaluation, scoring, client records and disclosure. Owns canonical PostgreSQL data. Works independently of optional engines.' }, + { id: 'database', name: 'Canonical PostgreSQL', caption: 'Core-owned data · disclosed Result DB', description: 'Canonical local datastore. The Core owns persistence, including the stored and verified Result DB projection.' }, + { id: 'port', name: 'Core read-only port', caption: 'Scoped results · provenance', description: 'Declared read-only Core port backed by read-only database access. Optional engines read authoritative results without changing stored data.' }, + { id: 'context', name: 'Context Engine', caption: 'Derived context · optional', description: 'Reads findings and GPO verdicts through the read-only Core port and read-only database access. Produces versioned clusters, confidence and evidence. In-memory results; no own persistence or Core writes.' }, + { id: 'report', name: 'Report Engine', caption: 'Read-only reporting · optional', description: 'Reads Core results through the read-only Core port and a separate read-only PostgreSQL pool. Produces report projections, management and technical reports. Own file archive; no Core writes.' }, + { id: 'ui', name: 'Core UI & exports', caption: 'Findings · scores · Result DB', description: 'Application views show authoritative Core findings, verdicts and scores. Result DB exports obey the Core disclosure policy.' }, + { id: 'context-ui', name: 'Context in the UI', caption: 'Work · Client Info · Pathfinder', description: 'Published Context results appear through existing UI extension points. They never override technical results or client records.' }, + { id: 'archive', name: 'Report outputs', caption: 'File archive · downloads', description: 'Generated reports are stored in the separate Report Engine file archive and offered for download; no report storage in Core tables.' } +]; +const relationships = [ + { from: 'input', to: 'core', label: 'Import', detail: 'Import and evaluation', path: 'M240 340 H310' }, + { from: 'core', to: 'database', label: 'Read / write', detail: 'Owns, reads and writes Core data, including disclosed Result DB rows', path: 'M420 390 V520', bidirectional: true }, + { from: 'database', to: 'port', label: 'Read only', detail: 'Read-only database access for optional engines', path: 'M530 570 H600' }, + { from: 'port', to: 'context', label: 'Read only', detail: 'Scoped authoritative results and provenance', path: 'M710 520 V390' }, + { from: 'port', to: 'report', label: 'Read only', detail: 'Scoped results, client records and disclosed projections', path: 'M820 570 H890' }, + { from: 'core', to: 'ui', label: 'Core results', detail: 'Authoritative findings, verdicts, scores and disclosure rules', path: 'M420 290 V160' }, + { from: 'context', to: 'context-ui', label: 'Context', detail: 'Published context results through existing UI extension points', path: 'M710 290 V160' }, + { from: 'report', to: 'archive', label: 'Reports', detail: 'Generated reports; separate from Core database storage', path: 'M1000 520 V390' }, + { from: 'context', to: 'report', label: 'Planned', detail: 'Published context results; documented target, not yet connected in report generation', path: 'M820 340 H855 V490 H940 V520', planned: true } +]; +const nodeName = (id: string) => nodes.find(node => node.id === id)!.name; + +export function EngineDataFlow() { + const markerId = useId().replace(/:/g, ''); + const [selected, setSelected] = useState('core'); + const active = nodes.find(node => node.id === selected); + const connections = relationships.filter(edge => edge.from === selected || edge.to === selected); + const related = new Set(connections.flatMap(edge => [edge.from, edge.to])); + return
+

Engines & relationships

Solid: implemented · Dashed: planned
+
+
+ + {relationships.map(edge => {edge.label})} + {nodes.map(node => )} +
+
+ {active ?
+

{active.name}

{active.description}

+
    {connections.map(edge =>
  • {nodeName(edge.from)} {edge.bidirectional ? '↔' : '→'} {nodeName(edge.to)}{edge.detail}{edge.planned ? ' · Planned' : ''}
  • )}
+
:

Select a module to inspect its data flow. Modules also support keyboard selection.

} +

Arrows show data delivery. Engine dependencies point toward the Core; the Core never depends on an optional engine. Engines cannot override verdicts, scores or client records. Every output follows Core privacy and Result DB disclosure rules.

+
All relationships (9)
+ + {relationships.map(edge => )} +
FromToData flowImplementation
{nodeName(edge.from)}{edge.bidirectional ? '↔' : '→'} {nodeName(edge.to)}{edge.detail}{edge.planned ? 'Planned' : 'Implemented'}
+

This architecture overview includes disabled engines. The shared Engines control governs optional UI requests; disabling an engine does not cancel submitted backend work.

+
; +} diff --git a/src/features/db-structure/db-structure-flow.ts b/src/features/db-structure/db-structure-flow.ts index cdbcfaa..9457085 100644 --- a/src/features/db-structure/db-structure-flow.ts +++ b/src/features/db-structure/db-structure-flow.ts @@ -45,7 +45,9 @@ export const DB_FLOW_STEPS: DbFlowStepDefinition[] = [ { id: 'review', lane: 'shared', title: 'Work & review', purpose: 'Client-owned review state and accepted risks for findings of both paths.', tables: ['reviews'] }, { id: 'comparison', lane: 'shared', title: 'Comparison over time', purpose: 'Timeslot comparisons and the lifecycle of findings between assessments.', - tables: ['comparisons', 'timeslot_evidence_comparisons', 'finding_obligations', 'finding_obligation_transitions', 'finding_obligation_scope_decisions'] } + tables: ['comparisons', 'timeslot_evidence_comparisons', 'finding_obligations', 'finding_obligation_transitions', 'finding_obligation_scope_decisions'] }, + { id: 'result-materialization', lane: 'shared', title: 'Result DB disclosure & storage', purpose: 'Core checks and stores the disclosed result projection when source data changes. UI and exports read the same verified rows.', + tables: ['result_db_rows', 'result_db_state'] } ]; export interface DbFlowStep extends DbFlowStepDefinition { @@ -92,8 +94,8 @@ export interface DbFlowViewDefinition { } export const DB_FLOW_VIEWS: DbFlowViewDefinition[] = [ - { id: 'result-db', title: 'Result DB', purpose: 'Read view, no table of its own: combined at query time from scanner findings, environment facts, GPO comparisons, GPO findings and GPO backup consistency.', - sources: ['finding_definitions', 'finding_occurrences', 'finding_occurrence_categories', 'result_categories', 'evidence', + { id: 'result-db', title: 'Result DB', purpose: 'UI and exports read the stored, disclosed projection. Original source tables below supply its Core-owned materialization.', + sources: ['result_db_rows', 'result_db_state', 'finding_definitions', 'finding_occurrences', 'finding_occurrence_categories', 'result_categories', 'evidence', 'environment_observations', 'policy_evaluation_results', 'policy_snapshot_states', 'policy_finding_occurrences', 'gpo_backup_instances'] } ]; diff --git a/src/styles/datahub.css b/src/styles/datahub.css index 34e7542..da39ddb 100644 --- a/src/styles/datahub.css +++ b/src/styles/datahub.css @@ -1870,6 +1870,57 @@ button.client-overall-ring:focus-visible { outline: var(--bw-emphasis) solid var Steps are flat tiles; connectors are neutral hairlines; selection uses the shared marking role. */ .db-flow { --db-flow-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, .8fr); --db-flow-gap: var(--space-5); display: grid; gap: var(--db-flow-gap); min-width: 0; margin: var(--space-5) 0; } .db-flow__header h3 { margin: 0; color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--fw-semibold); } +.db-engine-flow { display: grid; gap: var(--space-4); margin: var(--space-5) 0; min-width: 0; } +.db-engine-flow__heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-3); } +.db-engine-flow__heading h3 { margin: 0; color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--fw-semibold); } +.db-engine-flow__heading > span { color: var(--muted); font-size: var(--type-table-body-size); } +.db-engine-flow__map { position: relative; width: 1130px; height: 650px; margin: 0 auto; } +.db-engine-flow__edges { position: absolute; inset: 0; overflow: visible; pointer-events: none; } +.db-engine-flow__edges > path { fill: none; stroke: var(--line-strong); stroke-width: 2; } +.db-engine-flow__edges > path.is-related { stroke: var(--marking-color); } +.db-engine-flow__edges > path.is-planned { stroke-dasharray: 6 5; } +.db-engine-flow__edges marker path { fill: var(--line-strong); } +.db-engine-flow__node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: var(--space-2); width: 220px; min-height: 100px; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: 0; background: var(--tile-surface); color: var(--text); text-align: left; cursor: pointer; } +.db-engine-flow__node.is-engine { border-left: var(--marker-width) solid var(--line-strong); } +.db-engine-flow__node.is-related { border-color: var(--line-strong); } +.db-engine-flow__node[aria-pressed="true"] { outline: var(--bw-emphasis) solid var(--marking-color); outline-offset: -2px; border-left-color: var(--marking-color); } +.db-engine-flow__node:hover { background: var(--tile-surface-hover); } +.db-engine-flow__node:focus-visible { outline: var(--bw-emphasis) solid var(--focus-ring); outline-offset: 2px; } +.db-engine-flow__node strong { font-size: var(--type-h4-size); font-weight: var(--fw-semibold); } +.db-engine-flow__node > span { font-size: var(--type-table-body-size); color: var(--muted); line-height: 1.4; } +.db-engine-flow__edge-label { position: absolute; transform: translate(-50%, -50%); padding: var(--space-1); background: var(--surface-app); color: var(--muted); font-size: var(--type-table-body-size); white-space: nowrap; pointer-events: none; } +.db-engine-flow__node[data-node="input"] { left: 20px; top: 290px; } +.db-engine-flow__node[data-node="core"] { left: 310px; top: 290px; } +.db-engine-flow__node[data-node="database"] { left: 310px; top: 520px; } +.db-engine-flow__node[data-node="port"] { left: 600px; top: 520px; } +.db-engine-flow__node[data-node="context"] { left: 600px; top: 290px; } +.db-engine-flow__node[data-node="report"] { left: 890px; top: 520px; } +.db-engine-flow__node[data-node="ui"] { left: 310px; top: 60px; } +.db-engine-flow__node[data-node="context-ui"] { left: 600px; top: 60px; } +.db-engine-flow__node[data-node="archive"] { left: 890px; top: 290px; } +.db-engine-flow__edge-label[data-edge="input-core"] { left: 275px; top: 312px; } +.db-engine-flow__edge-label[data-edge="core-database"] { left: 420px; top: 455px; } +.db-engine-flow__edge-label[data-edge="database-port"] { left: 565px; top: 540px; } +.db-engine-flow__edge-label[data-edge="port-context"] { left: 710px; top: 455px; } +.db-engine-flow__edge-label[data-edge="port-report"] { left: 855px; top: 540px; } +.db-engine-flow__edge-label[data-edge="core-ui"] { left: 420px; top: 225px; } +.db-engine-flow__edge-label[data-edge="context-context-ui"] { left: 710px; top: 225px; } +.db-engine-flow__edge-label[data-edge="report-archive"] { left: 1000px; top: 455px; } +.db-engine-flow__edge-label[data-edge="context-report"] { left: 855px; top: 416px; } +.db-engine-flow__inspector { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-5); padding: var(--tile-inset); background: var(--tile-surface); border: var(--border-hair) solid var(--tile-border); } +.db-engine-flow__inspector h4 { margin: 0 0 var(--space-2); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); } +.db-engine-flow__inspector p { margin: 0; font-size: var(--type-body-size); line-height: 1.5; } +.db-engine-flow__inspector ul { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; } +.db-engine-flow__inspector li { display: grid; gap: var(--space-1); font-size: var(--type-table-body-size); line-height: 1.4; } +.db-engine-flow__inspector li strong { font-weight: var(--fw-semibold); } +.db-engine-flow__inspector li span { color: var(--muted); } +.db-engine-flow__dictionary > summary { color: var(--heading); font-size: var(--type-table-body-size); cursor: pointer; } +.db-engine-flow__boundary { margin: 0; color: var(--muted); font-size: var(--type-table-body-size); line-height: 1.45; } +.db-engine-flow__scroll { overflow-x: auto; } +.db-engine-flow table { width: 100%; border-collapse: collapse; color: var(--text); font-size: var(--type-table-body-size); } +.db-engine-flow th, .db-engine-flow td { padding: var(--space-3); text-align: left; vertical-align: top; border-bottom: var(--border-hair) solid var(--line); } +.db-engine-flow th { font-weight: var(--fw-semibold); } +@media (max-width: 1050px) { .db-engine-flow__inspector { grid-template-columns: minmax(0, 1fr); } } .db-flow__step { position: relative; z-index: 1; display: grid; align-content: start; gap: var(--space-2); min-width: 0; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); background: var(--tile-surface); } .db-flow__step > header { display: flex; align-items: baseline; gap: var(--space-2); } .db-flow__step h4 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); } diff --git a/tests/app/app-version.test.ts b/tests/app/app-version.test.ts index 45d4cbe..e66352f 100644 --- a/tests/app/app-version.test.ts +++ b/tests/app/app-version.test.ts @@ -9,7 +9,10 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor describe('ASSESSOR version in the brand header', () => { it('shows the centrally maintained product version and opens About from the brand', () => { - expect(ASSESSOR_PRODUCT_VERSION).toBe('2.11.1'); + expect(ASSESSOR_PRODUCT_VERSION).toBe('2.11.2'); + expect(app).toContain("'db-structure': 'Data Flow'"); + expect(app).toContain("['db-structure', 'Data Flow']"); + expect(app).not.toContain("'DB Structure'"); expect(app).toContain("import { ASSESSOR_PRODUCT_VERSION } from '../domain/assessor-release';"); expect(app).toContain('aria-label="About ASSESSOR"'); expect(app).toContain("import assessorLogo from '../assets/branding/assessor-icon-transparent.png';"); diff --git a/tests/features/db-structure-flow.test.tsx b/tests/features/db-structure-flow.test.tsx index 63875da..d52eaef 100644 --- a/tests/features/db-structure-flow.test.tsx +++ b/tests/features/db-structure-flow.test.tsx @@ -5,6 +5,7 @@ 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 { buildDbStructureFlow, dbStructureNeighbours, DB_FLOW_STEPS } from '../../src/features/db-structure/db-structure-flow'; const table = (name: string, references: string[] = [], rows: number | null = null): DbStructureTable => ({ @@ -16,6 +17,35 @@ const table = (name: string, references: string[] = [], rows: number | null = nu describe('DB Structure data flow', () => { afterEach(() => vi.restoreAllMocks()); + it('selects engine modules, highlights only their directed connections and discloses planned flow', 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(9); + expect(container.querySelectorAll('.db-engine-flow__edges > path')).toHaveLength(9); + expect(container.querySelectorAll('.db-engine-flow__edges > path[marker-start]')).toHaveLength(1); + 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('.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(3); + expect(container.querySelector('.db-engine-flow__edges > path.is-planned.is-related')).not.toBeNull(); + expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('not yet connected in report generation · 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); + expect(container.querySelectorAll('.db-engine-flow__node')).toHaveLength(9); + const dictionary = container.querySelector('.db-engine-flow__dictionary')!; + await act(async () => dictionary.querySelector('summary')!.click()); + expect(dictionary.open).toBe(true); + expect(dictionary.querySelectorAll('tbody tr')).toHaveLength(9); + 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); @@ -45,7 +75,20 @@ describe('DB Structure data flow', () => { 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__header h3')?.textContent).toBe('Data flow'); + 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']); + const relations = container.querySelector('table[aria-label="Directed engine relationships"]'); + expect(relations?.querySelectorAll('tbody tr')).toHaveLength(9); + expect(relations?.textContent).toContain('Core read-only port'); + expect(relations?.textContent).toContain('Planned'); + expect(relations?.textContent).toContain('not yet connected in report generation'); + await act(async () => { + container.querySelector('.db-structure-intro-actions button')!.click(); + await new Promise(resolve => setTimeout(resolve, 0)); + }); + expect(container.querySelectorAll('.db-engine-flow__node.is-engine')).toHaveLength(3); + expect(container.querySelectorAll('.db-engine-flow tbody tr')).toHaveLength(9); 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('.db-flow__table')]; @@ -77,6 +120,11 @@ describe('DB Structure data flow', () => { }); 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')]); diff --git a/tests/server/assessor-http-server.test.ts b/tests/server/assessor-http-server.test.ts index 8ab2a04..29500f4 100644 --- a/tests/server/assessor-http-server.test.ts +++ b/tests/server/assessor-http-server.test.ts @@ -59,7 +59,7 @@ describe('local ASSESSOR HTTP server', () => { const health = await fetch(`${baseUrl}/api/v1/health`); expect(await health.json()).toEqual({ - status: 'ok', application: 'ASSESSOR', productVersion: '2.11.1', apiVersion: 1, repositoryRevision: 0, persistence: 'memory' + status: 'ok', application: 'ASSESSOR', productVersion: '2.11.2', apiVersion: 1, repositoryRevision: 0, persistence: 'memory' }); expect(health.headers.get('access-control-allow-origin')).toBeNull(); });