65 lines
6.4 KiB
JavaScript
65 lines
6.4 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import ts from 'typescript'
|
|
import { readFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { resolve } from 'node:path'
|
|
const require=createRequire(import.meta.url),zod=require('zod'),here=import.meta.dirname
|
|
function evaluate(source,mocks={}){
|
|
const exports={},module={exports}
|
|
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText
|
|
new Function('require','exports','module',code)(id=>{if(id==='zod')return zod;if(Object.hasOwn(mocks,id))return mocks[id];throw Error('Unexpected dependency '+id)},exports,module)
|
|
return module.exports
|
|
}
|
|
const contracts=evaluate(readFileSync(resolve(here,'../../packages/contracts/src/traffic-flow.ts'),'utf8'))
|
|
let quality=null,slotReleases=0
|
|
const qualityHelper=evaluate(readFileSync(resolve(here,'../src/modules/mcp/flow-read-quality.ts'),'utf8'),{
|
|
'@mmapp/contracts/traffic-flow':contracts,'../../services/traffic-flow-ingest.js':{getFlowRuntimeCounters:async()=>({dataQuality:quality})},'./traffic-history-actions.js':{assertTrafficHistoryAccess:async()=>{}},
|
|
})
|
|
const server={id:1,name:'selected',type:'jump-host'},dto={bpsNow:0,bpsWire:0,bpsOverlay:0,bytesWire:0,live:false,rxSeries:[],txSeries:[],services:[]}
|
|
const window={rows:[],topRowsPerBucketLimit:20,topConversationsLimit:20,since:'from',until:'to'}
|
|
class McpError extends Error{constructor(code,message,statusCode){super(message);this.code=code;this.statusCode=statusCode}}
|
|
const mocks={
|
|
'./flow-read-quality.js':qualityHelper,
|
|
'@mmapp/contracts/traffic-flow':{...contracts,flowAnalyticsDtoSchema:{parse:x=>x},flowMapHopsDtoSchema:{parse:x=>x}},
|
|
'../../services/traffic-flow-analytics.js':{buildFlowAnalytics:async()=>dto,getFlowMonthly:async()=>({month:'2026-10',bytes:0,countries:[],services:[],asns:[]}),safeBuildLiveFlowSample:async()=>({event:'sample',data:dto}),LIVE_ANALYTICS_MINUTES:5},
|
|
'../../services/traffic-flow-map-hops.js':{buildFlowMapHops:async()=>({live:false,hops:[]})},
|
|
'../../services/traffic-flow-source-limits.js':{readFlowSourceLimits:async()=>({topN:20}),readFlowMapSourceLimits:async()=>({topN:20,mapServiceMinSharePct:5})},
|
|
'../servers/repository/servers-repository.js':{getServerRowById:async()=>server},
|
|
'../../services/traffic-flow-server-source.js':{getBoundedFlowServerRowById:async()=>server},
|
|
'./flow-source-scope.js':{assertBoundedFlowSource:async()=>{},flowCatalogDevices:async()=>({selected:[server],incomplete:false})},
|
|
'./traffic-history-actions.js':{assertTrafficHistoryAccess:async()=>{},assertTrafficHistorySource:async()=>{}},
|
|
'./security.js':{McpError},'node:util':require('node:util'),'node:timers/promises':require('node:timers/promises'),
|
|
'../../services/traffic-flow-live-slots.js':{FLOW_LIVE_TICK_MS:2000,tryAcquireFlowLiveSlot:()=>true,releaseFlowLiveSlot:()=>slotReleases++},
|
|
'../../services/traffic-flow-settings.js':{getTrafficFlowSettingsRow:async()=>({topN:20})},
|
|
'../../services/traffic-flow-ifindex.js':{resolveIfaceName:()=>''},
|
|
'../../services/traffic-flow-talkers-model.js':{projectFlowTalkers:()=>({talkers:[],totalConversations:0})},
|
|
'../../services/traffic-flow-catalog-read.js':{readFlowCatalogWindow:async()=>window,readFlowExporterInputs:async()=>({window,catalog:{list:[server]},configuredIds:new Set([1]),configurationMembershipVerified:true}),readFlowClientInputs:async()=>({users:[],total:0,window}),observeFlowExporter:()=>({id:'1'}),observeFlowClient:()=>({})},
|
|
'./catalog-scope.js':{catalogPaging:zod.z.object({offset:zod.z.number().default(0),limit:zod.z.number().default(50)}),catalogDevices:async()=>({selected:[server],incomplete:false})},
|
|
}
|
|
const actions=[]
|
|
for(const [file,method] of [['flow-catalog-actions.ts','registerFlowCatalogActions'],['flow-analytics-actions.ts','registerFlowAnalyticsActions'],['flow-map-actions.ts','registerFlowMapActions'],['flow-live-actions.ts','registerFlowLiveActions']]){
|
|
const actionModule=evaluate(readFileSync(resolve(here,'../src/modules/mcp',file),'utf8'),mocks)
|
|
actionModule[method]((...args)=>actions.push({id:args[0],schema:args[7],read:args[8].read}))
|
|
}
|
|
assert.equal(actions.length,7)
|
|
const base={version:1,collectorEpoch:'00000000-0000-4000-8000-000000000001',scope:'collector-global',exportCoverage:'unverified',localAdmission:'partial',records:'1',bytes:'10',packets:'1',knownLowerBounds:{records:'1',bytes:'10',packets:'1'},firstAt:'2026-10-29T00:00:00.000Z',lastAt:'2026-10-29T00:00:00.000Z',intervalUnknown:false,reasons:{'aggregate-capacity':true,'ring-capacity':false,'key-width':false,'unsafe-numeric':false},counterOverflow:false,rawRowsPolicy:'topN-and-pending-cap-not-complete',lifetimeScope:'current-collector-isolate'}
|
|
const clean={...base,localAdmission:'no-local-loss-observed',records:'0',bytes:'0',packets:'0',knownLowerBounds:{records:'0',bytes:'0',packets:'0'},firstAt:null,lastAt:null,reasons:{'aggregate-capacity':false,'ring-capacity':false,'key-width':false,'unsafe-numeric':false}}
|
|
const cases=[null,base,{...base,localAdmission:'loss-quantity-unknown',bytes:null},{...base,localAdmission:'no-local-loss-observed'},clean]
|
|
let count=0
|
|
for(const globalAllowed of [false,true])for(const state of cases){
|
|
quality=state
|
|
for(const action of actions){
|
|
const input=action.schema.parse(action.id==='traffic.flow.monthly.read'?{month:'2026-10'}:{serverId:1}),context={connection:{policy:{scope:{serverIds:[1],allowGlobal:globalAllowed,operationIds:globalAllowed?['netflow.settings.read']:[]}}}}
|
|
const output=await action.read(input,context),expected=contracts.flowIngestQualitySchema.safeParse(globalAllowed?state:null)
|
|
assert.deepEqual(output.dataQuality,expected.success?expected.data:null,action.id)
|
|
assert.equal(output.collectorQualityAvailable,expected.success,action.id)
|
|
assert.equal(output.collectorQualityScope,'current-collector-isolate',action.id)
|
|
assert.equal(output.incomplete,['traffic.flow.talkers.read','traffic.flow.analytics.read','traffic.flow.map.read','traffic.flow.live.read'].includes(action.id),action.id+' preserves original result completeness')
|
|
assert.equal(output.collectorObservationIncomplete,!expected.success||expected.data.localAdmission!=='no-local-loss-observed',action.id)
|
|
assert.equal(output.coverageVerified,false,action.id);assert.equal(output.exportCoverage,'unverified',action.id)
|
|
count++
|
|
}
|
|
}
|
|
assert.equal(slotReleases,10)
|
|
console.log('PASS '+count+' actual registered MCP read adapter invocations: unavailable, partial, unknown quantities and invalid healthy claim; inert source/auth adapters; real semantic quality schema')
|