Add non-metric category filters to compliance page

Adds a CategoryFilterBar with pill-shaped FilterChip components below the
metric health cards. Non-metric categories (Missing_AppID, Aging Vulns,
Missing_DF, etc.) are derived dynamically from device data and displayed
as color-coded filterable chips with device counts.

Unified filter state replaces the old metricFilter array, ensuring mutual
exclusivity between metric card filters and non-metric chip filters.
Includes 4 property-based tests validating derivation, filter predicate,
mutual exclusivity, and color resolution correctness.

Closes #26
This commit is contained in:
Jordan Ramos
2026-06-08 10:47:28 -06:00
parent 1f3833989a
commit d4c428248a
6 changed files with 550 additions and 11 deletions

View File

@@ -0,0 +1,71 @@
/**
* Property-Based Test: Color resolution with fallback
*
* Feature: compliance-nonmetric-filters, Property 4: Color resolution with fallback
* **Validates: Requirements 6.2, 6.3**
*
* For any non-metric category metric_id, the resolved color equals
* CATEGORY_COLORS[metricCategoriesConfig[metricId]] when both lookups succeed,
* else #94A3B8.
*/
import fc from 'fast-check';
import { deriveNonMetricCategories, CATEGORY_COLORS } from '../components/pages/CompliancePage';
// The color resolution logic extracted for direct testing
function resolveColor(metricId, categoriesConfig) {
const categoryName = categoriesConfig[metricId] || null;
return (categoryName && CATEGORY_COLORS[categoryName]) || '#94A3B8';
}
// Generators
const metricIdArb = fc.stringMatching(/^[A-Za-z0-9_.]{1,20}$/);
const knownCategories = Object.keys(CATEGORY_COLORS);
const categoryNameArb = fc.oneof(
fc.constantFrom(...knownCategories),
fc.string({ minLength: 1, maxLength: 20 }) // may not be in CATEGORY_COLORS
);
const categoriesConfigArb = fc.dictionary(metricIdArb, categoryNameArb);
describe('Compliance Non-Metric Filter — Property 4: Color resolution with fallback', () => {
it('color equals CATEGORY_COLORS[config[metricId]] when both lookups succeed, else #94A3B8', () => {
fc.assert(
fc.property(metricIdArb, categoriesConfigArb, (metricId, config) => {
const result = resolveColor(metricId, config);
const categoryName = config[metricId];
if (categoryName && CATEGORY_COLORS[categoryName]) {
expect(result).toBe(CATEGORY_COLORS[categoryName]);
} else {
expect(result).toBe('#94A3B8');
}
}),
{ numRuns: 100 }
);
});
it('deriveNonMetricCategories produces correct colors for all returned categories', () => {
const deviceWithMetricArb = metricIdArb.map(id => ({
hostname: 'host-' + id,
failing_metrics: [{ metric_id: id, metric_desc: '', category: '' }],
}));
fc.assert(
fc.property(
fc.array(deviceWithMetricArb, { minLength: 1, maxLength: 15 }),
categoriesConfigArb,
(devices, config) => {
// Empty summary means all device metric_ids are non-metric
const result = deriveNonMetricCategories(devices, [], config);
for (const item of result) {
const expectedColor = resolveColor(item.metricId, config);
expect(item.color).toBe(expectedColor);
}
}
),
{ numRuns: 100 }
);
});
});

View File

@@ -0,0 +1,112 @@
/**
* Property-Based Test: Non-metric category derivation is the set difference with accurate counts
*
* Feature: compliance-nonmetric-filters, Property 1: Non-metric category derivation
* **Validates: Requirements 1.1, 1.2, 2.2, 2.4**
*
* For any set of devices and any set of summary entries for a team,
* deriveNonMetricCategories returns exactly the metric_ids present in at least
* one device's failing_metrics array that do not appear in any summary entry's
* metric_id — deduplicated, sorted alphabetically by metricId, and each with a
* count equal to the number of devices whose failing_metrics contains that metric_id.
*/
import fc from 'fast-check';
import { deriveNonMetricCategories } from '../components/pages/CompliancePage';
// Generators
const metricIdArb = fc.stringMatching(/^[A-Za-z0-9_.]{1,20}$/);
const failingMetricArb = fc.record({
metric_id: metricIdArb,
metric_desc: fc.constant(''),
category: fc.constant(''),
});
const deviceArb = fc.record({
hostname: fc.string({ minLength: 1, maxLength: 30 }),
failing_metrics: fc.array(failingMetricArb, { minLength: 0, maxLength: 8 }),
});
const summaryEntryArb = fc.record({
metric_id: metricIdArb,
team: fc.constant('STEAM'),
compliance_pct: fc.double({ min: 0, max: 1 }),
status: fc.constantFrom('Meets/Exceeds Target', 'Within 15% of Target', 'Below 15% of Target'),
});
const categoriesConfigArb = fc.dictionary(metricIdArb, fc.constantFrom(
'Vulnerability Management', 'Access & MFA', 'Logging & Monitoring',
'Asset Data Quality', 'Endpoint Protection', 'Application Security',
'Disaster Recovery', 'Decommissioned Assets'
));
describe('Compliance Non-Metric Derivation — Property 1: Set difference with accurate counts', () => {
it('returned metric_ids are exactly the set difference of device metric_ids minus summary metric_ids', () => {
fc.assert(
fc.property(
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
categoriesConfigArb,
(devices, summaryEntries, config) => {
const result = deriveNonMetricCategories(devices, summaryEntries, config);
const summaryIds = new Set(summaryEntries.map(e => e.metric_id));
const deviceMetricIds = new Set();
for (const d of devices) {
for (const m of (d.failing_metrics || [])) {
if (m.metric_id) deviceMetricIds.add(m.metric_id);
}
}
const expectedIds = new Set([...deviceMetricIds].filter(id => !summaryIds.has(id)));
const resultIds = new Set(result.map(r => r.metricId));
expect(resultIds).toEqual(expectedIds);
}
),
{ numRuns: 100 }
);
});
it('returned list is deduplicated and sorted alphabetically by metricId', () => {
fc.assert(
fc.property(
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
categoriesConfigArb,
(devices, summaryEntries, config) => {
const result = deriveNonMetricCategories(devices, summaryEntries, config);
// Deduplicated
const ids = result.map(r => r.metricId);
expect(ids.length).toBe(new Set(ids).size);
// Sorted alphabetically
const sorted = [...ids].sort((a, b) => a.localeCompare(b));
expect(ids).toEqual(sorted);
}
),
{ numRuns: 100 }
);
});
it('each count equals the number of devices whose failing_metrics contains that metric_id', () => {
fc.assert(
fc.property(
fc.array(deviceArb, { minLength: 0, maxLength: 20 }),
fc.array(summaryEntryArb, { minLength: 0, maxLength: 10 }),
categoriesConfigArb,
(devices, summaryEntries, config) => {
const result = deriveNonMetricCategories(devices, summaryEntries, config);
for (const { metricId, count } of result) {
const expectedCount = devices.filter(d =>
(d.failing_metrics || []).some(m => m.metric_id === metricId)
).length;
expect(count).toBe(expectedCount);
}
}
),
{ numRuns: 100 }
);
});
});

View File

@@ -0,0 +1,85 @@
/**
* Property-Based Test: Filter predicate correctness
*
* Feature: compliance-nonmetric-filters, Property 2: Filter predicate correctness
* **Validates: Requirements 3.1, 5.2, 5.3, 5.4**
*
* For any filter state and any set of devices: when null, all devices pass;
* when metric, exactly devices with matching metric_id in ids pass;
* when nonmetric, exactly devices with matching metric_id pass.
*/
import fc from 'fast-check';
// Replicate the filter predicate logic from CompliancePage
function applyFilter(devices, filterState) {
return devices.filter(d => {
if (!filterState) return true;
if (filterState.type === 'metric') return d.failing_metrics.some(m => filterState.ids.includes(m.metric_id));
if (filterState.type === 'nonmetric') return d.failing_metrics.some(m => m.metric_id === filterState.id);
return true;
});
}
// Generators
const metricIdArb = fc.stringMatching(/^[A-Za-z0-9_.]{1,20}$/);
const failingMetricArb = fc.record({
metric_id: metricIdArb,
metric_desc: fc.constant(''),
category: fc.constant(''),
});
const deviceArb = fc.record({
hostname: fc.string({ minLength: 1, maxLength: 30 }),
failing_metrics: fc.array(failingMetricArb, { minLength: 0, maxLength: 8 }),
});
const devicesArb = fc.array(deviceArb, { minLength: 0, maxLength: 20 });
const filterStateArb = fc.oneof(
fc.constant(null),
fc.array(metricIdArb, { minLength: 1, maxLength: 5 }).map(ids => ({ type: 'metric', ids })),
metricIdArb.map(id => ({ type: 'nonmetric', id }))
);
describe('Compliance Non-Metric Filter — Property 2: Filter predicate correctness', () => {
it('when filterState is null, all devices pass', () => {
fc.assert(
fc.property(devicesArb, (devices) => {
const result = applyFilter(devices, null);
expect(result.length).toBe(devices.length);
}),
{ numRuns: 100 }
);
});
it('when filterState is metric, exactly devices with a matching metric_id pass', () => {
fc.assert(
fc.property(devicesArb, fc.array(metricIdArb, { minLength: 1, maxLength: 5 }), (devices, ids) => {
const filterState = { type: 'metric', ids };
const result = applyFilter(devices, filterState);
const expected = devices.filter(d =>
d.failing_metrics.some(m => ids.includes(m.metric_id))
);
expect(result).toEqual(expected);
}),
{ numRuns: 100 }
);
});
it('when filterState is nonmetric, exactly devices with that metric_id pass', () => {
fc.assert(
fc.property(devicesArb, metricIdArb, (devices, id) => {
const filterState = { type: 'nonmetric', id };
const result = applyFilter(devices, filterState);
const expected = devices.filter(d =>
d.failing_metrics.some(m => m.metric_id === id)
);
expect(result).toEqual(expected);
}),
{ numRuns: 100 }
);
});
});

View File

@@ -0,0 +1,128 @@
/**
* Property-Based Test: Filter state mutual exclusivity
*
* Feature: compliance-nonmetric-filters, Property 3: Filter state mutual exclusivity
* **Validates: Requirements 3.4, 3.5, 5.1**
*
* For any prior filter state and any filter action (metric card click, chip click,
* or clear), the resulting state is exactly one of: null, { type: 'metric', ids: [...] },
* or { type: 'nonmetric', id: string } — never undefined, never both.
*/
import fc from 'fast-check';
// Replicate the filter state transition logic from CompliancePage
function applyFilterAction(currentState, action) {
if (action.type === 'clear') {
return null;
}
if (action.type === 'metric_click') {
const familyIds = action.ids;
// If currently active metric with same ids, toggle off
if (currentState?.type === 'metric' &&
currentState.ids.length === familyIds.length &&
familyIds.every(id => currentState.ids.includes(id))) {
return null;
}
return { type: 'metric', ids: familyIds };
}
if (action.type === 'chip_click') {
const metricId = action.id;
// If currently active nonmetric with same id, toggle off
if (currentState?.type === 'nonmetric' && currentState.id === metricId) {
return null;
}
return { type: 'nonmetric', id: metricId };
}
return currentState;
}
function isValidFilterState(state) {
if (state === null) return true;
if (state && state.type === 'metric' && Array.isArray(state.ids) && state.ids.length > 0) return true;
if (state && state.type === 'nonmetric' && typeof state.id === 'string' && state.id.length > 0) return true;
return false;
}
// Generators
const metricIdArb = fc.stringMatching(/^[A-Za-z0-9_.]{1,20}$/);
const filterStateArb = fc.oneof(
fc.constant(null),
fc.array(metricIdArb, { minLength: 1, maxLength: 5 }).map(ids => ({ type: 'metric', ids })),
metricIdArb.map(id => ({ type: 'nonmetric', id }))
);
const filterActionArb = fc.oneof(
fc.constant({ type: 'clear' }),
fc.array(metricIdArb, { minLength: 1, maxLength: 5 }).map(ids => ({ type: 'metric_click', ids })),
metricIdArb.map(id => ({ type: 'chip_click', id }))
);
describe('Compliance Non-Metric Filter — Property 3: Filter state mutual exclusivity', () => {
it('resulting state is always exactly one of null, metric, or nonmetric — never undefined or combined', () => {
fc.assert(
fc.property(filterStateArb, filterActionArb, (priorState, action) => {
const result = applyFilterAction(priorState, action);
expect(result).not.toBeUndefined();
expect(isValidFilterState(result)).toBe(true);
// Never has both metric and nonmetric properties
if (result !== null) {
if (result.type === 'metric') {
expect(result).not.toHaveProperty('id');
}
if (result.type === 'nonmetric') {
expect(result).not.toHaveProperty('ids');
}
}
}),
{ numRuns: 100 }
);
});
it('clear action always results in null regardless of prior state', () => {
fc.assert(
fc.property(filterStateArb, (priorState) => {
const result = applyFilterAction(priorState, { type: 'clear' });
expect(result).toBeNull();
}),
{ numRuns: 100 }
);
});
it('metric click replaces any prior state with metric filter or null (toggle off)', () => {
fc.assert(
fc.property(filterStateArb, fc.array(metricIdArb, { minLength: 1, maxLength: 5 }), (priorState, ids) => {
const result = applyFilterAction(priorState, { type: 'metric_click', ids });
if (result === null) {
// Toggled off — prior must have been metric with same ids
expect(priorState?.type).toBe('metric');
} else {
expect(result.type).toBe('metric');
expect(result.ids).toEqual(ids);
}
}),
{ numRuns: 100 }
);
});
it('chip click replaces any prior state with nonmetric filter or null (toggle off)', () => {
fc.assert(
fc.property(filterStateArb, metricIdArb, (priorState, id) => {
const result = applyFilterAction(priorState, { type: 'chip_click', id });
if (result === null) {
// Toggled off — prior must have been nonmetric with same id
expect(priorState?.type).toBe('nonmetric');
expect(priorState?.id).toBe(id);
} else {
expect(result.type).toBe('nonmetric');
expect(result.id).toBe(id);
}
}),
{ numRuns: 100 }
);
});
});