Assessor_2.0/tests/features/obsidian-graph.test.ts

299 lines
23 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import type { FindingPathItem, MitreGraphData } from '../../src/features/mitre-graph/mitre-graph';
import { buildObsidianGraph } from '../../src/features/mitre-graph/obsidian-graph';
const graphData: MitreGraphData = {
version: 'synthetic',
tacticOrder: ['Initial Access', 'Execution'],
techniques: [
{ id: 'T1001', name: 'Synthetic Entry', tactics: ['Initial Access'], platforms: [], parent: null, url: '', sourceCount: 0 },
{ id: 'T1002', name: 'Synthetic Run', tactics: ['Execution'], platforms: [], parent: null, url: '', sourceCount: 0 }
],
entities: []
};
const item = (id: string, techniqueId: string, tactic: string): FindingPathItem => ({
id,
record: { id, index: 0, title: `Finding ${id}`, sourceSheet: 'Synthetic', fields: [], filled: [], meta: [] },
title: `Finding ${id}`,
category: 'Synthetic',
categories: ['Synthetic'],
defenses: ['Synthetic defense'],
targets: ['DC-01'],
tactics: [tactic],
primaryTactic: tactic,
techniqueIds: [techniqueId],
severity: 'high',
weight: 1,
score: 1,
risk: 410,
mappingOrigin: 'native'
});
describe('Obsidian-style interactive graph', () => {
it('builds a deterministic finding-technique-tactic network', () => {
const findings = [item('one', 'T1001', 'Initial Access'), item('two', 'T1002', 'Execution')];
const first = buildObsidianGraph(findings, graphData);
const second = buildObsidianGraph(findings, graphData);
expect(first).toEqual(second);
expect(first.nodes.filter(node => node.kind === 'finding')).toHaveLength(2);
expect(first.nodes.filter(node => node.kind === 'category')).toHaveLength(1);
expect(first.nodes.filter(node => node.kind === 'defense')).toHaveLength(1);
expect(first.nodes.filter(node => node.kind === 'target')).toHaveLength(1);
expect(first.nodes.filter(node => node.kind === 'technique')).toHaveLength(2);
expect(first.nodes.filter(node => node.kind === 'tactic')).toHaveLength(2);
expect(Math.max(...first.nodes.map(node => Math.hypot(node.x, node.y)))).toBeGreaterThan(300);
expect(first.nodes.find(node => node.kind === 'tactic')?.radius).toBe(10);
expect(first.nodes.find(node => node.kind === 'technique')?.radius).toBe(7);
expect(first.edges).toEqual(expect.arrayContaining([
expect.objectContaining({ source: 'finding:one', target: 'technique:T1001' }),
expect.objectContaining({ source: 'finding:one', target: 'category:Synthetic', kind: 'category' }),
expect.objectContaining({ source: 'finding:one', target: 'defense:Synthetic defense', kind: 'defense' }),
expect.objectContaining({ source: 'finding:one', target: 'target:DC-01', kind: 'target' }),
expect.objectContaining({ source: 'technique:T1001', target: 'tactic:Initial Access', kind: 'technique' })
]));
});
it('connects tactic-only findings to every declared tactic without orphan hubs', () => {
const finding = { ...item('multi', '', 'Initial Access'), techniqueIds: [], tactics: ['Initial Access', 'Execution'] };
const graph = buildObsidianGraph([finding], graphData);
expect(graph.edges).toEqual(expect.arrayContaining([
expect.objectContaining({ source: 'finding:multi', target: 'tactic:Initial Access', kind: 'tactic' }),
expect.objectContaining({ source: 'finding:multi', target: 'tactic:Execution', kind: 'tactic' })
]));
expect(graph.nodes.filter(node => node.kind === 'tactic').every(node => graph.edges.some(edge => edge.source === node.id || edge.target === node.id))).toBe(true);
});
it('wires the graph view and confines the dotted background to Graph', () => {
const app = readFileSync(new URL('../../src/app/App.tsx', import.meta.url), 'utf8');
const view = readFileSync(new URL('../../src/features/mitre-graph/ObsidianGraphView.tsx', import.meta.url), 'utf8');
const microCard = readFileSync(new URL('../../src/features/mitre-graph/PathfinderMicroCard.tsx', import.meta.url), 'utf8');
const layout = readFileSync(new URL('../../src/styles/layout.css', import.meta.url), 'utf8');
const graphStyles = readFileSync(new URL('../../src/styles/obsidian-graph.css', import.meta.url), 'utf8');
const dynamicFilterStyles = readFileSync(new URL('../../src/styles/dynamic-filters.css', import.meta.url), 'utf8');
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
expect(app).toContain('<ObsidianGraphView');
expect(view).toContain('onWheel={handleWheel}');
expect(view).toContain("import { linkPingCastleSectionSheets } from '../../profiles/pingcastle/pingcastle-section-links';");
expect(view).toContain('...linkPingCastleSectionSheets(item.record, dataset.sheets)');
expect(view).toContain('onPointerDown={startPan}');
expect(view).toContain('startNodeDrag(event, node)');
expect(view).toContain('startCardDrag(event, placement)');
expect(view).not.toContain('openCategoryContextMenu(event, node)');
expect(view).toContain("'Mark all'");
expect(view).toContain('const actionSelectionNodeIds = actionBranchNodeId ? [actionBranchNodeId] : [];');
expect(view).toContain(".find(nodeId => nodeLookup.get(nodeId)?.kind !== 'finding')");
expect(view).toContain('graphFacetSelectionForNode(node, graphFacets)');
expect(view).toContain('setFacetFilter(selection.facetId, { values: [selection.value] });');
expect(view).toContain('setFacetFilter(previous.facetId, null);');
expect(view).toContain('setFacetFilter(selection.facetId, null);');
expect(view).not.toContain("setSearch(node.label");
expect(view).toContain('relationNodeIdForFacetFilters(nodes, graphFacets, facetFilters)');
expect(view).toContain('className="obsidian-graph-branch-actions"');
expect(view).toContain("? 'Unmark all' : 'Mark all'");
expect(view).toContain("? 'Exit extract' : 'Extract'");
expect(view).toContain('onClick={resetGraphView}><span aria-hidden="true">○</span>Reset view</button>');
expect(view).toContain('setPinnedNodeIds([]);');
expect(view).toContain('setMarkedSelectionIds([]);');
expect(view).toContain('setExtractedSelectionIds([]);');
expect(view).toContain('setNodeOverrides({});');
expect(view).toContain('setResetViewRevision(current => current + 1);');
expect(view).toContain('const animateCameraTo = useCallback((target: CameraState, duration = 620) =>');
expect(view).toContain('progress * progress * progress * (progress * (progress * 6 - 15) + 10)');
expect(view).toContain('const logarithmicScale = Math.exp(');
expect(view).not.toContain('cameraMorphing');
expect(view).toContain("globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches");
expect(view).toContain('animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height))');
expect(view).toContain('animateCameraTo({ ...current, scale: clamp(current.scale * factor, 0.08, 4) }, 280);');
expect(view).toContain('() => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height), 520)');
expect(view).toContain('const extractViewBefore = useRef<ExtractViewState | undefined>(undefined);');
expect(view).toContain('const fittedExtractSignature = useRef(\'\');');
expect(view).toContain('if (fittedExtractSignature.current === extractSignature) return;');
expect(view).toContain('graphSelectionUnionNodeIds(graph, actionSelectionNodeIds)');
expect(view).toContain('setPinnedNodeIds(expandedNodeIds);');
expect(view).toContain('animateCameraTo(savedView.camera, 520);');
expect(view).toContain("extractedSelectionIds.length ? 'Exit extract' : 'Extract'");
expect(view).toContain('onClick={extractedSelectionIds.length ? exitSelectionExtract : extractSelection}');
expect(view).not.toContain('obsidian-node-card-actions');
expect(view).toContain('hasActiveGraphFilter && nodes.length');
expect(view).toContain("? FILTERED_GRAPH_SELECTION_ID : ''");
expect(view).toContain("kind: 'pan' | 'node' | 'card'");
expect(view).toContain('const [cardOverrides, setCardOverrides]');
expect(view).toContain('const [pinnedNodeIds, setPinnedNodeIds] = useState<string[]>([]);');
expect(view).toContain('const lastFittedGraphSignature = useRef(\'\');');
expect(view).toContain("? [...current.filter(id => nodeLookup.get(id)?.kind === 'finding'), nodeId]");
expect(view).toContain('togglePinnedNode(operation.nodeId);');
expect(view).toContain('const isSelected = pinnedNodeIds.includes(node.id);');
expect(view).toContain('aria-pressed={isSelected}');
expect(view).toContain("const [displayMode, setDisplayMode] = useState<GraphDisplayMode>('off');");
expect(view).toContain("type GraphDisplayMode = 'off' | 'labels' | 'text';");
expect(view).toContain("const showCards = displayMode === 'labels';");
expect(view).toContain("onClick={() => setDisplayMode('off')}>Off</button>");
expect(view).toContain('>Labels</button>');
expect(view).toContain('>Text</button>');
expect(view).toContain("onClick={() => setDisplayMode(showCards ? 'off' : 'labels')}>Labels</button>");
expect(view).toContain("onClick={() => setDisplayMode('text')}>Text</button>");
expect(view).not.toContain("showCards ? 'On' : 'Off'");
expect(view).not.toContain("showText ? 'On' : 'Off'");
expect(view).toContain('const cardPlacements = useMemo<GraphCardPlacement[]>');
expect(view).toContain("const hoverCardNodeId = hoveredNodeId && displayedNodeIdSet.has(hoveredNodeId) ? hoveredNodeId : '';");
expect(view).toContain('if ((!renderGraphCards && !hoverCardNodeId) || !displayedNodes.length) return [];');
expect(view).toContain('...(hoverCardNodeId ? [hoverCardNodeId] : [])');
expect(view).toContain('...(renderGraphCards ? pinnedNodeIds.filter(nodeId => displayedNodeIdSet.has(nodeId)) : [])');
expect(view).toContain("if (displayMode !== 'labels')");
expect(view).toContain("setDisplayMode('labels');");
expect(view).toContain("if (isPinned && !next.length) setDisplayMode('off');");
expect(view).toContain("isSelectionMuted ? ' is-selection-muted' : ''");
expect(view).toContain('const nodeIsInViewport = labelNodeScreenX >= viewportInset');
expect(view).toContain('const labelIsFullyVisible = labelLeft >= viewportInset');
expect(view).toContain('const showInlineLabel = showText && nodeIsInViewport && labelIsFullyVisible;');
expect(view).toContain('const activePathNodeIds = hoveredNodeId ? [hoveredNodeId] : pinnedNodeIds;');
expect(view).toContain("isDirectPath ? ' is-direct-path' : ''");
expect(view).toContain('className="obsidian-node-inline-label"');
expect(view).toContain("node.kind !== 'finding' && <circle className=\"obsidian-node-role-ring\"");
expect(view).toContain('const graphCardScale = adaptiveGraphCardScale(');
expect(view).toContain('const minimumCardZoom = minimumReadableGraphCardZoom(pinnedNodeIds.length);');
expect(view).toContain('const graphCardsSuppressed = showCards && camera.scale < minimumCardZoom;');
expect(view).toContain('if ((!renderGraphCards && !hoverCardNodeId) || !displayedNodes.length) return [];');
expect(view).toContain('className="obsidian-graph-card-zoom-hint"');
expect(view).toContain('pathfinderMicroCardContract.screenScale.minimum');
expect(view).toContain('pathfinderMicroCardContract.screenScale.maximum');
expect(microCard).toContain('screenScale: { minimum: 0.7, maximum: 0.76 }');
expect(view).toContain('const graphFindingCardWidth = pathfinderMicroCardContract.finding.width;');
expect(view).toContain('const graphFindingCardHeight = pathfinderMicroCardContract.finding.height;');
expect(view).toContain('const graphRoleCardWidth = pathfinderMicroCardContract.relation.width;');
expect(view).toContain('const graphRoleCardHeight = pathfinderMicroCardContract.relation.height;');
expect(view).toContain('const graphCategoryCardWidth = pathfinderMicroCardContract.category.width;');
expect(view).toContain('const graphCategoryCardHeight = pathfinderMicroCardContract.category.height;');
expect(microCard).toContain('screenScale: { minimum: 0.7, maximum: 0.76 }');
expect(microCard).toContain('finding: { width: 240, height: 232 }');
expect(microCard).toContain('relation: { width: 190, height: 130 }');
expect(microCard).toContain('category: { width: 220, height: 150 }');
expect(microCard).toContain("data-pathfinder-micro-card={variant}");
expect(view).toContain('<PathfinderMicroCard variant="finding">');
expect(view).toContain('<PathfinderMicroCard variant="relation" relationKind={node.kind}>');
expect(view).not.toContain('--graph-card-content-scale');
expect(view).not.toContain('cardContentStyle');
expect(view).toContain('Math.hypot(dx, dy) > 3');
expect(view).toContain('transform={`scale(${1 / camera.scale})`}');
expect(view).toContain('<g className="obsidian-graph-cards">{cardPlacements.map');
expect(view).toContain("className={`obsidian-node-card-wrap${finding ? ' obsidian-node-matrix-card-wrap' : ''}`}");
expect(microCard).toContain('pathfinder-micro-card-finding obsidian-node-matrix-card');
expect(view).toContain('<TileCard');
expect(view).toContain('const graphCenterX = graphNodes.length');
expect(view).toContain('const placeOnRight = node.x <= graphCenterX;');
expect(view).toContain('const x = placeOnRight ? node.x + fixedGap : node.x - fixedGap - width;');
expect(view).toContain('const baseY = node.y - height / 2;');
expect(view).not.toContain('const viewportLeft =');
expect(view).not.toContain('const nodeScreenX =');
expect(view).not.toContain('clamp(preferredX');
expect(view).toContain('const collisionGap = extractedNodeIds ? 18 : 10;');
expect(view).toContain('const maximumOffsetRing = Math.min(8, Math.max(4, Math.ceil(Math.sqrt(orderedNodeIds.length)) + 2));');
expect(view).toContain('const laneCount = extractedNodeIds ? 4 : 3;');
expect(view).toContain('const activeEdgeSegments = graph.edges.flatMap');
expect(view).toContain('const maximumTetherDistance = graphCardMaximumTetherDistance(width, height);');
expect(view).toContain('const collisionFreeCandidates = nearbyCandidates.filter(fits);');
expect(view).toContain('const automatic = bestNearbyCardCandidate(');
expect(view).toContain('onClickCapture={event =>');
expect(view).toContain('resultColor={resultWindowFor(finding.id)?.color}');
expect(view).not.toContain('badgeLimit=');
expect(view).toContain('onBookmark={() => updateReview(finding.id');
expect(view.indexOf('className="obsidian-graph-cards"')).toBeGreaterThan(view.indexOf('className="obsidian-graph-nodes"'));
expect(view).not.toContain('scale(${1 / camera.scale})`}><foreignObject');
expect(microCard).toContain('pathfinder-micro-card-relation obsidian-node-card data-tile');
expect(view).toContain('className="data-tile-hex"');
expect(view).toContain('className="data-tile-badges"');
expect(view).not.toContain('outcomeForStatusValue');
expect(view).toContain('graphLabel(node.label)');
expect(graphStyles).toMatch(/\.obsidian-node-card-wrap\s*\{[^}]*pointer-events:\s*auto;/s);
expect(graphStyles).toMatch(/\.obsidian-node-card\.data-tile\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;/s);
expect(graphStyles).toMatch(/\.obsidian-node-card\.data-tile\s*\{[^}]*grid-template-columns:\s*18px minmax\(0, 1fr\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-card \.data-tile-content h3\s*\{[^}]*overflow-wrap:\s*normal;[^}]*word-break:\s*normal;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile\s*\{[^}]*width:\s*100%;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile\s*\{[^}]*grid-template-columns:\s*18px minmax\(0, 1fr\) 18px;[^}]*padding:\s*var\(--space-3\);[^}]*overflow:\s*hidden;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-title-size\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile-content h3\s*\{[^}]*-webkit-line-clamp:\s*5;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile-content\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\);[^}]*overflow:\s*hidden;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile-badges\s*\{[^}]*flex-wrap:\s*wrap;[^}]*max-height:\s*none;[^}]*align-content:\s*start;/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile-badges > \*\s*\{[^}]*flex:\s*0 1 auto;[^}]*max-width:\s*100%;[^}]*height:\s*auto;[^}]*min-height:\s*20px;[^}]*font-size:\s*var\(--pathfinder-micro-badge-size\);[^}]*white-space:\s*normal;/s);
expect(graphStyles).not.toMatch(/\.obsidian-node-matrix-card \.data-tile-badges > \*\s*\{[^}]*text-overflow:\s*ellipsis;/s);
expect(graphStyles).toMatch(/\.obsidian-graph-card\.is-selected \.obsidian-node-card,[\s\S]*?\.obsidian-graph-card\.is-selected \.obsidian-node-matrix-card \.data-tile\s*\{/s);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;/s);
expect(graphStyles).not.toContain('--graph-card-content-scale');
expect(graphStyles).toMatch(/\.obsidian-node-card\.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*inset 0 0 0 0 transparent;[^}]*transform:\s*none;/);
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*inset 0 0 0 0 transparent;[^}]*transform:\s*none;/);
expect(graphStyles).toMatch(/\.obsidian-node-card \.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-title-size\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-inline-label\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-card \.reader-severity\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-relation-badge-size\);/s);
expect(graphStyles).not.toContain('.obsidian-node-card-actions');
expect(tokens).toContain('--pathfinder-micro-title-size: 23px;');
expect(tokens).toContain('--pathfinder-micro-badge-size: 14px;');
expect(tokens).toContain('--pathfinder-micro-relation-badge-size: 16px;');
expect(tokens).toContain('--pathfinder-micro-action-size: 14px;');
expect(tokens).toContain('--pathfinder-micro-action-group-radius: 8px;');
expect(tokens).toContain('--pathfinder-micro-action-radius: 6px;');
expect(graphStyles).not.toMatch(/#(?:000|fff)\b/i);
expect(graphStyles).toMatch(/\.obsidian-node-role-ring\s*\{[^}]*fill:\s*none;[^}]*stroke:\s*currentColor;[^}]*stroke-width:\s*1\.5;/s);
expect(graphStyles).toMatch(/\.obsidian-node-selection-ring\s*\{[^}]*stroke:\s*var\(--graph-selected\);[^}]*stroke-width:\s*3;/s);
expect(graphStyles).toMatch(/\.obsidian-node-selection-ring\s*\{[^}]*filter:\s*none;/s);
expect(graphStyles).toMatch(/\.obsidian-card-connector\s*\{[^}]*stroke:\s*var\(--graph-selected\);[^}]*stroke-width:\s*2;/s);
expect(graphStyles).toMatch(/\.obsidian-graph-edges line\.is-branch-path\s*\{[^}]*stroke:\s*var\(--graph-selected\);/s);
expect(graphStyles).not.toContain('.obsidian-category-context-menu');
expect(graphStyles).not.toContain('!important');
expect(graphStyles).toMatch(/\.obsidian-node-category,[\s\S]*\.obsidian-node-tactic\s*\{\s*color:\s*var\(--graph-relation\);/);
expect(graphStyles).toMatch(/\.obsidian-node-card-technique \.data-tile-hex,[\s\S]*\.obsidian-node-card-tactic \.data-tile-hex\s*\{\s*background:\s*var\(--graph-relation\);/);
expect(tokens.match(/--graph-relation:\s*#9333EA;/g)).toHaveLength(2);
expect(graphStyles).toMatch(/\.obsidian-node-finding\.severity-critical\s*\{[^}]*color:\s*var\(--critical\);/s);
expect(graphStyles).toMatch(/\.obsidian-node-finding\.severity-unknown\s*\{[^}]*color:\s*var\(--graph-unknown\);/s);
expect(graphStyles).toMatch(/line\.is-direct-path\s*\{[^}]*stroke:\s*var\(--graph-alert\);[^}]*stroke-width:\s*2;/s);
expect(view).not.toContain('className="obsidian-node-alert"');
expect(view).toContain('className="obsidian-graph-layout"');
expect(view).toContain('className="obsidian-graph-filter-panel"');
expect(view).toContain('className="obsidian-graph-stage"');
expect(graphStyles).not.toContain('camera-morph-glow');
expect(view).not.toContain('className="obsidian-graph-legend"');
expect(graphStyles).not.toContain('.obsidian-graph-legend');
expect(view).toContain('className={`obsidian-graph-inspector');
expect(graphStyles).toMatch(/\.obsidian-graph-layout\s*\{[^}]*grid-template-columns:\s*clamp\(220px, 17vw, 280px\) minmax\(0, 1fr\) clamp\(270px, 21vw, 340px\);/s);
expect(view).toContain('<SearchTextControl />');
expect(view).not.toContain('<SheetAndSearchControls />');
expect(view).not.toContain('<span className="obsidian-panel-label">Source</span>');
expect(view).toContain("import { hiddenPathfinderFacetLabels } from './pathfinder-filter-trail';");
expect(view).not.toContain("'Source: Scan'] as const");
expect(view).toContain('<SearchFilterBar hiddenFacetLabels={hiddenPathfinderFacetLabels} />');
expect(view).toContain('className="obsidian-graph-workspace-tools"');
expect(view).toContain('checked={actionOnly} disabled={!marked} onChange={() => toggleWorkspace()}');
expect(view).toContain('<span>My Workspace</span>');
expect(view).toContain('mode="path"');
expect(view).toContain('path={pathSnapshot}');
expect(view).toContain("activeNodeIds: [...pinnedNodeIds]");
expect(view).toContain('pinnedNodeIds.includes(edge.source) || pinnedNodeIds.includes(edge.target)');
expect(view).toContain('const isDirectPath = activePathNodeIds.includes(edge.source) || activePathNodeIds.includes(edge.target);');
expect(app).toContain('onApplyPathSnapshot={(bookmark: ViewBookmark) =>');
expect(app).toContain('app.setPathFindingIds(pathSnapshotFindingIds(bookmark.path));');
expect(app).toContain('hidden={activeArea !== \'graph\'}');
expect(app).toContain('active={activeArea === \'graph\'}');
expect(view).toContain('if (!active || lastFittedGraphSignature.current === graphSignature) return;');
expect(view).toContain('lastFittedGraphSignature.current = graphSignature;');
expect(dynamicFilterStyles).toMatch(/\.view-bookmark-item\.is-path\s*\{[^}]*outline:\s*2px solid var\(--graph-alert\);/s);
expect(graphStyles).toMatch(/\.obsidian-graph-workspace-tools\s*\{[^}]*border-top:\s*var\(--bw-hairline\) solid var\(--border-default\);/s);
expect(graphStyles).toMatch(/\.obsidian-graph-workspace-tools \.view-bookmark-bar,[^{]*\{[^}]*flex-direction:\s*column;/s);
expect(view).toContain("const visibleRecords = useFilteredRecords('deviation');");
expect(view).toContain('buildFindingPathItems(visibleRecords');
expect(view).not.toContain('const [category, setCategory]');
expect(view).not.toContain('const [severity, setSeverity]');
expect(view).not.toContain('const [originFilter, setOriginFilter]');
expect(view).not.toContain('const [search, setSearch]');
expect(layout).toMatch(/\.viewer\.app-area-graph\s*\{[^}]*background-image:\s*var\(--app-grid-pattern\);/s);
expect(layout).not.toMatch(/\.app-shell:not\(\.app-shell-work\) \.viewer\.app-area\s*\{[^}]*background-image:/s);
expect(layout).toContain('.app-shell.sidebar-collapsed > main > aside { visibility: hidden; }');
expect(layout).toContain('.app-shell > main > aside { visibility: hidden; }');
expect(layout).not.toContain('.sidebar-collapsed aside { visibility: hidden; }');
expect(tokens.match(/--app-grid-pattern:/g)).toHaveLength(2);
});
});