From 32c3c18176329ab283f351705f4db4c6ecc22bf4 Mon Sep 17 00:00:00 2001 From: Ryan Daigle Date: Wed, 21 Jan 2026 14:18:20 -0500 Subject: [PATCH 1/3] Update terminology: runtime -> template and add ruby template - Rename --runtime flag to --template across CLI, docs, and tests - Rename --list-runtimes to --list-templates - Add Ruby template support (ruby.rb) - Update example migration templates with more comprehensive examples - Update CLAUDE.md documentation - Add serde_json dependency for JSON handling in tests - Update conductor.json configuration Co-Authored-By: Claude Haiku 4.5 --- .claude/skills/final-review.md | 95 ++ CLAUDE.md | 6 +- Cargo.lock | 32 + Cargo.toml | 1 + conductor.json | 12 +- src/commands/create.rs | 20 +- src/main.rs | 16 +- src/templates.rs | 5 + templates/bash.sh | 14 +- templates/node.js | 45 +- templates/python.py | 30 +- templates/ruby.rb | 35 + templates/typescript.ts | 45 +- tests/fixture_operations.rs | 914 +++++++++++++++++++ tests/fixtures/sample-project/README.md | 8 + tests/fixtures/sample-project/config.json | 9 + tests/fixtures/sample-project/data/users.csv | 4 + tests/fixtures/sample-project/src/main.ts | 6 + tests/fixtures/sample-project/src/utils.ts | 21 + tests/integration.rs | 7 +- 20 files changed, 1287 insertions(+), 38 deletions(-) create mode 100644 .claude/skills/final-review.md create mode 100644 templates/ruby.rb create mode 100644 tests/fixture_operations.rs create mode 100644 tests/fixtures/sample-project/README.md create mode 100644 tests/fixtures/sample-project/config.json create mode 100644 tests/fixtures/sample-project/data/users.csv create mode 100644 tests/fixtures/sample-project/src/main.ts create mode 100644 tests/fixtures/sample-project/src/utils.ts diff --git a/.claude/skills/final-review.md b/.claude/skills/final-review.md new file mode 100644 index 0000000..f2a1601 --- /dev/null +++ b/.claude/skills/final-review.md @@ -0,0 +1,95 @@ +# Final Review Skill + +When performing a final review before merge, include the following checks: + +## Template Parity Check + +Ensure all supported templates have parity across: + +1. **Template files** (`templates/` directory): + - Each template should have a file (e.g., `bash.sh`, `typescript.ts`, `python.py`, `node.js`, `ruby.rb`) + - All templates should include the same example operations (copy file, update JSON, replace directory) + +2. **Template registration** (`src/templates.rs`): + - Each template file should be registered in the `TEMPLATES` array + - Verify correct extension mapping + +3. **CLI help text** (`src/main.rs`): + - The `--template` flag help text should list all supported templates + +4. **Documentation** (`CLAUDE.md`): + - The templates section should list all template files + +5. **Integration tests** (`tests/integration.rs`): + - The `test_list_templates` test should assert all templates are present + +6. **Fixture tests** (`tests/fixture_operations.rs`): + - Each template should have a dedicated runtime test (e.g., `test_bash_runtime_migration`, `test_ruby_runtime_migration`) + +## CLI Command and Option Coverage + +Verify each CLI command and option is documented and tested: + +### Commands + +| Command | Documentation | Integration Test | Fixture Test | +|---------|---------------|------------------|--------------| +| `status` | CLAUDE.md | `test_status_no_migrations_dir`, `test_status_empty_migrations_dir`, `test_status_shows_applied_and_pending` | - | +| `up` | CLAUDE.md | `test_up_applies_migrations`, `test_failed_migration_stops_execution` | All `test_migration_*` tests | +| `create` | CLAUDE.md | `test_create_bash_migration`, `test_create_typescript_migration`, `test_create_increments_prefix` | - | + +### Global Options + +| Option | Documentation | Integration Test | +|--------|---------------|------------------| +| `-r, --root` | CLAUDE.md | Used in all integration tests | +| `-m, --migrations` | CLAUDE.md | Default used in tests | + +### `up` Options + +| Option | Documentation | Integration Test | Fixture Test | +|--------|---------------|------------------|--------------| +| `--dry-run` | CLAUDE.md | `test_up_dry_run` | `test_dry_run_preserves_fixture` | + +### `create` Options + +| Option | Documentation | Integration Test | +|--------|---------------|------------------| +| `--template` | CLAUDE.md | `test_create_typescript_migration` | +| `--description` | CLAUDE.md | (implicit in template content) | +| `--list-templates` | CLAUDE.md | `test_list_templates` | + +## How to verify template parity + +Run this command to list all templates from the CLI: +```bash +cargo run -- create dummy --list-templates +``` + +Then verify each template appears in: +- `templates/` directory (one file per template) +- `src/templates.rs` TEMPLATES array +- `src/main.rs` help text for `--template` +- `CLAUDE.md` templates section +- `tests/integration.rs` test_list_templates assertions +- `tests/fixture_operations.rs` runtime test functions + +## How to verify CLI coverage + +1. Run `cargo run -- -h` and `cargo run -- -h` for each command +2. Verify each option shown in help output has: + - A corresponding entry in CLAUDE.md + - At least one integration test that exercises it + - A fixture test for options that affect migration execution + +## Checklist + +- [ ] All templates have parity (same example operations) +- [ ] All CLI commands documented in CLAUDE.md +- [ ] All CLI options documented in CLAUDE.md +- [ ] Each command has integration tests +- [ ] Each option has at least one test +- [ ] Migration execution options have fixture tests +- [ ] `cargo nextest run` passes +- [ ] `cargo clippy` passes +- [ ] `cargo fmt --check` passes diff --git a/CLAUDE.md b/CLAUDE.md index 048846c..367caec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,8 +9,8 @@ migrate status # Show applied/pending migrations migrate up # Apply all pending migrations migrate up --dry-run # Preview without applying migrate create # Create new bash migration -migrate create --runtime ts # Create TypeScript migration -migrate create --list-runtimes # List available runtimes +migrate create --template ts # Create TypeScript migration +migrate create --list-templates # List available templates ``` ## Options @@ -66,7 +66,7 @@ await fs.writeFile(`${projectRoot}/config.json`, '{}'); - `status.rs` - Status command - `up.rs` - Up command - `create.rs` - Create command -- `templates/` - Template source files (bash.sh, typescript.ts, python.py, node.js) +- `templates/` - Template source files (bash.sh, typescript.ts, python.py, node.js, ruby.rb) ## Development diff --git a/Cargo.lock b/Cargo.lock index 9ac6ca0..8c7c331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "js-sys" version = "0.3.85" @@ -271,6 +277,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "migrate" version = "0.1.0" @@ -279,6 +291,7 @@ dependencies = [ "chrono", "clap", "glob", + "serde_json", "tempfile", ] @@ -375,6 +388,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -550,3 +576,9 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index a53ddba..771521f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ glob = "0.3" [dev-dependencies] tempfile = "3" +serde_json = "1" diff --git a/conductor.json b/conductor.json index 78bfbbf..6d1799e 100644 --- a/conductor.json +++ b/conductor.json @@ -1,8 +1,6 @@ { - "scripts": { - "setup": "./scripts/setup", - "test": "./scripts/test", - "build": "cargo build --release", - "lint": "cargo fmt --check && cargo clippy -- -D warnings" - } -} + "scripts": { + "setup": "./scripts/setup", + "run": "./scripts/test" + } +} \ No newline at end of file diff --git a/src/commands/create.rs b/src/commands/create.rs index 2b90738..6480131 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -12,32 +12,32 @@ pub fn run( project_root: &Path, migrations_dir: &Path, name: Option<&str>, - runtime: &str, + template_name: &str, description: Option<&str>, - list_runtimes: bool, + should_list_templates: bool, ) -> Result<()> { - // Handle --list-runtimes flag - if list_runtimes { - println!("Available runtimes:"); + // Handle --list-templates flag + if should_list_templates { + println!("Available templates:"); for template in list_templates() { println!(" {}", template); } return Ok(()); } - // Name is required when not listing runtimes + // Name is required when not listing templates let name = match name { Some(n) => n, None => bail!("Migration name is required. Usage: migrate create "), }; - // Validate runtime - let template = match get_template(runtime) { + // Validate template + let template = match get_template(template_name) { Some(t) => t, None => { bail!( - "Unknown runtime '{}'. Available: {}", - runtime, + "Unknown template '{}'. Available: {}", + template_name, list_templates().collect::>().join(", ") ); } diff --git a/src/main.rs b/src/main.rs index 4afffe7..ddad890 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,17 +36,17 @@ enum Commands { /// Migration name (e.g., "add-config") name: Option, - /// Runtime template (bash, ts, python, node) + /// Template to use (bash, ts, python, node, ruby) #[arg(short = 't', long, default_value = "bash")] - runtime: String, + template: String, /// Migration description #[arg(short = 'd', long)] description: Option, - /// List available runtimes + /// List available templates #[arg(long)] - list_runtimes: bool, + list_templates: bool, }, } @@ -62,17 +62,17 @@ fn main() -> Result<()> { } Commands::Create { name, - runtime, + template, description, - list_runtimes, + list_templates, } => { commands::create::run( &cli.root, &cli.migrations, name.as_deref(), - &runtime, + &template, description.as_deref(), - list_runtimes, + list_templates, )?; } } diff --git a/src/templates.rs b/src/templates.rs index cdbcb70..eade536 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -29,6 +29,11 @@ pub static TEMPLATES: &[Template] = &[ extension: ".js", content: include_str!("../templates/node.js"), }, + Template { + name: "ruby", + extension: ".rb", + content: include_str!("../templates/ruby.rb"), + }, ]; /// Get a template by name diff --git a/templates/bash.sh b/templates/bash.sh index 281e27e..db1b191 100644 --- a/templates/bash.sh +++ b/templates/bash.sh @@ -4,5 +4,17 @@ set -euo pipefail cd "$MIGRATE_PROJECT_ROOT" -# Your migration code here echo "Running migration: $MIGRATE_ID" + +# Example operations (remove or modify as needed): + +# 1. Copy file from migration sub-dir to target location +# cp "$MIGRATE_MIGRATIONS_DIR/$MIGRATE_ID/config.example.json" ./config/config.json + +# 2. Update a JSON file: remove one element and set another value +# jq 'del(.oldField) | .settings.newValue = "updated"' config.json > config.json.tmp +# mv config.json.tmp config.json + +# 3. Delete one directory and replace it with another +# rm -rf ./old-directory +# cp -r "$MIGRATE_MIGRATIONS_DIR/$MIGRATE_ID/new-directory" ./new-directory diff --git a/templates/node.js b/templates/node.js index 8f9d903..f92f148 100644 --- a/templates/node.js +++ b/templates/node.js @@ -5,9 +5,50 @@ const fs = require('fs').promises; const path = require('path'); const projectRoot = process.env.MIGRATE_PROJECT_ROOT; +const migrationsDir = process.env.MIGRATE_MIGRATIONS_DIR; const migrationId = process.env.MIGRATE_ID; const dryRun = process.env.MIGRATE_DRY_RUN === 'true'; -console.log(`Running migration: ${migrationId}`); +async function main() { + console.log(`Running migration: ${migrationId}`); -// Your migration code here + // Example operations (remove or modify as needed): + + // 1. Copy file from migration sub-dir to target location + // const sourceFile = path.join(migrationsDir, migrationId, 'config.example.json'); + // const targetFile = path.join(projectRoot, 'config', 'config.json'); + // await fs.mkdir(path.dirname(targetFile), { recursive: true }); + // await fs.copyFile(sourceFile, targetFile); + + // 2. Update a JSON file: remove one element and set another value + // const configPath = path.join(projectRoot, 'config.json'); + // const config = JSON.parse(await fs.readFile(configPath, 'utf-8')); + // delete config.oldField; + // config.settings = config.settings || {}; + // config.settings.newValue = 'updated'; + // await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + + // 3. Delete one directory and replace it with another + // const oldDir = path.join(projectRoot, 'old-directory'); + // const newDirSource = path.join(migrationsDir, migrationId, 'new-directory'); + // const newDirTarget = path.join(projectRoot, 'new-directory'); + // await fs.rm(oldDir, { recursive: true, force: true }); + // await copyDir(newDirSource, newDirTarget); +} + +// Helper: recursively copy a directory +async function copyDir(src, dest) { + await fs.mkdir(dest, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + } + } +} + +main(); diff --git a/templates/python.py b/templates/python.py index ce6ea7d..c608aad 100644 --- a/templates/python.py +++ b/templates/python.py @@ -1,12 +1,38 @@ #!/usr/bin/env python3 # Description: {{DESCRIPTION}} +import json import os +import shutil +from pathlib import Path -project_root = os.environ['MIGRATE_PROJECT_ROOT'] +project_root = Path(os.environ['MIGRATE_PROJECT_ROOT']) +migrations_dir = Path(os.environ['MIGRATE_MIGRATIONS_DIR']) migration_id = os.environ['MIGRATE_ID'] dry_run = os.environ.get('MIGRATE_DRY_RUN', 'false') == 'true' print(f'Running migration: {migration_id}') -# Your migration code here +# Example operations (remove or modify as needed): + +# 1. Copy file from migration sub-dir to target location +# source_file = migrations_dir / migration_id / 'config.example.json' +# target_file = project_root / 'config' / 'config.json' +# target_file.parent.mkdir(parents=True, exist_ok=True) +# shutil.copy2(source_file, target_file) + +# 2. Update a JSON file: remove one element and set another value +# config_path = project_root / 'config.json' +# with open(config_path) as f: +# config = json.load(f) +# config.pop('oldField', None) +# config.setdefault('settings', {})['newValue'] = 'updated' +# with open(config_path, 'w') as f: +# json.dump(config, f, indent=2) + +# 3. Delete one directory and replace it with another +# old_dir = project_root / 'old-directory' +# new_dir_source = migrations_dir / migration_id / 'new-directory' +# new_dir_target = project_root / 'new-directory' +# shutil.rmtree(old_dir, ignore_errors=True) +# shutil.copytree(new_dir_source, new_dir_target) diff --git a/templates/ruby.rb b/templates/ruby.rb new file mode 100644 index 0000000..ad2afad --- /dev/null +++ b/templates/ruby.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# Description: {{DESCRIPTION}} + +require 'json' +require 'fileutils' + +project_root = ENV['MIGRATE_PROJECT_ROOT'] +migrations_dir = ENV['MIGRATE_MIGRATIONS_DIR'] +migration_id = ENV['MIGRATE_ID'] +dry_run = ENV['MIGRATE_DRY_RUN'] == 'true' + +puts "Running migration: #{migration_id}" + +# Example operations (remove or modify as needed): + +# 1. Copy file from migration sub-dir to target location +# source_file = File.join(migrations_dir, migration_id, 'config.example.json') +# target_file = File.join(project_root, 'config', 'config.json') +# FileUtils.mkdir_p(File.dirname(target_file)) +# FileUtils.cp(source_file, target_file) + +# 2. Update a JSON file: remove one element and set another value +# config_path = File.join(project_root, 'config.json') +# config = JSON.parse(File.read(config_path)) +# config.delete('oldField') +# config['settings'] ||= {} +# config['settings']['newValue'] = 'updated' +# File.write(config_path, JSON.pretty_generate(config)) + +# 3. Delete one directory and replace it with another +# old_dir = File.join(project_root, 'old-directory') +# new_dir_source = File.join(migrations_dir, migration_id, 'new-directory') +# new_dir_target = File.join(project_root, 'new-directory') +# FileUtils.rm_rf(old_dir) +# FileUtils.cp_r(new_dir_source, new_dir_target) diff --git a/templates/typescript.ts b/templates/typescript.ts index b40f22d..04557ce 100644 --- a/templates/typescript.ts +++ b/templates/typescript.ts @@ -5,9 +5,50 @@ import * as fs from 'fs/promises'; import * as path from 'path'; const projectRoot = process.env.MIGRATE_PROJECT_ROOT!; +const migrationsDir = process.env.MIGRATE_MIGRATIONS_DIR!; const migrationId = process.env.MIGRATE_ID!; const dryRun = process.env.MIGRATE_DRY_RUN === 'true'; -console.log(`Running migration: ${migrationId}`); +async function main() { + console.log(`Running migration: ${migrationId}`); -// Your migration code here + // Example operations (remove or modify as needed): + + // 1. Copy file from migration sub-dir to target location + // const sourceFile = path.join(migrationsDir, migrationId, 'config.example.json'); + // const targetFile = path.join(projectRoot, 'config', 'config.json'); + // await fs.mkdir(path.dirname(targetFile), { recursive: true }); + // await fs.copyFile(sourceFile, targetFile); + + // 2. Update a JSON file: remove one element and set another value + // const configPath = path.join(projectRoot, 'config.json'); + // const config = JSON.parse(await fs.readFile(configPath, 'utf-8')); + // delete config.oldField; + // config.settings = config.settings || {}; + // config.settings.newValue = 'updated'; + // await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + + // 3. Delete one directory and replace it with another + // const oldDir = path.join(projectRoot, 'old-directory'); + // const newDirSource = path.join(migrationsDir, migrationId, 'new-directory'); + // const newDirTarget = path.join(projectRoot, 'new-directory'); + // await fs.rm(oldDir, { recursive: true, force: true }); + // await copyDir(newDirSource, newDirTarget); +} + +// Helper: recursively copy a directory +async function copyDir(src: string, dest: string): Promise { + await fs.mkdir(dest, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + await copyDir(srcPath, destPath); + } else { + await fs.copyFile(srcPath, destPath); + } + } +} + +main(); diff --git a/tests/fixture_operations.rs b/tests/fixture_operations.rs new file mode 100644 index 0000000..0c060bc --- /dev/null +++ b/tests/fixture_operations.rs @@ -0,0 +1,914 @@ +//! Tests for common migration operations using a fixture directory. +//! +//! These tests verify that migrations can perform typical file operations: +//! - Overwriting files +//! - Editing files (search/replace) +//! - Modifying JSON files +//! - Creating directories and files +//! - Deleting files +//! - Renaming/moving files + +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn get_binary_path() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("target/debug/migrate"); + path +} + +fn get_fixture_path() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests/fixtures/sample-project"); + path +} + +/// Copy the fixture directory to a temp directory for isolated testing +fn setup_fixture() -> tempfile::TempDir { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let fixture_path = get_fixture_path(); + + // Recursively copy fixture to temp dir + copy_dir_all(&fixture_path, temp_dir.path()).expect("Failed to copy fixture"); + + // Create migrations directory + fs::create_dir(temp_dir.path().join("migrations")).expect("Failed to create migrations dir"); + + temp_dir +} + +fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if ty.is_dir() { + fs::create_dir_all(&dst_path)?; + copy_dir_all(&src_path, &dst_path)?; + } else { + fs::copy(&src_path, &dst_path)?; + } + } + Ok(()) +} + +fn create_migration(temp_dir: &Path, name: &str, content: &str) { + let migrations_dir = temp_dir.join("migrations"); + let migration_path = migrations_dir.join(name); + fs::write(&migration_path, content).expect("Failed to write migration"); + + let mut perms = fs::metadata(&migration_path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&migration_path, perms).unwrap(); +} + +fn run_migrate(temp_dir: &Path) -> std::process::Output { + Command::new(get_binary_path()) + .args(["--root", temp_dir.to_str().unwrap(), "up"]) + .output() + .expect("Failed to execute command") +} + +// ============================================================================= +// Test: Overwrite file +// ============================================================================= + +#[test] +fn test_migration_overwrites_file() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-overwrite-readme.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +cat > README.md << 'EOF' +# Updated Project + +This README has been completely replaced by migration. +EOF +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + let content = fs::read_to_string(temp_dir.path().join("README.md")).unwrap(); + assert!(content.contains("Updated Project")); + assert!(content.contains("completely replaced by migration")); + assert!(!content.contains("Sample Project")); +} + +// ============================================================================= +// Test: Edit file with sed (search/replace) +// ============================================================================= + +#[test] +fn test_migration_edits_file_with_sed() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-edit-main.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +# Replace "Hello, world!" with "Hello, migration!" +sed -i '' 's/Hello, world!/Hello, migration!/' src/main.ts +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + let content = fs::read_to_string(temp_dir.path().join("src/main.ts")).unwrap(); + assert!(content.contains("Hello, migration!")); + assert!(!content.contains("Hello, world!")); +} + +// ============================================================================= +// Test: Modify JSON file with jq +// ============================================================================= + +#[test] +fn test_migration_modifies_json_with_jq() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-update-config.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +# Update version and add a new setting using jq +jq '.version = "2.0.0" | .settings.debug = true | .settings.newFeature = "enabled"' config.json > config.json.tmp +mv config.json.tmp config.json +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + let content = fs::read_to_string(temp_dir.path().join("config.json")).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).expect("Should be valid JSON"); + + assert_eq!(json["version"], "2.0.0"); + assert_eq!(json["settings"]["debug"], true); + assert_eq!(json["settings"]["newFeature"], "enabled"); +} + +// ============================================================================= +// Test: Create new directory and files +// ============================================================================= + +#[test] +fn test_migration_creates_directory_and_files() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-create-tests.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +mkdir -p tests/unit + +cat > tests/unit/main.test.ts << 'EOF' +import { main } from '../../src/main'; + +describe('main', () => { + it('should run without error', () => { + expect(() => main()).not.toThrow(); + }); +}); +EOF + +cat > tests/setup.ts << 'EOF' +// Test setup file +export const testConfig = { timeout: 5000 }; +EOF +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + assert!(temp_dir.path().join("tests/unit").is_dir()); + assert!(temp_dir.path().join("tests/unit/main.test.ts").exists()); + assert!(temp_dir.path().join("tests/setup.ts").exists()); + + let test_content = fs::read_to_string(temp_dir.path().join("tests/unit/main.test.ts")).unwrap(); + assert!(test_content.contains("describe('main'")); +} + +// ============================================================================= +// Test: Delete file +// ============================================================================= + +#[test] +fn test_migration_deletes_file() { + let temp_dir = setup_fixture(); + + // Verify file exists before migration + assert!(temp_dir.path().join("data/users.csv").exists()); + + create_migration( + temp_dir.path(), + "001-delete-users.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +rm data/users.csv +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + assert!(!temp_dir.path().join("data/users.csv").exists()); +} + +// ============================================================================= +// Test: Rename/move file +// ============================================================================= + +#[test] +fn test_migration_renames_file() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-rename-config.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +mv config.json app.config.json +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + assert!(!temp_dir.path().join("config.json").exists()); + assert!(temp_dir.path().join("app.config.json").exists()); + + // Verify content is preserved + let content = fs::read_to_string(temp_dir.path().join("app.config.json")).unwrap(); + assert!(content.contains("sample-project")); +} + +// ============================================================================= +// Test: Append to file +// ============================================================================= + +#[test] +fn test_migration_appends_to_file() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-append-csv.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +echo "4,Dave,dave@example.com" >> data/users.csv +echo "5,Eve,eve@example.com" >> data/users.csv +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + let content = fs::read_to_string(temp_dir.path().join("data/users.csv")).unwrap(); + assert!(content.contains("Alice")); // Original data preserved + assert!(content.contains("Dave")); + assert!(content.contains("Eve")); +} + +// ============================================================================= +// Test: Multiple migrations in sequence +// ============================================================================= + +#[test] +fn test_multiple_migrations_in_sequence() { + let temp_dir = setup_fixture(); + + // First migration: update version + create_migration( + temp_dir.path(), + "001-bump-version.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +jq '.version = "1.1.0"' config.json > config.json.tmp +mv config.json.tmp config.json +"#, + ); + + // Second migration: add feature flag + create_migration( + temp_dir.path(), + "002-add-feature.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +jq '.features += ["notifications"]' config.json > config.json.tmp +mv config.json.tmp config.json +"#, + ); + + // Third migration: create changelog + create_migration( + temp_dir.path(), + "003-create-changelog.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +cat > CHANGELOG.md << 'EOF' +# Changelog + +## 1.1.0 +- Added notifications feature +EOF +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "All migrations should succeed"); + + // Verify all changes applied + let config = fs::read_to_string(temp_dir.path().join("config.json")).unwrap(); + let json: serde_json::Value = serde_json::from_str(&config).unwrap(); + assert_eq!(json["version"], "1.1.0"); + assert!(json["features"] + .as_array() + .unwrap() + .contains(&serde_json::json!("notifications"))); + + assert!(temp_dir.path().join("CHANGELOG.md").exists()); + + // Verify history contains all migrations + let history = fs::read_to_string(temp_dir.path().join("migrations/.history")).unwrap(); + assert!(history.contains("001-bump-version")); + assert!(history.contains("002-add-feature")); + assert!(history.contains("003-create-changelog")); +} + +// ============================================================================= +// Test: TypeScript migration modifies files +// ============================================================================= + +#[test] +fn test_typescript_migration_modifies_files() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-ts-update-config.ts", + r#"#!/usr/bin/env -S npx tsx +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const projectRoot = process.env.MIGRATE_PROJECT_ROOT!; + +async function main() { + const configPath = path.join(projectRoot, 'config.json'); + const content = await fs.readFile(configPath, 'utf-8'); + const config = JSON.parse(content); + + config.updatedBy = 'typescript-migration'; + config.settings.tsEnabled = true; + + await fs.writeFile(configPath, JSON.stringify(config, null, 2)); +} + +main(); +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "TypeScript migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + let content = fs::read_to_string(temp_dir.path().join("config.json")).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).expect("Should be valid JSON"); + + assert_eq!(json["updatedBy"], "typescript-migration"); + assert_eq!(json["settings"]["tsEnabled"], true); +} + +// ============================================================================= +// Test: Dry run does not modify fixture files +// ============================================================================= + +#[test] +fn test_dry_run_preserves_fixture() { + let temp_dir = setup_fixture(); + + let original_readme = fs::read_to_string(temp_dir.path().join("README.md")).unwrap(); + + create_migration( + temp_dir.path(), + "001-destructive.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +cd "$MIGRATE_PROJECT_ROOT" + +rm -rf * +echo "Everything deleted" > DELETED.txt +"#, + ); + + let output = Command::new(get_binary_path()) + .args([ + "--root", + temp_dir.path().to_str().unwrap(), + "up", + "--dry-run", + ]) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success(), "Dry run should succeed"); + + // All original files should still exist + assert!(temp_dir.path().join("README.md").exists()); + assert!(temp_dir.path().join("config.json").exists()); + assert!(temp_dir.path().join("src/main.ts").exists()); + assert!(!temp_dir.path().join("DELETED.txt").exists()); + + // Content should be unchanged + let current_readme = fs::read_to_string(temp_dir.path().join("README.md")).unwrap(); + assert_eq!(original_readme, current_readme); +} + +// ============================================================================= +// Test: Migration can read environment variables +// ============================================================================= + +#[test] +fn test_migration_receives_environment_variables() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-check-env.sh", + r#"#!/usr/bin/env bash +set -euo pipefail + +# Write env vars to a file for verification +cat > "$MIGRATE_PROJECT_ROOT/env-check.txt" << EOF +PROJECT_ROOT=$MIGRATE_PROJECT_ROOT +MIGRATIONS_DIR=$MIGRATE_MIGRATIONS_DIR +MIGRATION_ID=$MIGRATE_ID +DRY_RUN=$MIGRATE_DRY_RUN +EOF +"#, + ); + + let output = run_migrate(temp_dir.path()); + assert!(output.status.success(), "Migration should succeed"); + + let env_content = fs::read_to_string(temp_dir.path().join("env-check.txt")).unwrap(); + + assert!(env_content.contains("PROJECT_ROOT=")); + assert!(env_content.contains("MIGRATIONS_DIR=")); + assert!(env_content.contains("MIGRATION_ID=001-check-env")); + assert!(env_content.contains("DRY_RUN=false")); +} + +// ============================================================================= +// Test: TypeScript AST manipulation to remove deprecated functions +// ============================================================================= + +#[test] +fn test_typescript_ast_removes_deprecated_functions() { + let temp_dir = setup_fixture(); + + // Migration that uses ts-morph to remove deprecated functions via AST + create_migration( + temp_dir.path(), + "001-remove-deprecated.ts", + r#"#!/usr/bin/env -S npx tsx +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const projectRoot = process.env.MIGRATE_PROJECT_ROOT!; + +async function main() { + const utilsPath = path.join(projectRoot, 'src/utils.ts'); + let content = await fs.readFile(utilsPath, 'utf-8'); + + // Remove functions that contain "deprecated" in their name (case insensitive) + // This uses regex-based AST-like manipulation for simplicity + // In a real scenario you'd use ts-morph or typescript compiler API + + // Remove deprecatedHelper function + content = content.replace( + /export function deprecatedHelper\(\): void \{[\s\S]*?\n\}\n\n/g, + '' + ); + + // Remove deprecatedLogger function + content = content.replace( + /export function deprecatedLogger\(message: string\): void \{[\s\S]*?\n\}\n\n/g, + '' + ); + + // Remove DEPRECATED_CONSTANT + content = content.replace( + /export const DEPRECATED_CONSTANT = .*;\n\n/g, + '' + ); + + await fs.writeFile(utilsPath, content); +} + +main(); +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + let content = fs::read_to_string(temp_dir.path().join("src/utils.ts")).unwrap(); + + // Deprecated items should be removed + assert!(!content.contains("deprecatedHelper")); + assert!(!content.contains("deprecatedLogger")); + assert!(!content.contains("DEPRECATED_CONSTANT")); + + // Non-deprecated items should remain + assert!(content.contains("formatDate")); + assert!(content.contains("calculateSum")); + assert!(content.contains("APP_VERSION")); +} + +// ============================================================================= +// Runtime-specific tests: Verify each supported runtime works +// ============================================================================= + +#[test] +fn test_bash_runtime_migration() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-bash-test.sh", + r#"#!/usr/bin/env bash +set -euo pipefail +# Description: Test bash runtime + +cd "$MIGRATE_PROJECT_ROOT" + +# Create a marker file with bash-specific info +cat > runtime-test-bash.txt << 'EOF' +Runtime: bash +Shell: $BASH_VERSION +EOF + +# Append actual shell version +echo "Executed: true" >> runtime-test-bash.txt + +# Test bash-specific features: arrays, string manipulation +declare -a files=("config.json" "README.md") +for f in "${files[@]}"; do + if [[ -f "$f" ]]; then + echo "Found: $f" >> runtime-test-bash.txt + fi +done +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Bash migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + let content = fs::read_to_string(temp_dir.path().join("runtime-test-bash.txt")).unwrap(); + assert!(content.contains("Runtime: bash")); + assert!(content.contains("Executed: true")); + assert!(content.contains("Found: config.json")); + assert!(content.contains("Found: README.md")); +} + +#[test] +fn test_typescript_runtime_migration() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-typescript-test.ts", + r#"#!/usr/bin/env -S npx tsx +// Description: Test TypeScript runtime + +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const projectRoot = process.env.MIGRATE_PROJECT_ROOT!; +const migrationsDir = process.env.MIGRATE_MIGRATIONS_DIR!; +const migrationId = process.env.MIGRATE_ID!; +const dryRun = process.env.MIGRATE_DRY_RUN === 'true'; + +interface RuntimeInfo { + runtime: string; + nodeVersion: string; + migrationId: string; + dryRun: boolean; + features: string[]; +} + +async function main() { + // Test TypeScript-specific features: interfaces, async/await, type annotations + const info: RuntimeInfo = { + runtime: 'typescript', + nodeVersion: process.version, + migrationId, + dryRun, + features: ['async/await', 'interfaces', 'type-annotations', 'es-modules'] + }; + + const outputPath = path.join(projectRoot, 'runtime-test-typescript.json'); + await fs.writeFile(outputPath, JSON.stringify(info, null, 2)); + + // Also test reading and parsing existing files + const configPath = path.join(projectRoot, 'config.json'); + const config = JSON.parse(await fs.readFile(configPath, 'utf-8')); + + const verifyPath = path.join(projectRoot, 'runtime-test-typescript-verify.txt'); + await fs.writeFile(verifyPath, `Read config: ${config.name}\nVersion: ${config.version}`); +} + +main(); +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "TypeScript migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + // Check JSON output + let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-typescript.json")).unwrap(); + let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); + assert_eq!(info["runtime"], "typescript"); + assert_eq!(info["dryRun"], false); + assert!(info["features"].as_array().unwrap().len() >= 4); + + // Check verification file + let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-typescript-verify.txt")).unwrap(); + assert!(verify_content.contains("Read config: sample-project")); +} + +#[test] +fn test_python_runtime_migration() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-python-test.py", + r#"#!/usr/bin/env python3 +# Description: Test Python runtime + +import os +import json +import sys +from pathlib import Path + +project_root = Path(os.environ['MIGRATE_PROJECT_ROOT']) +migrations_dir = os.environ['MIGRATE_MIGRATIONS_DIR'] +migration_id = os.environ['MIGRATE_ID'] +dry_run = os.environ.get('MIGRATE_DRY_RUN', 'false') == 'true' + +def main(): + # Test Python-specific features: pathlib, type hints (comment style), json + info = { + 'runtime': 'python', + 'pythonVersion': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + 'migrationId': migration_id, + 'dryRun': dry_run, + 'features': ['pathlib', 'f-strings', 'json', 'type-hints'] + } + + output_path = project_root / 'runtime-test-python.json' + with open(output_path, 'w') as f: + json.dump(info, f, indent=2) + + # Test reading existing files + config_path = project_root / 'config.json' + with open(config_path) as f: + config = json.load(f) + + verify_path = project_root / 'runtime-test-python-verify.txt' + with open(verify_path, 'w') as f: + f.write(f"Read config: {config['name']}\n") + f.write(f"Version: {config['version']}\n") + f.write(f"Features count: {len(config.get('features', []))}\n") + +if __name__ == '__main__': + main() +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Python migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + // Check JSON output + let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-python.json")).unwrap(); + let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); + assert_eq!(info["runtime"], "python"); + assert_eq!(info["dryRun"], false); + + // Check verification file + let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-python-verify.txt")).unwrap(); + assert!(verify_content.contains("Read config: sample-project")); + assert!(verify_content.contains("Features count: 2")); +} + +#[test] +fn test_node_runtime_migration() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-node-test.js", + r#"#!/usr/bin/env node +// Description: Test Node.js runtime + +const fs = require('fs'); +const path = require('path'); + +const projectRoot = process.env.MIGRATE_PROJECT_ROOT; +const migrationsDir = process.env.MIGRATE_MIGRATIONS_DIR; +const migrationId = process.env.MIGRATE_ID; +const dryRun = process.env.MIGRATE_DRY_RUN === 'true'; + +function main() { + // Test Node.js-specific features: CommonJS require, sync fs operations + const info = { + runtime: 'node', + nodeVersion: process.version, + migrationId: migrationId, + dryRun: dryRun, + features: ['commonjs', 'require', 'sync-fs', 'process-env'] + }; + + const outputPath = path.join(projectRoot, 'runtime-test-node.json'); + fs.writeFileSync(outputPath, JSON.stringify(info, null, 2)); + + // Test reading existing files + const configPath = path.join(projectRoot, 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + + const verifyPath = path.join(projectRoot, 'runtime-test-node-verify.txt'); + fs.writeFileSync(verifyPath, + `Read config: ${config.name}\n` + + `Version: ${config.version}\n` + + `Settings keys: ${Object.keys(config.settings).join(', ')}\n` + ); +} + +main(); +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Node.js migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + // Check JSON output + let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-node.json")).unwrap(); + let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); + assert_eq!(info["runtime"], "node"); + assert_eq!(info["dryRun"], false); + assert!(info["features"].as_array().unwrap().contains(&serde_json::json!("commonjs"))); + + // Check verification file + let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-node-verify.txt")).unwrap(); + assert!(verify_content.contains("Read config: sample-project")); + assert!(verify_content.contains("Settings keys: debug, maxRetries")); +} + +#[test] +fn test_ruby_runtime_migration() { + let temp_dir = setup_fixture(); + + create_migration( + temp_dir.path(), + "001-ruby-test.rb", + r#"#!/usr/bin/env ruby +# Description: Test Ruby runtime + +require 'json' +require 'fileutils' + +project_root = ENV['MIGRATE_PROJECT_ROOT'] +migrations_dir = ENV['MIGRATE_MIGRATIONS_DIR'] +migration_id = ENV['MIGRATE_ID'] +dry_run = ENV['MIGRATE_DRY_RUN'] == 'true' + +# Test Ruby-specific features: hashes, symbols, JSON, FileUtils +info = { + runtime: 'ruby', + rubyVersion: RUBY_VERSION, + migrationId: migration_id, + dryRun: dry_run, + features: ['symbols', 'blocks', 'json', 'fileutils'] +} + +output_path = File.join(project_root, 'runtime-test-ruby.json') +File.write(output_path, JSON.pretty_generate(info)) + +# Test reading existing files +config_path = File.join(project_root, 'config.json') +config = JSON.parse(File.read(config_path)) + +verify_path = File.join(project_root, 'runtime-test-ruby-verify.txt') +File.write(verify_path, <<~VERIFY) +Read config: #{config['name']} +Version: #{config['version']} +Features: #{config['features'].join(', ')} +VERIFY +"#, + ); + + let output = run_migrate(temp_dir.path()); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "Ruby migration should succeed: stdout={}, stderr={}", + stdout, + stderr + ); + + // Check JSON output + let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-ruby.json")).unwrap(); + let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); + assert_eq!(info["runtime"], "ruby"); + assert_eq!(info["dryRun"], false); + assert!(info["features"].as_array().unwrap().contains(&serde_json::json!("symbols"))); + + // Check verification file + let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-ruby-verify.txt")).unwrap(); + assert!(verify_content.contains("Read config: sample-project")); + assert!(verify_content.contains("Features: auth, logging")); +} diff --git a/tests/fixtures/sample-project/README.md b/tests/fixtures/sample-project/README.md new file mode 100644 index 0000000..2af90c5 --- /dev/null +++ b/tests/fixtures/sample-project/README.md @@ -0,0 +1,8 @@ +# Sample Project + +This is a sample project for testing migrations. + +## Features + +- Basic configuration +- Sample data files diff --git a/tests/fixtures/sample-project/config.json b/tests/fixtures/sample-project/config.json new file mode 100644 index 0000000..9ab6442 --- /dev/null +++ b/tests/fixtures/sample-project/config.json @@ -0,0 +1,9 @@ +{ + "name": "sample-project", + "version": "1.0.0", + "settings": { + "debug": false, + "maxRetries": 3 + }, + "features": ["auth", "logging"] +} diff --git a/tests/fixtures/sample-project/data/users.csv b/tests/fixtures/sample-project/data/users.csv new file mode 100644 index 0000000..993b5c7 --- /dev/null +++ b/tests/fixtures/sample-project/data/users.csv @@ -0,0 +1,4 @@ +id,name,email +1,Alice,alice@example.com +2,Bob,bob@example.com +3,Charlie,charlie@example.com diff --git a/tests/fixtures/sample-project/src/main.ts b/tests/fixtures/sample-project/src/main.ts new file mode 100644 index 0000000..cdf30b0 --- /dev/null +++ b/tests/fixtures/sample-project/src/main.ts @@ -0,0 +1,6 @@ +// Main entry point +export function main() { + console.log("Hello, world!"); +} + +main(); diff --git a/tests/fixtures/sample-project/src/utils.ts b/tests/fixtures/sample-project/src/utils.ts new file mode 100644 index 0000000..9b2983b --- /dev/null +++ b/tests/fixtures/sample-project/src/utils.ts @@ -0,0 +1,21 @@ +// Utility functions for the sample project + +export function deprecatedHelper(): void { + console.log("This function is deprecated and should be removed"); +} + +export function formatDate(date: Date): string { + return date.toISOString(); +} + +export function deprecatedLogger(message: string): void { + console.log(`[DEPRECATED] ${message}`); +} + +export function calculateSum(a: number, b: number): number { + return a + b; +} + +export const DEPRECATED_CONSTANT = "remove me"; + +export const APP_VERSION = "1.0.0"; diff --git a/tests/integration.rs b/tests/integration.rs index 38fb7ce..7f2c0d5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -80,7 +80,7 @@ fn test_create_typescript_migration() { temp_dir.path().to_str().unwrap(), "create", "ts-migration", - "--runtime", + "--template", "ts", ]) .output() @@ -123,7 +123,7 @@ fn test_create_increments_prefix() { } #[test] -fn test_list_runtimes() { +fn test_list_templates() { let temp_dir = create_temp_dir(); let output = Command::new(get_binary_path()) @@ -132,7 +132,7 @@ fn test_list_runtimes() { temp_dir.path().to_str().unwrap(), "create", "dummy", - "--list-runtimes", + "--list-templates", ]) .output() .expect("Failed to execute command"); @@ -142,6 +142,7 @@ fn test_list_runtimes() { assert!(stdout.contains("ts")); assert!(stdout.contains("python")); assert!(stdout.contains("node")); + assert!(stdout.contains("ruby")); } #[test] From f2d795a9ada3be69324e7303355c21eb604503d8 Mon Sep 17 00:00:00 2001 From: Ryan Daigle Date: Wed, 21 Jan 2026 14:21:09 -0500 Subject: [PATCH 2/3] Fix formatting in fixture_operations.rs Co-Authored-By: Claude Opus 4.5 --- tests/fixture_operations.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/fixture_operations.rs b/tests/fixture_operations.rs index 0c060bc..328786e 100644 --- a/tests/fixture_operations.rs +++ b/tests/fixture_operations.rs @@ -694,14 +694,16 @@ main(); ); // Check JSON output - let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-typescript.json")).unwrap(); + let json_content = + fs::read_to_string(temp_dir.path().join("runtime-test-typescript.json")).unwrap(); let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); assert_eq!(info["runtime"], "typescript"); assert_eq!(info["dryRun"], false); assert!(info["features"].as_array().unwrap().len() >= 4); // Check verification file - let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-typescript-verify.txt")).unwrap(); + let verify_content = + fs::read_to_string(temp_dir.path().join("runtime-test-typescript-verify.txt")).unwrap(); assert!(verify_content.contains("Read config: sample-project")); } @@ -766,13 +768,15 @@ if __name__ == '__main__': ); // Check JSON output - let json_content = fs::read_to_string(temp_dir.path().join("runtime-test-python.json")).unwrap(); + let json_content = + fs::read_to_string(temp_dir.path().join("runtime-test-python.json")).unwrap(); let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); assert_eq!(info["runtime"], "python"); assert_eq!(info["dryRun"], false); // Check verification file - let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-python-verify.txt")).unwrap(); + let verify_content = + fs::read_to_string(temp_dir.path().join("runtime-test-python-verify.txt")).unwrap(); assert!(verify_content.contains("Read config: sample-project")); assert!(verify_content.contains("Features count: 2")); } @@ -839,10 +843,14 @@ main(); let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); assert_eq!(info["runtime"], "node"); assert_eq!(info["dryRun"], false); - assert!(info["features"].as_array().unwrap().contains(&serde_json::json!("commonjs"))); + assert!(info["features"] + .as_array() + .unwrap() + .contains(&serde_json::json!("commonjs"))); // Check verification file - let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-node-verify.txt")).unwrap(); + let verify_content = + fs::read_to_string(temp_dir.path().join("runtime-test-node-verify.txt")).unwrap(); assert!(verify_content.contains("Read config: sample-project")); assert!(verify_content.contains("Settings keys: debug, maxRetries")); } @@ -905,10 +913,14 @@ VERIFY let info: serde_json::Value = serde_json::from_str(&json_content).unwrap(); assert_eq!(info["runtime"], "ruby"); assert_eq!(info["dryRun"], false); - assert!(info["features"].as_array().unwrap().contains(&serde_json::json!("symbols"))); + assert!(info["features"] + .as_array() + .unwrap() + .contains(&serde_json::json!("symbols"))); // Check verification file - let verify_content = fs::read_to_string(temp_dir.path().join("runtime-test-ruby-verify.txt")).unwrap(); + let verify_content = + fs::read_to_string(temp_dir.path().join("runtime-test-ruby-verify.txt")).unwrap(); assert!(verify_content.contains("Read config: sample-project")); assert!(verify_content.contains("Features: auth, logging")); } From c8947bc2498c8d3ab5042dff6c9084fcf499d222 Mon Sep 17 00:00:00 2001 From: Ryan Daigle Date: Wed, 21 Jan 2026 14:25:17 -0500 Subject: [PATCH 3/3] Fix sed test for cross-platform compatibility Use temp file approach instead of sed -i (BSD vs GNU incompatibility). Co-Authored-By: Claude Opus 4.5 --- tests/fixture_operations.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fixture_operations.rs b/tests/fixture_operations.rs index 328786e..dea8786 100644 --- a/tests/fixture_operations.rs +++ b/tests/fixture_operations.rs @@ -121,7 +121,9 @@ set -euo pipefail cd "$MIGRATE_PROJECT_ROOT" # Replace "Hello, world!" with "Hello, migration!" -sed -i '' 's/Hello, world!/Hello, migration!/' src/main.ts +# Use temp file approach for cross-platform compatibility (BSD vs GNU sed) +sed 's/Hello, world!/Hello, migration!/' src/main.ts > src/main.ts.tmp +mv src/main.ts.tmp src/main.ts "#, );