From 25b52c740c138766ea6d155905cd7090b3fa8b23 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Wed, 17 Jun 2026 12:06:08 +0100 Subject: [PATCH 01/14] Add ran_custom_mdm_command activity when a custom MDM command is run --- server/fleet/activities.go | 21 +++++++++ server/service/integration_mdm_test.go | 16 +++++++ server/service/mdm.go | 22 +++++++++- server/service/mdm_test.go | 61 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 0ca5689ab71..04b42dfeab7 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -836,6 +836,27 @@ func (a ActivityTypeRanScript) WasFromAutomation() bool { return a.PolicyID != nil || a.FromSetupExperience } +type ActivityTypeRanCustomMDMCommand struct { + HostID uint `json:"host_id"` + HostDisplayName string `json:"host_display_name"` + HostUUID string `json:"host_uuid"` + CommandUUID string `json:"command_uuid"` + RequestType string `json:"request_type"` + Platform string `json:"platform"` +} + +func (a ActivityTypeRanCustomMDMCommand) ActivityName() string { + return "ran_custom_mdm_command" +} + +func (a ActivityTypeRanCustomMDMCommand) HostIDs() []uint { + return []uint{a.HostID} +} + +func (a ActivityTypeRanCustomMDMCommand) HostOnly() bool { + return false +} + type ActivityTypeAddedScript struct { ScriptName string `json:"script_name"` TeamID *uint `json:"team_id" renameto:"fleet_id"` diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 968ae67d828..78197c27f90 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -9793,6 +9793,14 @@ func (s *integrationMDMTestSuite) TestRunMDMCommands() { require.NotEmpty(t, runResp.CommandUUID) require.Equal(t, "windows", runResp.Platform) require.Equal(t, "./SetValues", runResp.RequestType) + s.lastActivityMatches(fleet.ActivityTypeRanCustomMDMCommand{}.ActivityName(), fmt.Sprintf(`{ + "host_id": %d, + "host_display_name": %q, + "host_uuid": %q, + "command_uuid": %q, + "request_type": "./SetValues", + "platform": "windows" + }`, enrolledWindows.ID, enrolledWindows.DisplayName(), enrolledWindows.UUID, runResp.CommandUUID), 0) // valid macOS runResp = runMDMCommandResponse{} @@ -9803,6 +9811,14 @@ func (s *integrationMDMTestSuite) TestRunMDMCommands() { require.NotEmpty(t, runResp.CommandUUID) require.Equal(t, "darwin", runResp.Platform) require.Equal(t, "ShutDownDevice", runResp.RequestType) + s.lastActivityMatches(fleet.ActivityTypeRanCustomMDMCommand{}.ActivityName(), fmt.Sprintf(`{ + "host_id": %d, + "host_display_name": %q, + "host_uuid": %q, + "command_uuid": %q, + "request_type": "ShutDownDevice", + "platform": "darwin" + }`, enrolledMac.ID, enrolledMac.DisplayName(), enrolledMac.UUID, runResp.CommandUUID), 0) } func (s *integrationMDMTestSuite) TestUpdateMDMWindowsEnrollmentsHostUUID() { diff --git a/server/service/mdm.go b/server/service/mdm.go index ecab6cedfd5..decebe96d16 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -581,10 +581,28 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host // the rest is platform-specific (validation of command payload, enqueueing, etc.) switch commandPlatform { case "windows": - return svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, hostUUIDs) + result, err = svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, hostUUIDs) default: - return svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, hostUUIDs) + result, err = svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, hostUUIDs) } + if err != nil { + return nil, err + } + + for _, h := range hosts { + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeRanCustomMDMCommand{ + HostID: h.ID, + HostDisplayName: h.DisplayName(), + HostUUID: h.UUID, + CommandUUID: result.CommandUUID, + RequestType: result.RequestType, + Platform: result.Platform, + }); err != nil { + return nil, ctxerr.Wrap(ctx, err, "log activity for ran custom mdm command") + } + } + + return result, nil } // validateAppleMDMCommand validates an Apple MDM command before it is enqueued. diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index d14f1b225bd..30ba2d1387b 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -649,6 +649,67 @@ func TestRunMDMCommandValidations(t *testing.T) { } } +func TestRunMDMCommandCreatesActivity(t *testing.T) { + ds := new(mock.Store) + opts := &TestServerOpts{SkipCreateTestUsers: true} + svc, ctx := newTestService(t, ds, nil, nil, opts) + ctx = test.UserContext(ctx, test.UserAdmin) + + windowsHost := &fleet.Host{ + ID: 42, + UUID: "win-uuid-1", + Platform: "windows", + Hostname: "DESKTOP-TEST", + ComputerName: "DESKTOP-TEST", + } + + ds.ListHostsLiteByUUIDsFunc = func(_ context.Context, _ fleet.TeamFilter, _ []string) ([]*fleet.Host, error) { + return []*fleet.Host{windowsHost}, nil + } + ds.AreHostsConnectedToFleetMDMFunc = func(_ context.Context, _ []*fleet.Host) (map[string]bool, error) { + return map[string]bool{windowsHost.UUID: true}, nil + } + ds.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + MDM: fleet.MDM{WindowsEnabledAndConfigured: true}, + }, nil + } + ds.MDMWindowsInsertCommandForHostsFunc = func(_ context.Context, _ []string, _ *fleet.MDMWindowsCommand) error { + return nil + } + + var capturedActivity activity_api.ActivityDetails + opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error { + capturedActivity = act + return nil + } + + rawCmd := ` + 1 + + + ./FooBar + + + ` + encoded := base64.StdEncoding.EncodeToString([]byte(rawCmd)) + + _, err := svc.RunMDMCommand(ctx, encoded, []string{windowsHost.UUID}) + require.NoError(t, err) + + require.True(t, opts.ActivityMock.NewActivityFuncInvoked) + require.NotNil(t, capturedActivity) + + act, ok := capturedActivity.(*fleet.ActivityTypeRanCustomMDMCommand) + require.True(t, ok, "expected *fleet.ActivityTypeRanCustomMDMCommand, got %T", capturedActivity) + assert.Equal(t, windowsHost.ID, act.HostID) + assert.Equal(t, windowsHost.DisplayName(), act.HostDisplayName) + assert.Equal(t, windowsHost.UUID, act.HostUUID) + assert.Equal(t, "./FooBar", act.RequestType) + assert.Equal(t, "windows", act.Platform) + assert.NotEmpty(t, act.CommandUUID) +} + func TestRunMDMCommandSetRecoveryLockBlocked(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) From fe2abf858018d2cdb9100d9f3c578eca605bd9cc Mon Sep 17 00:00:00 2001 From: andymFleet Date: Wed, 17 Jun 2026 13:45:33 +0100 Subject: [PATCH 02/14] add change file --- changes/45640-apple-windows-mdm-command-activities | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/45640-apple-windows-mdm-command-activities diff --git a/changes/45640-apple-windows-mdm-command-activities b/changes/45640-apple-windows-mdm-command-activities new file mode 100644 index 00000000000..262d91e57d0 --- /dev/null +++ b/changes/45640-apple-windows-mdm-command-activities @@ -0,0 +1 @@ +* Add activity feed entry when a user runs a custom Apple or Windows MDM command, visible in both the global activity feed and the host's activity feed. From 023f6ce8ea6ef941730faab2cfbf06db838d6f60 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Wed, 17 Jun 2026 14:33:35 +0100 Subject: [PATCH 03/14] Skip activity for hosts where MDM command enqueue partially fails --- server/service/mdm.go | 7 ++++ server/service/mdm_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/server/service/mdm.go b/server/service/mdm.go index decebe96d16..a8e0c4f9474 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -589,7 +589,14 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host return nil, err } + failedUUIDs := make(map[string]struct{}, len(result.FailedUUIDs)) + for _, uuid := range result.FailedUUIDs { + failedUUIDs[uuid] = struct{}{} + } for _, h := range hosts { + if _, failed := failedUUIDs[h.UUID]; failed { + continue + } if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeRanCustomMDMCommand{ HostID: h.ID, HostDisplayName: h.DisplayName(), diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 30ba2d1387b..4de00247b15 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -29,7 +29,10 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml" nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki" + nanomdm_mdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" + nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils" + mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" @@ -710,6 +713,85 @@ func TestRunMDMCommandCreatesActivity(t *testing.T) { assert.NotEmpty(t, act.CommandUUID) } +// mockAPNSPusher implements nanomdm_push.Pusher for unit tests, returning a +// push failure for any UUID in failUUIDs and success for all others. +type mockAPNSPusher struct { + failUUIDs map[string]bool +} + +func (m *mockAPNSPusher) Push(_ context.Context, ids []string) (map[string]*nanomdm_push.Response, error) { + result := make(map[string]*nanomdm_push.Response, len(ids)) + for _, id := range ids { + if m.failUUIDs[id] { + result[id] = &nanomdm_push.Response{Err: errors.New("push failed")} + } else { + result[id] = &nanomdm_push.Response{} + } + } + return result, nil +} + +func TestRunMDMCommandSkipsActivityForFailedHosts(t *testing.T) { + ds := new(mock.Store) + + mdmStorage := &mdmmock.MDMAppleStore{} + mdmStorage.EnqueueCommandFunc = func(_ context.Context, _ []string, _ *nanomdm_mdm.CommandWithSubtype) (map[string]error, error) { + return nil, nil + } + + host1 := &fleet.Host{ID: 1, UUID: "apple-uuid-1", Platform: "darwin", Hostname: "mac1", ComputerName: "mac1"} + host2 := &fleet.Host{ID: 2, UUID: "apple-uuid-2", Platform: "darwin", Hostname: "mac2", ComputerName: "mac2"} + + opts := &TestServerOpts{ + SkipCreateTestUsers: true, + MDMStorage: mdmStorage, + MDMPusher: &mockAPNSPusher{failUUIDs: map[string]bool{host2.UUID: true}}, + } + svc, ctx := newTestService(t, ds, nil, nil, opts) + ctx = test.UserContext(ctx, test.UserAdmin) + + ds.ListHostsLiteByUUIDsFunc = func(_ context.Context, _ fleet.TeamFilter, _ []string) ([]*fleet.Host, error) { + return []*fleet.Host{host1, host2}, nil + } + ds.AreHostsConnectedToFleetMDMFunc = func(_ context.Context, _ []*fleet.Host) (map[string]bool, error) { + return map[string]bool{host1.UUID: true, host2.UUID: true}, nil + } + ds.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + + var capturedActivities []*fleet.ActivityTypeRanCustomMDMCommand + opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error { + if a, ok := act.(*fleet.ActivityTypeRanCustomMDMCommand); ok { + capturedActivities = append(capturedActivities, a) + } + return nil + } + + rawCmd := ` + + + + CommandUUID + test-partial-fail-001 + Command + + RequestType + ShutDownDevice + + +` + encoded := base64.StdEncoding.EncodeToString([]byte(rawCmd)) + + _, err := svc.RunMDMCommand(ctx, encoded, []string{host1.UUID, host2.UUID}) + require.NoError(t, err) + + require.Len(t, capturedActivities, 1, "expected activity for 1 successful host only") + assert.Equal(t, host1.ID, capturedActivities[0].HostID) + assert.Equal(t, host1.UUID, capturedActivities[0].HostUUID) + assert.Equal(t, "ShutDownDevice", capturedActivities[0].RequestType) +} + func TestRunMDMCommandSetRecoveryLockBlocked(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) From 2b01096da8bd1f92aa549be3df4d1370bdee77c8 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Wed, 17 Jun 2026 15:07:15 +0100 Subject: [PATCH 04/14] Log activity errors instead of returning them after successful MDM command enqueue --- server/service/mdm.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/service/mdm.go b/server/service/mdm.go index a8e0c4f9474..0117f035144 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -605,7 +605,10 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host RequestType: result.RequestType, Platform: result.Platform, }); err != nil { - return nil, ctxerr.Wrap(ctx, err, "log activity for ran custom mdm command") + // Activity logging is best-effort: the command was already enqueued + // successfully, so returning an error here could cause clients to retry + // and send duplicate MDM commands to devices. + svc.logger.ErrorContext(ctx, "failed to log activity for ran custom mdm command", "err", err, "host_uuid", h.UUID) } } From 95414782d02f3c467f4663420f312a325613d3d0 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Wed, 17 Jun 2026 15:23:25 +0100 Subject: [PATCH 05/14] Assert activity user in MDM command tests --- server/service/mdm_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 4de00247b15..e474ffaa44e 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -681,8 +681,10 @@ func TestRunMDMCommandCreatesActivity(t *testing.T) { return nil } + var capturedUser *activity_api.User var capturedActivity activity_api.ActivityDetails - opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error { + opts.ActivityMock.NewActivityFunc = func(_ context.Context, u *activity_api.User, act activity_api.ActivityDetails) error { + capturedUser = u capturedActivity = act return nil } @@ -711,6 +713,10 @@ func TestRunMDMCommandCreatesActivity(t *testing.T) { assert.Equal(t, "./FooBar", act.RequestType) assert.Equal(t, "windows", act.Platform) assert.NotEmpty(t, act.CommandUUID) + + require.NotNil(t, capturedUser) + assert.Equal(t, test.UserAdmin.ID, capturedUser.ID) + assert.Equal(t, test.UserAdmin.Email, capturedUser.Email) } // mockAPNSPusher implements nanomdm_push.Pusher for unit tests, returning a @@ -761,9 +767,11 @@ func TestRunMDMCommandSkipsActivityForFailedHosts(t *testing.T) { } var capturedActivities []*fleet.ActivityTypeRanCustomMDMCommand - opts.ActivityMock.NewActivityFunc = func(_ context.Context, _ *activity_api.User, act activity_api.ActivityDetails) error { + var capturedUsers []*activity_api.User + opts.ActivityMock.NewActivityFunc = func(_ context.Context, u *activity_api.User, act activity_api.ActivityDetails) error { if a, ok := act.(*fleet.ActivityTypeRanCustomMDMCommand); ok { capturedActivities = append(capturedActivities, a) + capturedUsers = append(capturedUsers, u) } return nil } @@ -790,6 +798,10 @@ func TestRunMDMCommandSkipsActivityForFailedHosts(t *testing.T) { assert.Equal(t, host1.ID, capturedActivities[0].HostID) assert.Equal(t, host1.UUID, capturedActivities[0].HostUUID) assert.Equal(t, "ShutDownDevice", capturedActivities[0].RequestType) + + require.NotNil(t, capturedUsers[0]) + assert.Equal(t, test.UserAdmin.ID, capturedUsers[0].ID) + assert.Equal(t, test.UserAdmin.Email, capturedUsers[0].Email) } func TestRunMDMCommandSetRecoveryLockBlocked(t *testing.T) { From 460655b358ea78f94f14c727c9e0e92a03a8d8d9 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Thu, 18 Jun 2026 09:33:41 +0100 Subject: [PATCH 06/14] Enqueue MDM commands using resolved host UUIDs, not raw request input --- server/service/mdm.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/service/mdm.go b/server/service/mdm.go index 0117f035144..ecdaf97b9fe 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -578,12 +578,20 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host } } + // Use UUIDs from the resolved hosts so the enqueue and activity creation + // operate on the same validated set, not the raw (potentially duplicate or + // unknown) request input. + resolvedUUIDs := make([]string, len(hosts)) + for i, h := range hosts { + resolvedUUIDs[i] = h.UUID + } + // the rest is platform-specific (validation of command payload, enqueueing, etc.) switch commandPlatform { case "windows": - result, err = svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, hostUUIDs) + result, err = svc.enqueueMicrosoftMDMCommand(ctx, rawXMLCmd, resolvedUUIDs) default: - result, err = svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, hostUUIDs) + result, err = svc.enqueueAppleMDMCommand(ctx, rawXMLCmd, resolvedUUIDs) } if err != nil { return nil, err From 8ed91ed4bb6e6bca8cbffdbff95eaaa0a0a3e5b6 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Thu, 18 Jun 2026 10:06:48 +0100 Subject: [PATCH 07/14] Use commandPlatform for activity platform field --- server/service/mdm.go | 2 +- server/service/mdm_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/service/mdm.go b/server/service/mdm.go index ecdaf97b9fe..a9fcf8e2d33 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -611,7 +611,7 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host HostUUID: h.UUID, CommandUUID: result.CommandUUID, RequestType: result.RequestType, - Platform: result.Platform, + Platform: commandPlatform, }); err != nil { // Activity logging is best-effort: the command was already enqueued // successfully, so returning an error here could cause clients to retry diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index e474ffaa44e..6c833013f46 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -798,6 +798,7 @@ func TestRunMDMCommandSkipsActivityForFailedHosts(t *testing.T) { assert.Equal(t, host1.ID, capturedActivities[0].HostID) assert.Equal(t, host1.UUID, capturedActivities[0].HostUUID) assert.Equal(t, "ShutDownDevice", capturedActivities[0].RequestType) + assert.Equal(t, "darwin", capturedActivities[0].Platform) require.NotNil(t, capturedUsers[0]) assert.Equal(t, test.UserAdmin.ID, capturedUsers[0].ID) From fb1f40efa4e6fb9074aace8b1a5dc282633538ab Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 10:43:19 +0100 Subject: [PATCH 08/14] Add ran_custom_mdm_command activity feed UI --- frontend/interfaces/activity.ts | 6 +- .../cards/ActivityFeed/ActivityFeed.tsx | 74 +++++++++++++- .../GlobalActivityItem/GlobalActivityItem.tsx | 15 +++ .../CommandDetailsModal.tsx | 20 ++-- .../components/CommandDetailsModal/index.ts | 2 +- .../HostDetailsPage/HostDetailsPage.tsx | 97 ++++++++++++++++++- .../details/cards/Activity/ActivityConfig.tsx | 2 + .../RanCustomMdmCommandActivityItem.tsx | 30 ++++++ .../RanCustomMdmCommandActivityItem/index.ts | 1 + frontend/utilities/helpers.tsx | 23 +++++ 10 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/RanCustomMdmCommandActivityItem/RanCustomMdmCommandActivityItem.tsx create mode 100644 frontend/pages/hosts/details/cards/Activity/ActivityItems/RanCustomMdmCommandActivityItem/index.ts diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index df0c6c37ffc..00f9f3b459a 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -108,6 +108,7 @@ export enum ActivityType { EnabledWindowsMdmMigration = "enabled_windows_mdm_migration", DisabledWindowsMdmMigration = "disabled_windows_mdm_migration", RanScript = "ran_script", + RanCustomMdmCommand = "ran_custom_mdm_command", RanScriptBatch = "ran_script_batch", ScheduledScriptBatch = "scheduled_script_batch", CanceledScriptBatch = "canceled_script_batch", @@ -215,7 +216,8 @@ export type IHostPastActivityType = | ActivityType.CreatedManagedLocalAccount | ActivityType.RotatedManagedLocalAccountPassword | ActivityType.FailedToRotateManagedLocalAccountPassword - | ActivityType.FailedEnrollmentProfileRenewal; + | ActivityType.FailedEnrollmentProfileRenewal + | ActivityType.RanCustomMdmCommand; /** This is a subset of ActivityType that are shown only for the host upcoming activities */ export type IHostUpcomingActivityType = @@ -297,6 +299,7 @@ export interface IActivityDetails { query_ids?: number[]; query_name?: string; query_sql?: string; + request_type?: string; role?: UserRole; script_execution_id?: string; script_name?: string; @@ -469,6 +472,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { locked_host: "Locked host", mdm_enrolled: "MDM turned on", mdm_unenrolled: "MDM turned off", + ran_custom_mdm_command: "Ran custom MDM command", ran_script: "Ran script", ran_script_batch: "Bulk ran script", scheduled_script_batch: "Scheduled script batch", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 555fd66563e..347ac34d084 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -1,4 +1,5 @@ import React, { useMemo, useRef, useState } from "react"; +import { formatDistanceToNow } from "date-fns"; import { useQuery } from "react-query"; import { isEmpty } from "lodash"; import { InjectedRouter } from "react-router"; @@ -21,7 +22,11 @@ import { } from "interfaces/activity"; import { PerformanceImpactIndicator } from "interfaces/schedulable_query"; -import { getPerformanceImpactDescription } from "utilities/helpers"; +import { + formatMdmCommandNameForActivityItem, + getMdmCommandDisplayName, + getPerformanceImpactDescription, +} from "utilities/helpers"; import ShowQueryModal from "components/modals/ShowQueryModal"; import DataError from "components/DataError"; @@ -41,6 +46,11 @@ import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers"; import FailedEnrollmentProfileModal, { IFailedEnrollmentProfileModalProps, } from "components/modals/FailedEnrollmentProfileModal"; +import MdmCommandDetailsModal, { + GetIconName, + getVerbForCommandStatus, +} from "pages/hosts/components/CommandDetailsModal"; +import IconStatusMessage from "components/IconStatusMessage"; import GlobalActivityItem from "./GlobalActivityItem"; import ActivityAutomationDetailsModal from "./components/ActivityAutomationDetailsModal"; @@ -146,6 +156,11 @@ const ActivityFeed = ({ enrollmentProfileFailedDetails, setEnrollmentProfileFailedDetails, ] = useState | null>(null); + const [mdmCommandActivityDetails, setMdmCommandActivityDetails] = useState<{ + host_uuid?: string; + command_uuid: string; + actor_full_name?: string; + } | null>(null); const [searchQuery, setSearchQuery] = useState(""); const [createdAtDirection, setCreatedAtDirection] = useState("desc"); @@ -314,6 +329,13 @@ const ActivityFeed = ({ }, }); break; + case ActivityType.RanCustomMdmCommand: + setMdmCommandActivityDetails({ + command_uuid: details?.command_uuid || "", + host_uuid: details?.host_uuid, + actor_full_name, + }); + break; default: break; } @@ -483,6 +505,56 @@ const ActivityFeed = ({ onDone={() => setEnrollmentProfileFailedDetails(null)} /> )} + {!!mdmCommandActivityDetails && ( + { + const isPending = GetIconName(result.status) === "pending-outline"; + const cmdDisplayName = getMdmCommandDisplayName( + result.request_type + ); + const timeAgo = result.updated_at + ? ` (${formatDistanceToNow(new Date(result.updated_at), { + addSuffix: true, + })})` + : ""; + return ( + + {cmdDisplayName ? ( + <> + {"The "} + {cmdDisplayName} + {" custom MDM command"} + + ) : ( + "A custom MDM command" + )} + {" is pending on "} + {result.hostname} + {`${timeAgo}.`} + + ) : ( + + {mdmCommandActivityDetails.actor_full_name} + {` ${getVerbForCommandStatus(result.status)} `} + {formatMdmCommandNameForActivityItem(result.request_type)} + {" on "} + {result.hostname} + {"."} + + ) + } + /> + ); + }} + onDone={() => setMdmCommandActivityDetails(null)} + /> + )} ); }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 5bafa7ec4bb..bce6bb1010c 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -19,6 +19,7 @@ import { SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; import { + formatMdmCommandNameForActivityItem, formatScriptNameForActivityItem, getPerformanceImpactDescription, } from "utilities/helpers"; @@ -31,6 +32,7 @@ import { API_NO_TEAM_ID } from "interfaces/team"; const baseClass = "global-activity-item"; const ACTIVITIES_WITH_DETAILS = new Set([ + ActivityType.RanCustomMdmCommand, ActivityType.RanScript, ActivityType.AddedSoftware, ActivityType.EditedSoftware, @@ -1094,6 +1096,16 @@ const TAGGED_TEMPLATES = { ); }, + ranCustomMdmCommand: (activity: IActivity) => { + const { request_type, host_display_name } = activity.details || {}; + return ( + <> + {" "} + ran {formatMdmCommandNameForActivityItem(request_type)} on{" "} + {host_display_name}. + + ); + }, ranScript: (activity: IActivity) => { const { script_name, host_display_name, from_setup_experience } = activity.details || {}; @@ -2390,6 +2402,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.DisabledWindowsMdmMigration: { return TAGGED_TEMPLATES.disabledWindowsMdmMigration(); } + case ActivityType.RanCustomMdmCommand: { + return TAGGED_TEMPLATES.ranCustomMdmCommand(activity); + } case ActivityType.RanScript: { return TAGGED_TEMPLATES.ranScript(activity); } diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx index f3f536283f8..56573112dca 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx @@ -23,23 +23,31 @@ import Button from "components/buttons/Button"; const baseClass = "command-details-modal"; export const GetIconName = (status: string): IconNames => { + // Apple MDM status strings switch (status) { case "Error": - return "error"; case "CommandFormatError": return "error"; case "Acknowledged": return "success"; case "Pending": - return "pending-outline"; case "NotNow": return "pending-outline"; - default: - // FIXME: update for other platforms and design appropriate default handling for unknown - // statuses; for now, just return warning icon to indicate unknown state - return "warning"; + break; } + // Windows OMA-DM status codes (numeric strings): 101 = pending, 200-399 = ran, 400+ = failed + const code = parseInt(status, 10); + if (!Number.isNaN(code)) { + if (code >= 400) return "error"; + if (code >= 200) return "success"; + return "pending-outline"; + } + return "warning"; +}; + +export const getVerbForCommandStatus = (status: string): string => { + return GetIconName(status) === "error" ? "failed to run" : "ran"; }; const getStatusMessage = (result: ICommandResult): React.ReactNode => { diff --git a/frontend/pages/hosts/components/CommandDetailsModal/index.ts b/frontend/pages/hosts/components/CommandDetailsModal/index.ts index 31e35d5a58f..27db53e049e 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/index.ts +++ b/frontend/pages/hosts/components/CommandDetailsModal/index.ts @@ -1,2 +1,2 @@ export { default } from "./CommandDetailsModal"; -export { GetIconName } from "./CommandDetailsModal"; +export { GetIconName, getVerbForCommandStatus } from "./CommandDetailsModal"; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index bcf7c0b0f29..3e007f1ba9e 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -1,4 +1,5 @@ import React, { useContext, useState, useCallback, useEffect } from "react"; +import { formatDistanceToNow } from "date-fns"; import { Params, InjectedRouter } from "react-router/lib/Router"; import { useQuery } from "react-query"; import { useErrorHandler } from "react-error-boundary"; @@ -40,7 +41,12 @@ import { import { FLEET_FILEVAULT_PROFILE_DISPLAY_NAME } from "interfaces/mdm"; import { ICommand } from "interfaces/command"; -import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; +import { + formatMdmCommandNameForActivityItem, + getMdmCommandDisplayName, + normalizeEmptyValues, + wrapFleetHelper, +} from "utilities/helpers"; import permissions from "utilities/permissions"; import { DOCUMENT_TITLE_SUFFIX, @@ -91,7 +97,11 @@ import CertificateInstallDetailsModal, { } from "components/ActivityDetails/InstallDetails/CertificateInstallDetailsModal"; import { getDisplayedSoftwareName } from "pages/SoftwarePage/helpers"; -import CommandResultsModal from "pages/hosts/components/CommandDetailsModal"; +import CommandResultsModal, { + GetIconName, + getVerbForCommandStatus, +} from "pages/hosts/components/CommandDetailsModal"; +import IconStatusMessage from "components/IconStatusMessage"; import FailedEnrollmentProfileModal, { IFailedEnrollmentProfileModalProps, } from "components/modals/FailedEnrollmentProfileModal"; @@ -284,6 +294,11 @@ const HostDetailsPage = ({ const [mdmCommandDetails, setMdmCommandDetails] = useState( null ); + const [activityCommandDetails, setActivityCommandDetails] = useState<{ + host_uuid?: string; + command_uuid: string; + actor_full_name?: string; + } | null>(null); const [ enrollmentProfileFailedDetails, setEnrollmentProfileFailedDetails, @@ -892,6 +907,13 @@ const HostDetailsPage = ({ }, }); break; + case ActivityType.RanCustomMdmCommand: + setActivityCommandDetails({ + command_uuid: details?.command_uuid || "", + host_uuid: details?.host_uuid, + actor_full_name, + }); + break; default: // do nothing } }, @@ -1833,9 +1855,80 @@ const HostDetailsPage = ({ {!!mdmCommandDetails && ( ( + + {formatMdmCommandNameForActivityItem( + result.request_type + )} + {" on "} + {result.hostname} + {"."} + + } + /> + ) + : undefined + } onDone={onCancelMdmCommandDetailsModal} /> )} + {!!activityCommandDetails && ( + { + const isPending = + GetIconName(result.status) === "pending-outline"; + const cmdDisplayName = getMdmCommandDisplayName( + result.request_type + ); + const timeAgo = result.updated_at + ? ` (${formatDistanceToNow(new Date(result.updated_at), { + addSuffix: true, + })})` + : ""; + return ( + + {cmdDisplayName ? ( + <> + {"The "} + {cmdDisplayName} + {" custom MDM command"} + + ) : ( + "A custom MDM command" + )} + {" is pending on "} + {result.hostname} + {`${timeAgo}.`} + + ) : ( + + {activityCommandDetails.actor_full_name} + {` ${getVerbForCommandStatus(result.status)} `} + {formatMdmCommandNameForActivityItem( + result.request_type + )} + {" on this host."} + + ) + } + /> + ); + }} + onDone={() => setActivityCommandDetails(null)} + /> + )} {enrollmentProfileFailedDetails && ( { + return ( + + {activity.actor_full_name} + {" ran "} + {formatMdmCommandNameForActivityItem(activity.details?.request_type)} + {" on this host."} + + ); +}; + +export default RanCustomMdmCommandActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanCustomMdmCommandActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanCustomMdmCommandActivityItem/index.ts new file mode 100644 index 00000000000..bbcb4c30868 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RanCustomMdmCommandActivityItem/index.ts @@ -0,0 +1 @@ +export { default } from "./RanCustomMdmCommandActivityItem"; diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index 4fe433d1a4d..87fd8ee5548 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -450,6 +450,29 @@ export const formatScriptNameForActivityItem = (name: string | undefined) => { ); }; +export const getMdmCommandDisplayName = ( + requestType: string | undefined +): string => { + if (!requestType) return ""; + const segments = requestType.split("/"); + const lastSegment = segments[segments.length - 1]; + return segments.length > 1 ? `...${lastSegment}` : requestType; +}; + +export const formatMdmCommandNameForActivityItem = ( + requestType: string | undefined +) => { + const displayName = getMdmCommandDisplayName(requestType); + if (!displayName) { + return <>a custom MDM command; + } + return ( + <> + {displayName} as a custom MDM command + + ); +}; + export const generateRole = ( teams: ITeam[], globalRole: UserRole | null From 250bc09a5599965cd75970d2656b59a9e829301f Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 11:10:36 +0100 Subject: [PATCH 09/14] Fix empty bold element when actor_full_name is missing in MDM command modal --- .../pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx | 4 +++- .../pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx | 4 +++- frontend/utilities/helpers.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 347ac34d084..03140bfe8d5 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -540,7 +540,9 @@ const ActivityFeed = ({ ) : ( - {mdmCommandActivityDetails.actor_full_name} + {mdmCommandActivityDetails.actor_full_name && ( + {mdmCommandActivityDetails.actor_full_name} + )} {` ${getVerbForCommandStatus(result.status)} `} {formatMdmCommandNameForActivityItem(result.request_type)} {" on "} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 3e007f1ba9e..3866df90287 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -1914,7 +1914,9 @@ const HostDetailsPage = ({ ) : ( - {activityCommandDetails.actor_full_name} + {activityCommandDetails.actor_full_name && ( + {activityCommandDetails.actor_full_name} + )} {` ${getVerbForCommandStatus(result.status)} `} {formatMdmCommandNameForActivityItem( result.request_type diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index 87fd8ee5548..f0f5936fb99 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -456,7 +456,7 @@ export const getMdmCommandDisplayName = ( if (!requestType) return ""; const segments = requestType.split("/"); const lastSegment = segments[segments.length - 1]; - return segments.length > 1 ? `...${lastSegment}` : requestType; + return segments.length > 1 ? `.../${lastSegment}` : requestType; }; export const formatMdmCommandNameForActivityItem = ( From eb93746577b6b7a3cbe728378b552315d664c4e7 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 11:43:04 +0100 Subject: [PATCH 10/14] Guard against missing command_uuid and host_uuid in MDM command activity modal --- .../cards/ActivityFeed/ActivityFeed.tsx | 8 ++++++-- .../details/HostDetailsPage/HostDetailsPage.tsx | 13 +++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 03140bfe8d5..89cfcaa21de 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -329,13 +329,17 @@ const ActivityFeed = ({ }, }); break; - case ActivityType.RanCustomMdmCommand: + case ActivityType.RanCustomMdmCommand: { + if (!details?.command_uuid) { + break; + } setMdmCommandActivityDetails({ - command_uuid: details?.command_uuid || "", + command_uuid: details.command_uuid, host_uuid: details?.host_uuid, actor_full_name, }); break; + } default: break; } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 3866df90287..6581f3148c0 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -295,7 +295,7 @@ const HostDetailsPage = ({ null ); const [activityCommandDetails, setActivityCommandDetails] = useState<{ - host_uuid?: string; + host_uuid: string; command_uuid: string; actor_full_name?: string; } | null>(null); @@ -907,13 +907,18 @@ const HostDetailsPage = ({ }, }); break; - case ActivityType.RanCustomMdmCommand: + case ActivityType.RanCustomMdmCommand: { + const resolvedHostUuid = details?.host_uuid ?? host?.uuid; + if (!details?.command_uuid || !resolvedHostUuid) { + break; + } setActivityCommandDetails({ - command_uuid: details?.command_uuid || "", - host_uuid: details?.host_uuid, + command_uuid: details.command_uuid, + host_uuid: resolvedHostUuid, actor_full_name, }); break; + } default: // do nothing } }, From 88601dc023dedd7102eb31fded8ca7467ba67ae8 Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 11:54:41 +0100 Subject: [PATCH 11/14] Fix getMdmCommandDisplayName dropping empty last segment for trailing-slash paths --- frontend/utilities/helpers.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index f0f5936fb99..c80e9ab1316 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -454,9 +454,10 @@ export const getMdmCommandDisplayName = ( requestType: string | undefined ): string => { if (!requestType) return ""; - const segments = requestType.split("/"); + const segments = requestType.split("/").filter(Boolean); + if (segments.length === 0) return requestType; const lastSegment = segments[segments.length - 1]; - return segments.length > 1 ? `.../${lastSegment}` : requestType; + return segments.length > 1 ? `.../${lastSegment}` : lastSegment; }; export const formatMdmCommandNameForActivityItem = ( From 48db4cede28653829e4f0006c6d070948624fcdc Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 13:12:24 +0100 Subject: [PATCH 12/14] Use accurate verb based on MDM command status icon in activity modal --- .../CommandDetailsModal/CommandDetailsModal.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx index 56573112dca..f9bf538333c 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tsx @@ -47,7 +47,18 @@ export const GetIconName = (status: string): IconNames => { }; export const getVerbForCommandStatus = (status: string): string => { - return GetIconName(status) === "error" ? "failed to run" : "ran"; + const icon = GetIconName(status); + switch (icon) { + case "error": + return "failed to run"; + case "success": + return "ran"; + case "pending-outline": + return "sent"; + default: + // unknown status + return "sent"; + } }; const getStatusMessage = (result: ICommandResult): React.ReactNode => { From 42f27e2404b7e899835606e05be7d3b57391841f Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 13:51:19 +0100 Subject: [PATCH 13/14] Add additional tests for custom MDM command activity --- .../GlobalActivityItem.tests.tsx | 25 ++++++++ .../CommandDetailsModal.tests.tsx | 61 +++++++++++++++++++ frontend/utilities/helpers.tests.tsx | 29 +++++++++ 3 files changed, 115 insertions(+) create mode 100644 frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx index 06c316705c1..39b1c4dbe4d 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx @@ -2181,4 +2181,29 @@ describe("Activity Feed", () => { screen.getByText(/installed all the software in self-service/i) ).toBeInTheDocument(); }); + + it("renders a ran_custom_mdm_command activity with a command name", () => { + const activity = createMockActivity({ + type: ActivityType.RanCustomMdmCommand, + details: { + request_type: "./Device/Vendor/MSFT/DMClient/Provider/DEMO/EntDMID", + host_display_name: "Huck's MacBook Pro", + }, + }); + render(); + + expect(screen.getByText(".../EntDMID")).toBeInTheDocument(); + expect(screen.getByText("Huck's MacBook Pro")).toBeInTheDocument(); + }); + + it("renders a ran_custom_mdm_command activity without a command name", () => { + const activity = createMockActivity({ + type: ActivityType.RanCustomMdmCommand, + details: { host_display_name: "Huck's MacBook Pro" }, + }); + render(); + + expect(screen.getByText(/a custom MDM command/i)).toBeInTheDocument(); + expect(screen.getByText("Huck's MacBook Pro")).toBeInTheDocument(); + }); }); diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx new file mode 100644 index 00000000000..41b5312d941 --- /dev/null +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx @@ -0,0 +1,61 @@ +import { GetIconName, getVerbForCommandStatus } from "./CommandDetailsModal"; + +describe("GetIconName", () => { + it("returns error for Apple Error status", () => { + expect(GetIconName("Error")).toEqual("error"); + }); + + it("returns error for Apple CommandFormatError status", () => { + expect(GetIconName("CommandFormatError")).toEqual("error"); + }); + + it("returns success for Apple Acknowledged status", () => { + expect(GetIconName("Acknowledged")).toEqual("success"); + }); + + it("returns pending-outline for Apple Pending status", () => { + expect(GetIconName("Pending")).toEqual("pending-outline"); + }); + + it("returns pending-outline for Apple NotNow status", () => { + expect(GetIconName("NotNow")).toEqual("pending-outline"); + }); + + it("returns success for Windows 200 status", () => { + expect(GetIconName("200")).toEqual("success"); + }); + + it("returns error for Windows 400 status", () => { + expect(GetIconName("400")).toEqual("error"); + }); + + it("returns error for Windows 500 status", () => { + expect(GetIconName("500")).toEqual("error"); + }); + + it("returns pending-outline for Windows 101 status", () => { + expect(GetIconName("101")).toEqual("pending-outline"); + }); + + it("returns warning for an unknown status", () => { + expect(GetIconName("unknown")).toEqual("warning"); + }); +}); + +describe("getVerbForCommandStatus", () => { + it("returns 'ran' for a successful status", () => { + expect(getVerbForCommandStatus("Acknowledged")).toEqual("ran"); + }); + + it("returns 'failed to run' for an error status", () => { + expect(getVerbForCommandStatus("Error")).toEqual("failed to run"); + }); + + it("returns 'sent' for a pending status", () => { + expect(getVerbForCommandStatus("Pending")).toEqual("sent"); + }); + + it("returns 'sent' for an unknown status", () => { + expect(getVerbForCommandStatus("unknown")).toEqual("sent"); + }); +}); diff --git a/frontend/utilities/helpers.tests.tsx b/frontend/utilities/helpers.tests.tsx index 9ce2fc93956..a8f4e8b7ab1 100644 --- a/frontend/utilities/helpers.tests.tsx +++ b/frontend/utilities/helpers.tests.tsx @@ -4,6 +4,7 @@ import helpers, { removeOSPrefix, compareVersions, willExpireWithinXDays, + getMdmCommandDisplayName, } from "./helpers"; describe("helpers utilities", () => { @@ -98,4 +99,32 @@ describe("helpers utilities", () => { expect(result.org_info).toEqual({ org_name: "Fleet" }); }); }); + + describe("getMdmCommandDisplayName function", () => { + it("returns empty string for undefined", () => { + expect(getMdmCommandDisplayName(undefined)).toEqual(""); + }); + + it("returns empty string for empty string", () => { + expect(getMdmCommandDisplayName("")).toEqual(""); + }); + + it("returns the value as-is for a simple command name with no path separator", () => { + expect(getMdmCommandDisplayName("DeviceInformation")).toEqual( + "DeviceInformation" + ); + }); + + it("truncates a multi-segment Windows OMA-URI path to the last segment", () => { + expect( + getMdmCommandDisplayName( + "./Device/Vendor/MSFT/DMClient/Provider/DEMO/EntDMID" + ) + ).toEqual(".../EntDMID"); + }); + + it("handles a trailing slash by ignoring the empty final segment", () => { + expect(getMdmCommandDisplayName("./Vendor/MSFT/")).toEqual(".../MSFT"); + }); + }); }); From dd452f1ff791a243b6b6aa90a8de81fa547b470d Mon Sep 17 00:00:00 2001 From: andymFleet Date: Fri, 19 Jun 2026 14:34:51 +0100 Subject: [PATCH 14/14] Add boundary tests for Windows MDM status code ranges in GetIconName --- .../CommandDetailsModal/CommandDetailsModal.tests.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx index 41b5312d941..5310a7f7394 100644 --- a/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx +++ b/frontend/pages/hosts/components/CommandDetailsModal/CommandDetailsModal.tests.tsx @@ -37,6 +37,14 @@ describe("GetIconName", () => { expect(GetIconName("101")).toEqual("pending-outline"); }); + it("returns pending-outline for Windows 199 status (upper pending boundary)", () => { + expect(GetIconName("199")).toEqual("pending-outline"); + }); + + it("returns success for Windows 399 status (upper success boundary)", () => { + expect(GetIconName("399")).toEqual("success"); + }); + it("returns warning for an unknown status", () => { expect(GetIconName("unknown")).toEqual("warning"); });