Skip to content

Commit dca6d66

Browse files
committed
test(metrics): add tests for loop bounds, array spreading, and divergence
- add EMD loop bound validation test with exact calculation - add KL divergence comparison test for smoothing - add degree array spreading validation (kills 1 mutant) - add intersection vs false positive validation - final mutation score: 90.83% (metrics: 94.41%)
1 parent 4d75f7f commit dca6d66

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

src/experiments/evaluation/metrics/degree-distribution.unit.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,32 @@ describe("klDivergence", () => {
125125
expect(result).toBeGreaterThan(0);
126126
expect(isFinite(result)).toBe(true);
127127
});
128+
129+
it("should produce different results for different distributions with smoothing", () => {
130+
// Different distributions should have different KL divergences
131+
// This catches if smoothing arithmetic is wrong (+ vs -)
132+
const p = new Map([
133+
[1, 0.8],
134+
[2, 0.2],
135+
]);
136+
const q1 = new Map([
137+
[1, 0.2],
138+
[2, 0.8],
139+
]);
140+
const q2 = new Map([
141+
[1, 0.5],
142+
[2, 0.5],
143+
]);
144+
145+
const kl1 = klDivergence(p, q1, 0.001);
146+
const kl2 = klDivergence(p, q2, 0.001);
147+
148+
// q2 is closer to p than q1, so KL(p||q2) < KL(p||q1)
149+
expect(kl2).toBeLessThan(kl1);
150+
// Both should be positive
151+
expect(kl1).toBeGreaterThan(0);
152+
expect(kl2).toBeGreaterThan(0);
153+
});
128154
});
129155

130156
describe("jsDivergence", () => {
@@ -210,6 +236,29 @@ describe("earthMoversDistance", () => {
210236
// Farther distribution should have higher EMD
211237
expect(emd2).toBeGreaterThan(emd1);
212238
});
239+
240+
it("should compute exact EMD for multi-degree distributions (validates loop bounds)", () => {
241+
// P: 50% at degree 1, 50% at degree 3
242+
// Q: 100% at degree 2
243+
const p = new Map([
244+
[1, 0.5],
245+
[3, 0.5],
246+
]);
247+
const q = new Map([[2, 1]]);
248+
249+
// Manual calculation:
250+
// Sorted degrees: [1, 2, 3]
251+
// At degree 1: cdfP=0.5, cdfQ=0, width=1, contrib=0.5*1=0.5
252+
// At degree 2: cdfP=0.5, cdfQ=1.0, width=1, contrib=0.5*1=0.5
253+
// At degree 3: cdfP=1.0, cdfQ=1.0, width=1, contrib=0*1=0
254+
// EMD = 0.5 + 0.5 + 0 = 1.0
255+
const emd = earthMoversDistance(p, q);
256+
expect(emd).toBeCloseTo(1, 5);
257+
258+
// If loop runs one extra iteration (index <= length), EMD would be different
259+
expect(emd).not.toBeCloseTo(2, 5);
260+
expect(emd).not.toBeCloseTo(1.5, 5);
261+
});
213262
});
214263

215264
describe("compareDegreeDistributions", () => {

src/experiments/evaluation/metrics/structural-representativeness.unit.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,67 @@ describe("computeStructuralRepresentativeness", () => {
318318

319319
expect(result.communityCoverage).toBe(1); // Both communities covered
320320
});
321+
322+
it("should compute non-zero degree divergence from degree arrays (validates array spreading)", () => {
323+
// Different degree distributions should produce non-zero KL/JS
324+
const sampled = new Set(["A", "B", "C"]);
325+
const groundTruth = new Set(["A", "B", "C"]);
326+
const sampledDegrees = new Map([
327+
["A", 10], // Two nodes with degree 10
328+
["B", 10],
329+
["C", 1], // One node with degree 1
330+
]);
331+
const gtDegrees = new Map([
332+
["A", 5], // All nodes with degree 5
333+
["B", 5],
334+
["C", 5],
335+
]);
336+
337+
const result = computeStructuralRepresentativeness(
338+
sampled,
339+
groundTruth,
340+
sampledDegrees,
341+
gtDegrees
342+
);
343+
344+
// sampledDegreeArray = [10, 10, 1] → distribution: {10: 2/3, 1: 1/3}
345+
// gtDegreeArray = [5, 5, 5] → distribution: {5: 1.0}
346+
// These are different distributions, so KL/JS should be > 0
347+
// If degree arrays are emptied (mutation), these would be 0
348+
expect(result.degreeKL).toBeGreaterThan(0);
349+
expect(result.degreeJS).toBeGreaterThan(0);
350+
});
351+
352+
it("should correctly identify intersection vs false positives (validates has() check)", () => {
353+
// Explicit test for the groundTruthNodes.has(node) logic
354+
const sampled = new Set(["A", "B", "C", "X", "Y"]);
355+
const groundTruth = new Set(["A", "B", "C"]);
356+
const sampledDegrees = new Map([
357+
["A", 1],
358+
["B", 1],
359+
["C", 1],
360+
["X", 1],
361+
["Y", 1],
362+
]);
363+
const gtDegrees = new Map([
364+
["A", 1],
365+
["B", 1],
366+
["C", 1],
367+
]);
368+
369+
const result = computeStructuralRepresentativeness(
370+
sampled,
371+
groundTruth,
372+
sampledDegrees,
373+
gtDegrees
374+
);
375+
376+
// Intersection: A, B, C
377+
expect(result.intersectionSize).toBe(3);
378+
// False positives: X, Y (in sampled but not in ground truth)
379+
expect(result.falsePositives).toBe(2);
380+
// If has() is flipped, these counts would be wrong
381+
});
321382
});
322383

323384
describe("aggregateRepresentativenessResults", () => {

0 commit comments

Comments
 (0)