import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { parseServerInventory, buildWorkloadInventory, inventoryContainerMatch, remoteIdentity, stableWorkloadId, profileMatchesWorkload, sanitizeLegacyContainer, safeRelativeToBase, canonicalServerAppdataPath, deploymentRootCandidate, } = require('../src/main/server-inventory.cjs'); const b64 = (value) => Buffer.from(String(value)).toString('base64'); test('inventory parser handles every evidence record and ignores malformed payloads', () => { const legacy = { Id: 'legacy', Name: '/App', Config: { Image: 'app:1', Labels: { 'com.docker.compose.project': 'app' } }, State: { Running: true, Status: 'running', Health: { Status: 'healthy' } }, Mounts: null }; const safe = { id: 'safe', name: '/Safe', running: false, labels: null, mounts: null, ports: null, networks: null }; const projects = [{ Name: 'app', Status: 'running(1)', ConfigFiles: '/mnt/user/appdata/App/compose.yml,/mnt/user/appdata/App/extra.yml' }, { name: '', configFiles: [] }]; const output = [ 'noise', '__FORGEFLOW_INVENTORY__', `H\ttrue\tfalse\ttrue\ttrue\tfalse\ttrue\t${b64('Compose v2')}\t${b64('Linux')}`, `R\t${b64('/mnt/user/appdata/App')}\t${b64('git@gitea.test:Owner/App.git')}\t${'a'.repeat(40)}\t${b64('main')}`, `C\t${b64(JSON.stringify([legacy, null]))}`, `C\t${b64(JSON.stringify(safe))}`, `C\t${b64('{bad json')}`, `D\t${b64('App')}\t${b64('/templates/App.xml')}\t${b64('http://app')}\t${b64('/icon.png')}\t${b64('/bin/bash')}\t${b64('app:1')}\t${b64('bridge')}`, `P\t${b64(JSON.stringify(projects))}`, `P\t${b64(JSON.stringify({ name: 'single', status: 'exited', config_files: ['single.yml'] }))}`, `Y\t${b64('/mnt/user/appdata/App/')}\t${b64('compose.yml\ncompose.prod.yml\n')}\t${b64('app')}\t${b64('web\nworker')}\t${b64('app:1')}\ttrue\t${b64('')}`, `W\t${b64('partial docker inspect failure')}`, 'UNKNOWN\tignored', ].join('\n'); const parsed = parseServerInventory(output); assert.deepEqual(parsed.capabilities, { docker: true, compose: false, git: true, tar: true, checksum: false, baseWritable: true, composeVersion: 'Compose v2', platform: 'Linux' }); assert.equal(parsed.checkouts.length, 1); assert.equal(parsed.containers.length, 2); assert.equal(parsed.containers[0].health, 'healthy'); assert.deepEqual(parsed.containers[1].labels, {}); assert.equal(parsed.dockerMan[0].templatePath, '/templates/App.xml'); assert.equal(parsed.composeProjects.length, 2); assert.deepEqual(parsed.composeProjects[0].configFiles, ['/mnt/user/appdata/App/compose.yml', '/mnt/user/appdata/App/extra.yml']); assert.deepEqual(parsed.composeDefinitions[0].services, ['web', 'worker']); assert.deepEqual(parsed.warnings, ['partial docker inspect failure']); assert.throws(() => parseServerInventory('ordinary output'), /did not return/i); }); test('server path normalization keeps deployments inside canonical appdata', () => { assert.equal(safeRelativeToBase('/mnt/user/appdata/', '/mnt/user/appdata/App/'), 'App'); for (const value of ['', '/mnt/user/appdata', '/mnt/user/appdata/../etc', '/other/App']) assert.equal(safeRelativeToBase('/mnt/user/appdata', value), ''); assert.equal(canonicalServerAppdataPath('/mnt/user/appdata', '/mnt/cache/appdata/App'), '/mnt/user/appdata/App'); assert.equal(canonicalServerAppdataPath('/mnt/user/appdata', '/mnt/disk2/appdata/App/data'), '/mnt/user/appdata/App/data'); assert.equal(canonicalServerAppdataPath('', '/mnt/user/appdata/App'), '/mnt/user/appdata/App'); assert.equal(canonicalServerAppdataPath('/custom', '/outside/path'), '/outside/path'); assert.equal(canonicalServerAppdataPath('/custom', ''), ''); assert.equal(deploymentRootCandidate('App/source-pre-abcdef1/source'), 'App'); assert.equal(deploymentRootCandidate('App/.forgeflow/incoming'), 'App'); }); test('profile matching requires the same server and accepts each stable identity form', () => { const workload = { serverId: 'server', workloadId: 'workload', selector: { kind: 'compose', composeProject: 'app' }, compose: { project: 'app', workingDir: '/apps/App' }, remoteFolderCandidate: 'App', containers: [{ name: 'app-web' }] }; assert.equal(profileMatchesWorkload(null, workload), false); assert.equal(profileMatchesWorkload({ provider: 'gitea-actions', serverId: 'server' }, workload), false); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'other' }, workload), false); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', workloadIdentity: { workloadId: 'workload' } }, workload), true); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', workloadIdentity: { selector: workload.selector } }, workload), true); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', composeProject: 'app' }, workload), true); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', composeProject: 'app', composeWorkingDir: '/other' }, workload), false); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', remoteFolder: 'App' }, workload), true); assert.equal(profileMatchesWorkload({ provider: 'ssh-unraid', serverId: 'server', containerName: 'app-web' }, workload), true); assert.equal(stableWorkloadId('server', workload.selector), stableWorkloadId('server', workload.selector)); }); test('container matching prioritizes working directory, mounts, provenance and stable names', () => { const checkout = { root: '/apps/App', remote: 'git@gitea.test:Owner/App.git' }; const repository = { name: 'App' }; const base = { running: true, labels: {}, mounts: [], name: 'different' }; assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'com.docker.compose.project.working_dir': '/apps/App/' } }), 100); assert.equal(inventoryContainerMatch(checkout, repository, { ...base, mounts: [{ Source: '/apps/App/data' }] }), 90); assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'org.opencontainers.image.source': 'https://gitea.test/Owner/App' } }), 85); assert.equal(inventoryContainerMatch(checkout, repository, { ...base, labels: { 'com.docker.compose.project': 'app' } }), 70); assert.equal(inventoryContainerMatch(checkout, repository, { ...base, name: '/APP' }), 60); assert.equal(inventoryContainerMatch(checkout, repository, { ...base, running: false }), 0); assert.equal(inventoryContainerMatch(checkout, repository, base), 0); assert.equal(remoteIdentity(''), ''); }); test('workload builder merges runtime, Compose file and DockerMan evidence without backups', () => { const labels = { 'com.docker.compose.project': 'app', 'com.docker.compose.project.working_dir': '/mnt/cache/appdata/App', 'com.docker.compose.project.config_files': '/mnt/cache/appdata/App/compose.yml', 'com.docker.compose.service': 'web', 'tech.itworx.forgeflow.repository': 'git@gitea.test:Owner/App.git', 'tech.itworx.forgeflow.commit': 'b'.repeat(40), 'tech.itworx.forgeflow.branch': 'main', }; const inventory = { containers: [ { id: 'web', name: 'app-web', image: 'registry/app:1', imageId: 'image', running: true, status: 'running', health: 'unhealthy', labels, ports: { '8080/tcp': null, '3000/udp': [{ HostIp: '0.0.0.0', HostPort: '3000' }] }, mounts: [{ Type: 'bind', Source: '/mnt/disk1/appdata/App/data', Destination: '/data', RW: false }], networks: { frontend: {} }, restartPolicy: 'always' }, { id: 'worker', name: 'app-worker', image: 'registry/worker:1', imageId: 'worker', running: false, status: 'exited', health: null, labels: { ...labels, 'com.docker.compose.service': 'worker' }, ports: {}, mounts: [], networks: {}, restartPolicy: '' }, ], checkouts: [{ root: '/mnt/user/appdata/App', remote: 'git@gitea.test:Owner/App.git', liveSha: 'c'.repeat(40), branch: 'release' }], composeProjects: [{ name: 'app', status: 'running', configFiles: [] }, { name: 'headless', status: 'exited', configFiles: ['/mnt/user/appdata/Headless/compose.yml'] }], composeDefinitions: [ { workingDir: '/mnt/user/appdata/App', configFiles: ['/mnt/user/appdata/App/compose.yml'], projectName: 'app', services: ['web', 'worker'], images: ['registry/app:1'], valid: true, error: '' }, { workingDir: '/mnt/user/appdata/Backup/.forgeflow/releases/one', configFiles: ['compose.yml'], projectName: 'backup', services: [], images: [], valid: true }, { workingDir: '/mnt/user/appdata/Standalone', configFiles: ['/mnt/user/appdata/Standalone/compose.yml'], projectName: '', services: ['api'], images: ['standalone:1'], valid: false, error: 'invalid compose' }, ], dockerMan: [ { name: 'app-web', templatePath: '/templates/app.xml', webUiUrl: 'http://app', iconUrl: '/app.png', shell: '/bin/bash', repository: 'registry/app:1', network: 'frontend' }, { name: 'template-only', templatePath: '/templates/template.xml', webUiUrl: '', iconUrl: '', shell: '', repository: 'template:1', network: 'bridge' }, ], warnings: [], capabilities: {}, }; const repository = { fullName: 'Owner/App', name: 'App', cloneUrl: 'https://gitea.test/Owner/App.git' }; const workloads = buildWorkloadInventory({ inventory, server: { id: 'server', name: 'Unraid', basePath: '/mnt/user/appdata' }, repositories: [repository], profiles: [{ id: 'profile', provider: 'ssh-unraid', serverId: 'server', composeProject: 'app', repositoryFullName: 'Owner/App', adoptedFromServer: true }] }); assert.equal(workloads.some((workload) => workload.displayName === 'backup'), false); const app = workloads.find((workload) => workload.displayName === 'app'); assert.equal(app.status, 'linked'); assert.equal(app.runtime.running, true); assert.equal(app.runtime.allRunning, false); assert.equal(app.runtime.health, 'unhealthy'); assert.equal(app.runtime.ports.length, 2); assert.equal(app.containers[0].mounts[0].readOnly, true); assert.equal(app.remoteFolderCandidate, 'App'); assert.equal(app.candidates[0].exact, true); assert.equal(app.link.source, 'automatic'); const standalone = workloads.find((workload) => workload.displayName === 'Standalone'); assert.equal(standalone.metadata.composeDefinitionValid, false); assert.equal(standalone.metadata.composeDefinitionError, 'invalid compose'); const template = workloads.find((workload) => workload.displayName === 'template-only'); assert.equal(template.kind, 'dockerman-container'); assert.equal(template.runtime.running, false); assert.equal(template.metadata.shell, '/bin/sh'); }); test('legacy container sanitizer applies safe defaults to partial Docker inspect data', () => { assert.deepEqual(sanitizeLegacyContainer(null), { id: '', name: '', image: '', imageId: '', running: false, status: '', health: null, labels: { 'com.docker.compose.project': '', 'com.docker.compose.project.working_dir': '', 'com.docker.compose.project.config_files': '', 'com.docker.compose.service': '', 'org.opencontainers.image.source': '', 'org.opencontainers.image.revision': '', 'tech.itworx.forgeflow.repository': '', 'tech.itworx.forgeflow.commit': '', 'tech.itworx.forgeflow.branch': '', 'net.unraid.docker.webui': '', 'net.unraid.docker.icon': '', 'net.unraid.docker.shell': '', 'net.unraid.docker.managed': '', }, ports: {}, mounts: [], networks: {}, restartPolicy: '', }); });