From 7e08a432cc93a3f5e9892f106704422c210a79ec Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:34:32 +0000 Subject: [PATCH 1/2] test(cmd-backup): add integration tests for backup functionality - Refactor `cmdBackup` and `listBackups` in `src/cmd-backup.mjs` to accept `cwd` parameter for dependency injection. - Add `test/cmd-backup.test.mjs` to test backup creation, missing files handling, output override, and listing functionality. - Ensure tests run in isolated temporary directories using `fs.mkdtemp`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/cmd-backup.mjs | 17 ++++--- test/cmd-backup.test.mjs | 99 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 test/cmd-backup.test.mjs diff --git a/src/cmd-backup.mjs b/src/cmd-backup.mjs index ee053b2..806ce7d 100644 --- a/src/cmd-backup.mjs +++ b/src/cmd-backup.mjs @@ -2,8 +2,6 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { execSync } from 'node:child_process'; -const BACKUP_ROOT = path.resolve(process.cwd(), '.torch/backups'); - // Paths to snapshot on each backup run const STATE_SOURCES = [ '.scheduler-memory/memory-store.json', @@ -22,19 +20,21 @@ const STATE_SOURCES = [ * * @param {Object} [opts] * @param {string|null} [opts.output] - Override destination directory (default: .torch/backups/) + * @param {string} [opts.cwd] - Working directory (default: process.cwd()) * @returns {Promise<{backupDir: string, manifest: Object}>} */ -export async function cmdBackup({ output = null } = {}) { +export async function cmdBackup({ output = null, cwd = process.cwd() } = {}) { + const BACKUP_ROOT = path.resolve(cwd, '.torch/backups'); const ts = new Date().toISOString().replace(/[:.]/g, '-'); const backupDir = output - ? path.resolve(process.cwd(), output) + ? path.resolve(cwd, output) : path.join(BACKUP_ROOT, ts); await fs.mkdir(backupDir, { recursive: true }); let gitCommit = null; try { - gitCommit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim(); + gitCommit = execSync('git rev-parse HEAD', { stdio: 'pipe', cwd }).toString().trim(); } catch { // Not in a git repo or git unavailable } @@ -43,7 +43,7 @@ export async function cmdBackup({ output = null } = {}) { const skipped = []; for (const relPath of STATE_SOURCES) { - const src = path.resolve(process.cwd(), relPath); + const src = path.resolve(cwd, relPath); const dest = path.join(backupDir, relPath.replace(/\//g, '__')); try { await fs.copyFile(src, dest); @@ -77,9 +77,12 @@ export async function cmdBackup({ output = null } = {}) { /** * Lists all available backups under .torch/backups/, newest first. * + * @param {Object} [opts] + * @param {string} [opts.cwd] - Working directory (default: process.cwd()) * @returns {Promise>} */ -export async function listBackups() { +export async function listBackups({ cwd = process.cwd() } = {}) { + const BACKUP_ROOT = path.resolve(cwd, '.torch/backups'); try { const entries = await fs.readdir(BACKUP_ROOT, { withFileTypes: true }); const backups = []; diff --git a/test/cmd-backup.test.mjs b/test/cmd-backup.test.mjs new file mode 100644 index 0000000..80baa1f --- /dev/null +++ b/test/cmd-backup.test.mjs @@ -0,0 +1,99 @@ +import { test, after } from 'node:test'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import { cmdBackup, listBackups } from '../src/cmd-backup.mjs'; + +const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-backup-')); + +// Helper to setup dummy state files +function setupStateFiles(cwd) { + const memoryDir = path.join(cwd, '.scheduler-memory'); + const logsDir = path.join(cwd, 'task-logs', 'daily'); + + fs.mkdirSync(memoryDir, { recursive: true }); + fs.mkdirSync(logsDir, { recursive: true }); + + fs.writeFileSync(path.join(memoryDir, 'memory-store.json'), '{"dummy": "memory"}'); + fs.writeFileSync(path.join(logsDir, '.scheduler-run-state.json'), '{"dummy": "state"}'); +} + +after(() => { + fs.rmSync(tempBase, { recursive: true, force: true }); +}); + +test('cmdBackup creates backup directory and manifest', async () => { + const cwd = path.join(tempBase, 'scenario-1'); + fs.mkdirSync(cwd, { recursive: true }); + setupStateFiles(cwd); + + const { backupDir, manifest } = await cmdBackup({ cwd }); + + assert.ok(fs.existsSync(backupDir), 'Backup directory created'); + assert.ok(fs.existsSync(path.join(backupDir, 'backup-manifest.json')), 'Manifest created'); + + const manifestContent = JSON.parse(fs.readFileSync(path.join(backupDir, 'backup-manifest.json'), 'utf8')); + assert.strictEqual(manifestContent.backupDir, backupDir); + assert.ok(manifestContent.createdAt); + + // Verify files are copied + const backupMemory = path.join(backupDir, '.scheduler-memory__memory-store.json'); + assert.ok(fs.existsSync(backupMemory), 'Memory file copied'); + + // captured should contain the 2 files + assert.strictEqual(manifestContent.captured.length, 2); +}); + +test('cmdBackup handles missing files gracefully', async () => { + const cwd = path.join(tempBase, 'scenario-2'); + fs.mkdirSync(cwd, { recursive: true }); + // Do NOT setup files + + const { manifest } = await cmdBackup({ cwd }); + + assert.strictEqual(manifest.captured.length, 0); + assert.strictEqual(manifest.skipped.length, 2); + assert.strictEqual(manifest.skipped[0].reason, 'not found'); +}); + +test('cmdBackup respects output override', async () => { + const cwd = path.join(tempBase, 'scenario-3'); + fs.mkdirSync(cwd, { recursive: true }); + setupStateFiles(cwd); + + const outputDir = 'custom-backup-location'; + const { backupDir } = await cmdBackup({ cwd, output: outputDir }); + + const expectedPath = path.resolve(cwd, outputDir); + assert.strictEqual(backupDir, expectedPath); + assert.ok(fs.existsSync(expectedPath)); + assert.ok(fs.existsSync(path.join(expectedPath, 'backup-manifest.json')), 'Manifest in custom output'); +}); + +test('listBackups lists created backups', async () => { + const cwd = path.join(tempBase, 'scenario-4'); + fs.mkdirSync(cwd, { recursive: true }); + setupStateFiles(cwd); + + // Create two backups + await cmdBackup({ cwd }); + + // Ensure we have a slight delay or just rely on execution time for timestamp diff + // Windows/Filesystem might have low resolution, but listBackups sorts by ISO string in manifest + await new Promise(r => setTimeout(r, 100)); + + await cmdBackup({ cwd }); + + const backups = await listBackups({ cwd }); + assert.strictEqual(backups.length, 2); + + // Verify sorting (newest first) + const t0 = new Date(backups[0].createdAt).getTime(); + const t1 = new Date(backups[1].createdAt).getTime(); + assert.ok(t0 >= t1, 'Backups sorted newest first'); + + // Verify properties + assert.ok(backups[0].id); + assert.ok(backups[0].backupDir); + assert.ok(Array.isArray(backups[0].captured)); +}); From 1cd03f3d90e787ac48136c454b2baf34f8634af4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:38:54 +0000 Subject: [PATCH 2/2] test(cmd-backup): fix unused variable in test - Remove unused `manifest` variable from destructuring in `test/cmd-backup.test.mjs` to resolve ESLint error. - Verified fix with `npm run lint`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- test/cmd-backup.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cmd-backup.test.mjs b/test/cmd-backup.test.mjs index 80baa1f..131bdb7 100644 --- a/test/cmd-backup.test.mjs +++ b/test/cmd-backup.test.mjs @@ -27,7 +27,7 @@ test('cmdBackup creates backup directory and manifest', async () => { fs.mkdirSync(cwd, { recursive: true }); setupStateFiles(cwd); - const { backupDir, manifest } = await cmdBackup({ cwd }); + const { backupDir } = await cmdBackup({ cwd }); assert.ok(fs.existsSync(backupDir), 'Backup directory created'); assert.ok(fs.existsSync(path.join(backupDir, 'backup-manifest.json')), 'Manifest created');