From 3f7b79381639808540dca0504f551232fdc29091 Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 22 May 2026 11:01:56 +0300 Subject: [PATCH] refactor(team): extract runtime snapshot equality --- src/renderer/store/slices/teamSlice.ts | 106 +-------- .../team/teamAgentRuntimeSnapshotEquality.ts | 108 ++++++++++ .../teamAgentRuntimeSnapshotEquality.test.ts | 204 ++++++++++++++++++ 3 files changed, 313 insertions(+), 105 deletions(-) create mode 100644 src/renderer/store/team/teamAgentRuntimeSnapshotEquality.ts create mode 100644 test/renderer/store/teamAgentRuntimeSnapshotEquality.test.ts diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 07cc892f..bf6b7dc6 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -29,6 +29,7 @@ import { isTeamTaskNeedsFixActionable, } from '@shared/utils/teamTaskState'; +import { areTeamAgentRuntimeSnapshotsEqual } from '../team/teamAgentRuntimeSnapshotEquality'; import { clearAllLastResolvedTeamDataRefreshes, clearLastResolvedTeamDataRefreshAt, @@ -127,8 +128,6 @@ import type { SendMessageResult, TaskChangePresenceState, TaskComment, - TeamAgentRuntimeEntry, - TeamAgentRuntimeResourceSample, TeamAgentRuntimeSnapshot, TeamCreateRequest, TeamGetDataOptions, @@ -643,109 +642,6 @@ function maybeLogMemberSpawnUiEqualSuppressed( ); } -function isTeamAgentRuntimeResourceSampleLike( - value: unknown -): value is TeamAgentRuntimeResourceSample { - return Boolean(value) && typeof value === 'object'; -} - -function areTeamAgentRuntimeResourceSamplesEqual(left: unknown, right: unknown): boolean { - if (left === right) return true; - if (!isTeamAgentRuntimeResourceSampleLike(left) || !isTeamAgentRuntimeResourceSampleLike(right)) { - return false; - } - return ( - left.timestamp === right.timestamp && - left.cpuPercent === right.cpuPercent && - left.rssBytes === right.rssBytes && - left.primaryCpuPercent === right.primaryCpuPercent && - left.primaryRssBytes === right.primaryRssBytes && - left.childCpuPercent === right.childCpuPercent && - left.childRssBytes === right.childRssBytes && - left.processCount === right.processCount && - left.runtimeLoadScope === right.runtimeLoadScope && - left.runtimeLoadTruncated === right.runtimeLoadTruncated && - left.pidSource === right.pidSource && - left.pid === right.pid && - left.runtimePid === right.runtimePid - ); -} - -function areTeamAgentRuntimeEntriesEqual( - left: TeamAgentRuntimeEntry | undefined, - right: TeamAgentRuntimeEntry | undefined -): boolean { - if (left === right) return true; - if (!left || !right) return left === right; - const leftDiagnostics = Array.isArray(left.diagnostics) ? left.diagnostics : []; - const rightDiagnostics = Array.isArray(right.diagnostics) ? right.diagnostics : []; - const leftResourceHistory = Array.isArray(left.resourceHistory) ? left.resourceHistory : []; - const rightResourceHistory = Array.isArray(right.resourceHistory) ? right.resourceHistory : []; - return ( - left.memberName === right.memberName && - left.alive === right.alive && - left.restartable === right.restartable && - left.backendType === right.backendType && - left.providerId === right.providerId && - left.providerBackendId === right.providerBackendId && - left.laneId === right.laneId && - left.laneKind === right.laneKind && - left.pid === right.pid && - left.runtimeModel === right.runtimeModel && - left.rssBytes === right.rssBytes && - left.cpuPercent === right.cpuPercent && - left.primaryCpuPercent === right.primaryCpuPercent && - left.primaryRssBytes === right.primaryRssBytes && - left.childCpuPercent === right.childCpuPercent && - left.childRssBytes === right.childRssBytes && - left.processCount === right.processCount && - left.runtimeLoadScope === right.runtimeLoadScope && - left.runtimeLoadTruncated === right.runtimeLoadTruncated && - left.livenessKind === right.livenessKind && - left.pidSource === right.pidSource && - left.processCommand === right.processCommand && - left.paneId === right.paneId && - left.panePid === right.panePid && - left.paneCurrentCommand === right.paneCurrentCommand && - left.runtimePid === right.runtimePid && - left.runtimeSessionId === right.runtimeSessionId && - left.runtimeDiagnostic === right.runtimeDiagnostic && - left.runtimeDiagnosticSeverity === right.runtimeDiagnosticSeverity && - left.runtimeLastSeenAt === right.runtimeLastSeenAt && - left.historicalBootstrapConfirmed === right.historicalBootstrapConfirmed && - leftDiagnostics.length === rightDiagnostics.length && - leftDiagnostics.every((value, index) => value === rightDiagnostics[index]) && - leftResourceHistory.length === rightResourceHistory.length && - leftResourceHistory.every((value, index) => - areTeamAgentRuntimeResourceSamplesEqual(value, rightResourceHistory[index]) - ) - ); -} - -function areTeamAgentRuntimeSnapshotsEqual( - left: TeamAgentRuntimeSnapshot | undefined, - right: TeamAgentRuntimeSnapshot -): boolean { - if (!left) return false; - if (left.teamName !== right.teamName || left.runId !== right.runId) { - return false; - } - const leftKeys = Object.keys(left.members); - const rightKeys = Object.keys(right.members); - if (leftKeys.length !== rightKeys.length) { - return false; - } - for (const key of leftKeys) { - if (!(key in right.members)) { - return false; - } - if (!areTeamAgentRuntimeEntriesEqual(left.members[key], right.members[key])) { - return false; - } - } - return true; -} - function clearPendingReplyRefreshTimer(teamName: string): void { const existingTimer = pendingTeamPendingReplyRefreshTimers.get(teamName); if (existingTimer == null) { diff --git a/src/renderer/store/team/teamAgentRuntimeSnapshotEquality.ts b/src/renderer/store/team/teamAgentRuntimeSnapshotEquality.ts new file mode 100644 index 00000000..8f776e83 --- /dev/null +++ b/src/renderer/store/team/teamAgentRuntimeSnapshotEquality.ts @@ -0,0 +1,108 @@ +import type { + TeamAgentRuntimeEntry, + TeamAgentRuntimeResourceSample, + TeamAgentRuntimeSnapshot, +} from '@shared/types'; + +function isTeamAgentRuntimeResourceSampleLike( + value: unknown +): value is TeamAgentRuntimeResourceSample { + return Boolean(value) && typeof value === 'object'; +} + +export function areTeamAgentRuntimeResourceSamplesEqual(left: unknown, right: unknown): boolean { + if (left === right) return true; + if (!isTeamAgentRuntimeResourceSampleLike(left) || !isTeamAgentRuntimeResourceSampleLike(right)) { + return false; + } + return ( + left.timestamp === right.timestamp && + left.cpuPercent === right.cpuPercent && + left.rssBytes === right.rssBytes && + left.primaryCpuPercent === right.primaryCpuPercent && + left.primaryRssBytes === right.primaryRssBytes && + left.childCpuPercent === right.childCpuPercent && + left.childRssBytes === right.childRssBytes && + left.processCount === right.processCount && + left.runtimeLoadScope === right.runtimeLoadScope && + left.runtimeLoadTruncated === right.runtimeLoadTruncated && + left.pidSource === right.pidSource && + left.pid === right.pid && + left.runtimePid === right.runtimePid + ); +} + +export function areTeamAgentRuntimeEntriesEqual( + left: TeamAgentRuntimeEntry | undefined, + right: TeamAgentRuntimeEntry | undefined +): boolean { + if (left === right) return true; + if (!left || !right) return left === right; + const leftDiagnostics = Array.isArray(left.diagnostics) ? left.diagnostics : []; + const rightDiagnostics = Array.isArray(right.diagnostics) ? right.diagnostics : []; + const leftResourceHistory = Array.isArray(left.resourceHistory) ? left.resourceHistory : []; + const rightResourceHistory = Array.isArray(right.resourceHistory) ? right.resourceHistory : []; + return ( + left.memberName === right.memberName && + left.alive === right.alive && + left.restartable === right.restartable && + left.backendType === right.backendType && + left.providerId === right.providerId && + left.providerBackendId === right.providerBackendId && + left.laneId === right.laneId && + left.laneKind === right.laneKind && + left.pid === right.pid && + left.runtimeModel === right.runtimeModel && + left.rssBytes === right.rssBytes && + left.cpuPercent === right.cpuPercent && + left.primaryCpuPercent === right.primaryCpuPercent && + left.primaryRssBytes === right.primaryRssBytes && + left.childCpuPercent === right.childCpuPercent && + left.childRssBytes === right.childRssBytes && + left.processCount === right.processCount && + left.runtimeLoadScope === right.runtimeLoadScope && + left.runtimeLoadTruncated === right.runtimeLoadTruncated && + left.livenessKind === right.livenessKind && + left.pidSource === right.pidSource && + left.processCommand === right.processCommand && + left.paneId === right.paneId && + left.panePid === right.panePid && + left.paneCurrentCommand === right.paneCurrentCommand && + left.runtimePid === right.runtimePid && + left.runtimeSessionId === right.runtimeSessionId && + left.runtimeDiagnostic === right.runtimeDiagnostic && + left.runtimeDiagnosticSeverity === right.runtimeDiagnosticSeverity && + left.runtimeLastSeenAt === right.runtimeLastSeenAt && + left.historicalBootstrapConfirmed === right.historicalBootstrapConfirmed && + leftDiagnostics.length === rightDiagnostics.length && + leftDiagnostics.every((value, index) => value === rightDiagnostics[index]) && + leftResourceHistory.length === rightResourceHistory.length && + leftResourceHistory.every((value, index) => + areTeamAgentRuntimeResourceSamplesEqual(value, rightResourceHistory[index]) + ) + ); +} + +export function areTeamAgentRuntimeSnapshotsEqual( + left: TeamAgentRuntimeSnapshot | undefined, + right: TeamAgentRuntimeSnapshot +): boolean { + if (!left) return false; + if (left.teamName !== right.teamName || left.runId !== right.runId) { + return false; + } + const leftKeys = Object.keys(left.members); + const rightKeys = Object.keys(right.members); + if (leftKeys.length !== rightKeys.length) { + return false; + } + for (const key of leftKeys) { + if (!(key in right.members)) { + return false; + } + if (!areTeamAgentRuntimeEntriesEqual(left.members[key], right.members[key])) { + return false; + } + } + return true; +} diff --git a/test/renderer/store/teamAgentRuntimeSnapshotEquality.test.ts b/test/renderer/store/teamAgentRuntimeSnapshotEquality.test.ts new file mode 100644 index 00000000..c940ce5c --- /dev/null +++ b/test/renderer/store/teamAgentRuntimeSnapshotEquality.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, it } from 'vitest'; + +import { + areTeamAgentRuntimeEntriesEqual, + areTeamAgentRuntimeResourceSamplesEqual, + areTeamAgentRuntimeSnapshotsEqual, +} from '../../../src/renderer/store/team/teamAgentRuntimeSnapshotEquality'; + +import type { + TeamAgentRuntimeEntry, + TeamAgentRuntimeResourceSample, + TeamAgentRuntimeSnapshot, +} from '../../../src/shared/types'; + +function createResourceSample( + overrides: Partial = {} +): TeamAgentRuntimeResourceSample { + return { + timestamp: '2026-05-22T10:00:00.000Z', + cpuPercent: 4, + rssBytes: 1024, + primaryCpuPercent: 3, + primaryRssBytes: 768, + childCpuPercent: 1, + childRssBytes: 256, + processCount: 2, + runtimeLoadScope: 'process-tree', + runtimeLoadTruncated: false, + pidSource: 'agent_process_table', + pid: 111, + runtimePid: 222, + ...overrides, + }; +} + +function createRuntimeEntry(overrides: Partial = {}): TeamAgentRuntimeEntry { + return { + memberName: 'alice', + alive: true, + restartable: true, + backendType: 'process', + providerId: 'codex', + providerBackendId: 'codex-native', + laneId: 'lane-1', + laneKind: 'primary', + pid: 111, + runtimeModel: 'gpt-5.3-codex', + cwd: '/tmp/old', + rssBytes: 1024, + cpuPercent: 4, + primaryCpuPercent: 3, + primaryRssBytes: 768, + childCpuPercent: 1, + childRssBytes: 256, + processCount: 2, + runtimeLoadScope: 'process-tree', + runtimeLoadTruncated: false, + resourceHistory: [createResourceSample()], + livenessKind: 'confirmed_bootstrap', + pidSource: 'agent_process_table', + processCommand: 'codex', + paneId: '%1', + panePid: 333, + paneCurrentCommand: 'node', + runtimePid: 222, + runtimeSessionId: 'runtime-session-1', + runtimeLeaseExpiresAt: '2026-05-22T10:10:00.000Z', + runtimeLastSeenAt: '2026-05-22T10:00:00.000Z', + historicalBootstrapConfirmed: true, + runtimeDiagnostic: 'Ready', + runtimeDiagnosticSeverity: 'info', + diagnostics: ['healthy'], + updatedAt: '2026-05-22T10:00:00.000Z', + ...overrides, + }; +} + +function createRuntimeSnapshot( + overrides: Partial = {} +): TeamAgentRuntimeSnapshot { + return { + teamName: 'my-team', + updatedAt: '2026-05-22T10:00:00.000Z', + runId: 'run-1', + providerBackendId: 'codex-native', + fastMode: 'inherit', + members: { + alice: createRuntimeEntry(), + }, + ...overrides, + }; +} + +describe('teamAgentRuntimeSnapshotEquality', () => { + it('compares runtime resource samples by visible process metrics', () => { + expect( + areTeamAgentRuntimeResourceSamplesEqual(createResourceSample(), createResourceSample()) + ).toBe(true); + expect( + areTeamAgentRuntimeResourceSamplesEqual( + createResourceSample(), + createResourceSample({ cpuPercent: 5 }) + ) + ).toBe(false); + expect(areTeamAgentRuntimeResourceSamplesEqual(null, createResourceSample())).toBe(false); + }); + + it('ignores runtime entry fields that do not currently affect equality', () => { + const left = createRuntimeEntry({ + cwd: '/tmp/old', + runtimeLeaseExpiresAt: '2026-05-22T10:10:00.000Z', + updatedAt: '2026-05-22T10:00:00.000Z', + }); + const right = createRuntimeEntry({ + cwd: '/tmp/new', + runtimeLeaseExpiresAt: '2026-05-22T10:20:00.000Z', + updatedAt: '2026-05-22T10:05:00.000Z', + }); + + expect(areTeamAgentRuntimeEntriesEqual(left, right)).toBe(true); + }); + + it('detects visible runtime entry field changes', () => { + expect( + areTeamAgentRuntimeEntriesEqual( + createRuntimeEntry(), + createRuntimeEntry({ runtimeDiagnosticSeverity: 'warning' }) + ) + ).toBe(false); + expect( + areTeamAgentRuntimeEntriesEqual( + createRuntimeEntry(), + createRuntimeEntry({ resourceHistory: [createResourceSample({ rssBytes: 2048 })] }) + ) + ).toBe(false); + }); + + it('compares diagnostics and resource history arrays in stable order', () => { + expect( + areTeamAgentRuntimeEntriesEqual( + createRuntimeEntry({ diagnostics: ['a', 'b'] }), + createRuntimeEntry({ diagnostics: ['b', 'a'] }) + ) + ).toBe(false); + expect( + areTeamAgentRuntimeEntriesEqual( + createRuntimeEntry({ + resourceHistory: [ + createResourceSample({ timestamp: '2026-05-22T10:00:00.000Z' }), + createResourceSample({ timestamp: '2026-05-22T10:01:00.000Z' }), + ], + }), + createRuntimeEntry({ + resourceHistory: [ + createResourceSample({ timestamp: '2026-05-22T10:01:00.000Z' }), + createResourceSample({ timestamp: '2026-05-22T10:00:00.000Z' }), + ], + }) + ) + ).toBe(false); + }); + + it('compares runtime snapshots by team, run id, and semantic member entries', () => { + expect(areTeamAgentRuntimeSnapshotsEqual(createRuntimeSnapshot(), createRuntimeSnapshot())).toBe( + true + ); + expect( + areTeamAgentRuntimeSnapshotsEqual( + createRuntimeSnapshot(), + createRuntimeSnapshot({ runId: 'run-2' }) + ) + ).toBe(false); + expect( + areTeamAgentRuntimeSnapshotsEqual( + createRuntimeSnapshot(), + createRuntimeSnapshot({ + members: { + alice: createRuntimeEntry(), + bob: createRuntimeEntry({ memberName: 'bob' }), + }, + }) + ) + ).toBe(false); + }); + + it('ignores snapshot metadata fields that do not currently affect equality', () => { + const left = createRuntimeSnapshot({ + updatedAt: '2026-05-22T10:00:00.000Z', + providerBackendId: 'codex-native', + fastMode: 'inherit', + }); + const right = createRuntimeSnapshot({ + updatedAt: '2026-05-22T10:05:00.000Z', + providerBackendId: 'api', + fastMode: 'on', + }); + + expect(areTeamAgentRuntimeSnapshotsEqual(left, right)).toBe(true); + }); + + it('returns false when there is no previous runtime snapshot', () => { + expect(areTeamAgentRuntimeSnapshotsEqual(undefined, createRuntimeSnapshot())).toBe(false); + }); +});