feat: pin running instances to immutable published definition versions
Replace register/replace with publish so new graphs can ship without rewriting old ones, and keep in-flight work on the version it started with. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,8 +22,6 @@ import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.TaskAction;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.TransactionExecutor;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
|
||||
@@ -102,26 +100,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void register(ProcessDefinition definition) {
|
||||
public synchronized ProcessDefinition publish(ProcessDefinition definition) {
|
||||
Objects.requireNonNull(definition, "definition must not be null");
|
||||
transactionExecutor.execute(() -> {
|
||||
if (!definitionRepository.insertIfAbsent(definition)) {
|
||||
throw new DefinitionAlreadyExistsException(definition.id());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void replace(ProcessDefinition definition) {
|
||||
Objects.requireNonNull(definition, "definition must not be null");
|
||||
transactionExecutor.execute(() -> {
|
||||
if (instanceRepository.existsRunning(definition.id())) {
|
||||
throw new DefinitionInUseException(definition.id());
|
||||
}
|
||||
definitionRepository.upsert(definition);
|
||||
return null;
|
||||
});
|
||||
return transactionExecutor.execute(() -> definitionRepository.publish(definition));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -131,9 +112,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
List<PendingAction> queued = new ArrayList<>();
|
||||
List<ProcessEvent> events = new ArrayList<>();
|
||||
ProcessInstance instance = transactionExecutor.execute(() -> {
|
||||
ProcessDefinition definition = requireDefinition(definitionId);
|
||||
ProcessDefinition definition = requireLatestDefinition(definitionId);
|
||||
Instant now = clock.instant();
|
||||
ProcessInstance started = new ProcessInstance(nextId(), definition.id(), initiator,
|
||||
ProcessInstance started = new ProcessInstance(nextId(), definition.id(), definition.version(), initiator,
|
||||
ProcessStatus.RUNNING, now, null, context);
|
||||
instanceRepository.insert(started);
|
||||
record(events, started.id(), null, null, ProcessEventType.INSTANCE_STARTED, initiator, null, now);
|
||||
@@ -232,8 +213,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
throw new InstanceAlreadyCompletedException(instanceId);
|
||||
}
|
||||
Instant now = clock.instant();
|
||||
ProcessInstance finished = new ProcessInstance(instance.id(), instance.definitionId(), instance.initiator(),
|
||||
status, instance.startedAt(), now, instance.context());
|
||||
ProcessInstance finished = new ProcessInstance(instance.id(), instance.definitionId(),
|
||||
instance.definitionVersion(), instance.initiator(), status, instance.startedAt(), now,
|
||||
instance.context());
|
||||
if (!instanceRepository.completeIfRunning(finished)) {
|
||||
throw new InstanceAlreadyCompletedException(instanceId);
|
||||
}
|
||||
@@ -295,12 +277,34 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
return instanceRepository.query(query, pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<ProcessDefinition> findDefinition(String definitionId) {
|
||||
requireText(definitionId, "definition id");
|
||||
return definitionRepository.findLatest(definitionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<ProcessDefinition> findDefinition(String definitionId, int version) {
|
||||
requireText(definitionId, "definition id");
|
||||
if (version < 1) {
|
||||
throw new IllegalArgumentException("definition version must be positive");
|
||||
}
|
||||
return definitionRepository.find(definitionId, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Page<ProcessDefinition> queryDefinitions(PageRequest pageRequest) {
|
||||
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
|
||||
return definitionRepository.findAll(pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Page<ProcessDefinition> queryDefinitionVersions(String definitionId, PageRequest pageRequest) {
|
||||
requireText(definitionId, "definition id");
|
||||
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
|
||||
return definitionRepository.findVersions(definitionId, pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Page<ProcessEvent> queryHistory(String instanceId, PageRequest pageRequest) {
|
||||
requireText(instanceId, "instance id");
|
||||
@@ -345,7 +349,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
if (instance.status() != ProcessStatus.RUNNING) {
|
||||
return false;
|
||||
}
|
||||
ProcessDefinition definition = requireDefinition(instance.definitionId());
|
||||
ProcessDefinition definition = requireDefinition(instance);
|
||||
ApprovalStep step = requireStep(definition, overdue.stepId());
|
||||
StepDue due = step.due();
|
||||
if (due == null) {
|
||||
@@ -454,7 +458,7 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
private void advanceAfterDecision(ApprovalTask completedTask, Instant now, List<PendingAction> queued,
|
||||
List<ProcessEvent> events) {
|
||||
ProcessInstance instance = requireInstance(completedTask.instanceId());
|
||||
ProcessDefinition definition = requireDefinition(instance.definitionId());
|
||||
ProcessDefinition definition = requireDefinition(instance);
|
||||
ApprovalStep step = requireStep(definition, completedTask.stepId());
|
||||
List<ApprovalTask> siblings = taskRepository.findByInstanceIdAndStepId(instance.id(), step.id());
|
||||
|
||||
@@ -600,8 +604,9 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
|
||||
private void completeInstance(ProcessInstance instance, ProcessStatus status, Instant now,
|
||||
List<ProcessEvent> events) {
|
||||
ProcessInstance completed = new ProcessInstance(instance.id(), instance.definitionId(), instance.initiator(),
|
||||
status, instance.startedAt(), now, instance.context());
|
||||
ProcessInstance completed = new ProcessInstance(instance.id(), instance.definitionId(),
|
||||
instance.definitionVersion(), instance.initiator(), status, instance.startedAt(), now,
|
||||
instance.context());
|
||||
if (!instanceRepository.completeIfRunning(completed)) {
|
||||
throw new InstanceAlreadyCompletedException(instance.id());
|
||||
}
|
||||
@@ -610,11 +615,17 @@ public final class DefaultOrdoEngine implements OrdoEngine {
|
||||
record(events, instance.id(), null, null, type, null, null, now);
|
||||
}
|
||||
|
||||
private ProcessDefinition requireDefinition(String definitionId) {
|
||||
return definitionRepository.findById(definitionId)
|
||||
private ProcessDefinition requireLatestDefinition(String definitionId) {
|
||||
return definitionRepository.findLatest(definitionId)
|
||||
.orElseThrow(() -> new DefinitionNotFoundException(definitionId));
|
||||
}
|
||||
|
||||
private ProcessDefinition requireDefinition(ProcessInstance instance) {
|
||||
return definitionRepository.find(instance.definitionId(), instance.definitionVersion())
|
||||
.orElseThrow(() -> new DefinitionNotFoundException(instance.definitionId(),
|
||||
instance.definitionVersion()));
|
||||
}
|
||||
|
||||
private ProcessInstance requireInstance(String instanceId) {
|
||||
return instanceRepository.findById(instanceId)
|
||||
.orElseThrow(() -> new IllegalStateException("instance not found: " + instanceId));
|
||||
|
||||
@@ -76,13 +76,8 @@ public final class InMemoryOrdoEngine implements OrdoEngine {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ProcessDefinition definition) {
|
||||
delegate.register(definition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replace(ProcessDefinition definition) {
|
||||
delegate.replace(definition);
|
||||
public ProcessDefinition publish(ProcessDefinition definition) {
|
||||
return delegate.publish(definition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,11 +145,26 @@ public final class InMemoryOrdoEngine implements OrdoEngine {
|
||||
return delegate.queryInstances(query, pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProcessDefinition> findDefinition(String definitionId) {
|
||||
return delegate.findDefinition(definitionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProcessDefinition> findDefinition(String definitionId, int version) {
|
||||
return delegate.findDefinition(definitionId, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ProcessDefinition> queryDefinitions(PageRequest pageRequest) {
|
||||
return delegate.queryDefinitions(pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ProcessDefinition> queryDefinitionVersions(String definitionId, PageRequest pageRequest) {
|
||||
return delegate.queryDefinitionVersions(definitionId, pageRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ProcessEvent> queryHistory(String instanceId, PageRequest pageRequest) {
|
||||
return delegate.queryHistory(instanceId, pageRequest);
|
||||
|
||||
+49
-9
@@ -5,35 +5,63 @@ import com.jetlumen.ordo.api.query.Page;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Development-only in-memory implementation of the definition storage port. */
|
||||
public final class InMemoryProcessDefinitionRepository implements ProcessDefinitionRepository {
|
||||
private final Map<String, ProcessDefinition> definitions = new HashMap<>();
|
||||
private final Map<String, Integer> latestVersions = new HashMap<>();
|
||||
private final Map<String, Map<Integer, ProcessDefinition>> versions = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public synchronized boolean insertIfAbsent(ProcessDefinition definition) {
|
||||
return definitions.putIfAbsent(definition.id(), definition) == null;
|
||||
public synchronized ProcessDefinition publish(ProcessDefinition definition) {
|
||||
Objects.requireNonNull(definition, "definition must not be null");
|
||||
Integer current = latestVersions.get(definition.id());
|
||||
if (current == null) {
|
||||
ProcessDefinition first = definition.withVersion(1);
|
||||
versions.computeIfAbsent(definition.id(), id -> new HashMap<>()).put(1, first);
|
||||
latestVersions.put(definition.id(), 1);
|
||||
return first;
|
||||
}
|
||||
ProcessDefinition latest = versions.get(definition.id()).get(current);
|
||||
if (latest.sameGraph(definition)) {
|
||||
return latest;
|
||||
}
|
||||
int next = current + 1;
|
||||
ProcessDefinition published = definition.withVersion(next);
|
||||
versions.get(definition.id()).put(next, published);
|
||||
latestVersions.put(definition.id(), next);
|
||||
return published;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void upsert(ProcessDefinition definition) {
|
||||
definitions.put(definition.id(), definition);
|
||||
public synchronized Optional<ProcessDefinition> findLatest(String definitionId) {
|
||||
Integer version = latestVersions.get(definitionId);
|
||||
if (version == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(versions.get(definitionId).get(version));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Optional<ProcessDefinition> findById(String definitionId) {
|
||||
return Optional.ofNullable(definitions.get(definitionId));
|
||||
public synchronized Optional<ProcessDefinition> find(String definitionId, int version) {
|
||||
Map<Integer, ProcessDefinition> byVersion = versions.get(definitionId);
|
||||
if (byVersion == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(byVersion.get(version));
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Page<ProcessDefinition> findAll(PageRequest pageRequest) {
|
||||
List<ProcessDefinition> sorted = definitions.values().stream()
|
||||
.sorted(Comparator.comparing(ProcessDefinition::id))
|
||||
List<ProcessDefinition> sorted = latestVersions.keySet().stream()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.map(id -> versions.get(id).get(latestVersions.get(id)))
|
||||
.toList();
|
||||
List<ProcessDefinition> page = sorted.stream()
|
||||
.skip((long) pageRequest.offset())
|
||||
@@ -41,4 +69,16 @@ public final class InMemoryProcessDefinitionRepository implements ProcessDefinit
|
||||
.toList();
|
||||
return new Page<>(page, sorted.size(), pageRequest.page(), pageRequest.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Page<ProcessDefinition> findVersions(String definitionId, PageRequest pageRequest) {
|
||||
Map<Integer, ProcessDefinition> byVersion = versions.getOrDefault(definitionId, Map.of());
|
||||
List<ProcessDefinition> sorted = new ArrayList<>(byVersion.values());
|
||||
sorted.sort(Comparator.comparingInt(ProcessDefinition::version).reversed());
|
||||
List<ProcessDefinition> page = sorted.stream()
|
||||
.skip((long) pageRequest.offset())
|
||||
.limit(pageRequest.size())
|
||||
.toList();
|
||||
return new Page<>(page, sorted.size(), pageRequest.page(), pageRequest.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ import com.jetlumen.ordo.api.StepDue;
|
||||
import com.jetlumen.ordo.api.StepKind;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.TaskStatus;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionAlreadyExistsException;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
|
||||
import com.jetlumen.ordo.api.exception.DefinitionNotFoundException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
|
||||
import com.jetlumen.ordo.api.exception.InstanceNotFoundException;
|
||||
@@ -53,7 +51,7 @@ class InMemoryOrdoEngineTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
engine = new InMemoryOrdoEngine();
|
||||
engine.register(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
engine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
@@ -136,7 +134,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void reassignDoesNotAdvanceAnAllPolicyStepAndRejectsDuplicatePendingAssignee() {
|
||||
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
|
||||
allEngine.register(ProcessDefinition.linear("leave-all-reassign", "Leave request", List.of(
|
||||
allEngine.publish(ProcessDefinition.linear("leave-all-reassign", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL)
|
||||
)));
|
||||
var instance = allEngine.start("leave-all-reassign", "alice");
|
||||
@@ -157,51 +155,56 @@ class InMemoryOrdoEngineTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesSpecificExceptionsForMissingAndDuplicateResources() {
|
||||
void exposesSpecificExceptionsForMissingResources() {
|
||||
assertThrows(DefinitionNotFoundException.class, () -> engine.start("missing", "alice"));
|
||||
assertThrows(TaskNotFoundException.class, () -> engine.approve("missing", "maria"));
|
||||
assertThrows(DefinitionAlreadyExistsException.class, () -> engine.register(ProcessDefinition.linear(
|
||||
"leave", "Another leave request", List.of(ApprovalStep.single("lead", "Lead approval", "lee")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceInsertsWhenTheDefinitionIsMissing() {
|
||||
void publishInsertsVersionOneWhenTheDefinitionIsMissing() {
|
||||
InMemoryOrdoEngine empty = new InMemoryOrdoEngine();
|
||||
empty.replace(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
ProcessDefinition published = empty.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
ApprovalStep.single("director", "Director approval", "diana"))));
|
||||
|
||||
assertEquals(1, published.version());
|
||||
var instance = empty.start("expense", "alice");
|
||||
assertEquals(1, instance.definitionVersion());
|
||||
assertEquals("diana", empty.findTasks(instance.id()).get(0).assignee());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceSwapsTheGraphWhenNoInstanceIsRunning() {
|
||||
engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of(
|
||||
ApprovalStep.single("director", "Director approval", "diana"))));
|
||||
|
||||
var instance = engine.start("leave", "alice");
|
||||
ApprovalTask task = engine.findTasks(instance.id()).get(0);
|
||||
assertEquals("director", task.stepId());
|
||||
assertEquals("diana", task.assignee());
|
||||
void publishIsIdempotentWhenTheGraphIsUnchanged() {
|
||||
ProcessDefinition first = engine.findDefinition("leave").orElseThrow();
|
||||
ProcessDefinition second = engine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
assertEquals(first, second);
|
||||
assertEquals(1, engine.queryDefinitionVersions("leave", new PageRequest(0, 10)).totalElements());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceIsRejectedWhileAnInstanceIsRunning() {
|
||||
engine.start("leave", "alice");
|
||||
assertThrows(DefinitionInUseException.class, () -> engine.replace(ProcessDefinition.linear(
|
||||
"leave", "Leave request v2", List.of(ApprovalStep.single("director", "Director approval", "diana")))));
|
||||
}
|
||||
void publishCreatesANewVersionAndLocksRunningInstancesToTheOldGraph() {
|
||||
var running = engine.start("leave", "alice");
|
||||
assertEquals(1, running.definitionVersion());
|
||||
|
||||
@Test
|
||||
void replaceSucceedsAfterInstancesReachATerminalStatus() {
|
||||
var instance = engine.start("leave", "alice");
|
||||
engine.reject(engine.findTasks(instance.id()).get(0).id(), "maria");
|
||||
|
||||
engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of(
|
||||
engine.publish(ProcessDefinition.linear("leave", "Leave request v2", List.of(
|
||||
ApprovalStep.single("director", "Director approval", "diana"))));
|
||||
|
||||
assertEquals(2, engine.findDefinition("leave").orElseThrow().version());
|
||||
assertEquals("maria", engine.findTasks(running.id()).get(0).assignee());
|
||||
engine.approve(engine.findTasks(running.id()).get(0).id(), "maria");
|
||||
assertEquals("hr", engine.findPendingTasksByInstanceId(running.id()).get(0).stepId());
|
||||
|
||||
var next = engine.start("leave", "bob");
|
||||
assertEquals(2, next.definitionVersion());
|
||||
assertEquals("diana", engine.findTasks(next.id()).get(0).assignee());
|
||||
|
||||
Page<ProcessDefinition> latest = engine.queryDefinitions(new PageRequest(0, 10));
|
||||
assertEquals(List.of("leave"), latest.content().stream().map(ProcessDefinition::id).toList());
|
||||
assertEquals(List.of(2), latest.content().stream().map(ProcessDefinition::version).toList());
|
||||
Page<ProcessDefinition> versions = engine.queryDefinitionVersions("leave", new PageRequest(0, 10));
|
||||
assertEquals(List.of(2, 1), versions.content().stream().map(ProcessDefinition::version).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -352,7 +355,7 @@ class InMemoryOrdoEngineTest {
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.orElse(candidate));
|
||||
contextAwareEngine.register(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
contextAwareEngine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
@@ -372,7 +375,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void anyPolicyAdvancesOnFirstApprovalAndSkipsTheOtherCandidates() {
|
||||
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
|
||||
anyEngine.register(ProcessDefinition.linear("leave-any", "Leave request", List.of(
|
||||
anyEngine.publish(ProcessDefinition.linear("leave-any", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
@@ -401,7 +404,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void anyPolicyOnlyRejectsTheStepOnceEveryCandidateHasRejected() {
|
||||
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
|
||||
anyEngine.register(ProcessDefinition.linear("leave-any-reject", "Leave request", List.of(
|
||||
anyEngine.publish(ProcessDefinition.linear("leave-any-reject", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY)
|
||||
)));
|
||||
|
||||
@@ -421,7 +424,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void allPolicyOnlyAdvancesOnceEveryCandidateHasApproved() {
|
||||
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
|
||||
allEngine.register(ProcessDefinition.linear("leave-all", "Leave request", List.of(
|
||||
allEngine.publish(ProcessDefinition.linear("leave-all", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
@@ -446,7 +449,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void allPolicyFailsFastAndSkipsRemainingCandidatesOnASingleRejection() {
|
||||
InMemoryOrdoEngine allEngine = new InMemoryOrdoEngine();
|
||||
allEngine.register(ProcessDefinition.linear("leave-all-reject", "Leave request", List.of(
|
||||
allEngine.publish(ProcessDefinition.linear("leave-all-reject", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ALL)
|
||||
)));
|
||||
|
||||
@@ -478,7 +481,7 @@ class InMemoryOrdoEngineTest {
|
||||
List<ApprovalStep> steps = List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.single("director", "Director approval", "diana"));
|
||||
routingEngine.register(new ProcessDefinition("expense", "Expense request", steps, List.of(
|
||||
routingEngine.publish(new ProcessDefinition("expense", "Expense request", steps, List.of(
|
||||
StepTransition.when("manager", "director", "amount-gt-1000", 0),
|
||||
new StepTransition("manager", null, null, 1),
|
||||
StepTransition.end("director"))));
|
||||
@@ -500,7 +503,7 @@ class InMemoryOrdoEngineTest {
|
||||
List<String> executed = new java.util.ArrayList<>();
|
||||
InMemoryOrdoEngine actionEngine = new InMemoryOrdoEngine(Clock.systemUTC(), AssigneeResolver.direct(),
|
||||
RoutingCondition.always(), (key, context) -> executed.add(key));
|
||||
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.action("notify", "Notify HR", "leave-approved-mail"),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")),
|
||||
@@ -524,7 +527,7 @@ class InMemoryOrdoEngineTest {
|
||||
RoutingCondition.always(), (key, context) -> {
|
||||
throw new IllegalStateException("mail failed");
|
||||
});
|
||||
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.action("notify", "Notify HR", "leave-approved-mail")),
|
||||
List.of(
|
||||
@@ -541,7 +544,7 @@ class InMemoryOrdoEngineTest {
|
||||
List<String> executed = new java.util.ArrayList<>();
|
||||
InMemoryOrdoEngine actionEngine = new InMemoryOrdoEngine(Clock.systemUTC(), AssigneeResolver.direct(),
|
||||
RoutingCondition.always(), (key, context) -> executed.add(key));
|
||||
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
ApprovalStep.action("notify", "Notify manager", "leave-submitted-mail"),
|
||||
ApprovalStep.single("manager", "Manager approval", "maria")),
|
||||
List.of(
|
||||
@@ -557,7 +560,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void throwsWhenNoTransitionMatches() {
|
||||
InMemoryOrdoEngine routingEngine = new InMemoryOrdoEngine((key, context) -> false);
|
||||
routingEngine.register(new ProcessDefinition("expense", "Expense request",
|
||||
routingEngine.publish(new ProcessDefinition("expense", "Expense request",
|
||||
List.of(ApprovalStep.single("manager", "Manager approval", "maria")),
|
||||
List.of(StepTransition.endWhen("manager", "never", 0))));
|
||||
|
||||
@@ -568,7 +571,7 @@ class InMemoryOrdoEngineTest {
|
||||
|
||||
@Test
|
||||
void queryTasksFiltersByAssigneeDefinitionAndPaginates() {
|
||||
engine.register(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
engine.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
ApprovalStep.single("finance", "Finance approval", "frank"))));
|
||||
var leaveInstance = engine.start("leave", "alice");
|
||||
engine.start("expense", "alice");
|
||||
@@ -614,7 +617,7 @@ class InMemoryOrdoEngineTest {
|
||||
|
||||
@Test
|
||||
void queryDefinitionsPaginatesRegisteredDefinitions() {
|
||||
engine.register(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
engine.publish(ProcessDefinition.linear("expense", "Expense request", List.of(
|
||||
ApprovalStep.single("finance", "Finance approval", "frank"))));
|
||||
|
||||
Page<ProcessDefinition> all = engine.queryDefinitions(new PageRequest(0, 10));
|
||||
@@ -625,7 +628,7 @@ class InMemoryOrdoEngineTest {
|
||||
@Test
|
||||
void recordsHistoryForStartApproveSkipAndComplete() {
|
||||
InMemoryOrdoEngine anyEngine = new InMemoryOrdoEngine();
|
||||
anyEngine.register(ProcessDefinition.linear("leave-any", "Leave request", List.of(
|
||||
anyEngine.publish(ProcessDefinition.linear("leave-any", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria", "mike"), ApprovalPolicy.ANY),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")
|
||||
)));
|
||||
@@ -690,7 +693,7 @@ class InMemoryOrdoEngineTest {
|
||||
}
|
||||
received.add(event.type());
|
||||
}));
|
||||
listening.register(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
listening.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"))));
|
||||
|
||||
var instance = listening.start("leave", "alice");
|
||||
@@ -711,7 +714,7 @@ class InMemoryOrdoEngineTest {
|
||||
throw new IllegalStateException("mail failed");
|
||||
}
|
||||
});
|
||||
actionEngine.register(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
actionEngine.publish(new ProcessDefinition("leave", "Leave request", List.of(
|
||||
ApprovalStep.single("manager", "Manager approval", "maria"),
|
||||
ApprovalStep.action("notify", "Notify HR", "ok-mail"),
|
||||
ApprovalStep.action("fail", "Fail mail", "fail-mail")),
|
||||
@@ -743,7 +746,7 @@ class InMemoryOrdoEngineTest {
|
||||
void processDueReassignsAfterTheStepDueElapses() {
|
||||
MutableClock clock = new MutableClock(Instant.parse("2026-01-15T09:00:00Z"));
|
||||
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock);
|
||||
dueEngine.register(ProcessDefinition.linear("leave-due", "Leave request", List.of(
|
||||
dueEngine.publish(ProcessDefinition.linear("leave-due", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
|
||||
null, StepDue.reassign(java.time.Duration.ofHours(1), "diana")))));
|
||||
|
||||
@@ -770,7 +773,7 @@ class InMemoryOrdoEngineTest {
|
||||
List<String> actions = new java.util.ArrayList<>();
|
||||
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock, AssigneeResolver.direct(),
|
||||
RoutingCondition.always(), (key, context) -> actions.add(key));
|
||||
dueEngine.register(ProcessDefinition.linear("leave-notify", "Leave request", List.of(
|
||||
dueEngine.publish(ProcessDefinition.linear("leave-notify", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
|
||||
null, StepDue.notify(java.time.Duration.ofMinutes(30), "overdue-mail")))));
|
||||
|
||||
@@ -786,7 +789,7 @@ class InMemoryOrdoEngineTest {
|
||||
void processDueGotoSkipsTheStepAndEntersTheTarget() {
|
||||
MutableClock clock = new MutableClock(Instant.parse("2026-01-15T09:00:00Z"));
|
||||
InMemoryOrdoEngine dueEngine = new InMemoryOrdoEngine(clock);
|
||||
dueEngine.register(new ProcessDefinition("leave-goto", "Leave request", List.of(
|
||||
dueEngine.publish(new ProcessDefinition("leave-goto", "Leave request", List.of(
|
||||
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY, StepKind.APPROVAL,
|
||||
null, StepDue.gotoStep(java.time.Duration.ofHours(1), "hr")),
|
||||
ApprovalStep.single("hr", "HR approval", "henry")),
|
||||
|
||||
+2
-2
@@ -61,9 +61,9 @@ class InMemoryApprovalTaskRepositoryTest {
|
||||
@Test
|
||||
void definitionIdFilterResolvesThroughTheInstanceRepository() {
|
||||
InMemoryProcessInstanceRepository instanceRepository = new InMemoryProcessInstanceRepository();
|
||||
instanceRepository.insert(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
|
||||
instanceRepository.insert(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
|
||||
CREATED_AT, null, ProcessContext.empty()));
|
||||
instanceRepository.insert(new ProcessInstance("inst-2", "expense", "alice", ProcessStatus.RUNNING,
|
||||
instanceRepository.insert(new ProcessInstance("inst-2", "expense", 1, "alice", ProcessStatus.RUNNING,
|
||||
CREATED_AT, null, ProcessContext.empty()));
|
||||
InMemoryApprovalTaskRepository repository = new InMemoryApprovalTaskRepository(instanceRepository);
|
||||
ApprovalTask leaveTask = task("task-1", "inst-1", "maria", TaskStatus.PENDING, CREATED_AT);
|
||||
|
||||
+18
-4
@@ -14,11 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InMemoryProcessDefinitionRepositoryTest {
|
||||
@Test
|
||||
void findAllPaginatesDefinitionsOrderedById() {
|
||||
void findAllPaginatesLatestDefinitionsOrderedById() {
|
||||
InMemoryProcessDefinitionRepository repository = new InMemoryProcessDefinitionRepository();
|
||||
repository.insertIfAbsent(definition("c-def"));
|
||||
repository.insertIfAbsent(definition("a-def"));
|
||||
repository.insertIfAbsent(definition("b-def"));
|
||||
repository.publish(definition("c-def"));
|
||||
repository.publish(definition("a-def"));
|
||||
repository.publish(definition("b-def"));
|
||||
|
||||
Page<ProcessDefinition> pageOne = repository.findAll(new PageRequest(0, 2));
|
||||
assertEquals(3, pageOne.totalElements());
|
||||
@@ -31,6 +31,20 @@ class InMemoryProcessDefinitionRepositoryTest {
|
||||
assertFalse(pageTwo.hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishKeepsPreviousVersions() {
|
||||
InMemoryProcessDefinitionRepository repository = new InMemoryProcessDefinitionRepository();
|
||||
repository.publish(ProcessDefinition.linear("leave", "v1", List.of(
|
||||
ApprovalStep.single("lead", "Lead approval", "lee"))));
|
||||
repository.publish(ProcessDefinition.linear("leave", "v2", List.of(
|
||||
ApprovalStep.single("director", "Director approval", "diana"))));
|
||||
|
||||
assertEquals(2, repository.findLatest("leave").orElseThrow().version());
|
||||
assertEquals("v1", repository.find("leave", 1).orElseThrow().name());
|
||||
Page<ProcessDefinition> versions = repository.findVersions("leave", new PageRequest(0, 10));
|
||||
assertEquals(List.of(2, 1), versions.content().stream().map(ProcessDefinition::version).toList());
|
||||
}
|
||||
|
||||
private static ProcessDefinition definition(String id) {
|
||||
return ProcessDefinition.linear(id, id, List.of(ApprovalStep.single("lead", "Lead approval", "lee")));
|
||||
}
|
||||
|
||||
+1
-1
@@ -56,6 +56,6 @@ class InMemoryProcessInstanceRepositoryTest {
|
||||
|
||||
private static ProcessInstance instance(String id, String definitionId, String initiator, ProcessStatus status,
|
||||
Instant startedAt) {
|
||||
return new ProcessInstance(id, definitionId, initiator, status, startedAt, null, ProcessContext.empty());
|
||||
return new ProcessInstance(id, definitionId, 1, initiator, status, startedAt, null, ProcessContext.empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user