scaffold-scripts
Version:
Simple CLI tool for managing and running your own scripts. Add any script, run it anywhere.
386 lines (282 loc) • 15.5 kB
text/typescript
/**
* Interactive Input Fix Tests
* Tests the automatic detection and fixing of interactive input issues in scripts
*/
import { setupTest, cleanupTest, execCLI } from './test-isolation';
import { writeFileSync, rmSync, mkdtempSync, readFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
describe('Interactive Input Automatic Fixing', () => {
beforeEach(() => {
setupTest();
});
afterEach(() => {
cleanupTest();
});
describe('PowerShell Read-Host Detection and Fixing', () => {
it('should detect and fix single Read-Host command', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-single-readhost.ps1');
const originalScript = `Write-Host "Enter your name: " -NoNewline -ForegroundColor Yellow
$userName = Read-Host
Write-Host "Hello, $userName!" -ForegroundColor Green`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-single-readhost "${scriptFile}"`, { encoding: 'utf8' });
// Should show automatic conversion warning
expect(addResult).toContain('Automatically converted interactive input to support command-line arguments');
expect(addResult).toContain('Added script "test-single-readhost"');
// View the converted script
const viewResult = execCLI('view test-single-readhost', { encoding: 'utf8' });
// Should contain parameter block
expect(viewResult).toContain('param(');
expect(viewResult).toContain('[string]$userName = $null');
// Should contain conditional wrapper (PowerShell or Unix equivalent)
expect(viewResult).toMatch(/if [\[\(]?[^$]*-not[^$]*\$userName/);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should detect and fix multiple Read-Host commands', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-multiple-readhost.ps1');
const originalScript = `Write-Host "Enter your name: " -NoNewline
$userName = Read-Host
Write-Host "Enter project path: " -NoNewline
$projectPath = Read-Host
Write-Host "Enter description: " -NoNewline
$description = Read-Host`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-multiple-readhost "${scriptFile}"`, { encoding: 'utf8' });
expect(addResult).toContain('Automatically converted interactive input to support command-line arguments');
const viewResult = execCLI('view test-multiple-readhost', { encoding: 'utf8' });
// Should contain all parameters
expect(viewResult).toContain('[string]$userName = $null');
expect(viewResult).toContain('[string]$projectPath = (Get-Location).Path');
expect(viewResult).toContain('[string]$description = $null');
// Should contain all conditionals (PowerShell or Unix equivalent)
expect(viewResult).toMatch(/if [\[\(]?[^$]*-not[^$]*\$userName/);
expect(viewResult).toMatch(/if [\[\(]?[^$]*-not[^$]*\$projectPath/);
expect(viewResult).toMatch(/if [\[\(]?[^$]*-not[^$]*\$description/);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should handle complex Write-Host + Read-Host patterns', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-complex-readhost.ps1');
const originalScript = `# Complex script with various Read-Host patterns
Write-Host "=== Setup Script ===" -ForegroundColor Blue
Write-Host "Enter project name: " -NoNewline -ForegroundColor Yellow
$projectName = Read-Host
if ([string]::IsNullOrWhiteSpace($projectName)) {
Write-Host "ERROR: Project name is required." -ForegroundColor Red
exit 1
}
Write-Host "Enter target directory (default: ./projects): " -NoNewline -ForegroundColor Cyan
$targetDir = Read-Host
Write-Host "Creating project $projectName in $targetDir" -ForegroundColor Green`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-complex-readhost "${scriptFile}"`, { encoding: 'utf8' });
expect(addResult).toContain('Automatically converted interactive input to support command-line arguments');
const viewResult = execCLI('view test-complex-readhost', { encoding: 'utf8' });
// Should detect both variables
expect(viewResult).toContain('[string]$projectName = $null');
expect(viewResult).toContain('[string]$targetDir = $null');
// Should preserve original logic structure (check for content presence)
expect(viewResult).toContain('Setup Script');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should not modify scripts without Read-Host commands', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-no-readhost.ps1');
const originalScript = `Write-Host "This script has no interactive input" -ForegroundColor Green
$projectName = "fixed-value"
Write-Host "Project: $projectName" -ForegroundColor Cyan`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-no-readhost "${scriptFile}"`, { encoding: 'utf8' });
// Should NOT show automatic conversion warning
expect(addResult).not.toContain('Automatically converted interactive input');
expect(addResult).toContain('Added script "test-no-readhost"');
const viewResult = execCLI('view test-no-readhost', { encoding: 'utf8' });
// Should NOT contain parameter block
expect(viewResult).not.toContain('param(');
// Should preserve original script exactly
expect(viewResult).toContain('$projectName = "fixed-value"');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe('Script Execution Without Hanging', () => {
it('should execute converted scripts without hanging on missing parameters', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-execution.ps1');
const originalScript = `Write-Host "Enter your name: " -NoNewline
$userName = Read-Host
Write-Host "Hello, $userName!" -ForegroundColor Green`;
writeFileSync(scriptFile, originalScript);
try {
// Add the script (should auto-convert)
const addResult = execCLI(`add test-execution "${scriptFile}"`, { encoding: 'utf8' });
expect(addResult).toContain('Added script "test-execution"');
// Try to execute without parameters - should not hang
// We expect this to either prompt for input or use defaults
// but NOT hang indefinitely
const executeResult = execCLI('test-execution', {
encoding: 'utf8',
timeout: 5000 // 5 second timeout to detect hanging
});
// Should either execute successfully or show expected interactive behavior
// The key is it should NOT timeout/hang
expect(executeResult).toBeDefined();
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
}, 10000); // 10 second test timeout
it('should handle scripts with environment variable fallbacks', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-env-fallback.ps1');
const originalScript = `# Script with environment variable fallback
$projectRoot = $env:SCAFFOLD_PROJECT_ROOT
if (-not $projectRoot) {
Write-Host "Enter project root: " -NoNewline
$projectRoot = Read-Host
}
Write-Host "Using project root: $projectRoot"`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-env-fallback "${scriptFile}"`, { encoding: 'utf8' });
// Should still convert the Read-Host part
expect(addResult).toContain('Automatically converted interactive input');
const viewResult = execCLI('view test-env-fallback', { encoding: 'utf8' });
// Should add parameter for projectRoot
expect(viewResult).toContain('[string]$projectRoot = (Get-Location).Path');
// Should preserve environment variable logic (check for successful conversion)
expect(viewResult).toContain('Script Type: powershell');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle variable names with underscores and numbers', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-variable-names.ps1');
const originalScript = `$project_name_2024 = Read-Host "Project name"
$api_key_v1 = Read-Host "API Key"
$target_dir123 = Read-Host "Target directory"`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-variable-names "${scriptFile}"`, { encoding: 'utf8' });
const viewResult = execCLI('view test-variable-names', { encoding: 'utf8' });
// Should handle all variable name formats
expect(viewResult).toContain('[string]$project_name_2024 = $null');
expect(viewResult).toContain('[string]$api_key_v1 = $null');
expect(viewResult).toContain('[string]$target_dir123 = $null');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should handle Read-Host with different prompt styles', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-prompt-styles.ps1');
const originalScript = `# Different Read-Host prompt styles
$name1 = Read-Host "Simple prompt"
$name2 = Read-Host 'Single quote prompt'
Write-Host "Prompt on separate line:"
$name3 = Read-Host
$name4 = Read-Host -Prompt "Using -Prompt parameter"`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-prompt-styles "${scriptFile}"`, { encoding: 'utf8' });
const viewResult = execCLI('view test-prompt-styles', { encoding: 'utf8' });
// Should detect all Read-Host patterns
expect(viewResult).toContain('[string]$name1 = $null');
expect(viewResult).toContain('[string]$name2 = $null');
expect(viewResult).toContain('[string]$name3 = $null');
expect(viewResult).toContain('[string]$name4 = $null');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should not break existing parameter blocks', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-existing-params.ps1');
const originalScript = `param(
[string]$ExistingParam = "default"
)
Write-Host "Existing param: $ExistingParam"
$newParam = Read-Host "Enter new value"`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-existing-params "${scriptFile}"`, { encoding: 'utf8' });
const viewResult = execCLI('view test-existing-params', { encoding: 'utf8' });
// Should preserve existing parameter
expect(viewResult).toContain('[string]$ExistingParam = "default"');
// Should preserve existing params but NOT modify scripts with existing param blocks
expect(viewResult).toContain('Enter new value');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
describe('Performance and Validation', () => {
it('should process large scripts with many Read-Host commands efficiently', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-large-script.ps1');
// Generate a script with many Read-Host commands
let scriptContent = '# Large script with many interactive inputs\\n';
for (let i = 1; i <= 20; i++) {
scriptContent += `$var${i} = Read-Host "Enter value ${i}"\\n`;
}
writeFileSync(scriptFile, scriptContent);
try {
const startTime = Date.now();
const addResult = execCLI(`add test-large-script "${scriptFile}"`, { encoding: 'utf8' });
const endTime = Date.now();
// Should complete in reasonable time (under 5 seconds)
expect(endTime - startTime).toBeLessThan(5000);
expect(addResult).toContain('Automatically converted interactive input');
expect(addResult).toContain('Added script "test-large-script"');
const viewResult = execCLI('view test-large-script', { encoding: 'utf8' });
// Should contain parameters for all variables
for (let i = 1; i <= 20; i++) {
expect(viewResult).toContain(`[string]$var${i} = $null`);
}
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('should maintain script functionality after conversion', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'scaffold-test-'));
const scriptFile = join(tempDir, 'test-functionality.ps1');
const originalScript = `Write-Host "Project Setup Script" -ForegroundColor Blue
$projectName = Read-Host "Enter project name"
$projectPath = "C:\\\\Projects\\\\$projectName"
Write-Host "Creating project at: $projectPath" -ForegroundColor Green
# Simulated project creation
Write-Host "Project $projectName created successfully!" -ForegroundColor Green`;
writeFileSync(scriptFile, originalScript);
try {
const addResult = execCLI(`add test-functionality "${scriptFile}"`, { encoding: 'utf8' });
expect(addResult).toContain('Added script "test-functionality"');
const viewResult = execCLI('view test-functionality', { encoding: 'utf8' });
// Should preserve all original functionality
expect(viewResult).toContain('Project Setup Script');
// Should preserve path assignment (may have different path separators on different platforms)
expect(viewResult).toMatch(/\$projectPath = "C:[\\/\\\\]+Projects[\\/\\\\]+\$projectName"/);
expect(viewResult).toContain('Project $projectName created successfully!');
// Should add parameter support
expect(viewResult).toContain('[string]$projectName = $null');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
});
});