Merge branch 'feat/add-inferred-belief-node' into 'dev'
feat: added an inferred belief node to the editor See merge request ics/sp/2025/n25b/pepperplus-ui!42
This commit was merged in pull request #42.
This commit is contained in:
@@ -84,7 +84,10 @@
|
||||
filter: drop-shadow(0 0 0.25rem plum);
|
||||
}
|
||||
|
||||
|
||||
.node-inferred_belief {
|
||||
outline: mediumpurple solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem mediumpurple);
|
||||
}
|
||||
|
||||
.draggable-node {
|
||||
padding: 3px 10px;
|
||||
@@ -158,6 +161,14 @@
|
||||
filter: drop-shadow(0 0 0.25rem plum);
|
||||
}
|
||||
|
||||
.draggable-node-inferred_belief {
|
||||
padding: 3px 10px;
|
||||
background-color: canvas;
|
||||
border-radius: 5pt;
|
||||
outline: mediumpurple solid 2pt;
|
||||
filter: drop-shadow(0 0 0.25rem mediumpurple);
|
||||
}
|
||||
|
||||
.planNoIterate {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
|
||||
@@ -164,6 +164,7 @@ function runProgram() {
|
||||
// when the program was sent to the backend successfully:
|
||||
useProgramStore.getState().setProgramState(structuredClone(program));
|
||||
}).catch(() => console.log("Failed to send program to the backend."));
|
||||
console.log(program);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,3 +108,15 @@ export function useHandleRules(
|
||||
return evaluateRules(targetRules, connection, context);
|
||||
};
|
||||
}
|
||||
|
||||
export function validateConnectionWithRules(
|
||||
connection: Connection,
|
||||
context: ConnectionContext
|
||||
): RuleResult {
|
||||
const rules = useFlowStore.getState().getTargetRules(
|
||||
connection.target!,
|
||||
connection.targetHandle!
|
||||
);
|
||||
|
||||
return evaluateRules(rules,connection, context);
|
||||
}
|
||||
@@ -43,3 +43,4 @@ export const noSelfConnections : HandleRule =
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -52,15 +52,24 @@ import TriggerNode, {
|
||||
TriggerTooltip
|
||||
} from "./nodes/TriggerNode";
|
||||
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
|
||||
import InferredBeliefNode, {
|
||||
InferredBeliefConnectionTarget,
|
||||
InferredBeliefConnectionSource,
|
||||
InferredBeliefDisconnectionTarget,
|
||||
InferredBeliefDisconnectionSource,
|
||||
InferredBeliefReduce, InferredBeliefTooltip
|
||||
} from "./nodes/InferredBeliefNode";
|
||||
import { InferredBeliefNodeDefaults } from "./nodes/InferredBeliefNode.default";
|
||||
import BasicBeliefNode, {
|
||||
BasicBeliefConnectionSource,
|
||||
BasicBeliefConnectionTarget,
|
||||
BasicBeliefDisconnectionSource,
|
||||
BasicBeliefDisconnectionTarget,
|
||||
BasicBeliefReduce,
|
||||
BasicBeliefReduce
|
||||
,
|
||||
BasicBeliefTooltip
|
||||
} from "./nodes/BasicBeliefNode";
|
||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
|
||||
} from "./nodes/BasicBeliefNode.tsx";
|
||||
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default.ts";
|
||||
|
||||
/**
|
||||
* Registered node types in the visual programming system.
|
||||
@@ -76,6 +85,7 @@ export const NodeTypes = {
|
||||
goal: GoalNode,
|
||||
trigger: TriggerNode,
|
||||
basic_belief: BasicBeliefNode,
|
||||
inferred_belief: InferredBeliefNode,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,6 +101,7 @@ export const NodeDefaults = {
|
||||
goal: GoalNodeDefaults,
|
||||
trigger: TriggerNodeDefaults,
|
||||
basic_belief: BasicBeliefNodeDefaults,
|
||||
inferred_belief: InferredBeliefNodeDefaults,
|
||||
};
|
||||
|
||||
|
||||
@@ -108,6 +119,7 @@ export const NodeReduces = {
|
||||
goal: GoalReduce,
|
||||
trigger: TriggerReduce,
|
||||
basic_belief: BasicBeliefReduce,
|
||||
inferred_belief: InferredBeliefReduce,
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +138,7 @@ export const NodeConnections = {
|
||||
goal: GoalConnectionTarget,
|
||||
trigger: TriggerConnectionTarget,
|
||||
basic_belief: BasicBeliefConnectionTarget,
|
||||
inferred_belief: InferredBeliefConnectionTarget,
|
||||
},
|
||||
Sources: {
|
||||
start: StartConnectionSource,
|
||||
@@ -134,7 +147,8 @@ export const NodeConnections = {
|
||||
norm: NormConnectionSource,
|
||||
goal: GoalConnectionSource,
|
||||
trigger: TriggerConnectionSource,
|
||||
basic_belief: BasicBeliefConnectionSource
|
||||
basic_belief: BasicBeliefConnectionSource,
|
||||
inferred_belief: InferredBeliefConnectionSource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +167,7 @@ export const NodeDisconnections = {
|
||||
goal: GoalDisconnectionTarget,
|
||||
trigger: TriggerDisconnectionTarget,
|
||||
basic_belief: BasicBeliefDisconnectionTarget,
|
||||
inferred_belief: InferredBeliefDisconnectionTarget,
|
||||
},
|
||||
Sources: {
|
||||
start: StartDisconnectionSource,
|
||||
@@ -162,6 +177,7 @@ export const NodeDisconnections = {
|
||||
goal: GoalDisconnectionSource,
|
||||
trigger: TriggerDisconnectionSource,
|
||||
basic_belief: BasicBeliefDisconnectionSource,
|
||||
inferred_belief: InferredBeliefDisconnectionSource,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -186,6 +202,7 @@ export const NodesInPhase = {
|
||||
end: () => false,
|
||||
phase: () => false,
|
||||
basic_belief: () => false,
|
||||
inferred_belief: () => false,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,4 +216,5 @@ export const NodeTooltips = {
|
||||
goal: GoalTooltip,
|
||||
trigger: TriggerTooltip,
|
||||
basic_belief: BasicBeliefTooltip,
|
||||
inferred_belief: InferredBeliefTooltip,
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type XYPosition,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
|
||||
import type { FlowState } from './VisProgTypes';
|
||||
import {
|
||||
NodeDefaults,
|
||||
@@ -129,7 +130,41 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
* Handles reconnecting an edge between nodes.
|
||||
*/
|
||||
onReconnect: (oldEdge, newConnection) => {
|
||||
get().edgeReconnectSuccessful = true;
|
||||
|
||||
function createContext(
|
||||
source: {id: string, handleId: string},
|
||||
target: {id: string, handleId: string}
|
||||
) : ConnectionContext {
|
||||
const edges = get().edges;
|
||||
const targetConnections = edges.filter(edge => edge.target === target.id && edge.targetHandle === target.handleId).length
|
||||
return {
|
||||
connectionCount: targetConnections,
|
||||
source: source,
|
||||
target: target
|
||||
}
|
||||
}
|
||||
|
||||
// connection validation
|
||||
const context: ConnectionContext = oldEdge.source === newConnection.source
|
||||
? createContext({id: newConnection.source, handleId: newConnection.sourceHandle!}, {id: newConnection.target, handleId: newConnection.targetHandle!})
|
||||
: createContext({id: newConnection.target, handleId: newConnection.targetHandle!}, {id: newConnection.source, handleId: newConnection.sourceHandle!});
|
||||
|
||||
const result = validateConnectionWithRules(
|
||||
newConnection,
|
||||
context
|
||||
);
|
||||
|
||||
if (!result.isSatisfied) {
|
||||
set({
|
||||
edges: get().edges.map(e =>
|
||||
e.id === oldEdge.id ? oldEdge : e
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// further reconnect logic
|
||||
set({ edgeReconnectSuccessful: true });
|
||||
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||
|
||||
// We make sure to perform any required data updates on the newly reconnected nodes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BasicBeliefNodeData } from "./BasicBeliefNode";
|
||||
import type { BasicBeliefNodeData } from "./BasicBeliefNode.tsx";
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
Position,
|
||||
type Node,
|
||||
} from '@xyflow/react';
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import { Toolbar } from '../components/NodeComponents.tsx';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { TextField } from '../../../../components/TextField';
|
||||
import { MultilineTextField } from '../../../../components/MultilineTextField';
|
||||
import useFlowStore from '../VisProgStores.tsx';
|
||||
import { TextField } from '../../../../components/TextField.tsx';
|
||||
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
|
||||
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
||||
|
||||
/**
|
||||
* The default data structure for a BasicBelief node
|
||||
@@ -31,7 +32,7 @@ export type BasicBeliefNodeData = {
|
||||
};
|
||||
|
||||
// These are all the types a basic belief could be.
|
||||
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||
export type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
|
||||
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
|
||||
type Semantic = { type: "semantic", id: string, value: string, description: string, label: "Detected with LLM:"};
|
||||
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
|
||||
@@ -189,7 +190,8 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
|
||||
</div>
|
||||
)}
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
|
||||
noMatchingLeftRightBelief,
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"}]),
|
||||
]}/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {getOutgoers, type Node} from '@xyflow/react';
|
||||
import {type HandleRule, type RuleResult, ruleResult} from "../HandleRuleLogic.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import {BasicBeliefReduce} from "./BasicBeliefNode.tsx";
|
||||
import {type InferredBeliefNodeData, InferredBeliefReduce} from "./InferredBeliefNode.tsx";
|
||||
|
||||
export function BeliefGlobalReduce(beliefNode: Node, nodes: Node[]) {
|
||||
switch (beliefNode.type) {
|
||||
case 'basic_belief':
|
||||
return BasicBeliefReduce(beliefNode, nodes);
|
||||
case 'inferred_belief':
|
||||
return InferredBeliefReduce(beliefNode, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export const noMatchingLeftRightBelief : HandleRule = (connection, _)=> {
|
||||
const { nodes } = useFlowStore.getState();
|
||||
const thisNode = nodes.find(node => node.id === connection.target && node.type === 'inferred_belief');
|
||||
if (!thisNode) return ruleResult.satisfied;
|
||||
|
||||
const iBelief = (thisNode.data as InferredBeliefNodeData).inferredBelief;
|
||||
return (iBelief.left === connection.source || iBelief.right === connection.source)
|
||||
? ruleResult.notSatisfied("Connecting one belief to both input handles of an inferred belief node is not allowed")
|
||||
: ruleResult.satisfied;
|
||||
}
|
||||
/**
|
||||
* makes it impossible to connect Inferred belief nodes
|
||||
* if the connection would create a cyclical connection between inferred beliefs
|
||||
*/
|
||||
export const noBeliefCycles: HandleRule = (connection, _): RuleResult => {
|
||||
const {nodes, edges} = useFlowStore.getState();
|
||||
const defaultErrorMessage = "Cyclical connection exists between inferred beliefs";
|
||||
|
||||
/**
|
||||
* recursively checks for cyclical connections between InferredBelief nodes
|
||||
*
|
||||
* to check for a cycle provide the source of an attempted connection as the targetNode for the cycle check,
|
||||
* the currentNodeId should be initialised with the id of the targetNode of the attempted connection.
|
||||
*
|
||||
* @param {string} targetNodeId - the id of the node we are looking for as the endpoint of a cyclical connection
|
||||
* @param {string} currentNodeId - the id of the node we are checking for outgoing connections to the provided target node
|
||||
* @returns {RuleResult}
|
||||
*/
|
||||
function checkForCycle(targetNodeId: string, currentNodeId: string): RuleResult {
|
||||
const outgoingBeliefs = getOutgoers({id: currentNodeId}, nodes, edges)
|
||||
.filter(node => node.type === 'inferred_belief');
|
||||
|
||||
if (outgoingBeliefs.length === 0) return ruleResult.satisfied;
|
||||
if (outgoingBeliefs.some(node => node.id === targetNodeId)) return ruleResult
|
||||
.notSatisfied(defaultErrorMessage);
|
||||
|
||||
const next = outgoingBeliefs.map(node => checkForCycle(targetNodeId, node.id))
|
||||
.find(result => !result.isSatisfied);
|
||||
|
||||
return next
|
||||
? next
|
||||
: ruleResult.satisfied;
|
||||
}
|
||||
|
||||
return connection.source === connection.target
|
||||
? ruleResult.notSatisfied(defaultErrorMessage)
|
||||
: checkForCycle(connection.source, connection.target);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
|
||||
|
||||
|
||||
/**
|
||||
* Default data for this node
|
||||
*/
|
||||
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
|
||||
label: "AND/OR",
|
||||
droppable: true,
|
||||
inferredBelief: {
|
||||
left: undefined,
|
||||
operator: true,
|
||||
right: undefined
|
||||
},
|
||||
hasReduce: true,
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
.operator-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
/* Change this font-size to scale the whole component */
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* hide the default checkbox */
|
||||
.operator-switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The Track */
|
||||
.switch-visual {
|
||||
position: relative;
|
||||
/* height is now 3x the font size */
|
||||
height: 3em;
|
||||
aspect-ratio: 1 / 2;
|
||||
background-color: ButtonFace;
|
||||
border-radius: 2em;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
/* The Knob */
|
||||
.switch-visual::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.1em;
|
||||
left: 0.1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background: Canvas;
|
||||
border: 0.175em solid mediumpurple;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease-in-out, border-color 0.2s;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.switch-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 3em; /* Matches the track height */
|
||||
font-weight: 800;
|
||||
color: Canvas;
|
||||
line-height: 1.4;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
.operator-switch input:checked + .switch-visual::after {
|
||||
/* Moves the slider down */
|
||||
transform: translateY(1.4em);
|
||||
}
|
||||
|
||||
/*change the colours to highlight the selected operator*/
|
||||
.operator-switch input:checked ~ .switch-labels{
|
||||
:first-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: ButtonFace;
|
||||
}
|
||||
:last-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: mediumpurple;
|
||||
}
|
||||
}
|
||||
|
||||
.operator-switch input:not(:checked) ~ .switch-labels{
|
||||
:first-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: mediumpurple;
|
||||
}
|
||||
:last-child {
|
||||
transition: ease-in-out color 0.2s;
|
||||
color: ButtonFace;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import {getConnectedEdges, type Node, type NodeProps, Position} from '@xyflow/react';
|
||||
import {useState} from "react";
|
||||
import styles from '../../VisProg.module.css';
|
||||
import {Toolbar} from '../components/NodeComponents.tsx';
|
||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from "../VisProgStores.tsx";
|
||||
import {BeliefGlobalReduce, noBeliefCycles, noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
|
||||
import switchStyles from './InferredBeliefNode.module.css';
|
||||
|
||||
|
||||
/**
|
||||
* The default data structure for an InferredBelief node
|
||||
*/
|
||||
export type InferredBeliefNodeData = {
|
||||
label: string;
|
||||
droppable: boolean;
|
||||
inferredBelief: InferredBelief;
|
||||
hasReduce: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* stores a boolean to represent the operator
|
||||
* and a left and right BeliefNode (can be both an inferred and a basic belief)
|
||||
* in the form of their corresponding id's
|
||||
*/
|
||||
export type InferredBelief = {
|
||||
left: string | undefined,
|
||||
operator: boolean,
|
||||
right: string | undefined,
|
||||
}
|
||||
|
||||
export type InferredBeliefNode = Node<InferredBeliefNodeData>;
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type as the target
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _sourceNodeId the source of the received connection
|
||||
*/
|
||||
export function InferredBeliefConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as InferredBeliefNodeData;
|
||||
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId
|
||||
&& ['basic_belief', 'inferred_belief'].includes(node.type!)))
|
||||
) {
|
||||
const connectedEdges = getConnectedEdges([_thisNode], useFlowStore.getState().edges);
|
||||
switch(connectedEdges.find(edge => edge.source === _sourceNodeId)?.targetHandle){
|
||||
case 'beliefLeft': data.inferredBelief.left = _sourceNodeId; break;
|
||||
case 'beliefRight': data.inferredBelief.right = _sourceNodeId; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is made with this node type as the source
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _targetNodeId the target of the created connection
|
||||
*/
|
||||
export function InferredBeliefConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is disconnected with this node type as the target
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _sourceNodeId the source of the disconnected connection
|
||||
*/
|
||||
export function InferredBeliefDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as InferredBeliefNodeData;
|
||||
|
||||
if (_sourceNodeId === data.inferredBelief.left) data.inferredBelief.left = undefined;
|
||||
if (_sourceNodeId === data.inferredBelief.right) data.inferredBelief.right = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called whenever a connection is disconnected with this node type as the source
|
||||
* @param _thisNode the node of this node type which function is called
|
||||
* @param _targetNodeId the target of the diconnected connection
|
||||
*/
|
||||
export function InferredBeliefDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
}
|
||||
|
||||
export const InferredBeliefTooltip = `
|
||||
Combines two beliefs into a single belief using logical inference,
|
||||
the node can be toggled between using "AND" and "OR" mode for inference`;
|
||||
/**
|
||||
* Defines how an InferredBelief node should be rendered
|
||||
* @param {NodeProps<InferredBeliefNode>} props - Node properties provided by React Flow, including `id` and `data`.
|
||||
* @returns The rendered InferredBeliefNode React element. (React.JSX.Element)
|
||||
*/
|
||||
export default function InferredBeliefNode(props: NodeProps<InferredBeliefNode>) {
|
||||
const data = props.data;
|
||||
const { updateNodeData } = useFlowStore();
|
||||
// start of as an AND operator, true: "AND", false: "OR"
|
||||
const [enforceAllBeliefs, setEnforceAllBeliefs] = useState(true);
|
||||
|
||||
// used to toggle operator
|
||||
function onToggle() {
|
||||
const newOperator = !enforceAllBeliefs; // compute the new value
|
||||
setEnforceAllBeliefs(newOperator);
|
||||
|
||||
updateNodeData(props.id, {
|
||||
...data,
|
||||
inferredBelief: {
|
||||
...data.inferredBelief,
|
||||
operator: enforceAllBeliefs,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||
<div className={`${styles.defaultNode} ${styles.nodeInferredBelief}`}>
|
||||
{/* The checkbox used to toggle the operator between 'AND' and 'OR' */}
|
||||
<label className={switchStyles.operatorSwitch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={data.inferredBelief.operator}
|
||||
onChange={onToggle}
|
||||
/>
|
||||
<div className={switchStyles.switchVisual}></div>
|
||||
<div className={switchStyles.switchLabels}>
|
||||
<span title={"Belief is fulfilled if either of the supplied beliefs is true"}>OR</span>
|
||||
<span title={"Belief is fulfilled if all of the supplied beliefs are true"}>AND</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
{/* outgoing connections */}
|
||||
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
|
||||
allowOnlyConnectionsFromType(["norm", "trigger"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
|
||||
{/* incoming connections */}
|
||||
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '30%'}} id="beliefLeft" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
<SingleConnectionHandle type="target" position={Position.Left} style={{top: '70%'}} id="beliefRight" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"]),
|
||||
noBeliefCycles,
|
||||
noMatchingLeftRightBelief
|
||||
]}/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces each BasicBelief, including its children down into its core data.
|
||||
* @param {Node} node - The BasicBelief node to reduce.
|
||||
* @param {Node[]} nodes - The list of all nodes in the current flow graph.
|
||||
* @returns A simplified object containing the node label and its list of BasicBeliefs.
|
||||
*/
|
||||
export function InferredBeliefReduce(node: Node, nodes: Node[]) {
|
||||
const data = node.data as InferredBeliefNodeData;
|
||||
const leftBelief = nodes.find((node) => node.id === data.inferredBelief.left);
|
||||
const rightBelief = nodes.find((node) => node.id === data.inferredBelief.right);
|
||||
|
||||
if (!leftBelief) { throw new Error("No Left belief found")}
|
||||
if (!rightBelief) { throw new Error("No Right Belief found")}
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
id: node.id,
|
||||
left: BeliefGlobalReduce(leftBelief, nodes),
|
||||
operator: data.inferredBelief.operator ? "AND" : "OR",
|
||||
right: BeliefGlobalReduce(rightBelief, nodes),
|
||||
};
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { TextField } from '../../../../components/TextField';
|
||||
import {MultiConnectionHandle, SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||
import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts";
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
||||
|
||||
/**
|
||||
* The default data dot a phase node
|
||||
@@ -81,7 +81,7 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
allowOnlyConnectionsFromHandle([{nodeType:"phase",handleId:"data"}])
|
||||
]}/>
|
||||
<SingleConnectionHandle type="target" position={Position.Bottom} id="NormBeliefs" rules={[
|
||||
allowOnlyConnectionsFromType(["basic_belief"])
|
||||
allowOnlyConnectionsFromType(["basic_belief", "inferred_belief"])
|
||||
]}/>
|
||||
</div>
|
||||
</>;
|
||||
@@ -105,11 +105,10 @@ export function NormReduce(node: Node, nodes: Node[]) {
|
||||
};
|
||||
|
||||
if (data.condition) {
|
||||
const reducer = BasicBeliefReduce; // TODO: also add inferred.
|
||||
const conditionNode = nodes.find((node) => node.id === data.condition);
|
||||
// In case something went wrong, and our condition doesn't actually exist;
|
||||
if (conditionNode == undefined) return result;
|
||||
result["condition"] = reducer(conditionNode, nodes)
|
||||
result["condition"] = BeliefGlobalReduce(conditionNode, nodes)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -126,7 +125,7 @@ export const NormTooltip = `
|
||||
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const data = _thisNode.data as NormNodeData;
|
||||
// If we got a belief connected, this is the condition for the norm.
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && ['basic_belief', 'inferred_belief'].includes(node.type!)))) {
|
||||
data.condition = _sourceNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../H
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import {PlanReduce, type Plan } from '../components/Plan';
|
||||
import PlanEditorDialog from '../components/PlanEditor';
|
||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||
import {BeliefGlobalReduce} from "./BeliefGlobals.ts";
|
||||
import type { GoalNode } from './GoalNode.tsx';
|
||||
import { defaultPlan } from '../components/Plan.default.ts';
|
||||
import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx';
|
||||
@@ -72,7 +72,7 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
id="TriggerBeliefs"
|
||||
style={{ left: '40%' }}
|
||||
rules={[
|
||||
allowOnlyConnectionsFromType(['basic_belief']),
|
||||
allowOnlyConnectionsFromType(["basic_belief","inferred_belief"]),
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -102,13 +102,13 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
|
||||
/**
|
||||
* 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,
|
||||
@@ -136,7 +136,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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