Refuse workflows that move cards in a loop
Build package / xml-lint (push) Successful in 1m4s
Build package / php-lint (8.5) (push) Successful in 4m0s
Build package / unit-tests (push) Successful in 4m13s
Build package / php-lint (8.4) (push) Successful in 4m34s
Build package / package (push) Successful in 1m38s

Every enabled workflow is an edge "source stack -> target stack";
LoopDetector finds circles in that graph, a workflow whose source is its
own target being the shortest. Without a check, two workflows A -> B and
B -> A move the same cards back and forth on every run, even within a
single run, with a notice for each move. The trigger date is ignored on
purpose: by start date one way and by due date the other, a card with
both dates passed still circles. Chains stay allowed.

The controller rejects saving an enabled workflow that would close a loop
and names the workflows involved. Workflows stored before this check are
skipped by the runner (logged, not disabled) and flagged in the settings
list via a new `loop` field from index().
This commit is contained in:
Patrick Niebeling committed 2026-10-01 13:47:32 +02:00
1 parent 8268f1068d
commit 9e1a15bab0
8 files changed
+251 -5

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Tests\Unit\Service;
use OCA\WorkflowDeckAutomation\Service\LoopDetector;
use PHPUnit\Framework\TestCase;
class LoopDetectorTest extends TestCase {
public function testNoWorkflowsHaveNoLoop(): void {
$this->assertSame([], LoopDetector::findLoopingWorkflows([]));
}
public function testSameSourceAndTargetIsALoop(): void {
$this->assertSame([1], LoopDetector::findLoopingWorkflows([1 => [10, 10]]));
}
public function testTwoWorkflowsBackAndForthAreALoop(): void {
$this->assertSame([1, 2], LoopDetector::findLoopingWorkflows([
1 => [10, 20],
2 => [20, 10],
]));
}
public function testLongerCircleIsALoop(): void {
$this->assertSame([1, 2, 3], LoopDetector::findLoopingWorkflows([
1 => [10, 20],
2 => [20, 30],
3 => [30, 10],
]));
}
public function testChainIsNoLoop(): void {
$this->assertSame([], LoopDetector::findLoopingWorkflows([
1 => [10, 20],
2 => [20, 30],
]));
}
public function testOnlyWorkflowsOnTheCircleAreReported(): void {
// 1 feeds into the circle 2 → 3 → 2 and 4 leads out of it; neither
// lies on it.
$this->assertSame([2, 3], LoopDetector::findLoopingWorkflows([
1 => [10, 20],
2 => [20, 30],
3 => [30, 20],
4 => [30, 40],
]));
}
public function testTwoWorkflowsFromTheSameSourceAreNoLoop(): void {
$this->assertSame([], LoopDetector::findLoopingWorkflows([
1 => [10, 20],
2 => [10, 30],
]));
}
public function testFindPathListsTheWorkflowsInOrder(): void {
$edges = [
7 => [30, 10],
5 => [10, 20],
6 => [20, 30],
];
$this->assertSame([5, 6], LoopDetector::findPath($edges, 10, 30));
$this->assertSame([6, 7], LoopDetector::findPath($edges, 20, 10));
}
public function testFindPathWithoutConnectionIsNull(): void {
$this->assertNull(LoopDetector::findPath([1 => [10, 20]], 20, 10));
}
public function testFindPathToItselfIsEmpty(): void {
$this->assertSame([], LoopDetector::findPath([], 10, 10));
}
}