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, '..') async function loadHelpers() { const exports = {} const code = ts.transpileModule(await fs.readFile(path.join(root, 'src/main/gitModuleImport.ts'), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText vm.runInNewContext(code, { exports, require, URL }) return exports } test('repository addresses strip credentials and reject local/helper protocols', async () => { const api = await loadHelpers() assert.equal(api.repositoryAddress('https://user:secret@example.com/team/module.git?token=secret'), 'https://example.com/team/module.git') assert.equal(api.repositoryAddress('git@example.com:team/module.git'), 'ssh://git@example.com/team/module.git') for (const url of ['file:///tmp/repo', 'ext::command', '/tmp/repo']) assert.throws(() => api.repositoryAddress(url)) assert.equal(api.remoteBranches('a'.repeat(40) + '\trefs/heads/main\ninvalid')[0].name, 'main') }) test('local module discovery ignores malformed folders and rejects symlink escapes', async () => { const api = await loadHelpers() const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'lms-git-list-')) try { const workspace = path.join(dir, 'workspace') const folder = '[lms-nav2-custom]demo_v1.0.0_ws' await fs.mkdir(path.join(workspace, folder), { recursive: true }) await fs.writeFile(path.join(workspace, folder, '.autonomy-studio-module.json'), JSON.stringify({ sourceType: 'CUSTOM', name: 'Demo', category: 'Control', packageName: 'demo', launchFile: 'demo.launch.py' })) assert.equal((await api.listLocalGitModules(workspace))[0].name, 'Demo') await fs.symlink(dir, path.join(workspace, '[lms-nav2-custom]escape_v1.0.0_ws')) await assert.rejects(api.localGitModulePath(workspace, '[lms-nav2-custom]escape_v1.0.0_ws'), /outside/) await assert.rejects(api.localGitModulePath(workspace, '../escape'), /Invalid/) assert.equal((await api.listLocalGitModules(path.join(dir, 'missing'))).length, 0) } finally { await fs.rm(dir, { recursive: true, force: true }) } }) test('Git clone imports only the selected commit, refuses overwrite and cleans failed staging', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'lms-git-import-')) const repo = path.join(dir, 'source') const workspace = path.join(dir, 'remote') const folderName = '[lms-nav2-custom]demo_v1.0.0_ws' const git = (...args) => exec('git', args, { cwd: repo }) try { await fs.mkdir(path.join(repo, 'src/demo/launch'), { recursive: true }) await fs.writeFile(path.join(repo, 'src/demo/package.xml'), 'demo') await fs.writeFile(path.join(repo, 'src/demo/launch/demo.launch.py'), '# launch') for (const file of ['build.sh', 'run.sh']) await fs.writeFile(path.join(repo, file), '#!/bin/bash\nexit 0\n') await git('init', '-b', 'main') await git('add', '.') await git('-c', 'user.name=Test', '-c', 'user.email=test@example.com', 'commit', '-m', 'initial') const commit = (await git('rev-parse', 'HEAD')).stdout.trim() await fs.writeFile(path.join(repo, 'uncommitted.txt'), 'must not transfer') let dirty = false let pushed = true let requestedCommit = commit const index = await fs.readFile(path.join(root, 'src/main/index.ts'), 'utf8') const fn = index.slice(index.indexOf('async function importRemoteGitModule('), index.indexOf('async function transferLocalWorkspaceModuleToRemote(')) const quote = value => "'" + value.replaceAll("'", "'\\''") + "'" const context = { URL, Buffer, normalizeRemoteTarget: target => target, validateCustomModuleScaffold: input => input, inspectLocalGitModule: async () => ({ repository: 'https://example.com/module.git', dirty, pushed, branches: [{ name: 'main', commit: requestedCommit }] }), remoteRepositoryUrl: () => 'file://' + repo, withGitCredential: value => value, customModuleConfigContent: input => JSON.stringify(input), encodeBase64: value => Buffer.from(value).toString('base64'), quoteShellSingle: quote, expandRemotePathVar: () => [], runSshOutput: async (_target, script) => (await exec('bash', ['-c', script], { timeout: 15000 })).stdout, } vm.runInNewContext(ts.transpileModule(fn, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText, context) const input = { sourceFolder: folderName, folderName, branch: 'main', commit, name: 'Remote demo', category: 'Control', packageName: 'demo', launchFile: 'demo.launch.py', dockerImage: 'vehicle:arm64' } const run = () => context.importRemoteGitModule({ workspaceRoot: workspace }, repo, input) dirty = true await assert.rejects(run(), /커밋/) dirty = false pushed = false await assert.rejects(run(), /푸시/) pushed = true assert.equal((await run()).ok, true) const target = path.join(workspace, folderName) assert.equal((await exec('git', ['rev-parse', 'HEAD'], { cwd: target })).stdout.trim(), commit) await assert.rejects(fs.stat(path.join(target, 'uncommitted.txt')), { code: 'ENOENT' }) assert.equal(JSON.parse(await fs.readFile(path.join(target, '.autonomy-studio-module.json'), 'utf8')).dockerImage, 'vehicle:arm64') await fs.writeFile(path.join(target, 'keep.txt'), 'keep') await assert.rejects(run(), /복제에 실패/) assert.equal(await fs.readFile(path.join(target, 'keep.txt'), 'utf8'), 'keep') await fs.rm(target, { recursive: true }) requestedCommit = 'a'.repeat(40) input.commit = requestedCommit await assert.rejects(run(), /복제에 실패/) assert.equal((await fs.readdir(workspace)).length, 0) await fs.mkdir(path.join(repo, 'install')) await fs.writeFile(path.join(repo, 'install/binary'), 'not portable') await git('add', 'install') await git('-c', 'user.name=Test', '-c', 'user.email=test@example.com', 'commit', '-m', 'bad artifacts') input.commit = requestedCommit = (await git('rev-parse', 'HEAD')).stdout.trim() await assert.rejects(run(), /복제에 실패/) assert.equal((await fs.readdir(workspace)).length, 0) } finally { await fs.rm(dir, { recursive: true, force: true }) } })