Fix remediation plan and resolution date missing from compliance table

Add ci.resolution_date and ci.remediation_plan to the GET /items endpoint
SELECT clause and update groupByHostname() to aggregate them as first-non-null
across each hostname's metric rows. The frontend already rendered these columns
but the list endpoint never fetched the data from the database.

Includes exploration and preservation property tests for groupByHostname().
This commit is contained in:
Jordan Ramos
2026-05-27 12:54:31 -06:00
parent ea875e9193
commit d65411b0d7
3 changed files with 554 additions and 1 deletions

View File

@@ -0,0 +1,216 @@
/**
* Bug Condition Exploration Property Test: Compliance Remediation Display Fix
*
* Spec: .kiro/specs/compliance-remediation-display-fix/ (bugfix)
*
* BUG CONDITION:
* isBugCondition(row) = row.resolution_date != null OR row.remediation_plan != null
* Any row with metadata set will lose it through groupByHostname() because the
* function does not propagate resolution_date or remediation_plan into device objects.
*
* THIS TEST IS EXPECTED TO FAIL ON UNFIXED CODE.
* Failure confirms the bug exists — resolution_date and remediation_plan are undefined
* in the grouped device objects returned by groupByHostname().
*
* **Validates: Requirements 1.1, 1.2, 2.2**
*/
const fc = require('fast-check');
// --- Mocks (must be installed BEFORE requiring the route module) ---
jest.mock('../middleware/auth', () => ({
requireAuth: () => (req, res, next) => next(),
requireGroup: () => (req, res, next) => next(),
}));
jest.mock('../helpers/auditLog', () => jest.fn());
jest.mock('../helpers/driftChecker', () => ({
loadConfig: jest.fn(() => ({})),
compareSchemaToDrift: jest.fn(() => null),
reconcileConfig: jest.fn(() => ({ changes: [] })),
}));
jest.mock('../helpers/vclHelpers', () => ({
isValidDateString: jest.fn(() => true),
validateRemediationPlan: jest.fn(() => ({ valid: true })),
computeVCLStats: jest.fn(() => ({})),
categorizeNonCompliant: jest.fn(() => []),
rankHeavyHitters: jest.fn(() => []),
computeForecastBurndown: jest.fn(() => ({})),
matchByHostname: jest.fn(() => []),
computeBulkDiff: jest.fn(() => ({})),
mapColumnHeaders: jest.fn(() => ({})),
}));
const mockPool = {
query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })),
connect: jest.fn(() => Promise.resolve({
query: jest.fn(() => Promise.resolve({ rows: [], rowCount: 0 })),
release: jest.fn(),
})),
};
jest.mock('../db', () => mockPool);
const { groupByHostname } = require('../routes/compliance');
// --- Generators ---
/** Generate a date string in YYYY-MM-DD format (avoid toISOString on shrunk invalid dates) */
const arbDateString = fc.tuple(
fc.integer({ min: 2020, max: 2030 }),
fc.integer({ min: 1, max: 12 }),
fc.integer({ min: 1, max: 28 })
).map(([y, m, d]) => `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`);
/** Generate a non-empty remediation plan string */
const arbRemediationPlan = fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0);
/** Generate a hostname (alphanumeric with dashes, realistic) */
const arbHostname = fc.stringMatching(/^[A-Z][A-Z0-9\-]{2,20}$/);
/** Generate a metric_id like "7.1.1" or "3.2" */
const arbMetricId = fc.tuple(
fc.integer({ min: 1, max: 9 }),
fc.integer({ min: 1, max: 9 }),
fc.integer({ min: 1, max: 9 })
).map(([a, b, c]) => `${a}.${b}.${c}`);
/**
* Generate a compliance row with non-null resolution_date and/or remediation_plan.
* This is the bug condition: rows that have metadata which should be propagated.
*/
const arbComplianceRowWithMetadata = fc.record({
hostname: arbHostname,
ip_address: fc.ipV4(),
device_type: fc.constantFrom('Switch', 'Router', 'Firewall', 'Server'),
team: fc.constantFrom('STEAM', 'ACCESS-ENG'),
status: fc.constant('active'),
metric_id: arbMetricId,
metric_desc: fc.string({ minLength: 3, maxLength: 50 }),
category: fc.constantFrom('Configuration', 'Patching', 'Access Control'),
seen_count: fc.integer({ min: 1, max: 20 }),
first_seen: arbDateString,
last_seen: arbDateString,
resolved_on: fc.constant(null),
resolution_date: arbDateString,
remediation_plan: arbRemediationPlan,
});
// --- Property Test ---
describe('Bug Condition Exploration: resolution_date and remediation_plan in groupByHostname()', () => {
it('Property 1: groupByHostname() should propagate resolution_date from rows to device objects', () => {
fc.assert(
fc.property(arbComplianceRowWithMetadata, (row) => {
const rows = [row];
const noteHostnames = new Set();
const devices = groupByHostname(rows, noteHostnames);
// There should be exactly one device for the single hostname
expect(devices).toHaveLength(1);
const device = devices[0];
// BUG CONDITION: resolution_date should be propagated but is undefined on unfixed code
expect(device.resolution_date).toBe(row.resolution_date);
}),
{ numRuns: 100 }
);
});
it('Property 2: groupByHostname() should propagate remediation_plan from rows to device objects', () => {
fc.assert(
fc.property(arbComplianceRowWithMetadata, (row) => {
const rows = [row];
const noteHostnames = new Set();
const devices = groupByHostname(rows, noteHostnames);
expect(devices).toHaveLength(1);
const device = devices[0];
// BUG CONDITION: remediation_plan should be propagated but is undefined on unfixed code
expect(device.remediation_plan).toBe(row.remediation_plan);
}),
{ numRuns: 100 }
);
});
it('Property 3: groupByHostname() should pick first non-null resolution_date across multiple rows for same hostname', () => {
fc.assert(
fc.property(
arbHostname,
fc.array(arbMetricId, { minLength: 2, maxLength: 5 }),
arbDateString,
(hostname, metricIds, resolutionDate) => {
// Create multiple rows for the same hostname, first row has resolution_date
const rows = metricIds.map((mid, idx) => ({
hostname,
ip_address: '10.0.0.1',
device_type: 'Switch',
team: 'STEAM',
status: 'active',
metric_id: mid,
metric_desc: `Metric ${mid}`,
category: 'Configuration',
seen_count: 1,
first_seen: '2025-01-01',
last_seen: '2025-06-01',
resolved_on: null,
resolution_date: idx === 0 ? resolutionDate : null,
remediation_plan: null,
}));
const noteHostnames = new Set();
const devices = groupByHostname(rows, noteHostnames);
expect(devices).toHaveLength(1);
const device = devices[0];
// The first non-null resolution_date should be propagated
expect(device.resolution_date).toBe(resolutionDate);
}
),
{ numRuns: 100 }
);
});
it('Property 4: groupByHostname() should pick first non-null remediation_plan across multiple rows for same hostname', () => {
fc.assert(
fc.property(
arbHostname,
fc.array(arbMetricId, { minLength: 2, maxLength: 5 }),
arbRemediationPlan,
(hostname, metricIds, plan) => {
// Create multiple rows for the same hostname, first row has remediation_plan
const rows = metricIds.map((mid, idx) => ({
hostname,
ip_address: '10.0.0.1',
device_type: 'Switch',
team: 'STEAM',
status: 'active',
metric_id: mid,
metric_desc: `Metric ${mid}`,
category: 'Configuration',
seen_count: 1,
first_seen: '2025-01-01',
last_seen: '2025-06-01',
resolved_on: null,
resolution_date: null,
remediation_plan: idx === 0 ? plan : null,
}));
const noteHostnames = new Set();
const devices = groupByHostname(rows, noteHostnames);
expect(devices).toHaveLength(1);
const device = devices[0];
// The first non-null remediation_plan should be propagated
expect(device.remediation_plan).toBe(plan);
}
),
{ numRuns: 100 }
);
});
});