) {
type="target"
position={Position.Bottom}
id="TriggerBeliefs"
- style={{ left: '40%' }}
+ style={{ left: '40%' }}
rules={[
- allowOnlyConnectionsFromType(['basic_belief']),
+ allowOnlyConnectionsFromType(["basic_belief","inferred_belief"]),
]}
title="Connect to a beliefNode or a set of beliefs combined using the AND/OR node"
/>
@@ -171,13 +171,13 @@ export default function TriggerNode(props: NodeProps) {
/**
* Reduces each Trigger, including its children down into its core data.
* @param node - The Trigger node to reduce.
- * @param _nodes - The list of all nodes in the current flow graph.
+ * @param nodes - The list of all nodes in the current flow graph.
* @returns A simplified object containing the node label and its list of triggers.
*/
export function TriggerReduce(node: Node, nodes: Node[]) {
const data = node.data as TriggerNodeData;
const conditionNode = data.condition ? nodes.find((n)=>n.id===data.condition) : undefined
- const conditionData = conditionNode ? BasicBeliefReduce(conditionNode, nodes) : ""
+ const conditionData = conditionNode ? BeliefGlobalReduce(conditionNode, nodes) : ""
return {
id: node.id,
name: node.data.name,
@@ -205,7 +205,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
const otherNode = nodes.find((x) => x.id === _sourceNodeId)
if (!otherNode) return;
- if (otherNode.type === 'basic_belief' /* TODO: Add the option for an inferred belief */) {
+ if (['basic_belief', 'inferred_belief'].includes(otherNode.type!)) {
data.condition = _sourceNodeId;
}
@@ -241,7 +241,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin
const data = _thisNode.data as TriggerNodeData;
// remove if the target of disconnection was our condition
if (_sourceNodeId == data.condition) data.condition = undefined
-
+
data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId)
}
diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefGlobals.test.ts b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefGlobals.test.ts
new file mode 100644
index 0000000..54cfa7f
--- /dev/null
+++ b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefGlobals.test.ts
@@ -0,0 +1,274 @@
+import { describe, it, expect, jest, beforeEach } from '@jest/globals';
+import {type Connection, getOutgoers, type Node} from '@xyflow/react';
+import {ruleResult} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
+import {BasicBeliefReduce} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx";
+import {
+ BeliefGlobalReduce, noBeliefCycles,
+ noMatchingLeftRightBelief
+} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BeliefGlobals.ts";
+import { InferredBeliefReduce } from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx";
+import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
+import * as BasicModule from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode';
+import * as InferredModule from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx';
+import * as FlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
+
+
+describe('BeliefGlobalReduce', () => {
+ const nodes: Node[] = [];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('delegates to BasicBeliefReduce for basic_belief nodes', () => {
+ const spy = jest
+ .spyOn(BasicModule, 'BasicBeliefReduce')
+ .mockReturnValue('basic-result' as any);
+
+ const node = { id: '1', type: 'basic_belief' } as Node;
+
+ const result = BeliefGlobalReduce(node, nodes);
+
+ expect(spy).toHaveBeenCalledWith(node, nodes);
+ expect(result).toBe('basic-result');
+ });
+
+ it('delegates to InferredBeliefReduce for inferred_belief nodes', () => {
+ const spy = jest
+ .spyOn(InferredModule, 'InferredBeliefReduce')
+ .mockReturnValue('inferred-result' as any);
+
+ const node = { id: '2', type: 'inferred_belief' } as Node;
+
+ const result = BeliefGlobalReduce(node, nodes);
+
+ expect(spy).toHaveBeenCalledWith(node, nodes);
+ expect(result).toBe('inferred-result');
+ });
+
+ it('returns undefined for unknown node types', () => {
+ const node = { id: '3', type: 'other' } as Node;
+
+ const result = BeliefGlobalReduce(node, nodes);
+
+ expect(result).toBeUndefined();
+ expect(BasicBeliefReduce).not.toHaveBeenCalled();
+ expect(InferredBeliefReduce).not.toHaveBeenCalled();
+ });
+});
+
+describe('noMatchingLeftRightBelief rule', () => {
+ let getStateSpy: ReturnType;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getStateSpy = jest.spyOn(FlowStore.default, 'getState');
+ });
+
+ it('is satisfied when target node is not an inferred belief', () => {
+ getStateSpy.mockReturnValue({
+ nodes: [{ id: 't1', type: 'basic_belief' }],
+ } as any);
+
+ const result = noMatchingLeftRightBelief(
+ { source: 's1', target: 't1' } as Connection,
+ null as any
+ );
+
+ expect(result).toBe(ruleResult.satisfied);
+ });
+
+ it('is satisfied when inferred belief has no matching left/right', () => {
+ getStateSpy.mockReturnValue({
+ nodes: [
+ {
+ id: 't1',
+ type: 'inferred_belief',
+ data: {
+ inferredBelief: {
+ left: 'a',
+ right: 'b',
+ },
+ },
+ },
+ ],
+ } as any);
+
+ const result = noMatchingLeftRightBelief(
+ { source: 'c', target: 't1' } as Connection,
+ null as any
+ );
+
+ expect(result).toBe(ruleResult.satisfied);
+ });
+
+ it('is NOT satisfied when source matches left input', () => {
+ getStateSpy.mockReturnValue({
+ nodes: [
+ {
+ id: 't1',
+ type: 'inferred_belief',
+ data: {
+ inferredBelief: {
+ left: 's1',
+ right: 's2',
+ },
+ },
+ },
+ ],
+ } as any);
+
+ const result = noMatchingLeftRightBelief(
+ { source: 's1', target: 't1' } as Connection,
+ null as any
+ );
+
+ expect(result.isSatisfied).toBe(false);
+ if (!(result.isSatisfied)) {
+ expect(result.message).toContain(
+ 'Connecting one belief to both input handles of an inferred belief node is not allowed'
+ );
+ }
+ });
+
+ it('is NOT satisfied when source matches right input', () => {
+ getStateSpy.mockReturnValue({
+ nodes: [
+ {
+ id: 't1',
+ type: 'inferred_belief',
+ data: {
+ inferredBelief: {
+ left: 's1',
+ right: 's2',
+ },
+ },
+ },
+ ],
+ } as any);
+
+ const result = noMatchingLeftRightBelief(
+ { source: 's2', target: 't1' } as Connection,
+ null as any
+ );
+
+ expect(result.isSatisfied).toBe(false);
+ if (!(result.isSatisfied)) {
+ expect(result.message).toContain(
+ 'Connecting one belief to both input handles of an inferred belief node is not allowed'
+ );
+ }
+ });
+});
+
+
+jest.mock('@xyflow/react', () => ({
+ getOutgoers: jest.fn(),
+ getConnectedEdges: jest.fn(), // include if some tests require it
+}));
+
+describe('noBeliefCycles rule', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns notSatisfied when source === target', () => {
+ const result = noBeliefCycles({ source: 'n1', target: 'n1' } as any, null as any);
+ expect(result.isSatisfied).toBe(false);
+ if (!(result.isSatisfied)) {
+ expect(result.message).toContain('Cyclical connection exists');
+ }
+ });
+
+ it('returns satisfied when there are no outgoing inferred beliefs', () => {
+ jest.spyOn(useFlowStore, 'getState').mockReturnValue({
+ nodes: [{ id: 'n1', type: 'inferred_belief' }],
+ edges: [],
+ } as any);
+
+ (getOutgoers as jest.Mock).mockReturnValue([]);
+
+ const result = noBeliefCycles({ source: 'n1', target: 'n2' } as any, null as any);
+ expect(result).toBe(ruleResult.satisfied);
+ });
+
+ it('returns notSatisfied for direct cycle', () => {
+ jest.spyOn(useFlowStore, 'getState').mockReturnValue({
+ nodes: [
+ { id: 'n1', type: 'inferred_belief' },
+ { id: 'n2', type: 'inferred_belief' },
+ ],
+ edges: [{ source: 'n2', target: 'n1' }],
+ } as any);
+
+ // @ts-expect-error is acting up
+ (getOutgoers as jest.Mock).mockImplementation(({ id }) => {
+ if (id === 'n2') return [{ id: 'n1', type: 'inferred_belief' }];
+ return [];
+ });
+
+ const result = noBeliefCycles({ source: 'n1', target: 'n2' } as any, null as any);
+ expect(result.isSatisfied).toBe(false);
+ if (!(result.isSatisfied)) {
+ expect(result.message).toContain('Cyclical connection exists');
+ }
+ });
+
+ it('returns notSatisfied for indirect cycle', () => {
+ jest.spyOn(useFlowStore, 'getState').mockReturnValue({
+ nodes: [
+ { id: 'A', type: 'inferred_belief' },
+ { id: 'B', type: 'inferred_belief' },
+ { id: 'C', type: 'inferred_belief' },
+ ],
+ edges: [
+ { source: 'A', target: 'B' },
+ { source: 'B', target: 'C' },
+ { source: 'C', target: 'A' },
+ ],
+ } as any);
+
+ // @ts-expect-error is acting up
+ (getOutgoers as jest.Mock).mockImplementation(({ id }) => {
+ const mapping: Record = {
+ A: [{ id: 'B', type: 'inferred_belief' }],
+ B: [{ id: 'C', type: 'inferred_belief' }],
+ C: [{ id: 'A', type: 'inferred_belief' }],
+ };
+ return mapping[id] || [];
+ });
+
+ const result = noBeliefCycles({ source: 'A', target: 'B' } as any, null as any);
+ expect(result.isSatisfied).toBe(false);
+ if (!(result.isSatisfied)) {
+ expect(result.message).toContain('Cyclical connection exists');
+ }
+ });
+
+ it('returns satisfied when no cycle exists in a multi-node graph', () => {
+ jest.spyOn(useFlowStore, 'getState').mockReturnValue({
+ nodes: [
+ { id: 'A', type: 'inferred_belief' },
+ { id: 'B', type: 'inferred_belief' },
+ { id: 'C', type: 'inferred_belief' },
+ ],
+ edges: [
+ { source: 'A', target: 'B' },
+ { source: 'B', target: 'C' },
+ ],
+ } as any);
+
+ // @ts-expect-error is acting up
+ (getOutgoers as jest.Mock).mockImplementation(({ id }) => {
+ const mapping: Record = {
+ A: [{ id: 'B', type: 'inferred_belief' }],
+ B: [{ id: 'C', type: 'inferred_belief' }],
+ C: [],
+ };
+ return mapping[id] || [];
+ });
+
+ const result = noBeliefCycles({ source: 'A', target: 'B' } as any, null as any);
+ expect(result).toBe(ruleResult.satisfied);
+ });
+});
\ No newline at end of file
diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx
index 34872c9..a023769 100644
--- a/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx
+++ b/test/pages/visProgPage/visualProgrammingUI/nodes/BeliefNode.test.tsx
@@ -3,7 +3,7 @@ import { describe, it, beforeEach } from '@jest/globals';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../.././/./../../test-utils/test-utils';
-import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode';
+import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
import type { Node } from '@xyflow/react';
import '@testing-library/jest-dom';
diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/InferredBeliefNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/InferredBeliefNode.test.tsx
new file mode 100644
index 0000000..d683b23
--- /dev/null
+++ b/test/pages/visProgPage/visualProgrammingUI/nodes/InferredBeliefNode.test.tsx
@@ -0,0 +1,129 @@
+import { describe, it, expect, jest, beforeEach } from '@jest/globals';
+import type {Node, Edge} from '@xyflow/react';
+import * as FlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
+import {
+ type InferredBelief,
+ InferredBeliefConnectionTarget,
+ InferredBeliefDisconnectionTarget,
+ InferredBeliefReduce,
+} from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.tsx';
+
+// helper functions
+function inferredNode(overrides = {}): Node {
+ return {
+ id: 'i1',
+ type: 'inferred_belief',
+ position: {x: 0, y: 0},
+ data: {
+ inferredBelief: {
+ left: undefined,
+ operator: true,
+ right: undefined,
+ },
+ ...overrides,
+ },
+ } as Node;
+}
+
+describe('InferredBelief connection logic', () => {
+ let getStateSpy: ReturnType;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getStateSpy = jest.spyOn(FlowStore.default, 'getState');
+ });
+
+ it('sets left belief when connected on beliefLeft handle', () => {
+ const node = inferredNode();
+
+ getStateSpy.mockReturnValue({
+ nodes: [{ id: 'b1', type: 'basic_belief' }],
+ edges: [
+ {
+ source: 'b1',
+ target: 'i1',
+ targetHandle: 'beliefLeft',
+ } as Edge,
+ ],
+ } as any);
+
+ InferredBeliefConnectionTarget(node, 'b1');
+
+ expect((node.data.inferredBelief as InferredBelief).left).toBe('b1');
+ expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
+ });
+
+ it('sets right belief when connected on beliefRight handle', () => {
+ const node = inferredNode();
+
+ getStateSpy.mockReturnValue({
+ nodes: [{ id: 'b2', type: 'basic_belief' }],
+ edges: [
+ {
+ source: 'b2',
+ target: 'i1',
+ targetHandle: 'beliefRight',
+ } as Edge,
+ ],
+ } as any);
+
+ InferredBeliefConnectionTarget(node, 'b2');
+
+ expect((node.data.inferredBelief as InferredBelief).right).toBe('b2');
+ });
+
+ it('ignores connections from unsupported node types', () => {
+ const node = inferredNode();
+
+ getStateSpy.mockReturnValue({
+ nodes: [{ id: 'x', type: 'norm' }],
+ edges: [],
+ } as any);
+
+ InferredBeliefConnectionTarget(node, 'x');
+
+ expect((node.data.inferredBelief as InferredBelief).left).toBeUndefined();
+ expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
+ });
+
+ it('clears left or right belief on disconnection', () => {
+ const node = inferredNode({
+ inferredBelief: { left: 'a', right: 'b', operator: true },
+ });
+
+ InferredBeliefDisconnectionTarget(node, 'a');
+ expect((node.data.inferredBelief as InferredBelief).left).toBeUndefined();
+
+ InferredBeliefDisconnectionTarget(node, 'b');
+ expect((node.data.inferredBelief as InferredBelief).right).toBeUndefined();
+ });
+});
+
+describe('InferredBeliefReduce', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('throws if left belief is missing', () => {
+ const node = inferredNode({
+ inferredBelief: { left: 'l', right: 'r', operator: true },
+ });
+
+ expect(() =>
+ InferredBeliefReduce(node, [{ id: 'r' } as Node])
+ ).toThrow('No Left belief found');
+ });
+
+ it('throws if right belief is missing', () => {
+ const node = inferredNode({
+ inferredBelief: { left: 'l', right: 'r', operator: true },
+ });
+
+ expect(() =>
+ InferredBeliefReduce(node, [{ id: 'l' } as Node])
+ ).toThrow('No Right Belief found');
+ });
+
+});
+
+