feat: added an inferred belief node to the editor #42
@@ -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<typeof jest.spyOn>;
|
||||||
|
|
||||||
|
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<string, any[]> = {
|
||||||
|
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<string, any[]> = {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<typeof jest.spyOn>;
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user