humainflow-web/src/app/utilities/flow-loops.spec.ts

113 lines
3.9 KiB
TypeScript

// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import { findLoopBackEdgeIds, LoopGraph } from './flow-loops';
// The same shapes the server's FlowLoopsTest pins, so the two keep agreeing on which connection
// leads back.
describe('findLoopBackEdgeIds', () => {
const activity = (id: string) => ({ id, routesExclusively: false });
const router = (id: string) => ({ id, routesExclusively: true });
const connect = (id: string, sourceId: string, targetId: string, hasLoopSettings = false) =>
({ id, sourceId, targetId, hasLoopSettings });
it('finds the connection a router goes round by', () => {
const graph: LoopGraph = {
nodes: [activity('source'), activity('draft'), router('check'), activity('end')],
connections: [
connect('in', 'source', 'draft'),
connect('to-check', 'draft', 'check'),
connect('back', 'check', 'draft'),
connect('done', 'check', 'end')
]
};
expect([...findLoopBackEdgeIds(graph)]).toEqual(['back']);
});
it('finds nothing in a flow without cycles', () => {
const graph: LoopGraph = {
nodes: [activity('a'), router('b')],
connections: [connect('ab', 'a', 'b')]
};
expect(findLoopBackEdgeIds(graph).size).toBe(0);
});
it('finds nothing in a cycle no router closes', () => {
const graph: LoopGraph = {
nodes: [activity('a'), activity('b')],
connections: [connect('ab', 'a', 'b'), connect('ba', 'b', 'a')]
};
expect(findLoopBackEdgeIds(graph).size).toBe(0);
});
it('treats a router connected to itself as a loop of one', () => {
const graph: LoopGraph = {
nodes: [activity('source'), router('retry')],
connections: [connect('in', 'source', 'retry'), connect('again', 'retry', 'retry')]
};
expect([...findLoopBackEdgeIds(graph)]).toEqual(['again']);
});
it('is not confused by a branch inside the loop', () => {
const graph: LoopGraph = {
nodes: [activity('source'), activity('draft'), router('triage'), activity('trim'), activity('expand'),
activity('rejoin'), router('check')],
connections: [
connect('in', 'source', 'draft'),
connect('d-t', 'draft', 'triage'),
connect('short', 'triage', 'trim'),
connect('long', 'triage', 'expand'),
connect('t-r', 'trim', 'rejoin'),
connect('e-r', 'expand', 'rejoin'),
connect('r-c', 'rejoin', 'check'),
connect('back', 'check', 'draft')
]
};
expect([...findLoopBackEdgeIds(graph)]).toEqual(['back']);
});
it('picks the connection into where the loop is entered when two routers could close it', () => {
const graph: LoopGraph = {
nodes: [activity('source'), activity('draft'), router('screen'), router('check')],
connections: [
connect('in', 'source', 'draft'),
connect('d-s', 'draft', 'screen'),
connect('pass', 'screen', 'check'),
connect('back', 'check', 'draft')
]
};
expect([...findLoopBackEdgeIds(graph)]).toEqual(['back']);
});
it('lets the author decide when the shape alone cannot', () => {
const ring = (marked: boolean): LoopGraph => ({
nodes: [router('screen'), router('check')],
connections: [connect('pass', 'screen', 'check'), connect('back', 'check', 'screen', marked)]
});
expect(findLoopBackEdgeIds(ring(false)).size).toBe(0);
expect([...findLoopBackEdgeIds(ring(true))]).toEqual(['back']);
});
it('finds none where two loops share nodes', () => {
const graph: LoopGraph = {
nodes: [activity('draft'), router('first'), router('second')],
connections: [
connect('d-f', 'draft', 'first'),
connect('f-again', 'first', 'draft'),
connect('f-next', 'first', 'second'),
connect('s-again', 'second', 'draft')
]
};
expect(findLoopBackEdgeIds(graph).size).toBe(0);
});
});