feat(monitor): discover Docker application endpoints on demand

This commit is contained in:
VAIO73
2026-10-19 08:03:25 +00:00
parent 29778aa709
commit cfb90a094c
13 changed files with 901 additions and 13 deletions
@@ -0,0 +1,47 @@
// Real panel interactions with inert API fixtures; no guest or registration.
const assert = require('node:assert/strict')
const {fixture, text} = require('./test_native_app_discovery_ui.cjs')
const candidate = {
vmid:101,slug:'docker-'+'b'.repeat(24),discovery_id:'docker-'+'b'.repeat(24),
source:'docker_metadata',name:'Uncatalogued web app',default_ports:[8080],
installed_version:null,tracking_suggestion:null,
evidence:[{kind:'docker_container',value:'web-1'},{kind:'docker_image',value:'example/web:7'}],
}
async function main() {
const f = fixture({nativeCandidate:null,dockerCandidate:candidate})
f.render(); await f.settle()
assert.equal(f.calls.filter(c=>c.method==='POST').length,0)
await f.button('searchApplications').props.onClick(); f.render()
assert.equal(f.calls.filter(c=>c.url.endsWith('/docker-discovery')&&c.method==='POST').length,1)
assert.ok(text(f.tree()).includes('example/web:7'))
assert.ok(text(f.tree()).includes('dockerEndpointReview'))
assert.ok(text(f.tree()).includes('nativeVersionUnknown'))
await f.button('registerButton').props.onClick(); f.render()
f.button('cancelButton').props.onClick(); f.render()
assert.equal(f.calls.filter(c=>/\/apps$/.test(c.url)&&c.method==='POST').length,0)
await f.button('registerButton').props.onClick(); f.render()
await f.button('saveButton').props.onClick(); f.render()
const saved=JSON.parse(f.calls.find(c=>/\/apps$/.test(c.url)&&c.method==='POST').body)
assert.equal(saved.discovery_id,candidate.discovery_id)
assert.equal(saved.update_method,'none')
assert.ok(!saved.repo && !saved.helper_slug && !saved.update_command)
for (const status of ['error','partial','busy']) {
const failed=fixture({nativeCandidate:null,dockerCandidate:candidate})
failed.render(); await failed.settle(); failed.setDockerStatus(status)
await failed.button('searchApplications').props.onClick(); failed.render()
assert.ok(text(failed.tree()).includes(status==='partial'?'dockerScanPartial':'dockerScanFailed'))
assert.ok(!text(failed.tree()).includes('noApplicationsDetected'))
}
const changed=fixture({nativeCandidate:null,dockerCandidate:candidate})
changed.render(); await changed.settle(); changed.holdDocker()
const pending=changed.button('searchApplications').props.onClick()
await changed.settle(); changed.render({vmid:102,initialData:{sidecar:{apps:[]},suggestions:{extras:[],port_suggestions:[]}}})
changed.resolveDocker(); await pending; changed.render()
assert.ok(!text(changed.tree()).includes(candidate.name),'late Docker result never crosses CT')
const oci=fixture({nativeCandidate:null,dockerCandidate:candidate})
oci.render({vmid:101,oci:{instance:true},initialData:{sidecar:{apps:[]},suggestions:{oci_instance:{template_id:'image-demo'},port_suggestions:[]}}})
await oci.settle(); await oci.button('refreshData').props.onClick()
assert.equal(oci.calls.filter(c=>c.url.endsWith('/docker-discovery')&&c.method==='POST').length,0)
console.log('PASS Docker Find: explicit scan, generic evidence, Cancel/Save, failure/partial, CT switch, OCI no-scan')
}
main().catch(error=>{console.error(error);process.exitCode=1})
@@ -23,11 +23,13 @@ const candidate = {
evidence: [{kind: 'installation_file', value: '/opt/handmade/server'}], default_ports: [],
}
function fixture({nativeCandidate = candidate} = {}) {
function fixture({nativeCandidate = candidate, dockerCandidate = null} = {}) {
let cursor = 0, tree, props, effects = [], states = [], refs = [], effectStates = []
const calls = [], cachedWrites = []
let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false}
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete', scanIssues
let dockerStatus = "complete", dockerCached = {vmid:101,status:"not_scanned",candidates:[],stale:false}
let holdDocker, dockerGate
let cacheGate, resolveCache, cachePending = false
const seed = {sidecar: {vmid: 101, apps: []}, suggestions: {extras: [], port_suggestions: []}}
const react = {
@@ -56,10 +58,18 @@ function fixture({nativeCandidate = candidate} = {}) {
}
if (options.method === 'POST') {
if (scanGate) await scanGate
native = {...native, status: scanStatus, issues: scanIssues, candidates: [nativeCandidate]}
native = {...native, status: scanStatus, issues: scanIssues, candidates: nativeCandidate ? [nativeCandidate] : []}
}
return structuredClone(native)
}
if (url.endsWith('/docker-discovery')) {
if (options.method === 'POST') {
if (dockerGate) await dockerGate
dockerCached = {vmid:Number(url.split('/')[3]),status:dockerStatus,
stale:dockerStatus !== 'complete',candidates:dockerCandidate ? [dockerCandidate] : []}
}
return structuredClone(dockerCached)
}
if (url.endsWith('/apps/suggestions')) return seed.suggestions
if (url.endsWith('/apps') && options.method === 'POST') {
if (saveGate) await saveGate
@@ -102,6 +112,8 @@ function fixture({nativeCandidate = candidate} = {}) {
holdInitialCache: () => {cachePending = true; cacheGate = new Promise(resolve => {resolveCache = resolve})},
resolveInitialCache: () => resolveCache(),
setScanStatus: value => {scanStatus = value},
setDockerStatus: value => {dockerStatus = value},
holdDocker: () => {dockerGate = new Promise(resolve => {holdDocker = resolve})}, resolveDocker: () => holdDocker(),
setScanIssues: value => {scanIssues = value},
holdScan: () => {scanGate = new Promise(resolve => {holdScan = resolve})}, resolveScan: () => holdScan(),
holdSave: () => {saveGate = new Promise(resolve => {holdSave = resolve})}, resolveSave: () => holdSave(),
@@ -230,4 +242,5 @@ async function main() {
assert.equal(oci.calls.filter(c => c.url.endsWith('/native-discovery') && c.method === 'POST').length, 0, 'OCI refresh never native-scans')
console.log('PASS native discovery: real panel Find → editor → Cancel/Save, no automatic scan, safe payload, CT switching and OCI refresh (fixtures)')
}
main().catch(error => {console.error(error); process.exitCode = 1})
module.exports = {fixture, flatten, text}
if (require.main === module) main().catch(error => {console.error(error); process.exitCode = 1})