package com.xfestudio.xfeservermanager.core.trigger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.xfestudio.xfeservermanager.api.trigger.TriggerExtension;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class TriggerProgramV2Test {
@Test
void simulatesLoopsTypedExpressionsAndDeterministicRandomWithoutSideEffects() {
TriggerExpression counter = TriggerExpression.reference("integer", "counter");
TriggerExpression increment = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
"integer", null, "+", List.of(counter, TriggerExpression.literal("integer", 1L)));
TriggerExpression random = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
"integer", null, "random_int", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 100L)));
TriggerProgramV2.Statement body = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "counter", Map.of(), increment,
List.of(), List.of(), List.of());
TriggerProgramV2.Statement action = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.ACTION, "log", Map.of("roll", random,
"counter", TriggerExpression.reference("integer", "counter")),
null, List.of(), List.of(), List.of());
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), "counter", "integer",
TriggerExpression.literal("integer", 0L), "trigger", "trigger",
TriggerProgramV2.Lifetime.PERSISTENT, null)), List.of(),
List.of(new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.REPEAT,
"", Map.of(), TriggerExpression.literal("integer", 3L), List.of(),
List.of(body, action), List.of())));
TriggerSimulator.Request request = new TriggerSimulator.Request(
Map.of("event.type", "custom.test"), Map.of(), 42L, Instant.EPOCH);
TriggerSimulator.Result first = TriggerSimulator.simulate(program, request);
TriggerSimulator.Result second = TriggerSimulator.simulate(program, request);
assertEquals(TriggerSimulator.Status.COMPLETED, first.status());
assertEquals(3L, first.variables().get("counter"));
assertEquals(3, first.actions().size());
assertEquals(first.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList(),
second.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList());
}
@Test
void booleanOperatorsShortCircuit() {
TriggerExpression divisionByZero = new TriggerExpression(UUID.randomUUID(),
TriggerExpression.Kind.BINARY, "float", null, "/", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 0L)));
TriggerExpression expression = new TriggerExpression(UUID.randomUUID(),
TriggerExpression.Kind.BINARY, "bool", null, "and", List.of(
TriggerExpression.literal("bool", false), divisionByZero));
var evaluator = new TriggerExpressionEvaluator(Map.of(), new java.util.LinkedHashMap<>(),
0L, Instant.EPOCH, null);
assertFalse(evaluator.evaluateBoolean(expression));
}
@Test
void legacyCompilationUsesStableNodeIdsAndNewBudgetsAreEnforced() {
Instant now = Instant.EPOCH;
TriggerDefinition legacy = new TriggerDefinition(UUID.randomUUID(), UUID.randomUUID(), "Legacy", "",
true, TriggerDefinition.Mode.VISUAL,
new TriggerDefinition.TriggerEvent("player.join", Map.of()),
TriggerDefinition.MatchMode.ALL,
List.of(new TriggerDefinition.Condition("player.name", "eq", "Steve")),
List.of(new TriggerDefinition.Action("log", Map.of("message", "hello"))),
"", 0, "owner", now, now, false);
assertEquals(TriggerLegacyV2Compiler.compile(legacy), TriggerLegacyV2Compiler.compile(legacy));
TriggerDefinition.Action nested = new TriggerDefinition.Action("log", Map.of("message", "end"));
for (int index = 0; index < TriggerProgramValidator.MAX_NESTING_DEPTH; index++) {
nested = new TriggerDefinition.Action("condition", Map.of(
"field", "event.type", "operator", "exists", "value", ""), List.of(nested));
}
TriggerDefinition.Action tooDeep = nested;
assertThrows(IllegalArgumentException.class, () -> TriggerActionTree.validate(List.of(tooDeep)));
}
@Test
void functionArgumentsUseCallerSnapshotAndOutParametersWriteBack() {
TriggerProgramV2.Statement assignOutput = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "out", Map.of(),
new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
"integer", null, "+", List.of(
TriggerExpression.reference("integer", "x"),
TriggerExpression.reference("integer", "y"))),
List.of(), List.of(), List.of());
TriggerProgramV2.FunctionDeclaration swapAdd = new TriggerProgramV2.FunctionDeclaration(
UUID.randomUUID(), "swap_add", List.of(
new TriggerProgramV2.Parameter("x", "integer", TriggerProgramV2.ParameterMode.IN),
new TriggerProgramV2.Parameter("y", "integer", TriggerProgramV2.ParameterMode.IN),
new TriggerProgramV2.Parameter("out", "integer", TriggerProgramV2.ParameterMode.OUT)),
"void", List.of(), List.of(assignOutput));
TriggerProgramV2.Statement call = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.CALL, "swap_add", Map.of(
"x", TriggerExpression.reference("integer", "y"),
"y", TriggerExpression.reference("integer", "x"),
"out", TriggerExpression.reference("integer", "result")),
null, List.of(), List.of(), List.of());
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(variable("x", 2L), variable("y", 3L), variable("result", 0L)),
List.of(swapAdd), List.of(call));
TriggerSimulator.Result result = TriggerSimulator.simulate(program,
new TriggerSimulator.Request(Map.of(), Map.of(), 1L, Instant.EPOCH));
assertEquals(TriggerSimulator.Status.COMPLETED, result.status());
assertEquals(5L, result.variables().get("result"));
}
@Test
void rejectsStaticTypeMismatchesBeforeExecution() {
assertThrows(IllegalArgumentException.class, () -> new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(variable("counter", 0L)), List.of(),
List.of(new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "counter", Map.of(),
TriggerExpression.literal("string", "wrong"),
List.of(), List.of(), List.of()))));
}
@Test
void extensionsMayRaiseButNeverLowerBuiltInRisk() {
TriggerExtension.Descriptor builtIn = TriggerDescriptorCatalog.DESCRIPTORS.stream()
.filter(value -> value.kind() == TriggerExtension.Kind.ACTION
&& value.id().equals("server_command"))
.findFirst().orElseThrow();
Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
descriptors.put("ACTION:server_command", builtIn);
TriggerExtension extension = new TriggerExtension() {
@Override public String namespace() { return "example"; }
@Override public String version() { return "1.0.0"; }
@Override public List<Descriptor> descriptors() {
return List.of(new Descriptor("server_command", Kind.ACTION, "command", "",
List.of(), "void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER,
Risk.SAFE, Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
}
};
TriggerExtensionRegistry.register(extension, descriptors);
assertEquals(TriggerExtension.Risk.OWNER,
descriptors.get("ACTION:server_command").risk());
}
@Test
void extensionsBindOnlyNamespacedTransportSafeFunctionAndActionHandlers() throws Exception {
Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
Map<String, TriggerExtension.FunctionHandler> functions = new LinkedHashMap<>();
Map<String, TriggerExtension.ActionHandler> actions = new LinkedHashMap<>();
java.util.concurrent.atomic.AtomicReference<String> actionKey =
new java.util.concurrent.atomic.AtomicReference<>();
TriggerExtension extension = new TriggerExtension() {
@Override public String namespace() { return "example"; }
@Override public String version() { return "1.0.0"; }
@Override public List<Descriptor> descriptors() {
return List.of(
new Descriptor("example.double", Kind.VALUE_FUNCTION, "double", "",
List.of(new Parameter("value", "integer", true, null, "")),
"integer", Purity.PURE, ThreadAffinity.BACKGROUND, Risk.SAFE,
Set.of("forge-1.20.1"), Set.of("*"), Map.of()),
new Descriptor("example.audit", Kind.ACTION, "audit", "", List.of(),
"void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER, Risk.ADMINISTRATOR,
Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
}
@Override public Map<String, FunctionHandler> functionHandlers() {
return Map.of("example.double", invocation ->
((Number) invocation.arguments().get(0)).longValue() * 2L);
}
@Override public Map<String, ActionHandler> actionHandlers() {
return Map.of("example.audit", invocation -> actionKey.set(invocation.idempotencyKey()));
}
};
TriggerExtensionRegistry.register(extension, descriptors, functions, actions);
assertEquals(8L, functions.get("example.double").evaluate(
new TriggerExtension.FunctionInvocation("example.double", List.of(4L), Map.of(), Map.of(),
1L, Instant.EPOCH.toString())));
actions.get("example.audit").execute(new TriggerExtension.ActionInvocation(
"example.audit", Map.of(), Map.of(), "execution", UUID.randomUUID().toString(), "key"));
assertEquals("key", actionKey.get());
}
@Test
void liveVmResumesAfterWaitWithoutRepeatingEffectsOrRandomValues() {
TriggerProgramV2.Statement before = action("log", Map.of("message", randomRoll()));
TriggerProgramV2.Statement wait = action("wait", Map.of(
"mode", TriggerExpression.literal("string", "duration"),
"value", TriggerExpression.literal("string", "1")));
TriggerProgramV2.Statement after = action("log", Map.of("message", randomRoll()));
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.resume", Map.of())),
List.of(), List.of(), List.of(before, wait, after));
TriggerProgramExecutor.State started = TriggerProgramExecutor.start(
program, Map.of("event.type", "custom.resume"), Map.of(), 77L, Instant.EPOCH);
TriggerProgramExecutor.Result waiting = TriggerProgramExecutor.resume(program, started, 64);
TriggerProgramExecutor.Result completed = TriggerProgramExecutor.resume(program, waiting.state(), 64);
TriggerProgramExecutor.Result replayWaiting = TriggerProgramExecutor.resume(program,
TriggerProgramExecutor.start(program, Map.of("event.type", "custom.resume"),
Map.of(), 77L, Instant.EPOCH), 64);
TriggerProgramExecutor.Result replayCompleted = TriggerProgramExecutor.resume(
program, replayWaiting.state(), 64);
assertEquals(TriggerProgramExecutor.Status.WAITING, waiting.status());
assertEquals(List.of("log", "wait"), waiting.effects().stream()
.map(TriggerProgramExecutor.Effect::type).toList());
assertEquals(TriggerProgramExecutor.Status.COMPLETED, completed.status());
assertEquals(List.of("log"), completed.effects().stream()
.map(TriggerProgramExecutor.Effect::type).toList());
assertEquals(waiting.effects().get(0).parameters(), replayWaiting.effects().get(0).parameters());
assertEquals(completed.effects().get(0).parameters(), replayCompleted.effects().get(0).parameters());
assertEquals(2L, completed.state().randomCounter());
assertEquals(3, java.util.stream.Stream.concat(waiting.effects().stream(), completed.effects().stream())
.map(TriggerProgramExecutor.Effect::idempotencyKey).distinct().count());
}
private static TriggerProgramV2.Statement action(
String name, Map<String, TriggerExpression> inputs) {
return new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.ACTION,
name, inputs, null, List.of(), List.of(), List.of());
}
private static TriggerExpression randomRoll() {
return new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
"integer", null, "random_int", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 1_000L)));
}
private static TriggerProgramV2.VariableDeclaration variable(String name, long initial) {
return new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), name, "integer",
TriggerExpression.literal("integer", initial), "trigger", "trigger",
TriggerProgramV2.Lifetime.SESSION, null);
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.xfestudio.xfeservermanager.api.trigger.TriggerExtension;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
class TriggerProgramV2Test {
@Test
void simulatesLoopsTypedExpressionsAndDeterministicRandomWithoutSideEffects() {
TriggerExpression counter = TriggerExpression.reference("integer", "counter");
TriggerExpression increment = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
"integer", null, "+", List.of(counter, TriggerExpression.literal("integer", 1L)));
TriggerExpression random = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
"integer", null, "random_int", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 100L)));
TriggerProgramV2.Statement body = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "counter", Map.of(), increment,
List.of(), List.of(), List.of());
TriggerProgramV2.Statement action = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.ACTION, "log", Map.of("roll", random,
"counter", TriggerExpression.reference("integer", "counter")),
null, List.of(), List.of(), List.of());
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), "counter", "integer",
TriggerExpression.literal("integer", 0L), "trigger", "trigger",
TriggerProgramV2.Lifetime.PERSISTENT, null)), List.of(),
List.of(new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.REPEAT,
"", Map.of(), TriggerExpression.literal("integer", 3L), List.of(),
List.of(body, action), List.of())));
TriggerSimulator.Request request = new TriggerSimulator.Request(
Map.of("event.type", "custom.test"), Map.of(), 42L, Instant.EPOCH);
TriggerSimulator.Result first = TriggerSimulator.simulate(program, request);
TriggerSimulator.Result second = TriggerSimulator.simulate(program, request);
assertEquals(TriggerSimulator.Status.COMPLETED, first.status());
assertEquals(3L, first.variables().get("counter"));
assertEquals(3, first.actions().size());
assertEquals(first.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList(),
second.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList());
}
@Test
void booleanOperatorsShortCircuit() {
TriggerExpression divisionByZero = new TriggerExpression(UUID.randomUUID(),
TriggerExpression.Kind.BINARY, "float", null, "/", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 0L)));
TriggerExpression expression = new TriggerExpression(UUID.randomUUID(),
TriggerExpression.Kind.BINARY, "bool", null, "and", List.of(
TriggerExpression.literal("bool", false), divisionByZero));
var evaluator = new TriggerExpressionEvaluator(Map.of(), new java.util.LinkedHashMap<>(),
0L, Instant.EPOCH, null);
assertFalse(evaluator.evaluateBoolean(expression));
}
@Test
void legacyCompilationUsesStableNodeIdsAndNewBudgetsAreEnforced() {
Instant now = Instant.EPOCH;
TriggerDefinition legacy = new TriggerDefinition(UUID.randomUUID(), UUID.randomUUID(), "Legacy", "",
true, TriggerDefinition.Mode.VISUAL,
new TriggerDefinition.TriggerEvent("player.join", Map.of()),
TriggerDefinition.MatchMode.ALL,
List.of(new TriggerDefinition.Condition("player.name", "eq", "Steve")),
List.of(new TriggerDefinition.Action("log", Map.of("message", "hello"))),
"", 0, "owner", now, now, false);
assertEquals(TriggerLegacyV2Compiler.compile(legacy), TriggerLegacyV2Compiler.compile(legacy));
TriggerDefinition.Action nested = new TriggerDefinition.Action("log", Map.of("message", "end"));
for (int index = 0; index < TriggerProgramValidator.MAX_NESTING_DEPTH; index++) {
nested = new TriggerDefinition.Action("condition", Map.of(
"field", "event.type", "operator", "exists", "value", ""), List.of(nested));
}
TriggerDefinition.Action tooDeep = nested;
assertThrows(IllegalArgumentException.class, () -> TriggerActionTree.validate(List.of(tooDeep)));
}
@Test
void functionArgumentsUseCallerSnapshotAndOutParametersWriteBack() {
TriggerProgramV2.Statement assignOutput = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "out", Map.of(),
new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
"integer", null, "+", List.of(
TriggerExpression.reference("integer", "x"),
TriggerExpression.reference("integer", "y"))),
List.of(), List.of(), List.of());
TriggerProgramV2.FunctionDeclaration swapAdd = new TriggerProgramV2.FunctionDeclaration(
UUID.randomUUID(), "swap_add", List.of(
new TriggerProgramV2.Parameter("x", "integer", TriggerProgramV2.ParameterMode.IN),
new TriggerProgramV2.Parameter("y", "integer", TriggerProgramV2.ParameterMode.IN),
new TriggerProgramV2.Parameter("out", "integer", TriggerProgramV2.ParameterMode.OUT)),
"void", List.of(), List.of(assignOutput));
TriggerProgramV2.Statement call = new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.CALL, "swap_add", Map.of(
"x", TriggerExpression.reference("integer", "y"),
"y", TriggerExpression.reference("integer", "x"),
"out", TriggerExpression.reference("integer", "result")),
null, List.of(), List.of(), List.of());
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(variable("x", 2L), variable("y", 3L), variable("result", 0L)),
List.of(swapAdd), List.of(call));
TriggerSimulator.Result result = TriggerSimulator.simulate(program,
new TriggerSimulator.Request(Map.of(), Map.of(), 1L, Instant.EPOCH));
assertEquals(TriggerSimulator.Status.COMPLETED, result.status());
assertEquals(5L, result.variables().get("result"));
}
@Test
void rejectsStaticTypeMismatchesBeforeExecution() {
assertThrows(IllegalArgumentException.class, () -> new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
List.of(variable("counter", 0L)), List.of(),
List.of(new TriggerProgramV2.Statement(UUID.randomUUID(),
TriggerProgramV2.StatementKind.SET, "counter", Map.of(),
TriggerExpression.literal("string", "wrong"),
List.of(), List.of(), List.of()))));
}
@Test
void extensionsMayRaiseButNeverLowerBuiltInRisk() {
TriggerExtension.Descriptor builtIn = TriggerDescriptorCatalog.DESCRIPTORS.stream()
.filter(value -> value.kind() == TriggerExtension.Kind.ACTION
&& value.id().equals("server_command"))
.findFirst().orElseThrow();
Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
descriptors.put("ACTION:server_command", builtIn);
TriggerExtension extension = new TriggerExtension() {
@Override public String namespace() { return "example"; }
@Override public String version() { return "1.0.0"; }
@Override public List<Descriptor> descriptors() {
return List.of(new Descriptor("server_command", Kind.ACTION, "command", "",
List.of(), "void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER,
Risk.SAFE, Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
}
};
TriggerExtensionRegistry.register(extension, descriptors);
assertEquals(TriggerExtension.Risk.OWNER,
descriptors.get("ACTION:server_command").risk());
}
@Test
void extensionsBindOnlyNamespacedTransportSafeFunctionAndActionHandlers() throws Exception {
Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
Map<String, TriggerExtension.FunctionHandler> functions = new LinkedHashMap<>();
Map<String, TriggerExtension.ActionHandler> actions = new LinkedHashMap<>();
java.util.concurrent.atomic.AtomicReference<String> actionKey =
new java.util.concurrent.atomic.AtomicReference<>();
TriggerExtension extension = new TriggerExtension() {
@Override public String namespace() { return "example"; }
@Override public String version() { return "1.0.0"; }
@Override public List<Descriptor> descriptors() {
return List.of(
new Descriptor("example.double", Kind.VALUE_FUNCTION, "double", "",
List.of(new Parameter("value", "integer", true, null, "")),
"integer", Purity.PURE, ThreadAffinity.BACKGROUND, Risk.SAFE,
Set.of("forge-1.20.1"), Set.of("*"), Map.of()),
new Descriptor("example.audit", Kind.ACTION, "audit", "", List.of(),
"void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER, Risk.ADMINISTRATOR,
Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
}
@Override public Map<String, FunctionHandler> functionHandlers() {
return Map.of("example.double", invocation ->
((Number) invocation.arguments().get(0)).longValue() * 2L);
}
@Override public Map<String, ActionHandler> actionHandlers() {
return Map.of("example.audit", invocation -> actionKey.set(invocation.idempotencyKey()));
}
};
TriggerExtensionRegistry.register(extension, descriptors, functions, actions);
assertEquals(8L, functions.get("example.double").evaluate(
new TriggerExtension.FunctionInvocation("example.double", List.of(4L), Map.of(), Map.of(),
1L, Instant.EPOCH.toString())));
actions.get("example.audit").execute(new TriggerExtension.ActionInvocation(
"example.audit", Map.of(), Map.of(), "execution", UUID.randomUUID().toString(), "key"));
assertEquals("key", actionKey.get());
}
@Test
void liveVmResumesAfterWaitWithoutRepeatingEffectsOrRandomValues() {
TriggerProgramV2.Statement before = action("log", Map.of("message", randomRoll()));
TriggerProgramV2.Statement wait = action("wait", Map.of(
"mode", TriggerExpression.literal("string", "duration"),
"value", TriggerExpression.literal("string", "1")));
TriggerProgramV2.Statement after = action("log", Map.of("message", randomRoll()));
TriggerProgramV2 program = new TriggerProgramV2(2,
List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.resume", Map.of())),
List.of(), List.of(), List.of(before, wait, after));
TriggerProgramExecutor.State started = TriggerProgramExecutor.start(
program, Map.of("event.type", "custom.resume"), Map.of(), 77L, Instant.EPOCH);
TriggerProgramExecutor.Result waiting = TriggerProgramExecutor.resume(program, started, 64);
TriggerProgramExecutor.Result completed = TriggerProgramExecutor.resume(program, waiting.state(), 64);
TriggerProgramExecutor.Result replayWaiting = TriggerProgramExecutor.resume(program,
TriggerProgramExecutor.start(program, Map.of("event.type", "custom.resume"),
Map.of(), 77L, Instant.EPOCH), 64);
TriggerProgramExecutor.Result replayCompleted = TriggerProgramExecutor.resume(
program, replayWaiting.state(), 64);
assertEquals(TriggerProgramExecutor.Status.WAITING, waiting.status());
assertEquals(List.of("log", "wait"), waiting.effects().stream()
.map(TriggerProgramExecutor.Effect::type).toList());
assertEquals(TriggerProgramExecutor.Status.COMPLETED, completed.status());
assertEquals(List.of("log"), completed.effects().stream()
.map(TriggerProgramExecutor.Effect::type).toList());
assertEquals(waiting.effects().get(0).parameters(), replayWaiting.effects().get(0).parameters());
assertEquals(completed.effects().get(0).parameters(), replayCompleted.effects().get(0).parameters());
assertEquals(2L, completed.state().randomCounter());
assertEquals(3, java.util.stream.Stream.concat(waiting.effects().stream(), completed.effects().stream())
.map(TriggerProgramExecutor.Effect::idempotencyKey).distinct().count());
}
private static TriggerProgramV2.Statement action(
String name, Map<String, TriggerExpression> inputs) {
return new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.ACTION,
name, inputs, null, List.of(), List.of(), List.of());
}
private static TriggerExpression randomRoll() {
return new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
"integer", null, "random_int", List.of(
TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 1_000L)));
}
private static TriggerProgramV2.VariableDeclaration variable(String name, long initial) {
return new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), name, "integer",
TriggerExpression.literal("integer", initial), "trigger", "trigger",
TriggerProgramV2.Lifetime.SESSION, null);
}
}