Merge remote-tracking branch 'origin/demo' into feat/monitoringpage-pim

This commit is contained in:
Pim Hutting
2026-01-08 09:54:48 +01:00
parent 4356f201ab
commit a2b4847ca4
48 changed files with 3038 additions and 527 deletions

View File

@@ -14,7 +14,6 @@ describe('BasicBeliefNode', () => {
beforeEach(() => {
user = userEvent.setup();
});
describe('Rendering', () => {
it('should render the basic belief node with keyword type by default', () => {
const mockNode: Node<BasicBeliefNodeData> = {
@@ -59,7 +58,7 @@ describe('BasicBeliefNode', () => {
data: {
label: 'Belief',
droppable: true,
belief: { type: 'semantic', id: 'test', value: 'test value', label: 'Detected with LLM:' },
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
hasReduce: true,
},
};
@@ -333,7 +332,7 @@ describe('BasicBeliefNode', () => {
data: {
label: 'Belief',
droppable: true,
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
hasReduce: true,
},
};
@@ -360,10 +359,10 @@ describe('BasicBeliefNode', () => {
/>
);
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
const input = screen.getByDisplayValue('test value') as HTMLInputElement;
// Clear the input
for (let i = 0; i < 'initial'.length; i++) {
for (let i = 0; i < 'test value'.length; i++) {
await user.type(input, '{backspace}');
}
await user.type(input, 'new semantic value{enter}');
@@ -689,7 +688,7 @@ describe('BasicBeliefNode', () => {
data: {
label: 'Belief',
droppable: true,
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
hasReduce: true,
},
};
@@ -716,27 +715,27 @@ describe('BasicBeliefNode', () => {
/>
);
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
const input = screen.getByDisplayValue('test value') as HTMLInputElement;
await user.type(input, '1');
await waitFor(() => {
const state = useFlowStore.getState();
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
expect(nodeData.belief.value).toBe('initial');
expect(nodeData.belief.value).toBe('test value');
});
await user.type(input, '2');
await waitFor(() => {
const state = useFlowStore.getState();
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
expect(nodeData.belief.value).toBe('initial');
expect(nodeData.belief.value).toBe('test value');
});
await user.type(input, '{enter}');
await waitFor(() => {
const state = useFlowStore.getState();
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
expect(nodeData.belief.value).toBe('initial12');
expect(nodeData.belief.value).toBe('test value12');
});
});
});

View File

@@ -404,6 +404,16 @@ describe('NormNode', () => {
describe('NormReduce Function', () => {
it('should reduce a norm node to its essential data', () => {
const condition: Node = {
id: "belief-1",
type: 'basic_belief',
position: {x: 10, y: 10},
data: {
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
}
}
const normNode: Node = {
id: 'norm-1',
type: 'norm',
@@ -414,18 +424,21 @@ describe('NormNode', () => {
droppable: true,
norm: 'Never harm humans',
hasReduce: true,
condition: "belief-1"
},
};
const allNodes: Node[] = [normNode];
const allNodes: Node[] = [normNode, condition];
const result = NormReduce(normNode, allNodes);
expect(result).toEqual({
id: 'norm-1',
label: 'Safety Norm',
norm: 'Never harm humans',
critical: false,
basic_beliefs: [],
condition: {
id: "belief-1",
keyword: ""
},
});
});
@@ -917,17 +930,8 @@ describe('NormNode', () => {
}
};
const mockBelief2: Node = {
id: 'basic_belief-2',
type: 'basic_belief',
position: {x:300, y:300},
data: {
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
}
};
useFlowStore.setState({
nodes: [mockNode, mockBelief1, mockBelief2],
nodes: [mockNode, mockBelief1],
edges: [],
});
@@ -938,16 +942,11 @@ describe('NormNode', () => {
sourceHandle: null,
targetHandle: null,
});
useFlowStore.getState().onConnect({
source: 'basic_belief-2',
target: 'norm-1',
sourceHandle: null,
targetHandle: null,
});
const state = useFlowStore.getState();
const updatedNorm = state.nodes.find(n => n.id === 'norm-1');
expect(updatedNorm?.data.conditions).toEqual(["basic_belief-1", "basic_belief-2"]);
expect(updatedNorm?.data.condition).toEqual("basic_belief-1");
});
});
});

View File

@@ -78,8 +78,10 @@ describe('PhaseNode', () => {
// Find nodes
const nodes = useFlowStore.getState().nodes;
const p1 = nodes.find((x) => x.id === 'phase-1')!;
const p2 = nodes.find((x) => x.id === 'phase-2')!;
const phaseNodes = nodes.filter((x) => x.type === 'phase');
const p1 = phaseNodes[0];
const p2 = phaseNodes[1];
// expect same value, not same reference
expect(p1.data.children).not.toBe(p2.data.children);

View File

@@ -1,6 +1,5 @@
import { describe, it, beforeEach } from '@jest/globals';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
import TriggerNode, {
TriggerReduce,
@@ -11,12 +10,15 @@ import TriggerNode, {
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
import type { Node } from '@xyflow/react';
import '@testing-library/jest-dom';
import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.default.ts';
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
describe('TriggerNode', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
jest.clearAllMocks();
});
describe('Rendering', () => {
@@ -26,11 +28,7 @@ describe('TriggerNode', () => {
type: 'trigger',
position: { x: 0, y: 0 },
data: {
label: 'Keyword Trigger',
droppable: true,
triggerType: 'keywords',
triggers: [],
hasReduce: true,
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
},
};
@@ -51,161 +49,58 @@ describe('TriggerNode', () => {
/>
);
expect(screen.getByText(/Triggers when the keyword is spoken/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText('...')).toBeInTheDocument();
});
it('should render TriggerNode with emotion type', () => {
const mockNode: Node<TriggerNodeData> = {
id: 'trigger-2',
type: 'trigger',
position: { x: 0, y: 0 },
data: {
label: 'Emotion Trigger',
droppable: true,
triggerType: 'emotion',
triggers: [],
hasReduce: true,
},
};
renderWithProviders(
<TriggerNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
expect(screen.getByText(/Emotion\?/i)).toBeInTheDocument();
});
});
describe('User Interactions', () => {
it('should add a new keyword', async () => {
const mockNode: Node<TriggerNodeData> = {
id: 'trigger-1',
type: 'trigger',
position: { x: 0, y: 0 },
data: {
label: 'Keyword Trigger',
droppable: true,
triggerType: 'keywords',
triggers: [],
hasReduce: true,
},
};
useFlowStore.setState({ nodes: [mockNode], edges: [] });
renderWithProviders(
<TriggerNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
const input = screen.getByPlaceholderText('...');
await user.type(input, 'hello{enter}');
await waitFor(() => {
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
expect(node?.data.triggers.length).toBe(1);
expect(node?.data.triggers[0].keyword).toBe('hello');
});
});
it('should remove a keyword when cleared', async () => {
const mockNode: Node<TriggerNodeData> = {
id: 'trigger-1',
type: 'trigger',
position: { x: 0, y: 0 },
data: {
label: 'Keyword Trigger',
droppable: true,
triggerType: 'keywords',
triggers: [{ id: 'kw1', keyword: 'hello' }],
hasReduce: true,
},
};
useFlowStore.setState({ nodes: [mockNode], edges: [] });
renderWithProviders(
<TriggerNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
const input = screen.getByDisplayValue('hello');
for (let i = 0; i < 'hello'.length; i++) {
await user.type(input, '{backspace}');
}
await user.type(input, '{enter}');
await waitFor(() => {
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
expect(node?.data.triggers.length).toBe(0);
});
expect(screen.getByText(/Triggers when the condition is met/i)).toBeInTheDocument();
expect(screen.getByText(/Belief is currently/i)).toBeInTheDocument();
expect(screen.getByText(/Plan is currently/i)).toBeInTheDocument();
});
});
describe('TriggerReduce Function', () => {
it('should reduce a trigger node to its essential data', () => {
const conditionNode: Node = {
id: 'belief-1',
type: 'basic_belief',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)),
},
};
const triggerNode: Node = {
id: 'trigger-1',
type: 'trigger',
position: { x: 0, y: 0 },
data: {
label: 'Keyword Trigger',
droppable: true,
triggerType: 'keywords',
triggers: [{ id: 'kw1', keyword: 'hello' }],
hasReduce: true,
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
condition: "belief-1",
plan: defaultPlan
},
};
const allNodes: Node[] = [triggerNode];
const result = TriggerReduce(triggerNode, allNodes);
useFlowStore.setState({
nodes: [conditionNode, triggerNode],
edges: [],
});
useFlowStore.getState().onConnect({
source: 'belief-1',
target: 'trigger-1',
sourceHandle: null,
targetHandle: null,
});
const result = TriggerReduce(triggerNode, useFlowStore.getState().nodes);
expect(result).toEqual({
id: 'trigger-1',
type: 'keywords',
label: 'Keyword Trigger',
keywords: [{ id: 'kw1', keyword: 'hello' }],
});
condition: {
id: "belief-1",
keyword: "",
},
plan: {
id: expect.anything(),
steps: [],
},});
});
});
@@ -217,11 +112,8 @@ describe('TriggerNode', () => {
type: 'trigger',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
label: 'Trigger 1',
droppable: true,
triggerType: 'keywords',
triggers: [],
hasReduce: true,
},
};
@@ -230,10 +122,8 @@ describe('TriggerNode', () => {
type: 'norm',
position: { x: 100, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Norm 1',
droppable: true,
norm: 'test',
hasReduce: true,
},
};

View File

@@ -185,7 +185,7 @@ describe('Universal Nodes', () => {
// Verify the correct structure is present using NodesInPhase
expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
expect(result[0]).toHaveProperty('id', 'phase-1');
expect(result[0]).toHaveProperty('label', 'Test Phase');
expect(result[0]).toHaveProperty('name', 'Test Phase');
// Restore mocks
phaseReduceSpy.mockRestore();