diff --git a/src/ops.mjs b/src/ops.mjs index b573ad5..925e031 100644 --- a/src/ops.mjs +++ b/src/ops.mjs @@ -85,9 +85,16 @@ function getPaths(root, installDirName) { * @param {string} dest - Destination directory path. */ function copyDir(src, dest) { - if (fs.existsSync(src)) { - fs.cpSync(src, dest, { recursive: true }); + if (!fs.existsSync(src)) return false; + + const normalizedSrc = path.resolve(src); + const normalizedDest = path.resolve(dest); + if (normalizedSrc === normalizedDest) { + return false; } + + fs.cpSync(src, dest, { recursive: true }); + return true; } /** @@ -149,8 +156,12 @@ function syncAppDirectories(torchDir, verb = 'Copied') { const src = path.join(PKG_ROOT, dir); const dest = path.join(torchDir, dir); if (fs.existsSync(src)) { - copyDir(src, dest); - console.log(` ${verb} ${dir}/`); + const copied = copyDir(src, dest); + if (copied) { + console.log(` ${verb} ${dir}/`); + } else { + console.log(` Skipped ${dir}/ (source and destination are identical)`); + } } } } @@ -179,6 +190,11 @@ function syncAppFiles(torchDir, installDir, verb = 'Copied') { } } + if (path.resolve(src) === path.resolve(dest)) { + console.log(` Skipped ${file} (source and destination are identical)`); + continue; + } + fs.copyFileSync(src, dest); console.log(` ${verb} ${file}`); } diff --git a/test/ops.test.mjs b/test/ops.test.mjs index 3d9a3c9..930afbf 100644 --- a/test/ops.test.mjs +++ b/test/ops.test.mjs @@ -2,6 +2,7 @@ import { test, after } from 'node:test'; import assert from 'node:assert'; import fs from 'node:fs'; import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { WebSocketServer } from 'ws'; @@ -119,6 +120,32 @@ test('cmdUpdate creates backup', async () => { assert.ok(backupContent.includes('roster.json'), 'Backup contains roster.json'); }); +test('cmdUpdate succeeds when running from package root install (src and dest identical)', async () => { + const fixtureRoot = path.join(tempBase, 'project_self_root'); + fs.mkdirSync(fixtureRoot, { recursive: true }); + + const dirsToCopy = ['src', 'bin', 'dashboard', 'landing', 'assets', 'scripts']; + const filesToCopy = ['package.json', 'build.mjs', 'README.md', 'torch-config.example.json', 'TORCH.md', 'eslint.config.mjs']; + + for (const dir of dirsToCopy) { + fs.cpSync(path.join(process.cwd(), dir), path.join(fixtureRoot, dir), { recursive: true }); + } + for (const file of filesToCopy) { + fs.copyFileSync(path.join(process.cwd(), file), path.join(fixtureRoot, file)); + } + + const moduleUrl = pathToFileURL(path.join(fixtureRoot, 'src', 'ops.mjs')); + const { cmdUpdate: fixtureCmdUpdate } = await import(moduleUrl.href); + + await assert.doesNotReject(async () => { + await fixtureCmdUpdate(true, fixtureRoot); + }, 'update should not fail when package root is install target'); + + const backupRoot = path.join(fixtureRoot, '_backups'); + assert.ok(fs.existsSync(backupRoot), '_backups directory created for package-root update'); + assert.ok(fs.readdirSync(backupRoot).length > 0, 'backup entry created for package-root update'); +}); + test('torch-lock check respects local roster', async () => { const wss = new WebSocketServer({ port: 0 }); const port = wss.address().port;