2026-05-18 15:00:53 -06:00
/ * *
* Bug Condition Exploration Property Tests : Compliance Duplicate Chart Entries
*
* Spec : . kiro / specs / compliance - duplicate - chart - entries / ( bugfix )
*
* BUG CONDITION ( from design . md ) :
* EXISTS report _date d WHERE COUNT ( compliance _uploads WHERE report _date = d ) > 1
*
* Five compliance code paths share the root cause " key by ` compliance_uploads.id `
* instead of by ` compliance_uploads.report_date ` " :
* - GET / trends ( Property 1 , test case 1. A )
* - GET / top - recurring ( Property 2 , test case 1. B )
* - GET / category - trend ( Property 3 , test case 1. C )
* - GET / summary ( Property 4 , test case 1. D )
* - persistUpload ( ) snapshots ( Property 5 , test case 1. E )
*
* THIS TEST SUITE IS EXPECTED TO FAIL ON UNFIXED CODE .
* Failure of these five test cases is the SUCCESS CASE for the exploration —
* each failure is a counterexample that confirms the corresponding manifestation
* of the bug exists . After the five fixes from design . md are implemented ,
* these same cases will pass and become regression guards .
*
* Each case is anchored on the canonical fixture
* ( ` fixture_multi_vertical_single_date ` from design . md ) AND wrapped in a
* fast - check ` fc.assert ` against ` arbScenario ` so the property is also
* exercised on randomly - generated multi - vertical scenarios with colliding
* ` report_date ` s .
*
* * * Validates : Requirements 1.1 , 1.2 , 1.3 , 1.4 , 1.5 , 1.6 , 1.7 , 1.8 , 1.9 , 1.10 , 1.11 * *
* /
const http = require ( 'http' ) ;
const express = require ( 'express' ) ;
const fc = require ( 'fast-check' ) ;
// --- Mocks (must be installed BEFORE requiring the route module) ---
jest . mock ( '../middleware/auth' , ( ) => ( {
2026-06-24 11:36:25 -06:00
requireTeam : ( ) => ( req , res , next ) => { req . teamScope = null ; next ( ) ; } ,
2026-05-18 15:00:53 -06:00
requireAuth : ( ) => ( req , res , next ) => {
req . user = { id : 1 , username : 'testuser' , group : 'Admin' } ;
next ( ) ;
} ,
requireGroup : ( ) => ( req , res , next ) => next ( ) ,
} ) ) ;
jest . mock ( '../helpers/auditLog' , ( ) => jest . fn ( ) ) ;
// Programmable pg pool: each test installs a query handler that matches the
// actual SQL fragments emitted by backend/routes/compliance.js. The default
// handler returns an empty rowset.
let queryHandler = ( ) => Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
const recordedQueries = [ ] ;
const mockPool = {
query : jest . fn ( ( text , params ) => {
recordedQueries . push ( { text , params , on : 'pool' } ) ;
return queryHandler ( text , params ) ;
} ) ,
connect : jest . fn ( ( ) => Promise . resolve ( {
query : jest . fn ( ( text , params ) => {
recordedQueries . push ( { text , params , on : 'client' } ) ;
return queryHandler ( text , params ) ;
} ) ,
release : jest . fn ( ) ,
} ) ) ,
} ;
jest . mock ( '../db' , ( ) => mockPool ) ;
const { createComplianceRouter , persistUpload } = require ( '../routes/compliance' ) ;
// --- HTTP helper ---
function request ( server , method , urlPath , body ) {
return new Promise ( ( resolve , reject ) => {
const addr = server . address ( ) ;
const options = {
hostname : '127.0.0.1' ,
port : addr . port ,
path : urlPath ,
method ,
headers : { 'Content-Type' : 'application/json' } ,
} ;
const req = http . request ( options , ( res ) => {
const chunks = [ ] ;
res . on ( 'data' , ( chunk ) => chunks . push ( chunk ) ) ;
res . on ( 'end' , ( ) => {
const raw = Buffer . concat ( chunks ) . toString ( ) ;
let json ;
try { json = JSON . parse ( raw ) ; } catch { json = null ; }
resolve ( { statusCode : res . statusCode , body : json } ) ;
} ) ;
} ) ;
req . on ( 'error' , reject ) ;
if ( body !== undefined ) req . write ( JSON . stringify ( body ) ) ;
req . end ( ) ;
} ) ;
}
// --- Pool router: dispatch by SQL substring/regex ---
/ * *
* Build a query handler from an ordered list of routes . Each route ' s ` match `
* is a substring or RegExp tested against the SQL text . The first match wins .
* ` rows ` may be a static array or a function ( text , params ) => rows .
* /
function makeQueryHandler ( routes ) {
return ( text , params ) => {
for ( const route of routes ) {
const target = route . match ;
const hit = target instanceof RegExp ? target . test ( text ) : text . includes ( target ) ;
if ( hit ) {
const rows = typeof route . rows === 'function'
? ( route . rows ( text , params ) || [ ] )
: ( route . rows || [ ] ) ;
return Promise . resolve ( { rows , rowCount : rows . length } ) ;
}
}
return Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
} ;
}
// --- Fixture builders (per design.md "Test Fixtures Required") ---
const TEAMS = [ 'STEAM' , 'ACCESS-ENG' , 'ACCESS-OPS' , 'INTELDEV' ] ;
const CATEGORIES = [ 'Patching' , 'Configuration' , 'Vulnerability' , 'Other' ] ;
/ * *
* fixture _multi _vertical _single _date — three uploads sharing 2025 - 05 - 11.
* Reproduces the original GitLab # 12 scenario .
*
* Each upload has a distinct vertical ( NTS _AEO , SDIT _CISO , TSI ) , distinct
* counts , and 6 items spread across two teams ( STEAM , ACCESS - ENG ) and two
* categories ( Patching , Configuration ) . Item layout per upload :
* STEAM / Patching x2
* STEAM / Configuration x1
* ACCESS - ENG / Patching x1
* ACCESS - ENG / Configuration x2
* Per - date aggregate ( 3 uploads ) : 9 STEAM , 9 ACCESS - ENG , 9 Patching , 9 Configuration .
* /
function fixtureMultiVerticalSingleDate ( ) {
const verticals = [ 'NTS_AEO' , 'SDIT_CISO' , 'TSI' ] ;
const uploads = verticals . map ( ( v , idx ) => ( {
id : 300 + idx ,
report _date : '2025-05-11' ,
vertical : v ,
new _count : 3 + idx , // 3, 4, 5 sum = 12
recurring _count : 7 + idx * 2 , // 7, 9, 11 sum = 27
resolved _count : 1 + idx , // 1, 2, 3 sum = 6
uploaded _at : ` 2025-05-11T ${ 10 + idx } :00:00Z ` ,
summary _json : JSON . stringify ( {
entries : [ { team : 'STEAM' , metric : 'patching' , score : 80 + idx } ] ,
overall _scores : { patching : 80 + idx } ,
} ) ,
} ) ) ;
const items = [ ] ;
let itemId = 2000 ;
const layout = [
{ team : 'STEAM' , category : 'Patching' } ,
{ team : 'STEAM' , category : 'Patching' } ,
{ team : 'STEAM' , category : 'Configuration' } ,
{ team : 'ACCESS-ENG' , category : 'Patching' } ,
{ team : 'ACCESS-ENG' , category : 'Configuration' } ,
{ team : 'ACCESS-ENG' , category : 'Configuration' } ,
] ;
for ( const u of uploads ) {
for ( const l of layout ) {
items . push ( {
id : itemId ++ ,
upload _id : u . id ,
hostname : ` ${ u . vertical } -host- ${ itemId } ` ,
team : l . team ,
category : l . category ,
vertical : u . vertical ,
status : 'active' ,
} ) ;
}
}
return { uploads , items } ;
}
/ * *
* fixture _cross _vertical _items — two disjoint sets of compliance _items .
* NTS _AEO contributes 100 hosts on team STEAM .
* SDIT _CISO contributes 50 hosts on team ACCESS - ENG .
* Used to exercise the persistUpload ( ) vertical - isolation property ( 1. E ) .
* /
function fixtureCrossVerticalItems ( ) {
const items = [ ] ;
let id = 5000 ;
for ( let i = 1 ; i <= 100 ; i ++ ) {
items . push ( {
id : id ++ ,
hostname : ` nts-aeo-host- ${ i } ` ,
team : 'STEAM' ,
vertical : 'NTS_AEO' ,
status : 'active' ,
} ) ;
}
for ( let i = 1 ; i <= 50 ; i ++ ) {
items . push ( {
id : id ++ ,
hostname : ` sdit-ciso-host- ${ i } ` ,
team : 'ACCESS-ENG' ,
vertical : 'SDIT_CISO' ,
status : 'active' ,
} ) ;
}
return items ;
}
// --- fast-check arbitraries (design.md fixture_pbt_generators, restricted to
// scenarios where the bug condition holds: at least one report_date has
// two or more upload rows). ---
const arbReportDate = fc . constantFrom ( '2025-05-04' , '2025-05-11' , '2025-05-18' ) ;
const arbVertical = fc . constantFrom ( 'NTS_AEO' , 'SDIT_CISO' , 'TSI' , null ) ;
const arbUpload = fc . record ( {
report _date : arbReportDate ,
vertical : arbVertical ,
new _count : fc . integer ( { min : 0 , max : 30 } ) ,
recurring _count : fc . integer ( { min : 0 , max : 30 } ) ,
resolved _count : fc . integer ( { min : 0 , max : 30 } ) ,
} ) ;
/ * *
* arbScenario — a list of compliance _uploads rows where at least one
* report _date appears in two or more rows ( i . e . , the bug condition holds ) .
* The pre - condition is enforced post - generation via filter ( ) so fast - check
* shrinking still finds simple counterexamples .
* /
const arbScenario = fc . array ( arbUpload , { minLength : 2 , maxLength : 6 } )
. filter ( arr => {
const counts = { } ;
for ( const u of arr ) counts [ u . report _date ] = ( counts [ u . report _date ] || 0 ) + 1 ;
return Object . values ( counts ) . some ( c => c > 1 ) ;
} )
. map ( ( rawUploads ) => rawUploads . map ( ( u , i ) => ( {
id : 1000 + i ,
uploaded _at : ` ${ u . report _date } T ${ 10 + i } :00:00Z ` ,
summary _json : JSON . stringify ( {
entries : [ { team : 'STEAM' , metric : 'patching' , score : 80 } ] ,
overall _scores : { patching : 80 } ,
} ) ,
... u ,
} ) ) ) ;
/ * *
* arbScenarioWithItems — arbScenario plus a small set of compliance _items
* ( one to four per upload ) , used by the / category - trend property test .
* /
const arbScenarioWithItems = arbScenario . chain ( uploads => {
const itemArrays = uploads . map ( ( ) => fc . array (
fc . record ( {
team : fc . constantFrom ( ... TEAMS ) ,
category : fc . constantFrom ( ... CATEGORIES ) ,
} ) ,
{ minLength : 1 , maxLength : 4 } ,
) ) ;
return fc . tuple ( fc . constant ( uploads ) , fc . tuple ( ... itemArrays ) )
. map ( ( [ ups , perUpload ] ) => {
const items = [ ] ;
let itemId = 9000 ;
ups . forEach ( ( u , i ) => {
for ( const it of perUpload [ i ] ) {
items . push ( {
id : itemId ++ ,
upload _id : u . id ,
hostname : ` host- ${ itemId } ` ,
vertical : u . vertical ,
status : 'active' ,
... it ,
} ) ;
}
} ) ;
return { uploads : ups , items } ;
} ) ;
} ) ;
// --- Shared mock builders for the four read endpoints ---
/ * *
* Build a query handler that simulates the database state given a list of
* uploads and a list of items . The handler responds to BOTH the unfixed and
* fixed shapes of the SQL — the unfixed shape returns one row per upload ,
* the fixed shape groups by report _date — so the same test code is
* meaningful when re - run against fixed code in tasks 3.2 / 4.2 / 5.2 .
*
* The actual SQL fragments are taken verbatim from backend / routes / compliance . js .
* /
function installReadEndpointHandler ( uploads , items ) {
queryHandler = makeQueryHandler ( [
// ----- /trends and /top-recurring primary uploads listing -----
// Unfixed: SELECT id, report_date, ..., COALESCE(...) AS total_active FROM compliance_uploads ORDER BY report_date ASC
// Fixed: SELECT report_date, SUM(...) ... GROUP BY report_date ORDER BY report_date ASC
{
match : /FROM\s+compliance_uploads(?!\s+cu\b)[\s\S]*ORDER\s+BY\s+report_date\s+ASC/i ,
rows : ( text ) => {
if ( /GROUP\s+BY\s+report_date/i . test ( text ) ) {
// FIXED shape: aggregate by report_date
const byDate = { } ;
for ( const u of uploads ) {
const d = u . report _date ;
if ( ! byDate [ d ] ) {
byDate [ d ] = {
report _date : d ,
new _count : 0 ,
recurring _count : 0 ,
resolved _count : 0 ,
total _active : 0 ,
} ;
}
byDate [ d ] . new _count += u . new _count ;
byDate [ d ] . recurring _count += u . recurring _count ;
byDate [ d ] . resolved _count += u . resolved _count ;
byDate [ d ] . total _active += u . new _count + u . recurring _count ;
}
return Object . values ( byDate ) . sort ( ( a , b ) =>
a . report _date . localeCompare ( b . report _date ) ,
) ;
}
// UNFIXED shape: one row per upload
return [ ... uploads ]
. sort ( ( a , b ) => a . report _date . localeCompare ( b . report _date ) )
. map ( u => ( {
id : u . id ,
report _date : u . report _date ,
new _count : u . new _count ,
recurring _count : u . recurring _count ,
resolved _count : u . resolved _count ,
total _active : u . new _count + u . recurring _count ,
} ) ) ;
} ,
} ,
// ----- /trends per-team item-count query -----
// Unfixed: SELECT ci.upload_id, ci.team, COUNT(ci.id)::int FROM compliance_items ci WHERE ci.team IS NOT NULL GROUP BY ci.upload_id, ci.team
// Fixed: SELECT cu.report_date, ci.team, COUNT(...) FROM compliance_items ci JOIN compliance_uploads cu ON ci.upload_id = cu.id WHERE ci.team IS NOT NULL ... GROUP BY cu.report_date, ci.team
{
match : /FROM\s+compliance_items\s+ci\b[\s\S]*?WHERE\s+ci\.team\s+IS\s+NOT\s+NULL/i ,
rows : ( text ) => {
if ( /GROUP\s+BY\s+cu\.report_date/i . test ( text ) ) {
// FIXED shape
const grouped = { } ;
for ( const it of items ) {
if ( ! it . team ) continue ;
const u = uploads . find ( x => x . id === it . upload _id ) ;
if ( ! u || u . report _date == null ) continue ;
const k = ` ${ u . report _date } | ${ it . team } ` ;
grouped [ k ] = ( grouped [ k ] || 0 ) + 1 ;
}
return Object . entries ( grouped ) . map ( ( [ k , count ] ) => {
const [ report _date , team ] = k . split ( '|' ) ;
return { report _date , team , count } ;
} ) ;
}
// UNFIXED shape: keyed by upload_id
const grouped = { } ;
for ( const it of items ) {
if ( ! it . team ) continue ;
const k = ` ${ it . upload _id } | ${ it . team } ` ;
grouped [ k ] = ( grouped [ k ] || 0 ) + 1 ;
}
return Object . entries ( grouped ) . map ( ( [ k , count ] ) => {
const [ upload _id , team ] = k . split ( '|' ) ;
return { upload _id : Number ( upload _id ) , team , count } ;
} ) ;
} ,
} ,
// ----- /category-trend -----
// Unfixed: SELECT cu.report_date, COALESCE(ci.category, 'Unknown') AS category, COUNT(ci.id)::int AS count
// FROM compliance_uploads cu JOIN compliance_items ci ON ci.upload_id = cu.id
// GROUP BY cu.id, cu.report_date, category ORDER BY cu.report_date ASC
// Fixed: ... GROUP BY cu.report_date, COALESCE(ci.category, 'Unknown') ORDER BY ...
{
match : /FROM\s+compliance_uploads\s+cu\s+JOIN\s+compliance_items\s+ci/i ,
rows : ( text ) => {
if ( /GROUP\s+BY\s+cu\.id/i . test ( text ) ) {
// UNFIXED shape: group by (upload_id, date, category) → duplicate rows per date
const grouped = { } ;
for ( const it of items ) {
const u = uploads . find ( x => x . id === it . upload _id ) ;
if ( ! u || u . report _date == null ) continue ;
const cat = it . category || 'Unknown' ;
const k = ` ${ u . id } | ${ u . report _date } | ${ cat } ` ;
grouped [ k ] = ( grouped [ k ] || 0 ) + 1 ;
}
return Object . entries ( grouped )
. map ( ( [ k , count ] ) => {
const [ , report _date , category ] = k . split ( '|' ) ;
return { report _date , category , count } ;
} )
. sort ( ( a , b ) =>
a . report _date . localeCompare ( b . report _date ) ||
a . category . localeCompare ( b . category ) ,
) ;
}
// FIXED shape: group by (date, category)
const grouped = { } ;
for ( const it of items ) {
const u = uploads . find ( x => x . id === it . upload _id ) ;
if ( ! u || u . report _date == null ) continue ;
const cat = it . category || 'Unknown' ;
const k = ` ${ u . report _date } | ${ cat } ` ;
grouped [ k ] = ( grouped [ k ] || 0 ) + 1 ;
}
return Object . entries ( grouped )
. map ( ( [ k , count ] ) => {
const [ report _date , category ] = k . split ( '|' ) ;
return { report _date , category , count } ;
} )
. sort ( ( a , b ) =>
a . report _date . localeCompare ( b . report _date ) ||
a . category . localeCompare ( b . category ) ,
) ;
} ,
} ,
// ----- /summary primary upload selection -----
// Unfixed: WHERE vertical IS NULL ORDER BY id DESC LIMIT 1
{
match : /WHERE\s+vertical\s+IS\s+NULL\s+ORDER\s+BY\s+id\s+DESC\s+LIMIT\s+1/i ,
rows : ( ) => {
const candidates = uploads
. filter ( u => u . vertical == null && u . summary _json )
. sort ( ( a , b ) => b . id - a . id ) ;
if ( candidates . length === 0 ) return [ ] ;
const u = candidates [ 0 ] ;
return [ {
id : u . id ,
summary _json : u . summary _json ,
report _date : u . report _date ,
uploaded _at : u . uploaded _at ,
} ] ;
} ,
} ,
// Unfixed fallback: WHERE vertical = 'NTS_AEO' ORDER BY id DESC LIMIT 1
{
match : /WHERE\s+vertical\s*=\s*'NTS_AEO'\s+ORDER\s+BY\s+id\s+DESC\s+LIMIT\s+1/i ,
rows : ( ) => {
const candidates = uploads
. filter ( u => u . vertical === 'NTS_AEO' && u . summary _json )
. sort ( ( a , b ) => b . id - a . id ) ;
if ( candidates . length === 0 ) return [ ] ;
const u = candidates [ 0 ] ;
return [ {
id : u . id ,
summary _json : u . summary _json ,
report _date : u . report _date ,
uploaded _at : u . uploaded _at ,
} ] ;
} ,
} ,
// Fixed-side: sibling-disclosure query
// SELECT id, vertical, uploaded_at FROM compliance_uploads WHERE report_date = $1 AND id != $2 ORDER BY id ASC
{
match : /WHERE\s+report_date\s*=\s*\$1\s+AND\s+id\s*!=\s*\$2/i ,
rows : ( _text , params ) => {
const [ reportDate , excludeId ] = params || [ ] ;
return uploads
. filter ( u => u . report _date === reportDate && u . id !== excludeId )
. sort ( ( a , b ) => a . id - b . id )
. map ( u => ( { id : u . id , vertical : u . vertical , uploaded _at : u . uploaded _at } ) ) ;
} ,
} ,
] ) ;
}
// --- Express server setup ---
let app , server ;
beforeAll ( ( done ) => {
app = express ( ) ;
app . use ( express . json ( ) ) ;
const mockUpload = { single : ( ) => ( req , res , next ) => next ( ) } ;
app . use ( '/api/compliance' , createComplianceRouter ( mockUpload ) ) ;
server = app . listen ( 0 , '127.0.0.1' , done ) ;
} ) ;
afterAll ( ( done ) => {
server . close ( done ) ;
} ) ;
beforeEach ( ( ) => {
mockPool . query . mockClear ( ) ;
mockPool . connect . mockClear ( ) ;
recordedQueries . length = 0 ;
queryHandler = ( ) => Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
} ) ;
// =============================================================================
// Test Case 1.A — Property 1: Bug Condition — `/trends` returns one entry
// per unique report_date with summed counts and
// aggregated per-team totals.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
// With three compliance_uploads for 2025-05-11 (NTS_AEO, SDIT_CISO, TSI),
// GET /trends returns three entries with report_date='2025-05-11' instead
// of one. The handler runs `SELECT id, report_date, ... FROM
// compliance_uploads ORDER BY report_date ASC` and `.map()`s each row into
// a trend entry; per-team counts are pre-aggregated by upload_id and
// looked up by `u.id`, so duplicate-date rows produce duplicate-date
// trend entries with split per-team counts.
//
// **Validates: Requirements 1.1, 1.2, 1.3**
//
describe ( 'Bug Condition / Property 1 — GET /trends aggregates uploads by report_date' , ( ) => {
it ( '1.A canonical fixture — exactly one entry per unique report_date with summed counts and aggregated per-team totals' , async ( ) => {
const { uploads , items } = fixtureMultiVerticalSingleDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// (1) Exactly one entry per unique report_date
const targetDate = '2025-05-11' ;
const matchingTrends = res . body . trends . filter ( t => t . report _date === targetDate ) ;
expect ( matchingTrends ) . toHaveLength ( 1 ) ;
// (2) Counts equal SUM across all uploads sharing that date
const targetUploads = uploads . filter ( u => u . report _date === targetDate ) ;
const expectedNew = targetUploads . reduce ( ( s , u ) => s + u . new _count , 0 ) ;
const expectedRecurring = targetUploads . reduce ( ( s , u ) => s + u . recurring _count , 0 ) ;
const expectedResolved = targetUploads . reduce ( ( s , u ) => s + u . resolved _count , 0 ) ;
const expectedTotalActive = targetUploads . reduce ( ( s , u ) => s + u . new _count + u . recurring _count , 0 ) ;
const trend = matchingTrends [ 0 ] ;
expect ( trend . new _count ) . toBe ( expectedNew ) ;
expect ( trend . recurring _count ) . toBe ( expectedRecurring ) ;
expect ( trend . resolved _count ) . toBe ( expectedResolved ) ;
expect ( trend . total _active ) . toBe ( expectedTotalActive ) ;
// (3) Per-team counts equal SUM across uploads sharing that date.
// Layout per upload: 3 STEAM, 3 ACCESS-ENG. Three uploads share the
// date → 9 STEAM, 9 ACCESS-ENG, 0 ACCESS-OPS, 0 INTELDEV.
const targetItems = items . filter ( it =>
targetUploads . some ( u => u . id === it . upload _id ) ,
) ;
const expectedSTEAM = targetItems . filter ( it => it . team === 'STEAM' ) . length ;
const expectedAccessEng = targetItems . filter ( it => it . team === 'ACCESS-ENG' ) . length ;
const expectedAccessOps = targetItems . filter ( it => it . team === 'ACCESS-OPS' ) . length ;
const expectedIntelDev = targetItems . filter ( it => it . team === 'INTELDEV' ) . length ;
expect ( trend . STEAM ) . toBe ( expectedSTEAM ) ;
expect ( trend [ 'ACCESS-ENG' ] ) . toBe ( expectedAccessEng ) ;
expect ( trend [ 'ACCESS-OPS' ] ) . toBe ( expectedAccessOps ) ;
expect ( trend . INTELDEV ) . toBe ( expectedIntelDev ) ;
} ) ;
it ( '1.A property — GET /trends returns one entry per unique report_date for any multi-vertical scenario' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenario , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const uniqueDates = new Set ( uploads . map ( u => u . report _date ) ) ;
expect ( res . body . trends ) . toHaveLength ( uniqueDates . size ) ;
// For each unique date, the entry's counts are the SUM
// across all uploads sharing that date.
for ( const date of uniqueDates ) {
const sharing = uploads . filter ( u => u . report _date === date ) ;
const entry = res . body . trends . find ( t => t . report _date === date ) ;
expect ( entry ) . toBeDefined ( ) ;
expect ( entry . new _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . new _count , 0 ) ) ;
expect ( entry . recurring _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . recurring _count , 0 ) ) ;
expect ( entry . resolved _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . resolved _count , 0 ) ) ;
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 1.B — Property 2: Bug Condition — `/top-recurring` waterfall has
// one bar per unique report_date with correct
// running totals.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
// Three uploads for 2025-05-11 produce three waterfall bars labelled
// "2025-05-11". Worse, computeWaterfall() carries `start` forward across
// the three rows, so the second and third bars' start reflects the prior
// row's end inside the same date — the running totals misrepresent the
// date-level deltas. The fix aggregates uploads to one row per date
// before passing to computeWaterfall().
//
// **Validates: Requirements 1.4, 1.5**
//
describe ( 'Bug Condition / Property 2 — GET /top-recurring has one bar per unique report_date with running invariant' , ( ) => {
it ( '1.B canonical fixture — exactly one waterfall entry per unique report_date and running invariant holds' , async ( ) => {
const { uploads , items } = fixtureMultiVerticalSingleDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const wf = res . body . waterfall ;
// (1) One waterfall entry per unique report_date
const dates = wf . map ( w => w . date ) ;
const uniqueDates = new Set ( uploads . map ( u => u . report _date ) ) ;
expect ( wf ) . toHaveLength ( uniqueDates . size ) ;
expect ( new Set ( dates ) . size ) . toBe ( uniqueDates . size ) ;
// (2) Running invariant: entry[0].start === 0 AND
// entry[i].end === entry[i].start + new_count + recurring_count - resolved_count
// entry[i].start === entry[i-1].end (for i >= 1)
expect ( wf [ 0 ] . start ) . toBe ( 0 ) ;
for ( let i = 0 ; i < wf . length ; i ++ ) {
expect ( wf [ i ] . end ) . toBe (
wf [ i ] . start + wf [ i ] . new _count + wf [ i ] . recurring _count - wf [ i ] . resolved _count ,
) ;
if ( i > 0 ) {
expect ( wf [ i ] . start ) . toBe ( wf [ i - 1 ] . end ) ;
}
}
// (3) For 2025-05-11, the new/recurring/resolved counts equal the
// SUM across all uploads sharing that date.
const target = wf . find ( w => w . date === '2025-05-11' ) ;
const sharing = uploads . filter ( u => u . report _date === '2025-05-11' ) ;
expect ( target . new _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . new _count , 0 ) ) ;
expect ( target . recurring _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . recurring _count , 0 ) ) ;
expect ( target . resolved _count ) . toBe ( sharing . reduce ( ( s , u ) => s + u . resolved _count , 0 ) ) ;
} ) ;
it ( '1.B property — waterfall has exactly one entry per unique report_date and the running invariant always holds' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenario , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const wf = res . body . waterfall ;
const uniqueDates = new Set ( uploads . map ( u => u . report _date ) ) ;
expect ( wf ) . toHaveLength ( uniqueDates . size ) ;
if ( wf . length > 0 ) {
expect ( wf [ 0 ] . start ) . toBe ( 0 ) ;
}
for ( let i = 0 ; i < wf . length ; i ++ ) {
expect ( wf [ i ] . end ) . toBe (
wf [ i ] . start + wf [ i ] . new _count + wf [ i ] . recurring _count - wf [ i ] . resolved _count ,
) ;
if ( i > 0 ) {
expect ( wf [ i ] . start ) . toBe ( wf [ i - 1 ] . end ) ;
}
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 1.C — Property 3: Bug Condition — `/category-trend` returns one
// row per (report_date, category) pair.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
// The query `GROUP BY cu.id, cu.report_date, category` keeps `cu.id` in
// the grouping, so three uploads for 2025-05-11 each produce their own
// (date, category) rows. With items in two categories, the response
// contains 3 × 2 = 6 rows for 2025-05-11 instead of 2.
//
// **Validates: Requirements 1.6, 1.7**
//
describe ( 'Bug Condition / Property 3 — GET /category-trend returns one row per (date, category)' , ( ) => {
it ( '1.C canonical fixture — exactly one row per (report_date, category) and counts equal the SUM across uploads sharing the date' , async ( ) => {
const { uploads , items } = fixtureMultiVerticalSingleDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// (1) Exactly one row for ('2025-05-11', 'Patching')
const patching = res . body . categoryTrend . filter ( c =>
c . report _date === '2025-05-11' && c . category === 'Patching' ,
) ;
expect ( patching ) . toHaveLength ( 1 ) ;
// (2) That row's count equals the total compliance_items in
// 'Patching' across every upload sharing the date.
const expectedPatchingCount = items . filter ( it =>
uploads . some ( u => u . id === it . upload _id && u . report _date === '2025-05-11' ) &&
it . category === 'Patching' ,
) . length ;
expect ( patching [ 0 ] . count ) . toBe ( expectedPatchingCount ) ;
// (3) Same for 'Configuration'.
const configuration = res . body . categoryTrend . filter ( c =>
c . report _date === '2025-05-11' && c . category === 'Configuration' ,
) ;
expect ( configuration ) . toHaveLength ( 1 ) ;
const expectedConfigCount = items . filter ( it =>
uploads . some ( u => u . id === it . upload _id && u . report _date === '2025-05-11' ) &&
it . category === 'Configuration' ,
) . length ;
expect ( configuration [ 0 ] . count ) . toBe ( expectedConfigCount ) ;
} ) ;
it ( '1.C property — for any random multi-vertical scenario, every (date, category) appears at most once' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenarioWithItems , async ( { uploads , items } ) => {
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// (1) Each (date, category) pair appears at most once.
const seen = new Set ( ) ;
for ( const row of res . body . categoryTrend ) {
const key = ` ${ row . report _date } | ${ row . category } ` ;
expect ( seen . has ( key ) ) . toBe ( false ) ;
seen . add ( key ) ;
}
// (2) For every (date, category) pair, the count equals the
// SUM of compliance_items in that category across all
// uploads sharing the date.
for ( const row of res . body . categoryTrend ) {
const expected = items . filter ( it => {
const u = uploads . find ( x => x . id === it . upload _id ) ;
return u && u . report _date === row . report _date &&
( it . category || 'Unknown' ) === row . category ;
} ) . length ;
expect ( row . count ) . toBe ( expected ) ;
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 1.D — Property 4: Bug Condition — `/summary` does not silently
// drop sibling uploads.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
// With three uploads for 2025-05-11 (NTS_AEO, SDIT_CISO, TSI), the query
// `WHERE vertical IS NULL ORDER BY id DESC LIMIT 1` returns nothing
// (none have vertical=null), so the fallback `WHERE vertical = 'NTS_AEO'`
// selects only the NTS_AEO upload. The other two verticals' summary_json
// is silently dropped — no entries are merged AND no
// `multi_vertical_uploads` field exists on the response.
//
// The fix exposes a `multi_vertical_uploads` array (option (b) per
// design.md Fix 4); option (a) would merge entries from every sibling
// upload. Either of these resolves the bug.
//
// **Validates: Requirements 1.8, 1.9**
//
describe ( 'Bug Condition / Property 4 — GET /summary discloses or merges sibling uploads sharing the latest report_date' , ( ) => {
it ( '1.D canonical fixture — response merges sibling entries OR exposes a non-empty multi_vertical_uploads array of length 2' , async ( ) => {
const { uploads , items } = fixtureMultiVerticalSingleDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// The selected primary upload (NTS_AEO via fallback). Sibling uploads
// are SDIT_CISO and TSI for the same report_date.
const primary = uploads . find ( u => u . vertical === 'NTS_AEO' ) ;
const expectedSiblings = uploads
. filter ( u => u . report _date === primary . report _date && u . id !== primary . id )
. sort ( ( a , b ) => a . id - b . id ) ;
const mergedAllEntries =
Array . isArray ( res . body . entries ) &&
res . body . entries . length === uploads . length ;
const disclosedSiblings =
Array . isArray ( res . body . multi _vertical _uploads ) &&
res . body . multi _vertical _uploads . length === expectedSiblings . length ;
// The bug exists iff neither disclosure mechanism is in place.
expect ( mergedAllEntries || disclosedSiblings ) . toBe ( true ) ;
// If the response exposes multi_vertical_uploads, validate its shape.
if ( disclosedSiblings ) {
expect ( res . body . multi _vertical _uploads ) . toHaveLength ( 2 ) ;
const ids = res . body . multi _vertical _uploads . map ( s => s . id ) . sort ( ) ;
expect ( ids ) . toEqual ( expectedSiblings . map ( s => s . id ) . sort ( ) ) ;
const verticals = res . body . multi _vertical _uploads . map ( s => s . vertical ) . sort ( ) ;
expect ( verticals ) . toEqual ( expectedSiblings . map ( s => s . vertical ) . sort ( ) ) ;
}
} ) ;
it ( '1.D property — when two or more uploads share the latest report_date, the response merges entries OR discloses every sibling' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenario , async ( uploads ) => {
// Need at least one upload to have non-null summary_json AND
// be selectable by the existing fallback (vertical IS NULL or
// vertical = 'NTS_AEO'). Skip scenarios that do not exercise
// the /summary code path.
const selectable = uploads . find ( u =>
( u . vertical == null || u . vertical === 'NTS_AEO' ) && u . summary _json ,
) ;
fc . pre ( selectable !== undefined ) ;
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
if ( ! res . body . upload ) {
// No primary upload selected → nothing to disclose.
return ;
}
const primaryDate = res . body . upload . report _date ;
const siblings = uploads . filter ( u =>
u . report _date === primaryDate && u . id !== res . body . upload . id ,
) ;
if ( siblings . length === 0 ) {
// No siblings exist → no disclosure required.
return ;
}
const mergedAllEntries =
Array . isArray ( res . body . entries ) &&
res . body . entries . length >= 1 + siblings . length ;
const disclosedSiblings =
Array . isArray ( res . body . multi _vertical _uploads ) &&
res . body . multi _vertical _uploads . length === siblings . length ;
expect ( mergedAllEntries || disclosedSiblings ) . toBe ( true ) ;
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 1.E — Property 5: Bug Condition — `persistUpload()` snapshot
// reflects only the snapshotted vertical.
// =============================================================================
//
// EXPECTED COUNTEREXAMPLE on UNFIXED code:
// compliance_items has 100 NTS_AEO hosts on team STEAM and 50 SDIT_CISO
// hosts on team ACCESS-ENG. A new SDIT_CISO upload (one item on STEAM,
// distinct hostname) is persisted. The snapshot query has no vertical
// filter and uses `team AS vertical`, so it produces:
// - vertical = 'STEAM' → total_devices = 101 (100 NTS_AEO + 1 new)
// - vertical = 'ACCESS-ENG' → total_devices = 50 (all SDIT_CISO)
// No row exists for vertical='SDIT_CISO', and the row for STEAM is
// contaminated with 100 NTS_AEO hosts. The fix filters by the upload's
// vertical (`WHERE vertical IS NOT DISTINCT FROM $1`) and groups by
// (vertical, team), producing one snapshot row for SDIT_CISO whose
// total_devices reflects only SDIT_CISO hosts.
//
// **Validates: Requirements 1.10, 1.11**
//
describe ( 'Bug Condition / Property 5 — persistUpload() snapshot is filtered to the snapshotted vertical' , ( ) => {
it ( '1.E canonical fixture — compliance_snapshots row for SDIT_CISO reflects only SDIT_CISO items, not NTS_AEO items' , async ( ) => {
const items = fixtureCrossVerticalItems ( ) ; // 100 NTS_AEO STEAM + 50 SDIT_CISO ACCESS-ENG
const incomingItem = {
hostname : 'sdit-ciso-new-host-1' ,
ip _address : '10.0.0.1' ,
device _type : 'srv' ,
team : 'STEAM' ,
metric _id : 'M-NEW' ,
metric _desc : 'desc' ,
category : 'Patching' ,
extra _json : { } ,
} ;
const incomingVertical = 'SDIT_CISO' ;
const snapshotInserts = [ ] ;
// The pool.query handler must respond to:
// 1. SELECT id, hostname, metric_id, ... FROM compliance_items WHERE status = 'active'
// 2. The snapshot SELECT (unfixed: no vertical filter; fixed: vertical filter)
// 3. INSERT INTO compliance_snapshots ... ON CONFLICT ... DO UPDATE
// → captured into snapshotInserts
queryHandler = makeQueryHandler ( [
// (1) Initial active-items load
{
match : /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i ,
rows : ( ) => items . map ( it => ( {
id : it . id ,
hostname : it . hostname ,
metric _id : 'M-EXISTING' ,
seen _count : 1 ,
first _seen _upload _id : 1 ,
} ) ) ,
} ,
// (2a) UNFIXED snapshot query: SELECT team AS vertical ... FROM compliance_items WHERE team IS NOT NULL GROUP BY team
{
match : /SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i ,
rows : ( ) => {
// Aggregate ALL items by team (regardless of vertical) — bug condition.
const byTeam = { } ;
const allHosts = items . concat ( [ {
hostname : incomingItem . hostname ,
team : incomingItem . team ,
vertical : incomingVertical ,
status : 'active' ,
} ] ) ;
for ( const it of allHosts ) {
if ( ! it . team ) continue ;
if ( ! byTeam [ it . team ] ) {
byTeam [ it . team ] = { vertical : it . team , hosts : new Set ( ) , compliant : new Set ( ) , nonCompliant : new Set ( ) } ;
}
byTeam [ it . team ] . hosts . add ( it . hostname ) ;
if ( it . status === 'resolved' ) byTeam [ it . team ] . compliant . add ( it . hostname ) ;
if ( it . status === 'active' ) byTeam [ it . team ] . nonCompliant . add ( it . hostname ) ;
}
return Object . values ( byTeam ) . map ( b => ( {
vertical : b . vertical ,
total _devices : b . hosts . size ,
compliant : b . compliant . size ,
non _compliant : b . nonCompliant . size ,
} ) ) ;
} ,
} ,
// (2b) FIXED snapshot query: ... WHERE team IS NOT NULL AND vertical IS NOT DISTINCT FROM $1 GROUP BY vertical, team
{
match : /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i ,
rows : ( _text , params ) => {
const filterVertical = ( params || [ ] ) [ 0 ] ;
const allHosts = items . concat ( [ {
hostname : incomingItem . hostname ,
team : incomingItem . team ,
vertical : incomingVertical ,
status : 'active' ,
} ] ) ;
const filtered = allHosts . filter ( it => {
if ( filterVertical == null ) return it . vertical == null ;
return it . vertical === filterVertical ;
} ) ;
const byKey = { } ;
for ( const it of filtered ) {
if ( ! it . team ) continue ;
const k = ` ${ it . vertical } | ${ it . team } ` ;
if ( ! byKey [ k ] ) {
byKey [ k ] = {
vertical : it . vertical ,
team : it . team ,
hosts : new Set ( ) ,
compliant : new Set ( ) ,
nonCompliant : new Set ( ) ,
} ;
}
byKey [ k ] . hosts . add ( it . hostname ) ;
if ( it . status === 'resolved' ) byKey [ k ] . compliant . add ( it . hostname ) ;
if ( it . status === 'active' ) byKey [ k ] . nonCompliant . add ( it . hostname ) ;
}
return Object . values ( byKey ) . map ( b => ( {
vertical : b . vertical ,
team : b . team ,
total _devices : b . hosts . size ,
compliant : b . compliant . size ,
non _compliant : b . nonCompliant . size ,
} ) ) ;
} ,
} ,
// (3) Snapshot upsert — capture every insert
{
match : /INSERT\s+INTO\s+compliance_snapshots/i ,
rows : ( _text , params ) => {
const [ snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct ] = params || [ ] ;
snapshotInserts . push ( { snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct } ) ;
return [ ] ;
} ,
} ,
] ) ;
// The transactional client is also routed through queryHandler; it
// must answer the within-transaction queries (BEGIN/COMMIT, INSERT
// INTO compliance_uploads RETURNING id, item upserts, the resolved
// updates, and the final upload counts UPDATE).
const client = {
query : jest . fn ( ( text , params ) => {
if ( /^\s*BEGIN/i . test ( text ) || /^\s*COMMIT/i . test ( text ) || /^\s*ROLLBACK/i . test ( text ) ) {
return Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
}
if ( /INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i . test ( text ) ) {
return Promise . resolve ( { rows : [ { id : 9999 } ] , rowCount : 1 } ) ;
}
// All other within-transaction writes succeed silently.
return Promise . resolve ( { rows : [ ] , rowCount : 1 } ) ;
} ) ,
release : jest . fn ( ) ,
} ;
mockPool . connect . mockResolvedValueOnce ( client ) ;
await persistUpload ( {
items : [ incomingItem ] ,
summary : { entries : [ ] , overall _scores : { } } ,
reportDate : '2025-05-11' ,
filename : 'sdit-ciso-2025-05-11.xlsx' ,
userId : 1 ,
vertical : incomingVertical ,
} ) ;
// (1) A snapshot row was written for vertical = 'SDIT_CISO'.
const sditSnapshots = snapshotInserts . filter ( s => s . vertical === 'SDIT_CISO' ) ;
expect ( sditSnapshots . length ) . toBeGreaterThan ( 0 ) ;
// (2) Its total_devices reflects only SDIT_CISO items.
// Pre-existing SDIT_CISO hosts: 50 (on ACCESS-ENG team).
// Plus the one incoming SDIT_CISO host on STEAM.
// If the snapshot is grouped per (vertical, team), we expect
// either one row totalling 51 hosts or two rows that together
// total 51. Either way, total_devices for SDIT_CISO must NOT
// equal 151 (the inflated cross-vertical figure that includes
// the 100 NTS_AEO hosts).
const sditTotal = sditSnapshots . reduce ( ( s , r ) => s + r . total _devices , 0 ) ;
const ntsAeoHostCount = items . filter ( it => it . vertical === 'NTS_AEO' ) . length ;
expect ( sditTotal ) . toBeLessThan ( ntsAeoHostCount + 1 ) ;
expect ( sditTotal ) . toBe ( 50 + 1 ) ;
// (3) No snapshot row inflates total_devices with NTS_AEO hosts.
// The unfixed code emits vertical='STEAM' with total=101
// (100 NTS_AEO + 1 new SDIT_CISO host). The fix emits per-vertical
// rows, so any STEAM row must reflect only items whose vertical
// is the snapshotted vertical.
const steamSnapshots = snapshotInserts . filter ( s => s . vertical === 'STEAM' ) ;
for ( const s of steamSnapshots ) {
// Under the fix, a snapshot row keyed on vertical='STEAM' should
// not exist at all (the upload's vertical is SDIT_CISO). Even if
// legacy code paths still write a STEAM row, it must not include
// the 100 NTS_AEO hosts as a single combined total.
expect ( s . total _devices ) . toBeLessThan ( ntsAeoHostCount ) ;
}
} ) ;
} ) ;
// =============================================================================
// PRESERVATION TESTS (Task 2 — Property 2)
// =============================================================================
//
// These tests pin the BASELINE behavior of the unfixed code on inputs where
// the bug condition does NOT hold (single-upload-per-date scenarios, empty
// state, error paths, and unrelated query-parameter filtering). They MUST
// pass on the unfixed code; they will continue to pass after the five fixes
// land in tasks 3 through 7. Any future change that alters these responses
// in the non-bug-condition input space is a regression.
//
// Bug Condition negation (from design.md):
// FORALL report_date d, COUNT(compliance_uploads WHERE report_date = d) <= 1
//
// **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10**
//
// --- Preservation fixtures (per design.md "Test Fixtures Required") ---
/ * *
* fixture _empty — no compliance _uploads , no compliance _items .
* Used by 2. A .
* /
function fixtureEmpty ( ) {
return { uploads : [ ] , items : [ ] } ;
}
/ * *
* fixture _single _upload _aeo _legacy — one legacy AEO upload ( vertical IS NULL )
* dated 2025 - 04 - 01 with 20 items distributed across the four ALLOWED _TEAMS .
* Items are evenly tagged across two categories ( Patching , Configuration ) .
* Used by 2. B .
* /
function fixtureSingleUploadAeoLegacy ( ) {
const upload = {
id : 100 ,
report _date : '2025-04-01' ,
vertical : null ,
new _count : 12 ,
recurring _count : 8 ,
resolved _count : 3 ,
uploaded _at : '2025-04-01T09:00:00Z' ,
summary _json : JSON . stringify ( {
entries : [
{ team : 'STEAM' , metric : 'patching' , score : 82 } ,
{ team : 'ACCESS-ENG' , metric : 'patching' , score : 76 } ,
{ team : 'ACCESS-OPS' , metric : 'configuration' , score : 88 } ,
{ team : 'INTELDEV' , metric : 'configuration' , score : 91 } ,
] ,
overall _scores : { patching : 79 , configuration : 90 } ,
} ) ,
} ;
const teamLayout = [ 'STEAM' , 'STEAM' , 'STEAM' , 'STEAM' , 'STEAM' ,
'ACCESS-ENG' , 'ACCESS-ENG' , 'ACCESS-ENG' , 'ACCESS-ENG' , 'ACCESS-ENG' ,
'ACCESS-OPS' , 'ACCESS-OPS' , 'ACCESS-OPS' , 'ACCESS-OPS' , 'ACCESS-OPS' ,
'INTELDEV' , 'INTELDEV' , 'INTELDEV' , 'INTELDEV' , 'INTELDEV' ] ;
const items = teamLayout . map ( ( team , i ) => ( {
id : 4000 + i ,
upload _id : upload . id ,
hostname : ` aeo-host- ${ i + 1 } ` ,
team ,
category : i % 2 === 0 ? 'Patching' : 'Configuration' ,
vertical : null ,
status : 'active' ,
} ) ) ;
return { uploads : [ upload ] , items } ;
}
/ * *
* fixture _single _upload _per _date — five uploads on five distinct dates with
* varied vertical values , satisfying the bug - condition negation
* ( every report _date has exactly one upload row ) . 4 – 6 items per upload .
* Used by 2. C .
* /
function fixtureSingleUploadPerDate ( ) {
const spec = [
{ id : 200 , date : '2025-04-01' , vertical : null , new : 5 , rec : 3 , res : 1 } ,
{ id : 201 , date : '2025-04-08' , vertical : 'NTS_AEO' , new : 7 , rec : 2 , res : 4 } ,
{ id : 202 , date : '2025-04-15' , vertical : 'SDIT_CISO' , new : 4 , rec : 6 , res : 2 } ,
{ id : 203 , date : '2025-04-22' , vertical : 'TSI' , new : 9 , rec : 1 , res : 0 } ,
{ id : 204 , date : '2025-05-01' , vertical : null , new : 6 , rec : 4 , res : 5 } ,
] ;
const uploads = spec . map ( s => ( {
id : s . id ,
report _date : s . date ,
vertical : s . vertical ,
new _count : s . new ,
recurring _count : s . rec ,
resolved _count : s . res ,
uploaded _at : ` ${ s . date } T08:00:00Z ` ,
summary _json : JSON . stringify ( {
entries : [ { team : 'STEAM' , metric : 'patching' , score : 80 + ( s . id % 10 ) } ] ,
overall _scores : { patching : 80 + ( s . id % 10 ) } ,
} ) ,
} ) ) ;
const items = [ ] ;
let itemId = 6000 ;
for ( const u of uploads ) {
const layout = [
{ team : 'STEAM' , category : 'Patching' } ,
{ team : 'STEAM' , category : 'Configuration' } ,
{ team : 'ACCESS-ENG' , category : 'Patching' } ,
{ team : 'ACCESS-OPS' , category : 'Configuration' } ,
{ team : 'INTELDEV' , category : 'Patching' } ,
] ;
for ( const l of layout ) {
items . push ( {
id : itemId ++ ,
upload _id : u . id ,
hostname : ` ${ u . vertical || 'AEO' } -host- ${ itemId } ` ,
team : l . team ,
category : l . category ,
vertical : u . vertical ,
status : 'active' ,
} ) ;
}
}
return { uploads , items } ;
}
/ * *
* fixture _cross _vertical _items _single — only NTS _AEO items present in
* compliance _items ( no SDIT _CISO / TSI items ) . Used by 2. E to exercise
* the persistUpload ( ) single - vertical - month preservation path .
* /
function fixtureSingleVerticalItems ( ) {
const items = [ ] ;
let id = 7000 ;
for ( let i = 1 ; i <= 30 ; i ++ ) {
items . push ( {
id : id ++ ,
hostname : ` nts-aeo-only-host- ${ i } ` ,
team : i % 2 === 0 ? 'STEAM' : 'ACCESS-ENG' ,
vertical : 'NTS_AEO' ,
status : i % 5 === 0 ? 'resolved' : 'active' ,
} ) ;
}
return items ;
}
// --- arbScenario_singleUploadPerDate — fast-check generator restricted to
// scenarios where every report_date has exactly one upload row. ---
const arbUniqueDate = fc . constantFrom (
'2025-03-04' , '2025-03-11' , '2025-03-18' , '2025-03-25' ,
'2025-04-01' , '2025-04-08' , '2025-04-15' , '2025-04-22' ,
'2025-05-01' , '2025-05-18' , '2025-05-25' ,
) ;
const arbUploadUnique = fc . record ( {
report _date : arbUniqueDate ,
vertical : arbVertical ,
new _count : fc . integer ( { min : 0 , max : 30 } ) ,
recurring _count : fc . integer ( { min : 0 , max : 30 } ) ,
resolved _count : fc . integer ( { min : 0 , max : 30 } ) ,
} ) ;
/ * *
* arbScenarioSingleUploadPerDate — list of compliance _uploads rows where
* every report _date appears at most once . Enforced post - generation via a
* filter ; fast - check shrinking still finds simple counterexamples .
* /
const arbScenarioSingleUploadPerDate = fc . array ( arbUploadUnique , { minLength : 0 , maxLength : 6 } )
. filter ( arr => {
const dates = arr . map ( u => u . report _date ) ;
return new Set ( dates ) . size === dates . length ;
} )
. map ( ( rawUploads ) => rawUploads . map ( ( u , i ) => ( {
id : 8000 + i ,
uploaded _at : ` ${ u . report _date } T ${ 10 + i } :00:00Z ` ,
summary _json : JSON . stringify ( {
entries : [ { team : 'STEAM' , metric : 'patching' , score : 80 } ] ,
overall _scores : { patching : 80 } ,
} ) ,
... u ,
} ) ) ) ;
// =============================================================================
// Test Case 2.A — Empty-state preservation
// =============================================================================
//
// With no compliance_uploads and no compliance_items, every read endpoint
// SHALL return its documented empty-state shape unchanged.
//
// **Validates: Requirements 3.3, 3.10**
//
describe ( 'Preservation 2.A — empty-state response shapes are unchanged' , ( ) => {
it ( 'GET /trends returns { trends: [] } when no uploads exist' , async ( ) => {
const { uploads , items } = fixtureEmpty ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body ) . toEqual ( { trends : [ ] } ) ;
} ) ;
it ( 'GET /top-recurring returns { waterfall: [] } when no uploads exist' , async ( ) => {
const { uploads , items } = fixtureEmpty ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body ) . toEqual ( { waterfall : [ ] } ) ;
} ) ;
it ( 'GET /category-trend returns { categoryTrend: [] } when no uploads exist' , async ( ) => {
const { uploads , items } = fixtureEmpty ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body ) . toEqual ( { categoryTrend : [ ] } ) ;
} ) ;
it ( 'GET /summary returns { entries: [], overall_scores: {}, upload: null } when no uploads exist' , async ( ) => {
const { uploads , items } = fixtureEmpty ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body ) . toEqual ( { entries : [ ] , overall _scores : { } , upload : null } ) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 2.B — Single AEO-legacy-upload preservation
// =============================================================================
//
// One legacy upload with vertical IS NULL. The four read endpoints SHALL
// produce responses that match the captured baseline byte-for-byte.
// This is the "snapshot equality — single AEO-only upload" case from
// design.md "Preservation Checking → Test Cases".
//
// **Validates: Requirements 3.1, 3.2, 3.4, 3.5, 3.6**
//
describe ( 'Preservation 2.B — single AEO-legacy upload responses are byte-for-byte stable' , ( ) => {
it ( 'GET /trends returns one entry for the legacy upload with full per-team breakdown' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// The legacy fixture has 5 items per team across 4 teams = 20 items.
// Per-team breakdown is captured exactly so any future change that
// alters this aggregation surfaces as a regression.
expect ( res . body ) . toEqual ( {
trends : [
{
report _date : '2025-04-01' ,
new _count : 12 ,
recurring _count : 8 ,
resolved _count : 3 ,
total _active : 20 ,
STEAM : 5 ,
'ACCESS-ENG' : 5 ,
'ACCESS-OPS' : 5 ,
INTELDEV : 5 ,
} ,
] ,
} ) ;
} ) ;
it ( 'GET /top-recurring returns a single waterfall entry with start=0 and correct end' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// For a single legacy upload, the running-total invariant collapses
// to start=0, end = new + recurring - resolved = 12 + 8 - 3 = 17.
expect ( res . body ) . toEqual ( {
waterfall : [
{
date : '2025-04-01' ,
start : 0 ,
new _count : 12 ,
recurring _count : 8 ,
resolved _count : 3 ,
end : 17 ,
} ,
] ,
} ) ;
} ) ;
it ( 'GET /category-trend returns one row per category with the legacy upload counts' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// 20 items total split across two categories: items at even indexes
// are 'Patching' (10 items), odd indexes are 'Configuration' (10 items).
expect ( res . body ) . toEqual ( {
categoryTrend : [
{ report _date : '2025-04-01' , category : 'Configuration' , count : 10 } ,
{ report _date : '2025-04-01' , category : 'Patching' , count : 10 } ,
] ,
} ) ;
} ) ;
it ( 'GET /summary returns the legacy upload`s entries and overall_scores via the vertical IS NULL fallback' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// The vertical IS NULL → vertical = 'NTS_AEO' fallback selects the
// legacy upload directly (vertical IS NULL succeeds first). The
// response surfaces summary.entries and summary.overall_scores
// unchanged plus a stub upload reference.
expect ( res . body ) . toEqual ( {
entries : [
{ team : 'STEAM' , metric : 'patching' , score : 82 } ,
{ team : 'ACCESS-ENG' , metric : 'patching' , score : 76 } ,
{ team : 'ACCESS-OPS' , metric : 'configuration' , score : 88 } ,
{ team : 'INTELDEV' , metric : 'configuration' , score : 91 } ,
] ,
overall _scores : { patching : 79 , configuration : 90 } ,
upload : {
id : 100 ,
report _date : '2025-04-01' ,
uploaded _at : '2025-04-01T09:00:00Z' ,
} ,
} ) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 2.C — Multiple single-upload-per-date preservation
// =============================================================================
//
// Five distinct dates, one upload per date, varied vertical values. Every
// read endpoint SHALL produce results identical to the captured baseline.
// Bug condition negation holds (no two uploads share a report_date).
//
// **Validates: Requirements 3.1, 3.4, 3.5**
//
describe ( 'Preservation 2.C — multiple single-upload-per-date responses are byte-for-byte stable' , ( ) => {
it ( 'GET /trends returns one entry per date, ordered by report_date ASC, with correct per-team breakdowns' , async ( ) => {
const { uploads , items } = fixtureSingleUploadPerDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// Each upload has 5 items in the layout (2 STEAM, 1 ACCESS-ENG,
// 1 ACCESS-OPS, 1 INTELDEV). With one upload per date, every date
// produces an identical per-team breakdown.
const expectedTrends = uploads . map ( u => ( {
report _date : u . report _date ,
new _count : u . new _count ,
recurring _count : u . recurring _count ,
resolved _count : u . resolved _count ,
total _active : u . new _count + u . recurring _count ,
STEAM : 2 ,
'ACCESS-ENG' : 1 ,
'ACCESS-OPS' : 1 ,
INTELDEV : 1 ,
} ) ) ;
expect ( res . body ) . toEqual ( { trends : expectedTrends } ) ;
} ) ;
it ( 'GET /top-recurring emits one waterfall entry per date with start carrying forward from previous end' , async ( ) => {
const { uploads , items } = fixtureSingleUploadPerDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// Build the expected waterfall by walking uploads in date order and
// carrying `start` forward. Single-upload-per-date is the canonical
// case computeWaterfall() was designed for; the running-total
// semantics MUST match the pre-fix output exactly.
let start = 0 ;
const expectedWaterfall = uploads . map ( u => {
const end = start + u . new _count + u . recurring _count - u . resolved _count ;
const entry = {
date : u . report _date ,
start ,
new _count : u . new _count ,
recurring _count : u . recurring _count ,
resolved _count : u . resolved _count ,
end ,
} ;
start = end ;
return entry ;
} ) ;
expect ( res . body ) . toEqual ( { waterfall : expectedWaterfall } ) ;
} ) ;
it ( 'GET /category-trend returns one row per (date, category), ordered by date then category' , async ( ) => {
const { uploads , items } = fixtureSingleUploadPerDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// Each upload has 3 items in 'Patching' (indexes 0, 2, 4) and
// 2 items in 'Configuration' (indexes 1, 3). Single-upload-per-date
// means each (date, category) pair appears exactly once.
const expected = [ ] ;
for ( const u of uploads ) {
expected . push ( { report _date : u . report _date , category : 'Configuration' , count : 2 } ) ;
expected . push ( { report _date : u . report _date , category : 'Patching' , count : 3 } ) ;
}
expect ( res . body ) . toEqual ( { categoryTrend : expected } ) ;
} ) ;
it ( 'GET /summary surfaces the latest legacy/NTS_AEO upload via the existing fallback' , async ( ) => {
const { uploads , items } = fixtureSingleUploadPerDate ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
// The fallback prefers vertical IS NULL with the highest id. In this
// fixture, the highest-id upload with vertical IS NULL is id=204
// (date 2025-05-01).
const primary = uploads . find ( u => u . id === 204 ) ;
expect ( res . body ) . toEqual ( {
entries : [ { team : 'STEAM' , metric : 'patching' , score : 80 + ( 204 % 10 ) } ] ,
overall _scores : { patching : 80 + ( 204 % 10 ) } ,
upload : {
id : primary . id ,
report _date : primary . report _date ,
uploaded _at : primary . uploaded _at ,
} ,
} ) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 2.D — /summary `team` query parameter preservation
// =============================================================================
//
// The `team` query parameter still filters `entries` server-side and still
// rejects non-ALLOWED_TEAMS values with HTTP 400.
//
// **Validates: Requirement 3.7**
//
describe ( 'Preservation 2.D — GET /summary `team` query parameter is unchanged' , ( ) => {
it ( '?team=STEAM filters entries to STEAM only' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary?team=STEAM' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body . entries ) . toEqual ( [
{ team : 'STEAM' , metric : 'patching' , score : 82 } ,
] ) ;
// The unrelated overall_scores and upload fields remain unchanged
// when filtering by team.
expect ( res . body . overall _scores ) . toEqual ( { patching : 79 , configuration : 90 } ) ;
expect ( res . body . upload ) . toEqual ( {
id : 100 ,
report _date : '2025-04-01' ,
uploaded _at : '2025-04-01T09:00:00Z' ,
} ) ;
} ) ;
it ( '?team=OTHER (not in ALLOWED_TEAMS) returns HTTP 400 with { error }' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary?team=OTHER' ) ;
expect ( res . statusCode ) . toBe ( 400 ) ;
expect ( res . body ) . toEqual ( { error : 'Invalid team' } ) ;
} ) ;
it ( '?team=ACCESS-ENG filters entries to ACCESS-ENG only' , async ( ) => {
const { uploads , items } = fixtureSingleUploadAeoLegacy ( ) ;
installReadEndpointHandler ( uploads , items ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary?team=ACCESS-ENG' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body . entries ) . toEqual ( [
{ team : 'ACCESS-ENG' , metric : 'patching' , score : 76 } ,
] ) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 2.E — persistUpload() single-vertical-month preservation
// =============================================================================
//
// When compliance_items contains rows from only one vertical, the snapshot
// rows written by persistUpload() SHALL be identical to the pre-fix output.
// This is the single-vertical-month equivalent of the fixed-code expectation
// that snapshots reflect only the snapshotted vertical.
//
// **Validates: Requirement 3.8**
//
describe ( 'Preservation 2.E — persistUpload() snapshot is unchanged for single-vertical months' , ( ) => {
it ( 'snapshot rows reflect the single-vertical compliance_items state with no cross-contamination' , async ( ) => {
const items = fixtureSingleVerticalItems ( ) ; // only NTS_AEO items
const incomingItem = {
hostname : 'nts-aeo-new-host-1' ,
ip _address : '10.0.0.1' ,
device _type : 'srv' ,
team : 'STEAM' ,
metric _id : 'M-NEW' ,
metric _desc : 'desc' ,
category : 'Patching' ,
extra _json : { } ,
} ;
const incomingVertical = 'NTS_AEO' ;
const snapshotInserts = [ ] ;
queryHandler = makeQueryHandler ( [
// (1) Initial active-items load
{
match : /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i ,
rows : ( ) => items
. filter ( it => it . status === 'active' )
. map ( it => ( {
id : it . id ,
hostname : it . hostname ,
metric _id : 'M-EXISTING' ,
seen _count : 1 ,
first _seen _upload _id : 1 ,
} ) ) ,
} ,
// (2a) UNFIXED snapshot query: SELECT team AS vertical ... GROUP BY team
{
match : /SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i ,
rows : ( ) => {
// Single-vertical month: aggregating ALL items by team
// gives the same per-vertical answer as filtering by
// the snapshotted vertical, because no other verticals
// contribute. This is the preservation case.
const allHosts = items . concat ( [ {
hostname : incomingItem . hostname ,
team : incomingItem . team ,
vertical : incomingVertical ,
status : 'active' ,
} ] ) ;
const byTeam = { } ;
for ( const it of allHosts ) {
if ( ! it . team ) continue ;
if ( ! byTeam [ it . team ] ) {
byTeam [ it . team ] = { vertical : it . team , hosts : new Set ( ) , compliant : new Set ( ) , nonCompliant : new Set ( ) } ;
}
byTeam [ it . team ] . hosts . add ( it . hostname ) ;
if ( it . status === 'resolved' ) byTeam [ it . team ] . compliant . add ( it . hostname ) ;
if ( it . status === 'active' ) byTeam [ it . team ] . nonCompliant . add ( it . hostname ) ;
}
return Object . values ( byTeam ) . map ( b => ( {
vertical : b . vertical ,
total _devices : b . hosts . size ,
compliant : b . compliant . size ,
non _compliant : b . nonCompliant . size ,
} ) ) ;
} ,
} ,
// (2b) FIXED snapshot query: ... WHERE team IS NOT NULL AND vertical IS NOT DISTINCT FROM $1 GROUP BY vertical, team
//
// The /commit route calls persistUpload() without a vertical
// argument (legacy AEO uploads default to vertical = null), so
// the fixed SQL filters items via `vertical IS NOT DISTINCT FROM
// null`. For a single-vertical month, this is equivalent to
// aggregating by team alone — every contributing item shares the
// same (null) vertical bucket from the upload's perspective.
//
// Returning rows with `vertical: null, team: <team>` makes the
// production INSERT loop's `vs.vertical || vs.team` fallback
// resolve to the team name, which preserves the historical
// team-as-vertical snapshot key for legacy AEO upload contexts.
{
match : /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i ,
rows : ( ) => {
const allHosts = items . concat ( [ {
hostname : incomingItem . hostname ,
team : incomingItem . team ,
vertical : incomingVertical ,
status : 'active' ,
} ] ) ;
const byTeam = { } ;
for ( const it of allHosts ) {
if ( ! it . team ) continue ;
if ( ! byTeam [ it . team ] ) {
byTeam [ it . team ] = {
team : it . team ,
hosts : new Set ( ) ,
compliant : new Set ( ) ,
nonCompliant : new Set ( ) ,
} ;
}
byTeam [ it . team ] . hosts . add ( it . hostname ) ;
if ( it . status === 'resolved' ) byTeam [ it . team ] . compliant . add ( it . hostname ) ;
if ( it . status === 'active' ) byTeam [ it . team ] . nonCompliant . add ( it . hostname ) ;
}
return Object . values ( byTeam ) . map ( b => ( {
vertical : null ,
team : b . team ,
total _devices : b . hosts . size ,
compliant : b . compliant . size ,
non _compliant : b . nonCompliant . size ,
} ) ) ;
} ,
} ,
// (3) Snapshot upsert — capture every insert
{
match : /INSERT\s+INTO\s+compliance_snapshots/i ,
rows : ( _text , params ) => {
const [ snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct ] = params || [ ] ;
snapshotInserts . push ( { snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct } ) ;
return [ ] ;
} ,
} ,
] ) ;
const client = {
query : jest . fn ( ( text , _params ) => {
if ( /^\s*BEGIN/i . test ( text ) || /^\s*COMMIT/i . test ( text ) || /^\s*ROLLBACK/i . test ( text ) ) {
return Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
}
if ( /INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i . test ( text ) ) {
return Promise . resolve ( { rows : [ { id : 9999 } ] , rowCount : 1 } ) ;
}
return Promise . resolve ( { rows : [ ] , rowCount : 1 } ) ;
} ) ,
release : jest . fn ( ) ,
} ;
mockPool . connect . mockResolvedValueOnce ( client ) ;
const result = await persistUpload ( {
items : [ incomingItem ] ,
summary : { entries : [ ] , overall _scores : { } } ,
reportDate : '2025-04-01' ,
filename : 'nts-aeo-only-2025-04-01.xlsx' ,
userId : 1 ,
} ) ;
// (1) Upload commit succeeded with the expected uploadId.
expect ( result . uploadId ) . toBe ( 9999 ) ;
// (2) Snapshot rows were written. With only NTS_AEO items present,
// the per-team aggregation gives:
// - team='STEAM' → all 15 NTS_AEO STEAM hosts (even ids
// in the fixture, indexes 2..30 = 15 hosts) plus the new
// incoming STEAM host = 16 total.
// - team='ACCESS-ENG' → 15 NTS_AEO ACCESS-ENG hosts.
const steamSnap = snapshotInserts . find ( s => s . vertical === 'STEAM' ) ;
const accessSnap = snapshotInserts . find ( s => s . vertical === 'ACCESS-ENG' ) ;
expect ( steamSnap ) . toBeDefined ( ) ;
expect ( accessSnap ) . toBeDefined ( ) ;
// STEAM bucket: NTS_AEO indexes 2,4,...,30 = 15 hosts + the new one.
expect ( steamSnap . total _devices ) . toBe ( 16 ) ;
// ACCESS-ENG bucket: NTS_AEO indexes 1,3,...,29 = 15 hosts.
expect ( accessSnap . total _devices ) . toBe ( 15 ) ;
// (3) compliance_pct is consistent with the captured baseline:
// resolved hosts are at indexes divisible by 5 (5,10,15,20,25,30).
// STEAM: 10, 20, 30 → 3 resolved out of 16 → 18.75%.
// ACCESS-ENG: 5, 15, 25 → 3 resolved out of 15 → 20%.
expect ( steamSnap . compliant ) . toBe ( 3 ) ;
expect ( steamSnap . non _compliant ) . toBe ( 13 ) ; // 16 - 3 resolved = 13 active
expect ( steamSnap . compliance _pct ) . toBe ( 18.75 ) ;
expect ( accessSnap . compliant ) . toBe ( 3 ) ;
expect ( accessSnap . non _compliant ) . toBe ( 12 ) ;
expect ( accessSnap . compliance _pct ) . toBe ( 20 ) ;
} ) ;
} ) ;
// =============================================================================
// Test Case 2.F — persistUpload() snapshot error-path preservation
// =============================================================================
//
// When the snapshot SELECT query fails, persistUpload() SHALL still commit
// the upload, log the error to console.error, and not surface the error to
// the caller. The HTTP layer SHALL NOT respond with an error status.
//
// **Validates: Requirement 3.9**
//
describe ( 'Preservation 2.F — persistUpload() commits the upload when snapshot creation fails' , ( ) => {
it ( 'snapshot query rejection is caught — upload commits, error is logged, no error is thrown to caller' , async ( ) => {
const incomingItem = {
hostname : 'preserve-host-1' ,
ip _address : '10.0.0.99' ,
device _type : 'srv' ,
team : 'STEAM' ,
metric _id : 'M-PRESERVE' ,
metric _desc : 'desc' ,
category : 'Patching' ,
extra _json : { } ,
} ;
const snapshotInserts = [ ] ;
queryHandler = makeQueryHandler ( [
// (1) Initial active-items load — empty
{
match : /SELECT\s+id,\s+hostname,\s+metric_id,\s+seen_count,\s+first_seen_upload_id\s+FROM\s+compliance_items\s+WHERE\s+status\s*=\s*'active'/i ,
rows : ( ) => [ ] ,
} ,
// (2) Snapshot SELECT — REJECT to force the error path. We use
// a sentinel marker via a function-rows handler that throws.
// The makeQueryHandler infrastructure expects a row array, so
// we drop in a row provider that returns a Promise rejection
// by overriding the handler below.
] ) ;
// Wrap the existing handler so the snapshot SELECT rejects.
const baseHandler = queryHandler ;
queryHandler = ( text , params ) => {
if ( /SELECT\s+team\s+AS\s+vertical[\s\S]*FROM\s+compliance_items[\s\S]*WHERE\s+team\s+IS\s+NOT\s+NULL[\s\S]*GROUP\s+BY\s+team/i . test ( text )
|| /WHERE\s+team\s+IS\s+NOT\s+NULL\s+AND\s+vertical\s+IS\s+NOT\s+DISTINCT\s+FROM/i . test ( text ) ) {
return Promise . reject ( new Error ( 'simulated snapshot query failure' ) ) ;
}
if ( /INSERT\s+INTO\s+compliance_snapshots/i . test ( text ) ) {
const [ snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct ] = params || [ ] ;
snapshotInserts . push ( { snapshot _month , vertical , total _devices , compliant , non _compliant , compliance _pct } ) ;
return Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
}
return baseHandler ( text , params ) ;
} ;
const client = {
query : jest . fn ( ( text , _params ) => {
if ( /^\s*BEGIN/i . test ( text ) || /^\s*COMMIT/i . test ( text ) || /^\s*ROLLBACK/i . test ( text ) ) {
return Promise . resolve ( { rows : [ ] , rowCount : 0 } ) ;
}
if ( /INSERT\s+INTO\s+compliance_uploads[\s\S]*RETURNING\s+id/i . test ( text ) ) {
return Promise . resolve ( { rows : [ { id : 4242 } ] , rowCount : 1 } ) ;
}
return Promise . resolve ( { rows : [ ] , rowCount : 1 } ) ;
} ) ,
release : jest . fn ( ) ,
} ;
mockPool . connect . mockResolvedValueOnce ( client ) ;
// Silence the expected console.error so the test output stays clean.
const errSpy = jest . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
let result ;
let thrown ;
try {
result = await persistUpload ( {
items : [ incomingItem ] ,
summary : { entries : [ ] , overall _scores : { } } ,
reportDate : '2025-04-01' ,
filename : 'preserve-error-2025-04-01.xlsx' ,
userId : 1 ,
} ) ;
} catch ( err ) {
thrown = err ;
}
// (1) persistUpload() did NOT throw — the snapshot error is swallowed.
expect ( thrown ) . toBeUndefined ( ) ;
// (2) Upload commit returned the expected payload.
expect ( result ) . toBeDefined ( ) ;
expect ( result . uploadId ) . toBe ( 4242 ) ;
expect ( result . newCount ) . toBe ( 1 ) ;
expect ( result . recurringCount ) . toBe ( 0 ) ;
expect ( result . resolvedCount ) . toBe ( 0 ) ;
// (3) The transaction reached COMMIT (not ROLLBACK) before the
// snapshot block ran. Inspect the client.query mock for COMMIT.
const commitCalls = client . query . mock . calls . filter ( c => / ^ \ s * COMMIT / i . test ( c [ 0 ] ) ) ;
const rollbackCalls = client . query . mock . calls . filter ( c => / ^ \ s * ROLLBACK / i . test ( c [ 0 ] ) ) ;
expect ( commitCalls . length ) . toBe ( 1 ) ;
expect ( rollbackCalls . length ) . toBe ( 0 ) ;
// (4) The error WAS logged (preserves the existing error-path
// observability contract).
expect ( errSpy ) . toHaveBeenCalled ( ) ;
const loggedFirstArg = errSpy . mock . calls . map ( c => String ( c [ 0 ] ) ) . join ( '|' ) ;
expect ( loggedFirstArg ) . toContain ( '[Compliance] Snapshot creation error:' ) ;
// (5) No INSERT INTO compliance_snapshots fired because the SELECT
// rejected before the upsert loop.
expect ( snapshotInserts ) . toHaveLength ( 0 ) ;
errSpy . mockRestore ( ) ;
} ) ;
} ) ;
// =============================================================================
// Cross-endpoint preservation property — fast-check extension of Task 2
// =============================================================================
//
// For any randomly generated scenario where every report_date has exactly
// one upload row (the bug-condition negation), the four read endpoints
// SHALL satisfy the same shape and equality predicates that the captured
// baseline establishes for the canonical fixtures: one trend per date,
// one waterfall entry per date with the running invariant, one
// (date, category) row per pair, and a /summary with a non-null `upload`
// when any selectable upload exists.
//
// **Validates: Requirements 3.1, 3.2, 3.4, 3.5**
//
describe ( 'Preservation cross-endpoint property — single-upload-per-date scenarios match the baseline shape' , ( ) => {
it ( 'GET /trends produces one entry per report_date, in date-ascending order, with counts equal to the per-date upload values' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenarioSingleUploadPerDate , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/trends' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
expect ( res . body . trends ) . toHaveLength ( uploads . length ) ;
// Order: report_date ascending.
const sorted = [ ... uploads ] . sort ( ( a , b ) =>
a . report _date . localeCompare ( b . report _date ) ,
) ;
for ( let i = 0 ; i < sorted . length ; i ++ ) {
const u = sorted [ i ] ;
const t = res . body . trends [ i ] ;
expect ( t . report _date ) . toBe ( u . report _date ) ;
expect ( t . new _count ) . toBe ( u . new _count ) ;
expect ( t . recurring _count ) . toBe ( u . recurring _count ) ;
expect ( t . resolved _count ) . toBe ( u . resolved _count ) ;
expect ( t . total _active ) . toBe ( u . new _count + u . recurring _count ) ;
// No items in this scenario → all per-team counts are 0.
expect ( t . STEAM ) . toBe ( 0 ) ;
expect ( t [ 'ACCESS-ENG' ] ) . toBe ( 0 ) ;
expect ( t [ 'ACCESS-OPS' ] ) . toBe ( 0 ) ;
expect ( t . INTELDEV ) . toBe ( 0 ) ;
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
it ( 'GET /top-recurring produces one waterfall entry per report_date with the running invariant carried forward' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenarioSingleUploadPerDate , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/top-recurring' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const wf = res . body . waterfall ;
expect ( wf ) . toHaveLength ( uploads . length ) ;
if ( wf . length > 0 ) {
expect ( wf [ 0 ] . start ) . toBe ( 0 ) ;
}
for ( let i = 0 ; i < wf . length ; i ++ ) {
expect ( wf [ i ] . end ) . toBe (
wf [ i ] . start + wf [ i ] . new _count + wf [ i ] . recurring _count - wf [ i ] . resolved _count ,
) ;
if ( i > 0 ) {
expect ( wf [ i ] . start ) . toBe ( wf [ i - 1 ] . end ) ;
}
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
it ( 'GET /category-trend has at most one row per (date, category) pair' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenarioSingleUploadPerDate , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/category-trend' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const seen = new Set ( ) ;
for ( const row of res . body . categoryTrend ) {
const key = ` ${ row . report _date } | ${ row . category } ` ;
expect ( seen . has ( key ) ) . toBe ( false ) ;
seen . add ( key ) ;
}
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
it ( 'GET /summary returns null upload when no selectable upload exists, otherwise discloses no siblings' , async ( ) => {
await fc . assert (
fc . asyncProperty ( arbScenarioSingleUploadPerDate , async ( uploads ) => {
installReadEndpointHandler ( uploads , [ ] ) ;
const res = await request ( server , 'GET' , '/api/compliance/summary' ) ;
expect ( res . statusCode ) . toBe ( 200 ) ;
const selectable = uploads . filter ( u =>
( u . vertical == null || u . vertical === 'NTS_AEO' ) && u . summary _json ,
) ;
if ( selectable . length === 0 ) {
expect ( res . body ) . toEqual ( { entries : [ ] , overall _scores : { } , upload : null } ) ;
return ;
}
// A primary upload exists. Bug-condition negation holds, so
// no sibling upload shares its report_date.
expect ( res . body . upload ) . not . toBeNull ( ) ;
const primaryDate = res . body . upload . report _date ;
const siblings = uploads . filter ( u =>
u . report _date === primaryDate && u . id !== res . body . upload . id ,
) ;
expect ( siblings ) . toHaveLength ( 0 ) ;
} ) ,
{ numRuns : 25 } ,
) ;
} ) ;
} ) ;