//! Integration tests for the `unlock` command. use assert_cmd::Command; use predicates::prelude::*; fn cargo_command() -> Command { Command::cargo_bin("unlock").expect("binary") } #[test] #[ignore = "slow Windows file-lock force-delete integration; covered by just stable-test"] fn reports_file_lockers_and_can_force_delete() { let binary = assert_cmd::cargo::cargo_bin("unlock"); let script = format!( "$temp = Join-Path $env:TEMP ('mercury-unlock-test-' + [guid]::NewGuid()); \ New-Item -ItemType Directory -Path $temp | Out-Null; \ $path = Join-Path $temp 'locked.txt'; \ $locker = Join-Path $temp 'locker.ps1'; \ Set-Content -Path $path -Value 'locked'; \ Set-Content -Path $locker -Value \"param([string]`$Path)`n`$stream = [System.IO.File]::Open(`$Path, 'Open', 'ReadWrite', 'None')`nStart-Sleep -Seconds 10\"; \ $child = Start-Process pwsh -ArgumentList '-NoProfile','-File',$locker,$path -PassThru; \ Start-Sleep -Milliseconds 500; \ $delete = & '{}' delete --json --force --wait 0ms $path | ConvertFrom-Json; \ $exists = Test-Path $path; \ Stop-Process -Id $child.Id -Force -ErrorAction SilentlyContinue; \ Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue; \ [pscustomobject]@{{ delete = $delete; exists = $exists }} | ConvertTo-Json -Depth 8 -Compress", binary.display() ); let mut command = Command::new("pwsh"); command .arg("-NoProfile") .arg("-Command") .arg(script) .assert() .success() .stdout(predicate::str::contains("\"results\":[{")) .stdout(predicate::str::contains("\"summary\":{\"results\":1")) .stdout(predicate::str::contains("\"initial_blockers\"")) .stdout(predicate::str::contains("\"command_line\"")) .stdout(predicate::str::contains("\"action\":\"delete\"")) .stdout(predicate::str::contains("\"ok\":true")) .stdout(predicate::str::contains("\"exists\":false")); } #[test] fn help_includes_unlock_examples() { let mut command = cargo_command(); command .arg("--help") .assert() .success() .stdout(predicate::str::contains("who")) .stdout(predicate::str::contains("--deep")) .stdout(predicate::str::contains("delete")) .stdout(predicate::str::contains("ConvertFrom-Json")); } #[test] fn who_returns_success_for_unlocked_files() { let temp = std::env::temp_dir().join(format!( "unlock-who-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("clock") .as_nanos() )); std::fs::create_dir_all(&temp).expect("tempdir"); let path = temp.join("plain.txt"); std::fs::write(&path, "ready").expect("fixture"); let mut command = cargo_command(); command .arg("who") .arg("--json") .arg(&path) .assert() .success() .stdout(predicate::str::contains("\"summary\":{\"results\":1")) .stdout(predicate::str::contains("\"ok\":true")) .stdout(predicate::str::contains("\"final_blockers\":[]")); let _ = std::fs::remove_file(&path); let _ = std::fs::remove_dir_all(temp); } #[cfg(windows)] #[test] #[ignore = "slow Windows junction integration; covered by just stable-test"] fn delete_refuses_junction_directory_without_touching_target() { let binary = assert_cmd::cargo::cargo_bin("unlock"); let script = format!( "$temp = Join-Path $env:TEMP ('mercury-unlock-junction-' + [guid]::NewGuid()); \ $target = Join-Path $temp 'target'; \ $junction = Join-Path $temp 'junction'; \ $sentinel = Join-Path $target 'sentinel.txt'; \ try {{ \ New-Item -ItemType Directory -Path $target | Out-Null; \ Set-Content -LiteralPath $sentinel -Value 'keep'; \ New-Item -ItemType Junction -Path $junction -Target $target | Out-Null; \ $json = & '{}' delete --json $junction; \ $exit = $LASTEXITCODE; \ $report = $json | ConvertFrom-Json; \ if ($exit -eq 0) {{ throw 'unlock delete unexpectedly succeeded for a junction directory' }}; \ if (-not (Test-Path -LiteralPath $sentinel)) {{ throw 'junction target sentinel was deleted' }}; \ if (-not (Test-Path -LiteralPath $junction)) {{ throw 'junction itself was removed instead of refused' }}; \ if ($report.results[0].ok -ne $false) {{ throw 'unlock report should mark the junction delete as not ok' }}; \ if ($report.results[0].error -notmatch 'reparse') {{ throw ('expected reparse error, got: ' + $report.results[0].error) }}; \ [pscustomobject]@{{ exit = $exit; sentinel = (Test-Path -LiteralPath $sentinel); junction = (Test-Path -LiteralPath $junction); error = $report.results[0].error }} | ConvertTo-Json -Compress \ }} finally {{ \ if (Test-Path -LiteralPath $junction) {{ Remove-Item -LiteralPath $junction -Force -ErrorAction SilentlyContinue }}; \ if (Test-Path -LiteralPath $temp) {{ Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue }} \ }}", binary.display() ); let mut command = Command::new("pwsh"); command .arg("-NoProfile") .arg("-Command") .arg(script) .assert() .success() .stdout(predicate::str::contains("\"sentinel\":true")) .stdout(predicate::str::contains("\"junction\":true")) .stdout(predicate::str::contains("reparse")); } #[cfg(windows)] #[test] #[ignore = "slow Windows junction integration; covered by just stable-test"] fn copy_move_and_rename_refuse_junction_sources_with_reparse_errors() { let binary = assert_cmd::cargo::cargo_bin("unlock"); let script = format!( "$temp = Join-Path $env:TEMP ('mercury-unlock-junction-actions-' + [guid]::NewGuid()); \ $target = Join-Path $temp 'target'; \ $copyJunction = Join-Path $temp 'copy-junction'; \ $moveJunction = Join-Path $temp 'move-junction'; \ $renameJunction = Join-Path $temp 'rename-junction'; \ $copyDest = Join-Path $temp 'copy-dest'; \ $moveDest = Join-Path $temp 'move-dest'; \ $sentinel = Join-Path $target 'sentinel.txt'; \ function Invoke-Refused([string[]]$ArgumentList) {{ \ $json = & '{}' @ArgumentList; \ $exit = $LASTEXITCODE; \ $report = $json | ConvertFrom-Json; \ if ($exit -eq 0) {{ throw ('expected nonzero exit for ' + ($ArgumentList -join ' ')) }}; \ if ($report.results[0].ok -ne $false) {{ throw ('expected ok=false for ' + ($ArgumentList -join ' ')) }}; \ if ($report.results[0].error -notmatch 'reparse') {{ throw ('expected reparse error for ' + ($ArgumentList -join ' ') + ', got: ' + $report.results[0].error) }}; \ return $report.results[0].error \ }}; \ try {{ \ New-Item -ItemType Directory -Path $target | Out-Null; \ Set-Content -LiteralPath $sentinel -Value 'keep'; \ New-Item -ItemType Junction -Path $copyJunction -Target $target | Out-Null; \ New-Item -ItemType Junction -Path $moveJunction -Target $target | Out-Null; \ New-Item -ItemType Junction -Path $renameJunction -Target $target | Out-Null; \ $copyError = Invoke-Refused @('copy','--json',$copyJunction,$copyDest); \ $moveError = Invoke-Refused @('move','--json',$moveJunction,$moveDest); \ $renameError = Invoke-Refused @('rename','--json',$renameJunction,'renamed-junction'); \ if (-not (Test-Path -LiteralPath $sentinel)) {{ throw 'junction target sentinel was deleted' }}; \ if (-not (Test-Path -LiteralPath $copyJunction)) {{ throw 'copy junction was removed' }}; \ if (-not (Test-Path -LiteralPath $moveJunction)) {{ throw 'move junction was removed' }}; \ if (-not (Test-Path -LiteralPath $renameJunction)) {{ throw 'rename junction was removed' }}; \ if (Test-Path -LiteralPath $copyDest) {{ throw 'copy destination was created' }}; \ if (Test-Path -LiteralPath $moveDest) {{ throw 'move destination was created' }}; \ [pscustomobject]@{{ sentinel = (Test-Path -LiteralPath $sentinel); copy = $copyError; move = $moveError; rename = $renameError }} | ConvertTo-Json -Compress \ }} finally {{ \ foreach ($path in @($copyJunction,$moveJunction,$renameJunction)) {{ if (Test-Path -LiteralPath $path) {{ Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue }} }}; \ if (Test-Path -LiteralPath $temp) {{ Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue }} \ }}", binary.display() ); let mut command = Command::new("pwsh"); command .arg("-NoProfile") .arg("-Command") .arg(script) .assert() .success() .stdout(predicate::str::contains("\"sentinel\":true")) .stdout(predicate::str::contains("reparse")); }