diff --git a/decnet_web/src/components/AttackerDetail.behaviour_panel.test.tsx b/decnet_web/src/components/AttackerDetail.behaviour_panel.test.tsx new file mode 100644 index 00000000..9d94ec6a --- /dev/null +++ b/decnet_web/src/components/AttackerDetail.behaviour_panel.test.tsx @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; + +import { + BehaviouralPrimitivesPanel, + type BehaviouralObservation, +} from './AttackerDetail'; + +const _obs = ( + primitive: string, + value: unknown, + confidence = 0.85, +): BehaviouralObservation => ({ + primitive, + value, + confidence, + ts: 1714521660.456, + source: 'test', +}); + +describe('BehaviouralPrimitivesPanel', () => { + it('renders an empty-state placeholder when no observations', () => { + render(); + expect(screen.getByTestId('behaviour-empty')).toBeInTheDocument(); + }); + + it('places day-one priority primitives at the top of their group', () => { + // Mix priority + non-priority primitives in arbitrary input order. + const observations: BehaviouralObservation[] = [ + _obs('motor.keystroke_cadence', 'steady'), + _obs('cognitive.tool_vocabulary', 'broad'), + _obs('motor.input_modality', 'typed'), // priority #1 + _obs('cognitive.feedback_loop_engagement', 'closed_loop'), // priority #2 + _obs('cognitive.command_branch_diversity', 'adaptive_branching'), // #3 + _obs('cognitive.inter_command_latency_class', 'typing_speed'), // #4 + _obs('motor.error_correction', 'immediate'), + ]; + render(); + + // motor group: input_modality must precede the alphabetised rest. + const motorRows = Array.from( + document.querySelectorAll('[data-testid^="behaviour-row-motor."]'), + ).map((el) => el.getAttribute('data-testid')!); + expect(motorRows[0]).toBe('behaviour-row-motor.input_modality'); + + // cognitive group: the three priority primitives must be in the + // documented order at the top. + const cogRows = Array.from( + document.querySelectorAll('[data-testid^="behaviour-row-cognitive."]'), + ).map((el) => el.getAttribute('data-testid')!); + expect(cogRows.slice(0, 3)).toEqual([ + 'behaviour-row-cognitive.feedback_loop_engagement', + 'behaviour-row-cognitive.command_branch_diversity', + 'behaviour-row-cognitive.inter_command_latency_class', + ]); + }); + + it('renders the primitive value and a confidence badge', () => { + const observations: BehaviouralObservation[] = [ + _obs('motor.input_modality', 'pasted', 0.91), + ]; + render(); + const row = screen.getByTestId('behaviour-row-motor.input_modality'); + expect(row.textContent).toContain('input_modality'); + expect(row.textContent).toContain('pasted'); + expect(row.textContent).toContain('91%'); + }); + + it('groups by top-level domain in the canonical order', () => { + const observations: BehaviouralObservation[] = [ + _obs('emotional_valence.arousal', 'medium_engaged'), + _obs('temporal.session_duration', 'medium'), + _obs('motor.input_modality', 'typed'), + _obs('cognitive.cognitive_load', 'medium'), + ]; + render(); + const groups = Array.from( + document.querySelectorAll('[data-testid^="behaviour-group-"]'), + ).map((el) => el.getAttribute('data-testid')!); + expect(groups).toEqual([ + 'behaviour-group-motor', + 'behaviour-group-cognitive', + 'behaviour-group-temporal', + 'behaviour-group-emotional_valence', + ]); + }); +});