From 8bb0a99f0367f8064c12f349c080dd398afd38ee Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 24 Mar 2026 09:22:55 +0000 Subject: [PATCH] Align monitored system aliases with latest signal --- .../v6/internal/subsystems/agent-lifecycle.md | 4 +- .../v6/internal/subsystems/api-contracts.md | 5 ++ .../internal/subsystems/storage-recovery.md | 4 +- internal/api/contract_test.go | 69 +++++++++++++++++++ internal/api/monitored_system_ledger.go | 31 +++++---- internal/api/monitored_system_ledger_test.go | 38 ++++++++++ 6 files changed, 136 insertions(+), 15 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 7d99a6e8e..814abe38d 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -245,7 +245,9 @@ left only as rollout compatibility fields rather than promises that every grouped source is healthy at that moment. Lifecycle-adjacent consumers must not label it with generic single-source health wording, and should use the canonical object when they need attribution for which grouped surface reported -most recently. +most recently. When those flat fields still appear during rollout, they should +be treated only as aliases for `latest_included_signal.at` and +`latest_included_signal.source`, not as an independent lifecycle signal. Lifecycle-adjacent workspace copy must also keep the same commercial framing: infrastructure operations may point operators to Pulse Pro for billing, but it must describe that boundary in monitored-system, plan-limit, and license-status diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 189b70d6a..fdd8ca3b8 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -259,6 +259,11 @@ identify exactly which included top-level surface reported most recently. remain rollout compatibility fields only on the raw wire shape; normalized frontend clients should treat the object as the primary contract and must not re-export those flat aliases in their public response model. +When those flat fields are still emitted for rollout compatibility, the backend +must derive them directly from the canonical object rather than from a parallel +timestamp/source path, so `latest_included_signal_at` and `last_seen` mirror +`latest_included_signal.at` and `latest_included_signal_source` mirrors +`latest_included_signal.source`. That client contract must also fail closed when older or partial payloads omit the nested explanation object: the frontend may normalize missing explanation fields to empty reasons/surfaces plus a safe default summary, but it must not diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 7e45c933c..b44794002 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -384,7 +384,9 @@ left only as rollout compatibility fields rather than universal health timestamps. Storage- or recovery-adjacent consumers must not present that data with bare single-source `Last Seen` wording that hides grouped stale/offline conditions, and should use the canonical object when they need attribution for -which grouped surface most recently reported. +which grouped surface most recently reported. When those flat fields still +appear during rollout, they should be interpreted only as aliases for the +canonical object rather than as separate storage-facing freshness signals. That same shared `internal/api/` dependency now also assumes self-hosted commercial counting is canonical at the top-level monitored-system boundary: shared setup, deploy, entitlement, and API-backed monitoring helpers may not diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index b7f95aa0a..97405cdad 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -688,6 +688,75 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { assertJSONSnapshot(t, got, want) } +func TestContract_MonitoredSystemLedgerCompatibilityAliasesFollowLatestSignal(t *testing.T) { + entry := monitoredSystemLedgerEntry(unifiedresources.MonitoredSystemRecord{ + Name: "Tower", + Type: "host", + Status: unifiedresources.StatusWarning, + StatusExplanation: unifiedresources.MonitoredSystemStatusExplanation{ + Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", + Reasons: []unifiedresources.MonitoredSystemStatusReason{}, + }, + LastSeen: time.Date(2026, 3, 18, 17, 35, 0, 0, time.UTC), + LatestIncludedSignal: unifiedresources.MonitoredSystemLatestSignal{ + Name: "tower.local", + Type: "docker-host", + Source: "docker", + At: time.Date(2026, 3, 18, 17, 30, 0, 0, time.UTC), + }, + Source: "multiple", + Explanation: unifiedresources.MonitoredSystemGroupingExplanation{ + Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.", + Reasons: []unifiedresources.MonitoredSystemGroupingReason{}, + Surfaces: []unifiedresources.MonitoredSystemGroupingSurface{}, + }, + }) + + payload := MonitoredSystemLedgerResponse{ + Systems: []MonitoredSystemLedgerEntry{entry}, + Total: 1, + Limit: 5, + } + + got, err := json.Marshal(payload) + if err != nil { + t.Fatalf("marshal monitored system ledger response: %v", err) + } + + const want = `{ + "systems":[ + { + "name":"Tower", + "type":"host", + "status":"warning", + "status_explanation":{ + "summary":"At least one included source is stale, so Pulse marks this monitored system as warning.", + "reasons":[] + }, + "latest_included_signal":{ + "name":"tower.local", + "type":"docker-host", + "source":"docker", + "at":"2026-03-18T17:30:00Z" + }, + "latest_included_signal_at":"2026-03-18T17:30:00Z", + "latest_included_signal_source":"docker", + "last_seen":"2026-03-18T17:30:00Z", + "source":"multiple", + "explanation":{ + "summary":"Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.", + "reasons":[], + "surfaces":[] + } + } + ], + "total":1, + "limit":5 + }` + + assertJSONSnapshot(t, got, want) +} + func TestContract_ResolveAuthEnvPathUsesCanonicalRuntimeDataDir(t *testing.T) { envDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", envDir) diff --git a/internal/api/monitored_system_ledger.go b/internal/api/monitored_system_ledger.go index 6fe7f7f19..b7f97eebf 100644 --- a/internal/api/monitored_system_ledger.go +++ b/internal/api/monitored_system_ledger.go @@ -132,19 +132,7 @@ func (r *Router) handleMonitoredSystemLedger(w http.ResponseWriter, req *http.Re entries := make([]MonitoredSystemLedgerEntry, 0, len(systems)) for _, system := range systems { - status := normalizeStatus(string(system.Status)) - entries = append(entries, MonitoredSystemLedgerEntry{ - Name: system.Name, - Type: system.Type, - Status: status, - StatusExplanation: monitoredSystemLedgerStatusExplanation(system.StatusExplanation, status), - LatestIncludedSignal: monitoredSystemLedgerLatestSignal(system.LatestIncludedSignal), - LatestIncludedSignalAt: formatLastSeen(system.LastSeen), - LatestIncludedSignalSource: normalizeMonitoredSystemLedgerSource(system.LatestIncludedSignal.Source), - LastSeen: formatLastSeen(system.LastSeen), - Source: system.Source, - Explanation: monitoredSystemLedgerExplanation(system.Explanation), - }) + entries = append(entries, monitoredSystemLedgerEntry(system)) } limit := maxMonitoredSystemsLimitForContext(req.Context()) @@ -158,6 +146,23 @@ func (r *Router) handleMonitoredSystemLedger(w http.ResponseWriter, req *http.Re json.NewEncoder(w).Encode(resp.NormalizeCollections()) } +func monitoredSystemLedgerEntry(system unifiedresources.MonitoredSystemRecord) MonitoredSystemLedgerEntry { + status := normalizeStatus(string(system.Status)) + latestIncludedSignal := monitoredSystemLedgerLatestSignal(system.LatestIncludedSignal) + return MonitoredSystemLedgerEntry{ + Name: system.Name, + Type: system.Type, + Status: status, + StatusExplanation: monitoredSystemLedgerStatusExplanation(system.StatusExplanation, status), + LatestIncludedSignal: latestIncludedSignal, + LatestIncludedSignalAt: latestIncludedSignal.At, + LatestIncludedSignalSource: latestIncludedSignal.Source, + LastSeen: latestIncludedSignal.At, + Source: system.Source, + Explanation: monitoredSystemLedgerExplanation(system.Explanation), + } +} + // --------------------------------------------------------------------------- // Status helpers // --------------------------------------------------------------------------- diff --git a/internal/api/monitored_system_ledger_test.go b/internal/api/monitored_system_ledger_test.go index f723c0b7b..415b0a5f8 100644 --- a/internal/api/monitored_system_ledger_test.go +++ b/internal/api/monitored_system_ledger_test.go @@ -136,6 +136,44 @@ func TestMonitoredSystemLedgerStatusExplanation(t *testing.T) { } } +func TestMonitoredSystemLedgerEntryUsesLatestIncludedSignalForCompatibilityAliases(t *testing.T) { + got := monitoredSystemLedgerEntry(unifiedresources.MonitoredSystemRecord{ + Name: "Tower", + Type: "host", + Status: unifiedresources.StatusWarning, + StatusExplanation: unifiedresources.MonitoredSystemStatusExplanation{ + Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", + Reasons: []unifiedresources.MonitoredSystemStatusReason{}, + }, + LastSeen: time.Date(2026, 3, 23, 12, 5, 0, 0, time.UTC), + LatestIncludedSignal: unifiedresources.MonitoredSystemLatestSignal{ + Name: "tower.local", + Type: "docker-host", + Source: "docker", + At: time.Date(2026, 3, 23, 12, 0, 0, 0, time.UTC), + }, + Source: "multiple", + Explanation: unifiedresources.MonitoredSystemGroupingExplanation{ + Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.", + Reasons: []unifiedresources.MonitoredSystemGroupingReason{}, + Surfaces: []unifiedresources.MonitoredSystemGroupingSurface{}, + }, + }) + + if got.LatestIncludedSignal.At != "2026-03-23T12:00:00Z" { + t.Fatalf("expected canonical latest signal timestamp, got %+v", got.LatestIncludedSignal) + } + if got.LatestIncludedSignalAt != got.LatestIncludedSignal.At { + t.Fatalf("expected latest_included_signal_at to mirror latest_included_signal.at, got %+v", got) + } + if got.LastSeen != got.LatestIncludedSignal.At { + t.Fatalf("expected last_seen compatibility alias to mirror latest_included_signal.at, got %+v", got) + } + if got.LatestIncludedSignalSource != got.LatestIncludedSignal.Source { + t.Fatalf("expected latest_included_signal_source to mirror latest_included_signal.source, got %+v", got) + } +} + func TestMonitoredSystemLedgerResponseEmptyState(t *testing.T) { resp := EmptyMonitoredSystemLedgerResponse() data, err := json.Marshal(resp)