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 api = {} vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentPractice.ts'), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, { exports: api, DOMException, setTimeout }) const { AGENT_PRACTICE_PROFILES: profiles, inspectAgentPractice, verifyAgentPractice, prepareAgentPractice } = api const plain = value => JSON.parse(JSON.stringify(value)) const profile = profiles.find(item => item.id === 'mkmini-2d') const modulesFor = p => p.modules.map((name, sortOrder) => ({ name, sortOrder, category: name.includes('MKMini') || name.includes('TurtleBot') || name === 'NAV2-Yahboom' ? 'Vehicle' : name.includes('Gazebo') ? 'Simulator' : name.includes('Perception') ? 'Sensing' : 'System', state: 'stopped', parameters: [`sim_mode:=${p.environment === 'local'}`, `platform:=${p.platform}`, `robot:=${p.platform}`, `level:=${p.level}`] })) const probeFor = (p, overrides = {}) => ({ id: 'probe', name: 'ros.inspect', status: 'ok', target: 'test:container', observedAt: new Date().toISOString(), truncated: false, data: JSON.stringify({ topics: p.topics.map(name => ({ name, publishers: 1, samples: 3, validSamples: 3, frameIds: name === p.topics[0] ? ['laser'] : [] })), tfEdges: [{ parent: 'map', child: 'odom' }, { parent: 'odom', child: 'base_footprint' }, { parent: 'base_footprint', child: 'base_link' }, { parent: 'base_link', child: 'laser' }], ...overrides }) }) function scenario(overrides = {}) { const modules = modulesFor(profile) const starts = [] const controller = new AbortController() const options = { profileId: profile.id, environment: 'remote', signal: controller.signal, approvedStarts: [...profile.modules], list: async () => modules.map(module => ({ ...module, parameters: [...module.parameters] })), start: async module => { starts.push(module.name); modules.find(item => item.name === module.name).state = 'running'; return true }, probe: async () => probeFor(profile), progress: () => {}, pause: async () => {}, ...overrides } return { modules, starts, controller, options } } test('SLAM profiles follow the module guide module sequences', () => { const source = ts.createSourceFile('ModuleListView.tsx', fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/ModuleListView.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) let expression const visit = node => { if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'moduleGuidePages') expression = node.initializer.getText(source); ts.forEachChild(node, visit) } visit(source) const guide = {} vm.runInNewContext(ts.transpileModule(`exports.pages = ${expression}`, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, { exports: guide }) for (const p of profiles) assert.ok(guide.pages.some(page => JSON.stringify(page.slamModules) === JSON.stringify(p.modules))) }) test('preflight rejects wrong environments, missing modules, conflicting parameters and unrelated running modules', () => { const modules = modulesFor(profile) assert.equal(inspectAgentPractice(profile.id, modules, 'remote').canPrepare, true) assert.equal(inspectAgentPractice(profile.id, modules, 'local').canPrepare, false) assert.equal(inspectAgentPractice(profile.id, modules.slice(1), 'remote').canPrepare, false) modules[0].parameters[0] = 'sim_mode:=true' assert.match(inspectAgentPractice(profile.id, modules, 'remote').blockers.join(), /sim_mode/) modules[0].parameters = [] assert.match(inspectAgentPractice(profile.id, modules, 'remote').blockers.join(), /sim_mode/) const conflicting = [...modulesFor(profile), { name: 'NAV2-Control', category: 'Control', state: 'running', parameters: [] }] assert.match(inspectAgentPractice(profile.id, conflicting, 'remote').blockers.join(), /NAV2-Control/) assert.equal(inspectAgentPractice(profile.id, [...modulesFor(profile), { name: 'NAV2-Camera', category: 'Sensing', state: 'running', parameters: [] }], 'remote').canPrepare, true) }) test('readiness requires observed sensor, odometry, map data and a connected TF chain', () => { assert.equal(verifyAgentPractice(profile.id, probeFor(profile)).ready, true) assert.equal(verifyAgentPractice(profile.id, probeFor(profile, { tfEdges: [] })).ready, false) assert.equal(verifyAgentPractice(profile.id, probeFor(profile, { topics: profile.topics.map(name => ({ name, publishers: 1, samples: 0 })) })).ready, false) assert.equal(verifyAgentPractice(profile.id, probeFor(profile, { tfEdges: [{ parent: 'map', child: 'odom' }, { parent: 'odom', child: 'base_link' }] })).ready, false) assert.equal(verifyAgentPractice(profile.id, { ...probeFor(profile), status: 'error' }).ready, false) assert.equal(verifyAgentPractice(profile.id, { ...probeFor(profile), truncated: true }).ready, false) assert.equal(verifyAgentPractice(profile.id, { ...probeFor(profile), data: 'null' }).ready, false) }) test('prepares only missing starts, in dependency order, and verifies in the SLAM container', async () => { const s = scenario() s.modules[0].state = 'running' let observed s.options.probe = async (module, topics) => { observed = { module: module.name, topics }; return probeFor(profile) } const result = await prepareAgentPractice(s.options) assert.equal(result.ready, true) assert.deepEqual(s.starts, [...profile.modules].slice(1)) assert.equal(observed.module, 'NAV2-SLAM') assert.ok(observed.topics.includes('/tf_static')) assert.equal(result.steps[0].status, 'already-running') }) test('a failed start stops subsequent starts and does not issue rollback stop commands', async () => { const s = scenario() s.options.start = async module => { s.starts.push(module.name); if (s.starts.length === 2) return false; s.modules[0].state = 'running'; return true } const result = await prepareAgentPractice(s.options) assert.equal(result.ready, false) assert.deepEqual(s.starts, [...profile.modules].slice(0, 2)) assert.match(result.failures[0], /자동 종료하지/) }) test('does not start modules that became stopped after approval', async () => { const s = scenario({ approvedStarts: ['NAV2-SLAM'] }) await assert.rejects(prepareAgentPractice(s.options), /승인 이후/) assert.equal(s.starts.length, 0) }) test('does not start with changed parameters after approval', async () => { const s = scenario() s.options.approvedParameters = Object.fromEntries(s.modules.map(module => [module.name, [...module.parameters]])) s.modules[0].parameters.push('custom_speed:=9') await assert.rejects(prepareAgentPractice(s.options), /실행 파라미터가 바뀌/) assert.equal(s.starts.length, 0) }) test('cancellation during a start prevents any subsequent module start', async () => { const s = scenario() s.options.start = async module => { s.starts.push(module.name); s.controller.abort(); return true } await assert.rejects(prepareAgentPractice(s.options), { name: 'AbortError' }) assert.equal(s.starts.length, 1) }) test('verification retries are bounded and never report ready for publisher-only data', async () => { const s = scenario() let probes = 0 s.options.probe = async () => { probes++; return probeFor(profile, { topics: [] }) } const result = await prepareAgentPractice(s.options) assert.equal(result.ready, false) assert.equal(probes, 3) assert.equal(result.failures.length, 4) assert.ok(result.failures.some(message => message.includes('센서 프레임'))) }) test('the common confirmation and apply-before-build guard are connected in the workspace', () => { const source = fs.readFileSync(require.resolve('../src/renderer/src/App.tsx'), 'utf8') assert.match(source, //) })