const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs/promises') const path = require('node:path') const os = require('node:os') const vm = require('node:vm') const ts = require('typescript') const { execFile } = require('node:child_process') const { promisify } = require('node:util') const exec = promisify(execFile) const root = path.resolve(__dirname, '..') const placeholder = 'Binary or large file preview is not available.' const xml = '\nmkmini_gazebo주행 모델\n' function compile(source, context = {}) { const exports = {} vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, { exports, require, Buffer, ...context }) return exports } async function fixture(run) { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'lms-xml-editor-')) try { for (const name of ['model.config', 'model.sdf', 'simulation.world', 'MODEL.CONFIG']) await fs.writeFile(path.join(dir, name), xml) await fs.writeFile(path.join(dir, 'large.config'), 'x'.repeat(1024 * 1024 + 1)) await fs.writeFile(path.join(dir, 'model.glb'), Buffer.from([0, 1, 2, 3])) await run(dir) } finally { await fs.rm(dir, { recursive: true, force: true }) } } test('local module and root collectors read small Gazebo XML files without changing binary and size exclusions', async () => { const source = ts.createSourceFile('index.ts', await fs.readFile(path.join(root, 'src/main/index.ts'), 'utf8'), ts.ScriptTarget.Latest, true) const functions = source.statements.filter(node => ts.isFunctionDeclaration(node) && ['collectModuleFiles', 'collectRootFiles'].includes(node.name?.text)) const pattern = source.statements.find(node => ts.isVariableStatement(node) && node.declarationList.declarations[0].name.getText(source) === 'textFilePattern') assert.equal(functions.length, 2) assert.ok(pattern) await fixture(async dir => { const api = compile([pattern.getText(source), ...functions.map(node => `export ${node.getText(source)}`)].join('\n'), { readdir: fs.readdir, stat: fs.stat, readFile: fs.readFile, resolve: path.resolve, basename: path.basename, skippedCacheDirs: new Set(['.git', 'build', 'install', 'log', 'node_modules', 'docker']), findModuleRoot: async () => dir, toRendererModulePath: (name, file, base) => `modules/${name}/${path.relative(base, file)}`, toRendererRootPath: (name, file, base) => `${name}/${path.relative(base, file)}` }) for (const result of [await api.collectModuleFiles('NAV2-MKMini'), await api.collectRootFiles(dir, 'examples')]) { const files = Object.fromEntries(Object.entries(result.files).map(([file, content]) => [path.basename(file), content])) for (const name of ['model.config', 'model.sdf', 'simulation.world', 'MODEL.CONFIG']) assert.equal(files[name], xml) for (const name of ['large.config', 'model.glb']) assert.equal(files[name], placeholder) } }) }) test('remote listing, content and search include Gazebo XML while preserving size, NUL and path protections', async () => { const api = compile(await fs.readFile(path.join(root, 'src/main/remoteModuleFiles.ts'), 'utf8')) await fixture(async dir => { const request = async input => JSON.parse((await exec('bash', ['-c', api.remoteModuleFilesScript(dir, input)], { timeout: 10000 })).stdout) const listing = await request({ action: 'list' }) assert.ok(listing.filePaths.includes('model.config')) for (const file of ['model.config', 'model.sdf', 'simulation.world', 'MODEL.CONFIG']) assert.equal((await request({ action: 'content', path: file })).content, xml) for (const file of ['large.config', 'model.glb']) assert.equal((await request({ action: 'content', path: file })).content, placeholder) await fs.writeFile(path.join(dir, 'binary.config'), Buffer.from('text\0binary')) assert.equal((await request({ action: 'content', path: 'binary.config' })).content, placeholder) const matches = await request({ action: 'search', query: 'mkmini_gazebo' }) assert.deepEqual(matches.map(match => match.file).sort(), ['MODEL.CONFIG', 'model.config', 'model.sdf', 'simulation.world'].sort()) await assert.rejects(request({ action: 'content', path: '../outside.config' }), /Invalid module path/) await fs.symlink(path.join(dir, '..'), path.join(dir, 'escape')) await assert.rejects(request({ action: 'content', path: 'escape/outside.config' }), /Invalid module path/) }) }) test('Gazebo model.config, SDF and world files use XML highlighting without treating arbitrary config formats as XML', async () => { const api = compile(await fs.readFile(path.join(root, 'src/renderer/src/features/workspace/workspaceFiles.ts'), 'utf8')) for (const file of ['model.config', 'models/mkmini/model.config', 'models/MODEL.CONFIG', 'model.sdf', 'simulation.world', 'WORLD.SDF', 'package.xml', 'robot.urdf', 'robot.xacro']) assert.equal(api.getEditorLanguage(file), 'xml', file) assert.equal(api.getEditorLanguage('app.config'), 'plaintext') assert.equal(api.getEditorLanguage('control.yaml'), 'yaml') assert.equal(api.getEditorLanguage('model.glb'), 'plaintext') })