diff --git a/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/schedule/ScriptScheduleResponse.java b/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/schedule/ScriptScheduleResponse.java
index 505262ada5..eec2413cfd 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/schedule/ScriptScheduleResponse.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/schedule/ScriptScheduleResponse.java
@@ -54,4 +54,5 @@ public class ScriptScheduleResponse {
private Instant statusChangedAt;
private Instant createdAt;
private Instant updatedAt;
+ private boolean testScript;
}
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/script/ScriptResponse.java b/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/script/ScriptResponse.java
index 452db6ff0f..b8bb38e390 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/script/ScriptResponse.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/dto/rmm/script/ScriptResponse.java
@@ -36,4 +36,5 @@ public class ScriptResponse {
private Instant statusChangedAt;
private Instant createdAt;
private Instant updatedAt;
+ private boolean testScript;
}
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptMapper.java b/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptMapper.java
index 5fdb592bd2..53f918c39f 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptMapper.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptMapper.java
@@ -8,21 +8,17 @@
import com.openframe.data.document.rmm.script.Script;
import com.openframe.data.document.rmm.script.ScriptEnvVar;
import com.openframe.data.document.rmm.script.ScriptStatus;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
-/**
- * Pure entity ↔ DTO mapping for scripts. Lives in {@code openframe-api-lib}
- * so it can be reused by any service that talks to the script repository,
- * regardless of transport (GraphQL / REST / messaging).
- *
- *
GraphQL-specific concerns (cursor pagination, Relay Connection / Edge
- * envelope) live in {@code GraphQLScriptMapper} alongside the DGS resolver.
- */
@Component
public class ScriptMapper {
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
public Script toEntity(String tenantId, CreateScriptInput input) {
return Script.builder()
.tenantId(tenantId)
@@ -35,6 +31,7 @@ public Script toEntity(String tenantId, CreateScriptInput input) {
.defaultTimeoutSeconds(input.getDefaultTimeoutSeconds())
.defaultArgs(input.getDefaultArgs())
.envVars(ScriptEnvVarMapper.toEntity(input.getEnvVars()))
+ .testScript(testModeEnabled)
.build();
}
@@ -67,6 +64,7 @@ public ScriptResponse toResponse(Script entity) {
.statusChangedAt(entity.getStatusChangedAt())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
+ .testScript(entity.isTestScript())
.build();
}
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptScheduleMapper.java b/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptScheduleMapper.java
index b768ad0468..1b85883309 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptScheduleMapper.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/mapper/ScriptScheduleMapper.java
@@ -11,6 +11,7 @@
import com.openframe.data.document.rmm.schedule.ScheduleScriptTrigger;
import com.openframe.data.document.rmm.schedule.ScheduleTimeReference;
import com.openframe.data.document.rmm.script.ScriptStatus;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -23,6 +24,9 @@
@Component
public class ScriptScheduleMapper {
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
public ScheduleScript toEntity(String tenantId, CreateScriptScheduleInput input) {
return ScheduleScript.builder()
.tenantId(tenantId)
@@ -37,6 +41,7 @@ public ScheduleScript toEntity(String tenantId, CreateScriptScheduleInput input)
.reconnectWindowSeconds(input.getReconnectWindowSeconds())
.startAt(input.getStartAt())
.repeat(input.getRepeat())
+ .testScript(testModeEnabled)
.build();
}
@@ -94,6 +99,7 @@ public ScriptScheduleResponse toResponse(ScheduleScript entity) {
.statusChangedAt(entity.getStatusChangedAt())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
+ .testScript(entity.isTestScript())
.build();
}
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandDispatchService.java b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandDispatchService.java
index d9bf16edcc..b2b9e69e65 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandDispatchService.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandDispatchService.java
@@ -10,6 +10,7 @@
import com.openframe.data.nats.rmm.publisher.CommandNatsPublisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
@@ -37,6 +38,9 @@ public class CommandDispatchService {
private final DeviceService deviceService;
private final CommandExecutionService commandExecutionService;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
public DispatchResponse runCommand(RunCommandInput input) {
deviceService.verifyDispatchable(input.getMachineId());
@@ -76,7 +80,8 @@ public DispatchResponse batchRunCommand(BatchRunCommandInput input, String initi
// Persist one RUNNING row per machine (tenant-scoped, via the service) before
// anything hits the wire — the agent's result transitions each row later.
commandExecutionService.createBatch(executionId, input.getCommand(), input.getShell(),
- machineIds, input.getPrivilegeLevel(), input.getTimeoutSeconds(), initiatedBy);
+ machineIds, input.getPrivilegeLevel(), input.getTimeoutSeconds(), initiatedBy,
+ testModeEnabled);
// Fan out the same payload (one executionId) to every machine.
CommandMessage message = CommandMessage.builder()
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandExecutionService.java b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandExecutionService.java
index fc1f89a8b7..9d24fc6937 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandExecutionService.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/command/CommandExecutionService.java
@@ -38,11 +38,12 @@ public List createBatch(String executionId,
List machineIds,
PrivilegeLevel privilegeLevel,
Integer timeoutSeconds,
- String initiatedBy) {
+ String initiatedBy,
+ boolean testCommand) {
Instant now = Instant.now();
List rows = machineIds.stream()
.map(machineId -> buildRunningRow(executionId, command, shell, machineId,
- privilegeLevel, timeoutSeconds, initiatedBy, now))
+ privilegeLevel, timeoutSeconds, initiatedBy, now, testCommand))
.toList();
List saved = commandExecutionRepository.saveAll(rows);
log.info("Persisted batch command execution rows: executionId={} machineCount={} initiatedBy={} status=RUNNING",
@@ -72,7 +73,8 @@ private CommandExecution buildRunningRow(String executionId,
PrivilegeLevel privilegeLevel,
Integer timeoutSeconds,
String initiatedBy,
- Instant now) {
+ Instant now,
+ boolean testCommand) {
return CommandExecution.builder()
.tenantId(tenantIdProvider.getTenantId())
.executionId(executionId)
@@ -85,6 +87,7 @@ private CommandExecution buildRunningRow(String executionId,
.status(ExecutionStatus.RUNNING)
.dispatchedAt(now)
.statusChangedAt(now)
+ .testCommand(testCommand)
.build();
}
}
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptDispatchService.java b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptDispatchService.java
index 696b26d88b..ba63a4308d 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptDispatchService.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptDispatchService.java
@@ -27,6 +27,7 @@
import com.openframe.data.service.TenantIdProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
@@ -64,6 +65,8 @@ public class ScriptDispatchService {
private final ScheduleScriptExecutionRepository scheduleScriptExecutionRepository;
private final TenantIdProvider tenantIdProvider;
private final ScriptTimeoutValidator timeoutValidator;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
public DispatchResponse runScript(RunScriptInput input, String initiatedBy, ExecutionSource source) {
timeoutValidator.validate(input.getTimeoutSeconds());
@@ -77,7 +80,8 @@ public DispatchResponse runScript(RunScriptInput input, String initiatedBy, Exec
// Persist the effective timeout on the row so the watchdog can derive a
// per-execution stuck-threshold from it.
scriptExecutionService.create(executionId, script.getId(),
- input.getMachineId(), input.getPrivilegeLevel(), timeoutSeconds, initiatedBy, source);
+ input.getMachineId(), input.getPrivilegeLevel(), timeoutSeconds, initiatedBy, source,
+ testModeEnabled);
ScriptMessage message = ScriptMessage.builder()
.executionId(executionId)
@@ -185,6 +189,7 @@ public DispatchResponse runSchedule(String scheduleId, String initiatedBy) {
.status(ExecutionStatus.RUNNING)
.totalMachineCount(machineIds.size())
.dispatchedAt(now)
+ .testScript(testModeEnabled)
.build());
// 2. Leaves: N × M ScriptExecution rows (persist per-script batch), so the watchdog
@@ -196,7 +201,7 @@ public DispatchResponse runSchedule(String scheduleId, String initiatedBy) {
scriptExecutionService.createBatch(executionId, script.getId(), scheduleId, machineIds,
script.getPrivilegeLevel(),
effectiveTimeout(null, script.getDefaultTimeoutSeconds()),
- initiatedBy, ExecutionSource.SCHEDULED);
+ initiatedBy, ExecutionSource.SCHEDULED, testModeEnabled);
}
// 3. Build the batched agent payload once — shared across every target machine. Per-script
@@ -245,7 +250,8 @@ private DispatchResponse dispatchBatch(String executionId, ScriptResponse script
// Persist the effective timeout per row so the watchdog can derive a
// per-execution stuck-threshold from it.
- scriptExecutionService.createBatch(executionId, script.getId(), null, machineIds, privilegeLevel, timeoutSeconds, initiatedBy, source);
+ scriptExecutionService.createBatch(executionId, script.getId(), null, machineIds, privilegeLevel,
+ timeoutSeconds, initiatedBy, source, testModeEnabled);
List args = ScriptArgsTokenizer.tokenize(argsOverride != null ? argsOverride : script.getDefaultArgs());
List envVars = mergeEnvVars(script.getEnvVars(), envVarsOverride);
diff --git a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptExecutionService.java b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptExecutionService.java
index 8a7f9106ca..4907f79e8a 100644
--- a/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptExecutionService.java
+++ b/openframe-api-lib/src/main/java/com/openframe/api/service/rmm/script/ScriptExecutionService.java
@@ -69,7 +69,8 @@ public ScriptExecutionResponse create(String executionId,
PrivilegeLevel privilegeLevel,
Integer timeoutSeconds,
String initiatedBy,
- ExecutionSource source) {
+ ExecutionSource source,
+ boolean testScript) {
// Single ad-hoc run (runScript) never originates from a schedule → scheduleId null.
List saved = scriptExecutionRepository.saveRunning(RunningExecutionRows.builder()
.tenantId(tenantIdProvider.getTenantId())
@@ -80,6 +81,7 @@ public ScriptExecutionResponse create(String executionId,
.timeoutSeconds(timeoutSeconds)
.initiatedBy(initiatedBy)
.source(source)
+ .testScript(testScript)
.build());
log.info("Persisted execution row: executionId={} scriptId={} machineId={} initiatedBy={} source={} status=RUNNING",
executionId, scriptId, machineId, initiatedBy, source);
@@ -101,7 +103,8 @@ public List createBatch(String executionId,
PrivilegeLevel privilegeLevel,
Integer timeoutSeconds,
String initiatedBy,
- ExecutionSource source) {
+ ExecutionSource source,
+ boolean testScript) {
List saved = scriptExecutionRepository.saveRunning(RunningExecutionRows.builder()
.tenantId(tenantIdProvider.getTenantId())
.executionId(executionId)
@@ -112,6 +115,7 @@ public List createBatch(String executionId,
.timeoutSeconds(timeoutSeconds)
.initiatedBy(initiatedBy)
.source(source)
+ .testScript(testScript)
.build());
log.info("Persisted batch execution rows: executionId={} scriptId={} scheduleId={} machineCount={} initiatedBy={} source={} status=RUNNING",
executionId, scriptId, scheduleId, machineIds.size(), initiatedBy, source);
@@ -152,6 +156,7 @@ public List createSoftwareBatch(String executionId,
.packageManager(packageManager)
.packageName(packageName)
.softwareAction(softwareAction)
+ .testScript(false)
.build());
log.info("Persisted software batch rows: executionId={} scriptId={} packageManager={} packageName={} action={} machineCount={} initiatedBy={} source={} status=RUNNING",
executionId, scriptId, packageManager, packageName, softwareAction, machineIds.size(), initiatedBy, source);
diff --git a/openframe-api-service-core/src/main/resources/schema/script-schedule.graphqls b/openframe-api-service-core/src/main/resources/schema/script-schedule.graphqls
index 9ba57c48ed..5e0b12a7c0 100644
--- a/openframe-api-service-core/src/main/resources/schema/script-schedule.graphqls
+++ b/openframe-api-service-core/src/main/resources/schema/script-schedule.graphqls
@@ -152,6 +152,9 @@ type ScriptSchedule implements Node {
updatedAt: Instant
"""The creating user (resolved from the internal createdBy id via the user DataLoader)."""
author: User
+ """When true, this is a monitoring / QA fixture. Dispatch still runs, user-facing reads hide it.
+ Immutable at create-time."""
+ testScript: Boolean!
}
enum ScriptScheduleTrigger {
diff --git a/openframe-api-service-core/src/main/resources/schema/script.graphqls b/openframe-api-service-core/src/main/resources/schema/script.graphqls
index c3f4417158..d1a3e77045 100644
--- a/openframe-api-service-core/src/main/resources/schema/script.graphqls
+++ b/openframe-api-service-core/src/main/resources/schema/script.graphqls
@@ -110,6 +110,10 @@ type Script implements Node {
updatedAt: Instant
"""The creating user (resolved from the internal createdBy id via the user DataLoader). The raw createdBy id is not exposed — filter by author via authorIds."""
author: User
+ """When true, this is a monitoring / QA fixture created by the monitoring pipeline. Immutable at
+ create-time. Set by the pipeline via createScript; hidden from user-facing list/facets/getById
+ reads (query-builder shield) — visible only on direct id lookup performed by the pipeline itself."""
+ testScript: Boolean!
}
type ScriptEnvVar {
diff --git a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandDispatchServiceTest.java b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandDispatchServiceTest.java
index 4f38dcad59..ad3833b39a 100644
--- a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandDispatchServiceTest.java
+++ b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandDispatchServiceTest.java
@@ -216,7 +216,7 @@ void batchRunCommand_persistsPendingThenFansOut() {
// carrying the shared executionId + command/shell/privilege/timeout + initiatedBy.
verify(commandExecutionService).createBatch(
eq(response.getExecutionId()), eq("uptime"), eq(ScriptShell.BASH),
- eq(machines), eq(PrivilegeLevel.ADMIN), eq(30), eq(INITIATED_BY));
+ eq(machines), eq(PrivilegeLevel.ADMIN), eq(30), eq(INITIATED_BY), eq(false));
// One publish per machine — every published payload must carry the FULL wire contract
// (executionId, code, shell, privilegeLevel, timeout), not just executionId+code, so a
@@ -242,7 +242,7 @@ void batchRunCommand_savesBeforePublishing() {
InOrder order = inOrder(commandExecutionService, commandNatsPublisher);
order.verify(commandExecutionService).createBatch(any(), any(), any(),
- org.mockito.ArgumentMatchers.anyList(), any(), any(), any());
+ org.mockito.ArgumentMatchers.anyList(), any(), any(), any(), org.mockito.ArgumentMatchers.anyBoolean());
order.verify(commandNatsPublisher).publishCommand(eq("machine-1"), any(CommandMessage.class));
}
@@ -253,7 +253,7 @@ void batchRunCommand_dedupsMachineIds() {
commandDispatchService.batchRunCommand(batchInput(List.of("machine-1", "machine-1")), INITIATED_BY);
verify(commandExecutionService).createBatch(any(), any(), any(),
- eq(List.of("machine-1")), any(), any(), any());
+ eq(List.of("machine-1")), any(), any(), any(), org.mockito.ArgumentMatchers.anyBoolean());
verify(commandNatsPublisher).publishCommand(eq("machine-1"), any(CommandMessage.class));
}
diff --git a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandExecutionServiceTest.java b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandExecutionServiceTest.java
index a320df0e75..7cb9d15745 100644
--- a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandExecutionServiceTest.java
+++ b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/CommandExecutionServiceTest.java
@@ -48,7 +48,7 @@ void createBatch_persistsOneRunningRowPerMachine() {
when(commandExecutionRepository.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0));
service.createBatch(EXECUTION_ID, "uptime", ScriptShell.BASH, List.of("m-1", "m-2"),
- PrivilegeLevel.ADMIN, 30, "alice");
+ PrivilegeLevel.ADMIN, 30, "alice", false);
@SuppressWarnings("unchecked")
ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
diff --git a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptDispatchServiceTest.java b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptDispatchServiceTest.java
index b536af3706..3c49ac43f4 100644
--- a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptDispatchServiceTest.java
+++ b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptDispatchServiceTest.java
@@ -147,7 +147,8 @@ void runScript_persistsExecutionRowBeforeNatsPublish() {
eq(PrivilegeLevel.ADMIN),
eq(60), // effective timeout (script default, no override) — persisted for the watchdog
eq(USER_ID),
- eq(ExecutionSource.MANUAL));
+ eq(ExecutionSource.MANUAL),
+ eq(false));
inOrder.verify(scriptNatsPublisher).publishScript(eq(MACHINE_ID), any(ScriptMessage.class));
}
@@ -163,7 +164,8 @@ void runScript_nullInitiatedBy_persistedAsNull() {
eq(PrivilegeLevel.ADMIN),
eq(60),
eq((String) null),
- eq(ExecutionSource.MANUAL));
+ eq(ExecutionSource.MANUAL),
+ eq(false));
}
@Test
@@ -232,7 +234,7 @@ void runScript_persistsEffectiveTimeoutOnRow() {
scriptDispatchService.runScript(input, USER_ID, ExecutionSource.MANUAL);
verify(scriptExecutionService).create(
- any(String.class), eq(SCRIPT_ID), eq(MACHINE_ID), eq(PrivilegeLevel.ADMIN), eq(90), eq(USER_ID), eq(ExecutionSource.MANUAL));
+ any(String.class), eq(SCRIPT_ID), eq(MACHINE_ID), eq(PrivilegeLevel.ADMIN), eq(90), eq(USER_ID), eq(ExecutionSource.MANUAL), eq(false));
assertThat(capturePublished().getTimeoutSeconds()).isEqualTo(90);
}
@@ -344,7 +346,8 @@ void batchRunScript_fansOutWithSharedExecutionId() {
eq(PrivilegeLevel.ADMIN),
eq(60),
eq(USER_ID),
- eq(ExecutionSource.MANUAL));
+ eq(ExecutionSource.MANUAL),
+ eq(false));
ArgumentCaptor captor = ArgumentCaptor.forClass(ScriptMessage.class);
for (String id : machines) {
@@ -398,7 +401,7 @@ void batchRunScript_dedupsMachineIds() {
scriptDispatchService.batchRunScript(batchInput(List.of("machine-1", "machine-1")), USER_ID, ExecutionSource.MANUAL);
verify(scriptExecutionService).createBatch(
- any(), eq(SCRIPT_ID), eq((String) null), eq(List.of("machine-1")), eq(PrivilegeLevel.ADMIN), eq(60), eq(USER_ID), eq(ExecutionSource.MANUAL));
+ any(), eq(SCRIPT_ID), eq((String) null), eq(List.of("machine-1")), eq(PrivilegeLevel.ADMIN), eq(60), eq(USER_ID), eq(ExecutionSource.MANUAL), eq(false));
verify(scriptNatsPublisher, times(1)).publishScript(eq("machine-1"), any(ScriptMessage.class));
}
diff --git a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptExecutionServiceTest.java b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptExecutionServiceTest.java
index e167431814..4d849f2f96 100644
--- a/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptExecutionServiceTest.java
+++ b/openframe-api-service-core/src/test/java/com/openframe/api/service/rmm/ScriptExecutionServiceTest.java
@@ -96,7 +96,7 @@ void get_throwsWhenMissing() {
@Test
@DisplayName("create: hands the repository a RunningExecutionRows with tenant scope + scriptId + one machine (no schedule origin), and maps the saved row to a DTO")
void create_forwardsRunningExecutionRows() {
- ScriptExecutionResponse result = service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL);
+ ScriptExecutionResponse result = service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(RunningExecutionRows.class);
verify(scriptExecutionRepository).saveRunning(captor.capture());
@@ -122,7 +122,7 @@ void create_forwardsRunningExecutionRows() {
@Test
@DisplayName("create: tenantId is taken from TenantIdProvider, NOT from any caller-supplied input — locks in the pod-scoped tenant contract")
void create_alwaysUsesTenantIdProvider() {
- service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.USER, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL);
+ service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.USER, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL, false);
verify(tenantIdProvider).getTenantId();
ArgumentCaptor captor = ArgumentCaptor.forClass(RunningExecutionRows.class);
@@ -133,7 +133,7 @@ void create_alwaysUsesTenantIdProvider() {
@Test
@DisplayName("create: a null initiatedBy is forwarded as null — defensive fallback so an authenticated request without a fully-formed principal still produces a History row instead of NPE-ing the whole dispatch")
void create_acceptsNullInitiatedBy() {
- service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, null, ExecutionSource.MANUAL);
+ service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, null, ExecutionSource.MANUAL, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(RunningExecutionRows.class);
verify(scriptExecutionRepository).saveRunning(captor.capture());
@@ -143,7 +143,7 @@ void create_acceptsNullInitiatedBy() {
@Test
@DisplayName("create: privilegeLevel is forwarded verbatim — USER vs ADMIN reaches the request exactly as the dispatch carried it")
void create_forwardsPrivilegeLevelVerbatim() {
- service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.USER, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL);
+ service.create(EXECUTION_ID, SCRIPT_ID, MACHINE_ID, PrivilegeLevel.USER, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.MANUAL, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(RunningExecutionRows.class);
verify(scriptExecutionRepository).saveRunning(captor.capture());
@@ -156,7 +156,7 @@ void createBatch_forwardsAllMachinesUnderSharedExecution() {
List machines = List.of("m-1", "m-2", "m-3");
List results = service.createBatch(EXECUTION_ID, SCRIPT_ID, "sched-1", machines,
- PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.SCHEDULED);
+ PrivilegeLevel.ADMIN, TIMEOUT_SECONDS, INITIATED_BY, ExecutionSource.SCHEDULED, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(RunningExecutionRows.class);
verify(scriptExecutionRepository).saveRunning(captor.capture());
diff --git a/openframe-client-core/src/main/java/com/openframe/client/service/rmm/ScheduleFireDispatcher.java b/openframe-client-core/src/main/java/com/openframe/client/service/rmm/ScheduleFireDispatcher.java
index 698d97493c..ff50d8154c 100644
--- a/openframe-client-core/src/main/java/com/openframe/client/service/rmm/ScheduleFireDispatcher.java
+++ b/openframe-client-core/src/main/java/com/openframe/client/service/rmm/ScheduleFireDispatcher.java
@@ -28,6 +28,7 @@
import com.openframe.data.service.rmm.ScheduleDeviceTargetResolver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
@@ -55,6 +56,8 @@ public class ScheduleFireDispatcher {
private final MachineRepository machineRepository;
private final DeviceOnlineDispatchRepository dispatchRepository;
private final ScriptDeliveryRetryStore retryStore;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
public void dispatch(ScheduleScript schedule, Instant now) {
List targets = targetResolver.resolveTargetMachineIds(schedule);
@@ -185,6 +188,7 @@ private void saveHeader(Fire fire) {
.status(ExecutionStatus.RUNNING)
.totalMachineCount(fire.machineIds().size())
.dispatchedAt(fire.now())
+ .testScript(testModeEnabled)
.build());
}
@@ -204,6 +208,7 @@ private void saveLeafRows(Fire fire) {
.status(ExecutionStatus.QUEUED)
.dispatchedAt(fire.now())
.statusChangedAt(fire.now())
+ .testScript(testModeEnabled)
.build()))
.toList();
scriptExecutionRepository.saveAll(rows);
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/command/CommandExecution.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/command/CommandExecution.java
index 1c1278d1e0..87ad130b5c 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/command/CommandExecution.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/command/CommandExecution.java
@@ -89,4 +89,5 @@ public class CommandExecution implements TenantScoped {
private String stderr;
private Boolean stderrTruncated;
private String error;
+ private boolean testCommand;
}
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScript.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScript.java
index f74c1d48d2..7c01fd911a 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScript.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScript.java
@@ -83,4 +83,5 @@ public class ScheduleScript implements TenantScoped {
private ScriptStatus status = ScriptStatus.ACTIVE;
private Instant statusChangedAt;
+ private boolean testScript;
}
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScriptExecution.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScriptExecution.java
index c8a76f4cdd..05ef4f3a8a 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScriptExecution.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/schedule/ScheduleScriptExecution.java
@@ -56,4 +56,5 @@ public class ScheduleScriptExecution implements TenantScoped {
@CreatedDate
private Instant createdAt;
+ private boolean testScript;
}
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/RunningExecutionRows.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/RunningExecutionRows.java
index 7892d076e8..166338d96a 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/RunningExecutionRows.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/RunningExecutionRows.java
@@ -24,4 +24,5 @@ public class RunningExecutionRows {
PackageManagerType packageManager;
String packageName;
SoftwareAction softwareAction;
+ boolean testScript;
}
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/Script.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/Script.java
index ddd1e4a110..e9b6932adb 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/Script.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/Script.java
@@ -111,4 +111,5 @@ public class Script implements TenantScoped {
*/
private Instant statusChangedAt;
private String contentHash;
+ private boolean testScript;
}
diff --git a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/ScriptExecution.java b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/ScriptExecution.java
index 3df9e21c46..3652c138e0 100644
--- a/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/ScriptExecution.java
+++ b/openframe-data-mongo-common/src/main/java/com/openframe/data/document/rmm/script/ScriptExecution.java
@@ -93,4 +93,5 @@ public class ScriptExecution implements TenantScoped {
private String stderr;
private Boolean stderrTruncated;
private String error;
+ private boolean testScript;
}
diff --git a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScheduleScriptExecutionRepositoryImpl.java b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScheduleScriptExecutionRepositoryImpl.java
index 9a1079f4bb..ca0d766230 100644
--- a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScheduleScriptExecutionRepositoryImpl.java
+++ b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScheduleScriptExecutionRepositoryImpl.java
@@ -9,6 +9,7 @@
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -54,11 +55,15 @@ public class CustomScheduleScriptExecutionRepositoryImpl implements CustomSchedu
private static final String FIELD_DISPATCHED_AT = "dispatchedAt";
private static final String FIELD_COUNT = "count";
private static final String CURSOR_SEPARATOR = "|";
+ private static final String FIELD_TEST_SCRIPT = "testScript";
private static final Set SORTABLE_FIELDS = Set.of(FIELD_ID, FIELD_DISPATCHED_AT);
private final MongoTemplate mongoTemplate;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
@Override
public List findPageForSchedule(String tenantId,
String scriptScheduleId,
@@ -204,9 +209,12 @@ private static Criteria facetCriteria(String tenantId, String scriptScheduleId,
return criteria;
}
- private static Criteria baseCriteria(String tenantId, String scriptScheduleId, ScheduleRunQueryFilter filter) {
+ private Criteria baseCriteria(String tenantId, String scriptScheduleId, ScheduleRunQueryFilter filter) {
Criteria criteria = Criteria.where(FIELD_TENANT_ID).is(tenantId)
.and(FIELD_SCRIPT_SCHEDULE_ID).is(scriptScheduleId);
+ if (testModeEnabled) {
+ criteria.and(FIELD_TEST_SCRIPT).ne(true);
+ }
if (filter == null) {
return criteria;
}
diff --git a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptExecutionRepositoryImpl.java b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptExecutionRepositoryImpl.java
index a372835bb7..5020a2d032 100644
--- a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptExecutionRepositoryImpl.java
+++ b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptExecutionRepositoryImpl.java
@@ -11,6 +11,7 @@
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -54,6 +55,7 @@ public class CustomScriptExecutionRepositoryImpl implements CustomScriptExecutio
private static final String FIELD_FINISHED_AT = "finishedAt";
private static final String FIELD_STATUS_CHANGED_AT = "statusChangedAt";
private static final String FIELD_COUNT = "count";
+ private static final String FIELD_TEST_SCRIPT = "testScript";
/** Sort-field allowlist. Anything not in here falls back to {@link #getDefaultSortField()}. */
private static final Set SORTABLE_FIELDS = Set.of(
@@ -72,6 +74,9 @@ public class CustomScriptExecutionRepositoryImpl implements CustomScriptExecutio
private final MongoTemplate mongoTemplate;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
@Override
public List saveRunning(RunningExecutionRows request) {
return save(request, ExecutionStatus.RUNNING);
@@ -98,6 +103,7 @@ private List save(RunningExecutionRows request, ExecutionStatus
.packageManager(request.getPackageManager())
.packageName(request.getPackageName())
.softwareAction(request.getSoftwareAction())
+ .testScript(request.isTestScript())
.status(initialStatus)
.dispatchedAt(now)
.statusChangedAt(now)
@@ -204,10 +210,13 @@ private static void applyOwner(Criteria criteria, ExecutionOwnerScope owner) {
}
}
- private static Criteria baseCriteria(String tenantId, ExecutionOwnerScope owner,
- ScriptExecutionQueryFilter filter, String excludedField) {
+ private Criteria baseCriteria(String tenantId, ExecutionOwnerScope owner,
+ ScriptExecutionQueryFilter filter, String excludedField) {
Criteria criteria = Criteria.where(FIELD_TENANT_ID).is(tenantId);
applyOwner(criteria, owner);
+ if (testModeEnabled) {
+ criteria.and(FIELD_TEST_SCRIPT).ne(true);
+ }
if (filter == null) {
return criteria;
}
diff --git a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptRepositoryImpl.java b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptRepositoryImpl.java
index 9a010c80f9..3be7979b42 100644
--- a/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptRepositoryImpl.java
+++ b/openframe-data-mongo-sync/src/main/java/com/openframe/data/repository/rmm/CustomScriptRepositoryImpl.java
@@ -10,6 +10,7 @@
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
@@ -59,6 +60,7 @@ public class CustomScriptRepositoryImpl implements CustomScriptRepository {
private static final String FIELD_UPDATED_AT = "updatedAt";
private static final String FIELD_CREATED_BY = "createdBy";
private static final String FIELD_TYPE = "type";
+ private static final String FIELD_TEST_SCRIPT = "testScript";
// tag_assignments fields used to resolve the tagIds filter into script ids.
private static final String FIELD_TA_TAG_ID = "tagId";
@@ -71,6 +73,9 @@ public class CustomScriptRepositoryImpl implements CustomScriptRepository {
private final MongoTemplate mongoTemplate;
+ @Value("${openframe.rmm.test-mode.enabled}")
+ private boolean testModeEnabled;
+
@Override
public List