// Pure drift computation: compares the POSHManager plan (expected) against what // Microsoft Graph currently reports (live), for the win32LobApp metadata, its // assignments, and its supersedence/dependency relationships. No I/O — the // controller fetches live state and feeds it here, so this is fully unit-tested. // Fields on the win32LobApp we treat as authoritative from the plan. const APP_DRIFT_FIELDS = [ 'displayName', 'publisher', 'installCommandLine', 'uninstallCommandLine', 'applicableArchitectures' ]; function fieldDrift(expected = {}, actual = {}, fields = APP_DRIFT_FIELDS) { const drift = []; for (const field of fields) { const want = expected[field] ?? ''; const got = actual[field] ?? ''; if (String(want) !== String(got)) drift.push({ field, expected: want, actual: got }); } return drift; } function assignmentKey(assignment = {}) { const target = assignment.target || {}; const type = (target['@odata.type'] || '').replace('#microsoft.graph.', ''); return `${assignment.intent || ''}|${type}|${target.groupId || ''}`; } function relationshipKey(rel = {}) { const type = (rel['@odata.type'] || '').replace('#microsoft.graph.', ''); return `${type}|${rel.targetId || ''}`; } // Generic set diff by key: returns items present only in expected (missing in // tenant) and only in actual (extra in tenant). function setDiff(expected, actual, keyFn) { const expectedKeys = new Map(expected.map((item) => [keyFn(item), item])); const actualKeys = new Map(actual.map((item) => [keyFn(item), item])); const missing = [...expectedKeys.keys()].filter((k) => !actualKeys.has(k)); const extra = [...actualKeys.keys()].filter((k) => !expectedKeys.has(k)); return { missing, extra }; } export function computeDrift({ expectedApp = {}, liveApp = {}, expectedAssignments = [], liveAssignments = [], expectedRelationships = [], liveRelationships = [] } = {}) { const appDrift = fieldDrift(expectedApp, liveApp); const assignmentDrift = setDiff(expectedAssignments, liveAssignments, assignmentKey); const relationshipDrift = setDiff(expectedRelationships, liveRelationships, relationshipKey); const inSync = appDrift.length === 0 && assignmentDrift.missing.length === 0 && assignmentDrift.extra.length === 0 && relationshipDrift.missing.length === 0 && relationshipDrift.extra.length === 0; return { inSync, appDrift, assignmentDrift, relationshipDrift }; }