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