@willh/gemini-translator
Version:
A tool to translate SRT, WebVTT, ASS and Markdown files from English to Traditional Chinese using Google Gemini API
404 lines (329 loc) • 13.7 kB
JavaScript
#!/usr/bin/env node
// Simple validation test that mimics CI behavior
import fs from 'fs';
console.log('🔍 Basic validation tests');
// Test 1: Files exist
import { spawn } from 'child_process';
console.log('🧪 Running Gemini Translator Tests');
console.log('=====================================');
let testsPassed = 0;
let testsFailed = 0;
async function runTest(name, testFn) {
try {
console.log(`\n▶️ ${name}`);
await testFn();
console.log(`✅ ${name} - PASSED`);
testsPassed++;
} catch (error) {
console.log(`❌ ${name} - FAILED`);
console.log(` Error: ${error.message}`);
testsFailed++;
}
}
function assertTrue(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function checkSyntax(filename) {
return new Promise((resolve, reject) => {
const child = spawn('node', ['-c', filename], {
cwd: process.cwd(),
stdio: 'pipe'
});
let stderr = '';
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Syntax error in ${filename}: ${stderr}`));
} else {
resolve();
}
});
});
}
// Main test execution
async function runAllTests() {
// Test 1: Check if main files exist
await runTest('Main files exist', () => {
assertTrue(fs.existsSync('main.js'), 'main.js should exist');
assertTrue(fs.existsSync('promisePool.js'), 'promisePool.js should exist');
assertTrue(fs.existsSync('package.json'), 'package.json should exist');
});
// Test 2: Check package.json structure
await runTest('Package.json validation', () => {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
assertTrue(pkg.name, 'Package should have a name');
assertTrue(pkg.version, 'Package should have a version');
assertTrue(pkg.main, 'Package should have a main entry point');
assertTrue(pkg.bin, 'Package should have bin configuration');
assertTrue(pkg.dependencies, 'Package should have dependencies');
// Check required dependencies
assertTrue(pkg.dependencies.axios, 'axios dependency should be present');
assertTrue(pkg.dependencies.yargs, 'yargs dependency should be present');
});
// Test 3: Check if main.js is syntactically correct
await runTest('Main.js syntax check', async () => {
await checkSyntax('main.js');
});
// Test 4: Check if promisePool.js is syntactically correct
await runTest('PromisePool.js syntax check', async () => {
await checkSyntax('promisePool.js');
});
// Test 5: Check test files exist
await runTest('Test files exist', () => {
const testDir = 'tests';
if (fs.existsSync(testDir)) {
const files = fs.readdirSync(testDir);
assertTrue(files.length > 0, 'Tests directory should contain test files');
// Check for SRT test file
const srtFiles = files.filter(f => f.endsWith('.srt'));
assertTrue(srtFiles.length > 0, 'Should have at least one SRT test file');
}
});
// Test 6: Check CLI help command
await runTest('CLI help command', async () => {
return new Promise((resolve, reject) => {
const child = spawn('node', ['main.js', '--help'], {
cwd: process.cwd(),
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`CLI help failed with code ${code}: ${stderr}`));
} else if (!stdout.includes('用法:')) {
reject(new Error('Help output does not contain expected content'));
} else {
resolve();
}
});
});
});
// Test 6.5: Enhanced Markdown parsing test
await runTest('Enhanced Markdown parsing', () => {
// Test if enhanced Markdown detection works properly
const testMd = `# Test Header
This is a test with various Markdown elements:
- List item 1
- List item 2
- Nested item
\`\`\`javascript
function test() {
console.log("test");
}
\`\`\`
[Link](https://example.com)
::: tip
This is a tip
:::
$$
E = mc^2
$$
`;
// Test that the markdown contains various elements we can now detect
assertTrue(testMd.includes('# Test'), 'Should contain headers');
assertTrue(testMd.includes('- List'), 'Should contain lists');
assertTrue(testMd.includes('```'), 'Should contain code blocks');
assertTrue(testMd.includes('[Link]'), 'Should contain links');
assertTrue(testMd.includes(':::'), 'Should contain special syntax');
assertTrue(testMd.includes('$$'), 'Should contain math blocks');
console.log(' ✅ Enhanced Markdown elements detected correctly');
});
// Test 7: Check overwrite detection (input same as output)
await runTest('Overwrite detection test', async () => {
// Create a temporary test file
const testContent = `1
00:00:01,000 --> 00:00:05,000
Hello World
2
00:00:06,000 --> 00:00:10,000
This is a test subtitle
`;
const tempFile = './temp-test.srt';
fs.writeFileSync(tempFile, testContent, 'utf8');
try {
return new Promise((resolve, reject) => {
// Test with same input and output file (should detect overwrite mode)
const child = spawn('node', ['main.js', '-i', tempFile, '-o', tempFile], {
cwd: process.cwd(),
stdio: 'pipe',
env: { ...process.env, GEMINI_API_KEY: 'fake-key-for-test' }
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
// We expect this to fail due to fake API key, but it should detect overwrite mode first
if (stdout.includes('偵測到輸入與輸出檔案相同,將自動覆蓋原檔案') ||
stderr.includes('偵測到輸入與輸出檔案相同,將自動覆蓋原檔案')) {
resolve();
} else {
reject(new Error(`Expected overwrite detection message not found. Stdout: ${stdout}, Stderr: ${stderr}`));
}
});
});
} finally {
// Clean up temp file
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
});
// Test 8: Check extractMarkdownCodeBlocks fix for list continuation lines
await runTest('Markdown code blocks detection fix', () => {
// Read main.js content to extract functions for testing
const mainJsContent = fs.readFileSync('main.js', 'utf8');
// Extract the extractMarkdownCodeBlocks function
const functionStart = mainJsContent.indexOf('function extractMarkdownCodeBlocks(text) {');
let braceCount = 0;
let functionEnd = functionStart;
let started = false;
for (let i = functionStart; i < mainJsContent.length; i++) {
if (mainJsContent[i] === '{') {
braceCount++;
started = true;
} else if (mainJsContent[i] === '}') {
braceCount--;
if (started && braceCount === 0) {
functionEnd = i + 1;
break;
}
}
}
const extractMarkdownCodeBlocksCode = mainJsContent.substring(functionStart, functionEnd);
// Extract the isPartOfList function
const isPartOfListStart = mainJsContent.indexOf('function isPartOfList(lines, index) {');
let isPartOfListBraceCount = 0;
let isPartOfListEnd = isPartOfListStart;
let isPartOfListStarted = false;
for (let i = isPartOfListStart; i < mainJsContent.length; i++) {
if (mainJsContent[i] === '{') {
isPartOfListBraceCount++;
isPartOfListStarted = true;
} else if (mainJsContent[i] === '}') {
isPartOfListBraceCount--;
if (isPartOfListStarted && isPartOfListBraceCount === 0) {
isPartOfListEnd = i + 1;
break;
}
}
}
const isPartOfListCode = mainJsContent.substring(isPartOfListStart, isPartOfListEnd);
// Test the issue case - should only detect inline code blocks, not indented ones in lists
const testMarkdownWithLists = `## Authentication
- **Error: \`Failed to login. Message: Request contains an invalid argument\`**
- Users with Google Workspace accounts, or users with Google Cloud accounts
associated with their Gmail accounts may not be able to activate the free
tier of the Google Code Assist plan.
- For Google Cloud accounts, you can work around this by setting
\`GOOGLE_CLOUD_PROJECT\` to your project ID.
- You can also grab an API key from [AI
Studio](http://aistudio.google.com/app/apikey), which also includes a
separate free tier.`;
// Test the fix by evaluating the functions in a sandbox
const testCode = `
${isPartOfListCode}
${extractMarkdownCodeBlocksCode}
const blocks = extractMarkdownCodeBlocks(\`${testMarkdownWithLists.replace(/`/g, '\\`')}\`);
blocks;
`;
const blocks = eval(testCode);
// Should only find 2 inline code blocks, not the indented list continuation lines
assertTrue(blocks.length === 2, `Expected 2 code blocks, but found ${blocks.length}`);
assertTrue(blocks.every(block => block.type === 'inline'), 'All blocks should be inline type');
console.log(' ✅ Correctly identified only inline code blocks in lists');
console.log(` ✅ Found ${blocks.length} code blocks (expected 2)`);
});
// Test debug functionality doesn't truncate content
await runTest('Debug output shows full content without truncation', async () => {
const longContent = 'This is a very long piece of content that definitely exceeds 150 characters and should be displayed in full when debug mode is enabled. It contains important information that users need to see for debugging purposes. The content should not be truncated at all.';
// Mock console.error to capture output
const originalConsoleError = console.error;
const capturedOutput = [];
console.error = (...args) => {
capturedOutput.push(args.join(' '));
};
try {
// Create test blocks with long content
const originalBlocks = [{ text: longContent }];
const translatedBlocks = [{ text: longContent + ' (translated)' }];
const errors = ['Test error'];
// Extract the showMarkdownFormatDebug function
const showMarkdownFormatDebugCode = `
function showMarkdownFormatDebug(originalBlocks, translatedBlocks, errors, isDebugMode, inputPath) {
if (!isDebugMode) return;
console.error('\\n=== Markdown 格式檢查除錯資訊 ===');
console.error(\`正在處理檔案: \${inputPath}\`);
console.error(\`發現 \${errors.length} 個格式問題:\`);
errors.forEach((error, index) => {
console.error(\` \${index + 1}. \${error}\`);
});
console.error('\\n詳細區塊比對:');
const maxBlocks = Math.max(originalBlocks.length, translatedBlocks.length);
for (let i = 0; i < maxBlocks; i++) {
console.error(\`\\n--- 區塊 \${i + 1} ---\`);
if (i < originalBlocks.length) {
const originalText = originalBlocks[i].text || '';
console.error(\`原始: \${originalText.replace(/\\n/g, '\\\\n')}\`);
} else {
console.error('原始: [不存在]');
}
if (i < translatedBlocks.length) {
const translatedText = translatedBlocks[i].text || '';
console.error(\`翻譯: \${translatedText.replace(/\\n/g, '\\\\n')}\`);
} else {
console.error('翻譯: [不存在]');
}
}
console.error('\\n=== Markdown 格式檢查除錯資訊結束 ===\\n');
}
showMarkdownFormatDebug(originalBlocks, translatedBlocks, errors, true, 'test.md');
`;
eval(showMarkdownFormatDebugCode);
// Check that the full content is displayed (no truncation)
const fullOutput = capturedOutput.join(' ');
assertTrue(fullOutput.includes(longContent), 'Full original content should be displayed');
assertTrue(fullOutput.includes(longContent + ' (translated)'), 'Full translated content should be displayed');
assertTrue(!fullOutput.includes('...'), 'Output should not contain truncation indicators');
console.log(' ✅ Debug output shows full content without truncation');
console.log(` ✅ Content length: ${longContent.length} characters (exceeds old 150 limit)`);
console.log(' ✅ No truncation indicators found in output');
} finally {
// Restore console.error
console.error = originalConsoleError;
}
});
// Summary
console.log('\n📊 Test Summary');
console.log('===============');
console.log(`✅ Passed: ${testsPassed}`);
console.log(`❌ Failed: ${testsFailed}`);
console.log(`📈 Total: ${testsPassed + testsFailed}`);
if (testsFailed > 0) {
console.log('\n❌ Some tests failed!');
process.exit(1);
} else {
console.log('\n🎉 All tests passed!');
process.exit(0);
}
}
// Run all tests
runAllTests().catch(error => {
console.error('Test execution failed:', error);
process.exit(1);
});