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',
+ ]);
+ });
+});