mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Ship active alerts as per-client keyed deltas
Second increment of the resource-payload-static-metadata gap: the full
activeAlerts array (~37-75KB at mock-estate churn) re-shipped whenever
any single alert changed. Alerts now ride the generalized keyed delta
transport: the per-field keying and transition handling in the state
delta engine is a declarative keyedDeltaFields list shared with the
connected-infrastructure projection, and activeAlertsDelta carries
id-keyed merge patches, removed ids, and order. Measured on the mock
estate the per-tick alert payload drops to 184-1981 bytes.
One boundary held deliberately fixed: alert application timing does not
change. The client applies alert deltas to an isolated baseline and
feeds the exact same applyActiveAlerts path immediately on arrival,
input-active or not — alert lifecycle truth never waits for the
operator-input idle gate (alerts subsystem boundary, documented in
6ecdc81d1), and the pending-acknowledgment safeguards see byte-
equivalent records. Alerts handed to the store are cloned so it never
adopts baseline-owned objects. Deltas without a baseline are ignored
and heal on the next full payload; unkeyable entries fall back to
whole-field diffing.
Remaining gap scope unchanged: snapshot-side static metadata
(canonicalIdentity alias history ~609KB per snapshot, and the partially
regressed aiSafeSummary/policy omissions) stays open under
resource-payload-static-metadata.
This commit is contained in:
@@ -6774,10 +6774,13 @@ Over the state WebSocket that projection now travels the same per-client
|
||||
keyed delta transport as resources: once a full payload establishes the
|
||||
client's baseline, `connectedInfrastructureDelta` carries id-keyed JSON merge
|
||||
patches plus removed ids and order instead of re-shipping the whole
|
||||
projection on every broadcast. An entry that cannot key by id drops the
|
||||
whole field back to plain whole-payload diffing, and a client without a
|
||||
baseline ignores the delta and heals on the next full payload, so the keyed
|
||||
path can never corrupt the projection.
|
||||
projection on every broadcast. `activeAlerts` rides the identical transport
|
||||
as `activeAlertsDelta`, with one boundary held fixed: the client applies
|
||||
alert deltas immediately on arrival — alert lifecycle truth never waits for
|
||||
the operator-input idle gate that defers resource work. An entry that cannot
|
||||
key by id drops the whole field back to plain whole-payload diffing, and a
|
||||
client without a baseline ignores the delta and heals on the next full
|
||||
payload, so the keyed path can never corrupt either array.
|
||||
That same install-command payload continuity now also applies when auth is
|
||||
optional: copied install and upgrade commands must omit token arguments
|
||||
entirely on token-optional Pulse instances rather than serializing a fake
|
||||
|
||||
@@ -257,7 +257,10 @@ id-keyed merge patches for changed items only (at 50-node scale, timestamp
|
||||
patches of a few hundred bytes instead of the full ~55KB projection every
|
||||
tick), and the client applies them to an isolated baseline then reconciles
|
||||
the store with reference-stable untouched items so the per-tick deep walk of
|
||||
the whole projection is gone. Workload table rows derived from canonical
|
||||
the whole projection is gone. Active alerts travel the same keyed transport
|
||||
(`activeAlertsDelta`, replacing the ~37KB whole-array re-ship whenever any
|
||||
alert changed) while their application stays immediate per the alerts
|
||||
subsystem boundary. Workload table rows derived from canonical
|
||||
snapshots reuse the previous row object whenever the serialized row is
|
||||
unchanged, and a refresh that changes nothing returns the previous row array
|
||||
itself, so per-tick row identity churn stays bounded to guests whose data
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "20503086ffc1eca6a4bb2121fe2c2ebd67160a0b",
|
||||
"verified_at": "2026-08-26T17:08:31Z",
|
||||
"base_sha": "f3d34346528fb981631238a394672d375d7092e9",
|
||||
"verified_at": "2026-08-26T17:23:36Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/stores/websocket.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/stores/websocket.ts": "c648fbef42e36c2ae8502d258a6da2ab009f3d6bb9718e36192ba05085c3b760"
|
||||
"frontend-modern/src/stores/websocket.ts": "a1f05a8bbe44d9e99349d9f3651304823458a04fc20949ecc305f5bc22bab9af"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview",
|
||||
"/settings/infrastructure"
|
||||
"/proxmox/overview"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
@@ -24,17 +23,15 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"isolated test backend built from this change serving the 50-node/929-guest mock estate over the state websocket",
|
||||
"fresh socket connection: initialState ships the full connectedInfrastructure projection once to establish the client baseline",
|
||||
"steady realtime ticks carrying connectedInfrastructureDelta merge patches with zero full projection re-ships",
|
||||
"Connected systems settings surface rendering the delta-maintained projection with live per-item freshness",
|
||||
"narrow 700px viewport rendering the same projection surface"
|
||||
"isolated test backend built from this change serving the mock estate with churning alerts (13 active / 62 unacknowledged)",
|
||||
"fresh socket: initialState ships the full activeAlerts array once to establish the client baseline",
|
||||
"steady realtime ticks carrying activeAlertsDelta merge patches with zero full alert-array re-ships",
|
||||
"alert badge and estate rendering live throughout on desktop and narrow viewports"
|
||||
],
|
||||
"interactions": [
|
||||
"raw websocket capture: ticks carry connectedInfrastructureDelta at 0.7-4KB of id+lastSeen merge patches instead of the ~55-60KB full projection previously re-shipped every tick; the full array shipped exactly once at initialState",
|
||||
"client message counting over 30s: 3 delta ticks consumed, 0 full projection re-ships, 140 workload rows mounted, no console errors",
|
||||
"Connected systems surface observed live at /settings/infrastructure: 71 systems with per-item freshness reading 1-5s ago, values that now only travel as merge patches, updating across ticks",
|
||||
"unit-verified reference stability: untouched projection items keep their store identity across a delta sync, so reconcile no longer deep-walks the whole projection per tick",
|
||||
"narrow 700px emulated viewport rendered the projection surface with live freshness text"
|
||||
"raw websocket capture over 40s: 4 activeAlertsDelta payloads at 184-1981 bytes replacing the previous ~37-75KB whole-array re-ship whenever any alert changed; zero full re-ships after initialState",
|
||||
"client consumed all delta ticks with 140 workload rows mounted and no console errors",
|
||||
"unit-verified: alert level escalation, new-alert arrival, and resolution removal propagate to the store immediately even while operator input is active, encoding the alerts-subsystem immediacy boundary",
|
||||
"narrow 700px emulated viewport rendered the alerts badge and estate live"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1133,6 +1133,76 @@ describe('websocket store unified resource contract', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('applies alert deltas immediately even while operator input is active', async () => {
|
||||
const { store, dispose } = await createStoreHarness();
|
||||
try {
|
||||
await waitForOpenTick();
|
||||
emitMessage({
|
||||
type: 'initialState',
|
||||
data: {
|
||||
resources: [],
|
||||
lastUpdate: 100,
|
||||
activeAlerts: [
|
||||
{ id: 'alert-1', type: 'cpu', level: 'warning', resourceId: 'vm-1', value: 85 },
|
||||
],
|
||||
recentlyResolved: [],
|
||||
},
|
||||
});
|
||||
expect(store.state.activeAlerts).toHaveLength(1);
|
||||
expect(store.state.activeAlerts[0]?.level).toBe('warning');
|
||||
|
||||
// Alert lifecycle truth does not wait for input idle: a delta arriving
|
||||
// mid-gesture escalates and adds alerts immediately.
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
emitMessage({
|
||||
type: 'rawData',
|
||||
data: {
|
||||
lastUpdate: 200,
|
||||
activeAlertsDelta: {
|
||||
upserts: [
|
||||
{ id: 'alert-1', level: 'critical', value: 97 },
|
||||
{ id: 'alert-2', type: 'memory', level: 'warning', resourceId: 'vm-2', value: 91 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const byId = new Map(store.state.activeAlerts.map((alert) => [alert.id, alert]));
|
||||
expect(byId.get('alert-1')?.level).toBe('critical');
|
||||
expect(byId.get('alert-1')?.value).toBe(97);
|
||||
expect(byId.get('alert-1')?.resourceId).toBe('vm-1');
|
||||
expect(byId.get('alert-2')?.level).toBe('warning');
|
||||
|
||||
// Resolution rides the same payload and is equally immediate.
|
||||
emitMessage({
|
||||
type: 'rawData',
|
||||
data: {
|
||||
lastUpdate: 300,
|
||||
activeAlertsDelta: { removed: ['alert-1'] },
|
||||
},
|
||||
});
|
||||
expect(store.state.activeAlerts.map((alert) => alert.id)).toEqual(['alert-2']);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('ignores alert deltas that arrive without an alert baseline', async () => {
|
||||
const { store, dispose } = await createStoreHarness();
|
||||
try {
|
||||
await waitForOpenTick();
|
||||
emitMessage({
|
||||
type: 'rawData',
|
||||
data: {
|
||||
lastUpdate: 200,
|
||||
activeAlertsDelta: { upserts: [{ id: 'alert-1', level: 'warning' }] },
|
||||
},
|
||||
});
|
||||
expect(store.state.activeAlerts).toEqual([]);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('drops scroll-queued deltas when a full snapshot supersedes their baseline', async () => {
|
||||
const { store, dispose } = await createStoreHarness();
|
||||
try {
|
||||
|
||||
@@ -464,6 +464,10 @@ export function createWebSocketStore(url: string) {
|
||||
let rawConnectedInfrastructure: ConnectedInfrastructureItem[] | null = null;
|
||||
const pendingInfrastructureIds = new Set<string>();
|
||||
let pendingInfrastructureFull = false;
|
||||
// Active alerts ride the same keyed delta transport, but their application
|
||||
// is never gated: alert lifecycle updates stay immediate during operator
|
||||
// input (alerts subsystem contract), so this baseline only slims the wire.
|
||||
let rawActiveAlerts: Alert[] | null = null;
|
||||
// lastUpdate is the realtime tick token every downstream consumer keys on
|
||||
// (unified resource projections, workload remaps). It defers with the rest
|
||||
// so a mid-interaction tick triggers no derived recomputation at all.
|
||||
@@ -569,6 +573,7 @@ export function createWebSocketStore(url: string) {
|
||||
rawConnectedInfrastructure = null;
|
||||
pendingInfrastructureIds.clear();
|
||||
pendingInfrastructureFull = false;
|
||||
rawActiveAlerts = null;
|
||||
deferredLastUpdate = null;
|
||||
lastFullStateRecoveryAt = 0;
|
||||
oversizedSnapshotObserved = false;
|
||||
@@ -1156,15 +1161,41 @@ export function createWebSocketStore(url: string) {
|
||||
}
|
||||
// Sync active alerts from state
|
||||
if (message.data.activeAlerts !== undefined) {
|
||||
const alertsArray =
|
||||
message.data.activeAlerts && Array.isArray(message.data.activeAlerts)
|
||||
? (message.data.activeAlerts as Alert[])
|
||||
: [];
|
||||
// A full payload re-establishes the keyed alert baseline.
|
||||
rawActiveAlerts = structuredClone(alertsArray);
|
||||
const newAlerts: Record<string, Alert> = {};
|
||||
if (message.data.activeAlerts && Array.isArray(message.data.activeAlerts)) {
|
||||
message.data.activeAlerts.forEach((alert: Alert) => {
|
||||
newAlerts[alert.id] = alert;
|
||||
});
|
||||
}
|
||||
alertsArray.forEach((alert: Alert) => {
|
||||
newAlerts[alert.id] = alert;
|
||||
});
|
||||
|
||||
lastActiveAlertsPayload = newAlerts;
|
||||
applyActiveAlerts(alertsEnabled ? newAlerts : {});
|
||||
} else if (
|
||||
'activeAlertsDelta' in message.data &&
|
||||
message.data.activeAlertsDelta !== undefined
|
||||
) {
|
||||
if (rawActiveAlerts !== null) {
|
||||
const appliedAlerts = applyKeyedStateDelta(
|
||||
rawActiveAlerts,
|
||||
message.data.activeAlertsDelta as KeyedStateDelta,
|
||||
);
|
||||
rawActiveAlerts = appliedAlerts.entries;
|
||||
// Cloned on the way out so the alerts store never adopts
|
||||
// baseline-owned objects. Applied immediately, input-active or
|
||||
// not: alert lifecycle truth does not wait for idle.
|
||||
const newAlerts: Record<string, Alert> = {};
|
||||
for (const alert of rawActiveAlerts) {
|
||||
newAlerts[alert.id] = structuredClone(alert);
|
||||
}
|
||||
lastActiveAlertsPayload = newAlerts;
|
||||
applyActiveAlerts(alertsEnabled ? newAlerts : {});
|
||||
}
|
||||
// No baseline: leave current alerts; the next full payload
|
||||
// (reconnect or snapshot) re-establishes it.
|
||||
}
|
||||
// Sync recently resolved alerts
|
||||
if (message.data.recentlyResolved !== undefined) {
|
||||
|
||||
@@ -11,20 +11,35 @@ import (
|
||||
const resourceDeltaField = "resourceDelta"
|
||||
const infrastructureField = "connectedInfrastructure"
|
||||
const infrastructureDeltaField = "connectedInfrastructureDelta"
|
||||
const activeAlertsField = "activeAlerts"
|
||||
const activeAlertsDeltaField = "activeAlertsDelta"
|
||||
|
||||
// Keyed delta fields are id-keyed arrays that travel as per-item merge
|
||||
// patches once a client baseline exists, instead of re-shipping the whole
|
||||
// array on every broadcast. When a payload cannot be keyed (an entry without
|
||||
// an id), it stays in fields and diffs whole, so the keyed path can never
|
||||
// corrupt the projection.
|
||||
var keyedDeltaFields = []struct {
|
||||
field string
|
||||
deltaField string
|
||||
}{
|
||||
{infrastructureField, infrastructureDeltaField},
|
||||
{activeAlertsField, activeAlertsDeltaField},
|
||||
}
|
||||
|
||||
type keyedFieldSnapshot struct {
|
||||
entries map[string]json.RawMessage
|
||||
order []string
|
||||
raw json.RawMessage
|
||||
}
|
||||
|
||||
type clientStateSnapshot struct {
|
||||
fields map[string]json.RawMessage
|
||||
resources map[string]json.RawMessage
|
||||
resourceOrder []string
|
||||
// The connected-infrastructure projection is keyed the same way as
|
||||
// resources so per-item merge patches replace re-shipping the whole
|
||||
// projection on every broadcast. When the payload cannot be keyed (an
|
||||
// entry without an id), it stays in fields and diffs whole, so the
|
||||
// keyed path can never corrupt the projection.
|
||||
infrastructure map[string]json.RawMessage
|
||||
infrastructureOrder []string
|
||||
infrastructureRaw json.RawMessage
|
||||
infrastructureKeyed bool
|
||||
// Keyed snapshots by field name; a field is present only when every
|
||||
// entry keyed by id.
|
||||
keyed map[string]*keyedFieldSnapshot
|
||||
}
|
||||
|
||||
type resourceDeltaPayload struct {
|
||||
@@ -87,20 +102,24 @@ func buildClientStateSnapshot(state interface{}) (*clientStateSnapshot, error) {
|
||||
fields: fields,
|
||||
resources: resources,
|
||||
resourceOrder: resourceOrder,
|
||||
keyed: make(map[string]*keyedFieldSnapshot),
|
||||
}
|
||||
|
||||
if encodedInfrastructure, ok := fields[infrastructureField]; ok {
|
||||
infrastructure, infrastructureOrder, keyErr := extractKeyedEntries(
|
||||
encodedInfrastructure,
|
||||
infrastructureField,
|
||||
)
|
||||
if keyErr == nil {
|
||||
snapshot.infrastructure = infrastructure
|
||||
snapshot.infrastructureOrder = infrastructureOrder
|
||||
snapshot.infrastructureRaw = append(json.RawMessage(nil), encodedInfrastructure...)
|
||||
snapshot.infrastructureKeyed = true
|
||||
delete(fields, infrastructureField)
|
||||
for _, keyedField := range keyedDeltaFields {
|
||||
encodedField, ok := fields[keyedField.field]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
entries, order, keyErr := extractKeyedEntries(encodedField, keyedField.field)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
snapshot.keyed[keyedField.field] = &keyedFieldSnapshot{
|
||||
entries: entries,
|
||||
order: order,
|
||||
raw: append(json.RawMessage(nil), encodedField...),
|
||||
}
|
||||
delete(fields, keyedField.field)
|
||||
}
|
||||
|
||||
return snapshot, nil
|
||||
@@ -175,35 +194,40 @@ func buildClientStateDelta(previous, current *clientStateSnapshot) (map[string]i
|
||||
delta[resourceDeltaField] = resourceDelta
|
||||
}
|
||||
|
||||
switch {
|
||||
case previous.infrastructureKeyed && current.infrastructureKeyed:
|
||||
infrastructureDelta, err := buildKeyedArrayDelta(
|
||||
previous.infrastructure,
|
||||
current.infrastructure,
|
||||
previous.infrastructureOrder,
|
||||
current.infrastructureOrder,
|
||||
infrastructureField,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !infrastructureDelta.isEmpty() {
|
||||
delta[infrastructureDeltaField] = infrastructureDelta
|
||||
}
|
||||
case current.infrastructureKeyed:
|
||||
// The previous snapshot carried the projection as a plain field (or
|
||||
// not at all): fall back to shipping the full array once so the
|
||||
// client re-adopts a clean baseline.
|
||||
if previousRaw, ok := previous.fields[infrastructureField]; !ok ||
|
||||
!bytes.Equal(previousRaw, current.infrastructureRaw) {
|
||||
delta[infrastructureField] = current.infrastructureRaw
|
||||
}
|
||||
case previous.infrastructureKeyed:
|
||||
// The projection left the keyed path. If the current snapshot still
|
||||
// carries it as a plain field, the fields loop above already shipped
|
||||
// it whole; if it vanished entirely, clear it like any removed field.
|
||||
if _, ok := current.fields[infrastructureField]; !ok {
|
||||
delta[infrastructureField] = nil
|
||||
for _, keyedField := range keyedDeltaFields {
|
||||
previousSnapshot := previous.keyed[keyedField.field]
|
||||
currentSnapshot := current.keyed[keyedField.field]
|
||||
switch {
|
||||
case previousSnapshot != nil && currentSnapshot != nil:
|
||||
keyedDelta, err := buildKeyedArrayDelta(
|
||||
previousSnapshot.entries,
|
||||
currentSnapshot.entries,
|
||||
previousSnapshot.order,
|
||||
currentSnapshot.order,
|
||||
keyedField.field,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !keyedDelta.isEmpty() {
|
||||
delta[keyedField.deltaField] = keyedDelta
|
||||
}
|
||||
case currentSnapshot != nil:
|
||||
// The previous snapshot carried the array as a plain field (or
|
||||
// not at all): fall back to shipping the full array once so the
|
||||
// client re-adopts a clean baseline.
|
||||
if previousRaw, ok := previous.fields[keyedField.field]; !ok ||
|
||||
!bytes.Equal(previousRaw, currentSnapshot.raw) {
|
||||
delta[keyedField.field] = currentSnapshot.raw
|
||||
}
|
||||
case previousSnapshot != nil:
|
||||
// The array left the keyed path. If the current snapshot still
|
||||
// carries it as a plain field, the fields loop above already
|
||||
// shipped it whole; if it vanished entirely, clear it like any
|
||||
// removed field.
|
||||
if _, ok := current.fields[keyedField.field]; !ok {
|
||||
delta[keyedField.field] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ func TestClientStateDeltaFallsBackToFullInfrastructureWhenUnkeyed(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if previous.infrastructureKeyed {
|
||||
if previous.keyed[infrastructureField] != nil {
|
||||
t.Fatal("id-less projection must not key")
|
||||
}
|
||||
current, err := buildClientStateSnapshot(currentState)
|
||||
@@ -251,6 +251,89 @@ func TestClientStateDeltaShipsFullInfrastructureWhenBaselineWasUnkeyed(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientStateDeltaShipsActiveAlertsAsKeyedPatches(t *testing.T) {
|
||||
previousState := models.EmptyStateFrontend()
|
||||
previousState.ActiveAlerts = []models.Alert{
|
||||
{
|
||||
ID: "alert-1", Type: "cpu", Level: "warning", ResourceID: "vm-1",
|
||||
Message: strings.Repeat("m", 4*1024), Value: 85,
|
||||
},
|
||||
{ID: "alert-2", Type: "memory", Level: "warning", ResourceID: "vm-2", Value: 70},
|
||||
}
|
||||
currentState := previousState
|
||||
currentState.ActiveAlerts = append([]models.Alert(nil), previousState.ActiveAlerts...)
|
||||
currentState.ActiveAlerts[0].Level = "critical"
|
||||
currentState.ActiveAlerts[0].Value = 97
|
||||
|
||||
previous, err := buildClientStateSnapshot(previousState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
current, err := buildClientStateSnapshot(currentState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delta, err := buildClientStateDelta(previous, current)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := delta[activeAlertsField]; ok {
|
||||
t.Fatal("state delta re-shipped the full activeAlerts array")
|
||||
}
|
||||
payload, ok := delta[activeAlertsDeltaField].(resourceDeltaPayload)
|
||||
if !ok {
|
||||
t.Fatalf("alerts delta type = %T", delta[activeAlertsDeltaField])
|
||||
}
|
||||
if len(payload.Upserts) != 1 {
|
||||
t.Fatalf("upserts = %d, want 1", len(payload.Upserts))
|
||||
}
|
||||
patch := string(payload.Upserts[0])
|
||||
if !strings.Contains(patch, `"id":"alert-1"`) || !strings.Contains(patch, `"level":"critical"`) {
|
||||
t.Fatalf("patch = %s, want id + level", patch)
|
||||
}
|
||||
if strings.Contains(patch, strings.Repeat("m", 128)) {
|
||||
t.Fatal("patch re-shipped the unchanged alert message")
|
||||
}
|
||||
if len(patch) >= 256 {
|
||||
t.Fatalf("patch = %d bytes, want a bounded lifecycle patch", len(patch))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientStateDeltaTracksResolvedAlertRemoval(t *testing.T) {
|
||||
previousState := models.EmptyStateFrontend()
|
||||
previousState.ActiveAlerts = []models.Alert{
|
||||
{ID: "alert-1", Type: "cpu", Level: "warning", ResourceID: "vm-1"},
|
||||
{ID: "alert-2", Type: "memory", Level: "warning", ResourceID: "vm-2"},
|
||||
}
|
||||
currentState := models.EmptyStateFrontend()
|
||||
currentState.ActiveAlerts = []models.Alert{
|
||||
{ID: "alert-2", Type: "memory", Level: "warning", ResourceID: "vm-2"},
|
||||
}
|
||||
|
||||
previous, err := buildClientStateSnapshot(previousState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
current, err := buildClientStateSnapshot(currentState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
delta, err := buildClientStateDelta(previous, current)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, ok := delta[activeAlertsDeltaField].(resourceDeltaPayload)
|
||||
if !ok {
|
||||
t.Fatalf("alerts delta type = %T", delta[activeAlertsDeltaField])
|
||||
}
|
||||
if len(payload.Removed) != 1 || payload.Removed[0] != "alert-1" {
|
||||
t.Fatalf("removed = %#v, want [alert-1]", payload.Removed)
|
||||
}
|
||||
if len(payload.Upserts) != 0 {
|
||||
t.Fatalf("upserts = %d, want none", len(payload.Upserts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientStateBaselineAdvancesOnlyAfterDeltaIsQueued(t *testing.T) {
|
||||
initial := models.EmptyStateFrontend()
|
||||
initial.LastUpdate = 100
|
||||
|
||||
Reference in New Issue
Block a user