const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const vm = require('node:vm') const ts = require('typescript') const vue = require('vue') const agentId = 'b48c0e33-1abd-4b11-89fa-1bb7b39a3057' const token = 'fedeo_agent_' + 'a'.repeat(64) const scanner = 'fujitsu:ScanSnap iX1600:test' const baseAgent = { id: agentId, name: 'Linux Büro', active: true, scannerNames: [], scanDefaults: {}, capabilities: {} } async function setup(t, initialAgent = null, overrides = {}) { const helpers = await import('../utils/agentInstaller.mjs') const requests = [], notices = [], events = [], downloads = [], modals = [] const timers = new Map() const mounted = [], unmounted = [] let timerId = 0 let reportedAgent = { ...baseAgent } const context = vm.createContext({ ...helpers, ref: vue.ref, computed: vue.computed, watch: vue.watch, defineProps: () => ({ initialAgent }), defineEmits: () => (...args) => events.push(args), onMounted: handler => mounted.push(handler), onBeforeUnmount: handler => unmounted.push(handler), useAdmin: () => ({ createInstanceAgent: async body => { requests.push(['create', body]); return { agent: { ...baseAgent }, token } }, updateInstanceAgent: async (id, body) => { requests.push(['update', id, body]); return { agent: { ...baseAgent, ...body } } }, rotateInstanceAgentToken: async id => { requests.push(['rotate', id]); return { token } }, getInstanceAgents: async () => ({ agents: [reportedAgent] }), ...overrides, }), useToast: () => ({ add: notice => notices.push(notice) }), useModal: () => ({ open: (...args) => modals.push(args) }), useRuntimeConfig: () => ({ public: { apiBase: '/backend' } }), window: { location: { origin: 'https://fedeo.example.com' } }, bundle: JSON.parse(fs.readFileSync(__dirname + '/../assets/device-agent/bundle.json', 'utf8')), FileScanModal: 'FileScanModal', Blob, URL: { createObjectURL: blob => { downloads.push(blob); return 'blob:test' }, revokeObjectURL() {} }, document: { createElement: () => ({ click() {}, remove() {} }), body: { appendChild() {} } }, setTimeout: callback => { timers.set(++timerId, callback); return timerId }, clearTimeout: id => timers.delete(id), setInterval: () => -1, clearInterval() {}, }) const source = fs.readFileSync(__dirname + '/../components/AgentSetupWizard.vue', 'utf8') .split('')[0].replace(/^import .*$/gm, '') vm.runInContext(ts.transpile(source, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None }), context) mounted.forEach(handler => handler()) await new Promise(setImmediate) const close = () => unmounted.forEach(handler => handler()) t.after(close) return { run: code => vm.runInContext(code, context), requests, notices, events, downloads, modals, timers, close, report: value => { reportedAgent = value } } } test('new setup creates one agent and downloads software with the correct reverse-proxy URL', async t => { const ui = await setup(t) await ui.run('prepareAgent()') await new Promise(setImmediate) assert.equal(ui.run('step.value'), 1) ui.run('downloadInstaller()') assert.equal(ui.requests.filter(item => item[0] === 'create').length, 1) assert.equal(ui.downloads.length, 1) assert.ok((await ui.downloads[0].text()).startsWith('#!/usr/bin/env bash')) assert.equal(ui.run('endpointPreview.value'), 'https://fedeo.example.com/backend/instance-agent/heartbeat') assert.equal(ui.run('downloaded.value'), true) ui.run('backendUrl.value="https://other.example.com/backend"') await new Promise(setImmediate) assert.equal(ui.run('downloaded.value'), false) }) test('existing agent requires explicit acknowledgement before token rotation', async t => { const ui = await setup(t, { ...baseAgent }) await ui.run('prepareAgent()') await ui.run('rotateToken()') assert.equal(ui.requests.length, 0) ui.run('rotateConfirmed.value=true') await ui.run('rotateToken()') assert.equal(ui.requests[0][0], 'rotate') assert.equal(ui.run('tokenReady.value'), true) assert.equal(ui.run('rotateConfirmed.value'), false) }) test('stale or inactive agents cannot finish setup; fresh reports save the chosen duplex scanner', async t => { const ui = await setup(t, { ...baseAgent }) ui.report({ ...baseAgent, lastSeenAt: new Date(Date.now() - 300000).toISOString(), scannerNames: [scanner], capabilities: { scan: true } }) ui.run('step.value=2; checkingSince.value=Date.now()-1000') await ui.run('checkConnection()') await ui.run('finishSetup()') assert.equal(ui.run('online.value'), false) assert.equal(ui.requests.length, 0) ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true } }) await ui.run('checkConnection()') ui.run('profile.value="ix1600"; step.value=3') await ui.run('finishSetup()') assert.equal(ui.run('saved.value'), true) assert.equal(ui.requests[0][2].preferredScannerName, scanner) assert.equal(ui.requests[0][2].scanDefaults.source, 'ADF Duplex') assert.equal(ui.run('token.value'), '') assert.equal(ui.timers.size, 0) ui.run('openTestScan()') assert.equal(ui.modals[0][1].scanData.agentId, agentId) }) test('online agent without a detected scanner remains incomplete', async t => { const ui = await setup(t, { ...baseAgent }) ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), capabilities: { scan: true } }) await ui.run('checkConnection()') await ui.run('finishSetup()') assert.equal(ui.run('online.value'), true) assert.equal(ui.run('readyToSave.value'), false) assert.equal(ui.requests.length, 0) }) test('connection errors stop automatic requests and are visible for retry', async t => { const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: async () => { throw Error('offline') } }) ui.run('step.value=2') await ui.run('checkConnection()') assert.equal(ui.run('connectionError.value'), 'offline') assert.equal(ui.timers.size, 0) }) test('closing setup clears credentials and does not schedule polling after an in-flight response', async t => { let resolve const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: () => new Promise(done => { resolve = done }) }) ui.run('step.value=2; token.value="' + token + '"') const pending = ui.run('checkConnection()') ui.close() resolve({ agents: [{ ...baseAgent, lastSeenAt: new Date().toISOString() }] }) await pending assert.equal(ui.run('token.value'), '') assert.equal(ui.timers.size, 0) })