diff --git a/src/readers/diffCheck.ts b/src/readers/diffCheck.ts index ba3dd09995ef4a2070b372f7e2363a18ca85f957..eba2c6cc1a131b5a8434019cfe8affebbd47792b 100644 --- a/src/readers/diffCheck.ts +++ b/src/readers/diffCheck.ts @@ -5,6 +5,10 @@ import type { GraphQueryResultMetaFromBackend } from 'ts-common/src/model/webSoc import { ums } from '../variables'; import type { InsightModel } from 'ts-common'; +export const compareHashedQueryResults = (previousHash: string | null, currentHash: string): boolean => { + return !previousHash || !hashIsEqual(currentHash, previousHash); +}; + export const diffCheck = async ( insight: InsightModel, ss: SaveState, @@ -20,7 +24,8 @@ export const diffCheck = async ( }); log.debug('Comparing hash values from current and previous query'); - const changed = !previousQueryResult || !hashIsEqual(queryResultHash, previousQueryResult); + const changed = compareHashedQueryResults(previousQueryResult, queryResultHash); + insight.status ||= changed; log.debug('Updated node and edge ids in SaveState'); diff --git a/src/readers/statCheck.ts b/src/readers/statCheck.ts index 96ea6556b1db0472ddba29e5fa8def1036598bf6..578f9cb365ae8bc894aba24c471773aedf630bff 100644 --- a/src/readers/statCheck.ts +++ b/src/readers/statCheck.ts @@ -1,7 +1,7 @@ import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; import { log } from '../logger'; -function processAlarmStats(alarmStat: InsightModel, resultQuery: GraphQueryResultMetaFromBackend): boolean { +export function processAlarmStats(alarmStat: InsightModel, resultQuery: GraphQueryResultMetaFromBackend): boolean { for (const condition of alarmStat.conditionsCheck) { const ssInsightNode = condition.nodeLabel; const ssInsightStatistic = condition.statistic; diff --git a/src/tests/insights/diffCheck.test.ts b/src/tests/insights/diffCheck.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..58325862cdfdc5aaa5e2ea47babf8d619cb4c8dd --- /dev/null +++ b/src/tests/insights/diffCheck.test.ts @@ -0,0 +1,64 @@ +import { expect, test, describe, it } from 'bun:test'; +import type { GraphQueryResultMetaFromBackend } from 'ts-common'; +import { hashDictionary, hashIsEqual } from '../../utils/hashing'; +import { compareHashedQueryResults } from './../../readers/diffCheck'; + +describe('Hash Comparison Tests', () => { + it('should detect different hashes for different graph structures', () => { + // First query result + const query1: GraphQueryResultMetaFromBackend = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + // Second query result with different structure + const query2: GraphQueryResultMetaFromBackend = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }, { _id: 'node3' }], + edges: [{ _id: 'edge1' }, { _id: 'edge2' }], + } as GraphQueryResultMetaFromBackend; + + const hash1 = hashDictionary({ + nodes: query1.nodes.map(node => node._id), + edges: query1.edges.map(edge => edge._id), + }); + + const hash2 = hashDictionary({ + nodes: query2.nodes.map(node => node._id), + edges: query2.edges.map(edge => edge._id), + }); + + // Test direct hash comparison + expect(hashIsEqual(hash1, hash2)).toBe(false); + + // Test using compareHashedQueryResults + expect(compareHashedQueryResults(hash1, hash2)).toBe(true); + }); + + it('should detect identical hashes for same graph structures', () => { + const query1 = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + const query2 = { + nodes: [{ _id: 'node1' }, { _id: 'node2' }], + edges: [{ _id: 'edge1' }], + } as GraphQueryResultMetaFromBackend; + + const hash1 = hashDictionary({ + nodes: query1.nodes.map(node => node._id), + edges: query1.edges.map(edge => edge._id), + }); + + const hash2 = hashDictionary({ + nodes: query2.nodes.map(node => node._id), + edges: query2.edges.map(edge => edge._id), + }); + + // Test direct hash comparison + expect(hashIsEqual(hash1, hash2)).toBe(true); + + // Test using compareHashedQueryResults + expect(compareHashedQueryResults(hash1, hash2)).toBe(false); + }); +}); diff --git a/src/tests/insights/statCheck.test.ts b/src/tests/insights/statCheck.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b79e5ae8553778ba46ef2042275a2b83c94de442 --- /dev/null +++ b/src/tests/insights/statCheck.test.ts @@ -0,0 +1,376 @@ +import { expect, test, describe, it } from 'bun:test'; +import type { GraphQueryResultMetaFromBackend, InsightModel } from 'ts-common'; +import { processAlarmStats } from './../../readers/statCheck'; + +const baseInsight: Omit<InsightModel, 'conditionsCheck'> = { + id: 1, + name: 'Test Insight', + description: 'Base insight for testing', + recipients: ['test@example.com'], + frequency: 'daily', + template: 'default', + saveStateId: 'save-state-1', + type: 'alert', + alarmMode: 'conditional', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + previousResultHash: 'abc123', + lastProcessedAt: new Date().toISOString(), + status: true, +}; + +describe('QueryprocessAlarmStats', () => { + it('should return true when condition is met', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'TestNode', + statistic: 'count', + operator: '>', + value: 1, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(true); + }); + + it('should return false when condition is not met', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'TestNode', + statistic: 'count', + operator: '<', + value: 1, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(false); + }); + + it('should return false when nodeLabel is not found', () => { + const alarmStat: InsightModel = { + ...baseInsight, + conditionsCheck: [ + { + nodeLabel: 'NonExistentNode', + statistic: 'count', + operator: '>', + value: 5, + }, + ], + }; + + const resultQuery: GraphQueryResultMetaFromBackend = { + nodes: [ + { + _id: 'node1', + label: 'TestNode', + attributes: { + name: 'Test Node 1', + value: 123, + }, + }, + { + _id: 'node2', + label: 'TestNode', + attributes: { + name: 'Test Node 2', + value: 456, + }, + }, + ], + edges: [ + { + _id: 'edge1', + label: 'CONNECTS', + from: 'node1', + to: 'node2', + attributes: { + weight: 1, + timestamp: '2025-03-06', + }, + }, + ], + metaData: { + topological: { + density: 0.5, + self_loops: 0, + }, + nodes: { + count: 2, + labels: ['TestNode'], + types: { + TestNode: { + count: 2, + avgDegreeIn: 0.5, + avgDegreeOut: 0.5, + attributes: { + name: { + attributeType: 'string', + statistics: { + uniqueItems: 2, + values: ['Test Node 1', 'Test Node 2'], + mode: 'Test Node 1', + count: 2, + }, + }, + value: { + attributeType: 'number', + statistics: { + min: 123, + max: 456, + average: 289.5, + count: 2, + }, + }, + }, + }, + }, + }, + edges: { + count: 1, + labels: ['CONNECTS'], + types: { + CONNECTS: { + count: 1, + attributes: { + weight: { + attributeType: 'number', + statistics: { + min: 1, + max: 1, + average: 1, + count: 1, + }, + }, + timestamp: { + attributeType: 'datetime', + statistics: { + min: 1743782400, + max: 1743782400, + range: 0, + }, + }, + }, + }, + }, + }, + }, + nodeCounts: { + TestNode: 2, + updatedAt: 122331, + }, + }; + + expect(processAlarmStats(alarmStat, resultQuery)).toBe(false); + }); +});