diff --git a/.eslintrc.js b/.eslintrc.js index 90ed3b9..3ace944 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,28 +1,25 @@ module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended' - ], + parser: "@typescript-eslint/parser", + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], parserOptions: { ecmaVersion: 2022, - sourceType: 'module', - project: './tsconfig.json' + sourceType: "module", + project: "./tsconfig.json", }, env: { node: true, es2022: true, - jest: true + jest: true, }, - plugins: ['@typescript-eslint'], + plugins: ["@typescript-eslint"], rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - 'no-console': 'off', - 'semi': ['error', 'always'], - 'quotes': ['error', 'single'], - 'comma-dangle': ['error', 'never'] + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "no-console": "off", + semi: ["error", "always"], + quotes: ["error", "single"], + "comma-dangle": ["error", "never"], }, - ignorePatterns: ['dist/', 'node_modules/', 'coverage/', '*.js'] -}; \ No newline at end of file + ignorePatterns: ["dist/", "node_modules/", "coverage/", "*.js"], +}; diff --git a/TODO.md b/TODO.md index 60dbd72..a9ccdd3 100644 --- a/TODO.md +++ b/TODO.md @@ -12,4 +12,4 @@ - a. Let user merge changes from remote to local: We would need to implement a conflict resolver somehow. - b. If conflicts arise, we could just block the operation and let user dump the current state in order not to lose work. This is the simplest option. - Either way, we need to think about how to apply new commits from the remote, because changes currently only flow from the sandbox to the shadow repo. -- [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly \ No newline at end of file +- [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly diff --git a/docs/git-operations-plan.md b/docs/git-operations-plan.md index 1f0aa88..280ffa9 100644 --- a/docs/git-operations-plan.md +++ b/docs/git-operations-plan.md @@ -39,6 +39,7 @@ This plan outlines how to safely handle Git operations (commit, push, PR) outsid ``` 2. **Shadow Repository Setup (Optimized)** + ```typescript class ShadowRepository { private shadowPath: string; diff --git a/jest.config.js b/jest.config.js index 4034422..39cf025 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,39 +1,36 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/test'], + preset: "ts-jest", + testEnvironment: "node", + roots: ["/test"], testMatch: [ - '**/test/**/*.test.ts', - '**/test/**/*.test.js', - '**/test/**/*.spec.ts', - '**/test/**/*.spec.js' + "**/test/**/*.test.ts", + "**/test/**/*.test.js", + "**/test/**/*.spec.ts", + "**/test/**/*.spec.js", ], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], transform: { - '^.+\\.tsx?$': 'ts-jest', - '^.+\\.jsx?$': 'babel-jest' + "^.+\\.tsx?$": "ts-jest", + "^.+\\.jsx?$": "babel-jest", }, collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', - '!src/**/*.d.ts', - '!src/**/*.test.{ts,tsx}', - '!src/**/*.spec.{ts,tsx}' - ], - coverageDirectory: '/coverage', - coverageReporters: ['text', 'lcov', 'html'], - testPathIgnorePatterns: [ - '/node_modules/', - '/dist/' + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/*.test.{ts,tsx}", + "!src/**/*.spec.{ts,tsx}", ], + coverageDirectory: "/coverage", + coverageReporters: ["text", "lcov", "html"], + testPathIgnorePatterns: ["/node_modules/", "/dist/"], moduleNameMapper: { - '^@/(.*)$': '/src/$1' + "^@/(.*)$": "/src/$1", }, globals: { - 'ts-jest': { + "ts-jest": { tsconfig: { esModuleInterop: true, - allowJs: true - } - } - } -}; \ No newline at end of file + allowJs: true, + }, + }, + }, +}; diff --git a/src/container.ts b/src/container.ts index fde17a3..4e08084 100644 --- a/src/container.ts +++ b/src/container.ts @@ -573,11 +573,14 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\ // Also copy .git directory to preserve git history console.log(chalk.blue("• Copying git history...")); const gitTarFile = `/tmp/claude-sandbox-git-${Date.now()}.tar`; - // Exclude macOS resource fork files when creating git archive - execSync(`tar -cf "${gitTarFile}" --exclude="._*" .git`, { - cwd: workDir, - stdio: "pipe", - }); + // Exclude macOS resource fork files and .DS_Store when creating git archive + execSync( + `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store" .git`, + { + cwd: workDir, + stdio: "pipe", + }, + ); try { const gitStream = fs.createReadStream(gitTarFile); diff --git a/src/git/shadow-repository.ts b/src/git/shadow-repository.ts index 07a5999..13102f5 100644 --- a/src/git/shadow-repository.ts +++ b/src/git/shadow-repository.ts @@ -166,9 +166,12 @@ export class ShadowRepository { try { await execAsync("git add .", { cwd: this.shadowPath }); console.log(chalk.gray(" Staged all files for tracking")); - + // Create initial commit to ensure deletions can be tracked - await execAsync('git commit -m "Initial snapshot of working directory" --allow-empty', { cwd: this.shadowPath }); + await execAsync( + 'git commit -m "Initial snapshot of working directory" --allow-empty', + { cwd: this.shadowPath }, + ); console.log(chalk.gray(" Created initial commit for change tracking")); } catch (stageError: any) { console.log(chalk.gray(" Could not stage files:", stageError.message)); @@ -426,8 +429,10 @@ export class ShadowRepository { // Rsync directly from container to shadow repo with proper deletion handling // First, clear the shadow repo (except .git) to ensure deletions are reflected - await execAsync(`find ${this.shadowPath} -mindepth 1 -not -path '${this.shadowPath}/.git*' -delete`); - + await execAsync( + `find ${this.shadowPath} -mindepth 1 -not -path '${this.shadowPath}/.git*' -delete`, + ); + // Rsync within container to staging area using exclude file const rsyncCmd = `docker exec ${containerId} rsync -av --delete \ --exclude-from=${containerExcludeFile} \ diff --git a/test/README.md b/test/README.md index 5221b2b..804a65f 100644 --- a/test/README.md +++ b/test/README.md @@ -48,12 +48,12 @@ Tests are written using Jest with TypeScript support. The Jest configuration is ### Example Unit Test ```typescript -import { someFunction } from '../../src/someModule'; +import { someFunction } from "../../src/someModule"; -describe('someFunction', () => { - it('should do something', () => { - const result = someFunction('input'); - expect(result).toBe('expected output'); +describe("someFunction", () => { + it("should do something", () => { + const result = someFunction("input"); + expect(result).toBe("expected output"); }); }); ``` @@ -61,9 +61,10 @@ describe('someFunction', () => { ## E2E Tests End-to-end tests are located in `test/e2e/` and test the complete workflow of the CLI tool. These tests: + - Create actual Docker containers - Run Claude commands - Verify git operations - Test the full user experience -Run E2E tests with: `npm run test:e2e` \ No newline at end of file +Run E2E tests with: `npm run test:e2e` diff --git a/test/e2e/README.md b/test/e2e/README.md index 2adeb0b..dc232a3 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -35,12 +35,14 @@ The tests create a temporary git repository, start a claude-sandbox instance, pe ## Running Tests ### Quick Test (Recommended) + ```bash # Run core functionality tests node core-functionality-test.js ``` ### Individual Tests + ```bash # Test repository to container sync node repo-to-container-sync-test.js @@ -53,6 +55,7 @@ node test-suite.js ``` ### Automated Test Runner + ```bash # Run all tests with cleanup ./run-tests.sh @@ -67,11 +70,13 @@ node test-suite.js ## Test Process 1. **Setup Phase** + - Creates temporary git repository with dummy files - Starts claude-sandbox instance - Connects to web UI for monitoring sync events 2. **Test Execution** + - Performs file operations inside the container - Waits for synchronization to complete - Verifies shadow repository state @@ -84,6 +89,7 @@ node test-suite.js ## Key Features Tested ### Repository to Container Sync + - ✅ One-to-one file mapping from test repo to container - ✅ No extra files in container (only test repo files) - ✅ File content integrity verification @@ -91,6 +97,7 @@ node test-suite.js - ✅ Correct branch creation ### File Synchronization + - ✅ New file creation and content sync - ✅ File modification and content updates - ✅ File deletion and proper removal @@ -99,12 +106,14 @@ node test-suite.js - ✅ Special characters in filenames ### Git Integration + - ✅ Staging of additions (`A` status) - ✅ Tracking of modifications (`M` status) - ✅ Detection of deletions (`D` status) - ✅ Proper git commit workflow ### Web UI Integration + - ✅ Real-time sync event notifications - ✅ Change summary reporting - ✅ WebSocket communication @@ -114,25 +123,30 @@ node test-suite.js ### Common Issues **Container startup timeout** + - Increase timeout values in test framework - Check Docker daemon is running - Verify claude-sandbox image exists **Git lock conflicts** + - Tests automatically handle concurrent git operations - Temporary `.git/index.lock` files are cleaned up **Port conflicts** + - Tests use dynamic port allocation - Multiple tests can run sequentially **WebSocket connection issues** + - Framework includes connection retry logic - Fallback to polling if WebSocket fails ### Test Failure Analysis Tests provide detailed error messages indicating: + - Which specific operation failed - Expected vs actual file states - Git status differences @@ -145,33 +159,36 @@ Tests provide detailed error messages indicating: 1. Create test function in appropriate test file 2. Use framework methods for file operations: ```javascript - await framework.addFile('path/file.txt', 'content'); - await framework.modifyFile('path/file.txt', 'new content'); - await framework.deleteFile('path/file.txt'); + await framework.addFile("path/file.txt", "content"); + await framework.modifyFile("path/file.txt", "new content"); + await framework.deleteFile("path/file.txt"); ``` 3. Verify results using assertion methods: ```javascript - const exists = await framework.shadowFileExists('file.txt'); - const content = await framework.getShadowFileContent('file.txt'); + const exists = await framework.shadowFileExists("file.txt"); + const content = await framework.getShadowFileContent("file.txt"); const gitStatus = await framework.getGitStatus(); ``` ### Framework API **File Operations** + - `addFile(path, content)` - Create new file -- `modifyFile(path, content)` - Update existing file +- `modifyFile(path, content)` - Update existing file - `deleteFile(path)` - Remove file - `moveFile(from, to)` - Rename/move file - `createDirectory(path)` - Create directory **Verification Methods** + - `shadowFileExists(path)` - Check file existence - `getShadowFileContent(path)` - Read file content - `getGitStatus()` - Get git status output - `waitForSync()` - Wait for synchronization **Event Monitoring** + - `receivedSyncEvents` - Array of sync notifications - WebSocket connection automatically established @@ -188,4 +205,4 @@ These tests are designed to run in automated environments: ./run-tests.sh ``` -The tests provide proper exit codes (0 for success, 1 for failure) and detailed logging for debugging purposes. \ No newline at end of file +The tests provide proper exit codes (0 for success, 1 for failure) and detailed logging for debugging purposes. diff --git a/test/e2e/core-functionality-test.js b/test/e2e/core-functionality-test.js index 4f68994..0b4f983 100755 --- a/test/e2e/core-functionality-test.js +++ b/test/e2e/core-functionality-test.js @@ -1,159 +1,181 @@ #!/usr/bin/env node -const { SyncTestFramework } = require('./sync-test-framework'); +const { SyncTestFramework } = require("./sync-test-framework"); async function runCoreTests() { - console.log('🚀 Core File Sync Functionality Tests'); - console.log('====================================='); - + console.log("🚀 Core File Sync Functionality Tests"); + console.log("====================================="); + const framework = new SyncTestFramework(); const tests = []; - + try { await framework.setup(); - + // Test 0: Initial Repository Sync - console.log('\n🧪 Test 0: Initial Repository to Container Sync'); - + console.log("\n🧪 Test 0: Initial Repository to Container Sync"); + // Verify that repository files are properly synced to container const repoFiles = await framework.listRepoFiles(); const containerFiles = await framework.listContainerFiles(); - + // Check that key files exist in container - const expectedFiles = ['README.md', 'package.json', 'src/main.js', 'src/utils.js']; + const expectedFiles = [ + "README.md", + "package.json", + "src/main.js", + "src/utils.js", + ]; for (const file of expectedFiles) { const exists = await framework.containerFileExists(file); if (!exists) { throw new Error(`Expected file ${file} not found in container`); } - + // Verify content matches - const repoContent = await require('fs').promises.readFile( - require('path').join(framework.testRepo, file), 'utf8' + const repoContent = await require("fs").promises.readFile( + require("path").join(framework.testRepo, file), + "utf8", ); const containerContent = await framework.getContainerFileContent(file); - + if (repoContent.trim() !== containerContent.trim()) { throw new Error(`Content mismatch for ${file}`); } } - - console.log('✅ Initial repository sync test passed'); - tests.push({ name: 'Initial Repository Sync', passed: true }); - + + console.log("✅ Initial repository sync test passed"); + tests.push({ name: "Initial Repository Sync", passed: true }); + // Test 1: File Addition - console.log('\n🧪 Test 1: File Addition'); - await framework.addFile('addition-test.txt', 'New file content'); - - const addExists = await framework.shadowFileExists('addition-test.txt'); - if (!addExists) throw new Error('File addition failed'); - - const addContent = await framework.getShadowFileContent('addition-test.txt'); - if (addContent.trim() !== 'New file content') throw new Error('File content mismatch'); - + console.log("\n🧪 Test 1: File Addition"); + await framework.addFile("addition-test.txt", "New file content"); + + const addExists = await framework.shadowFileExists("addition-test.txt"); + if (!addExists) throw new Error("File addition failed"); + + const addContent = + await framework.getShadowFileContent("addition-test.txt"); + if (addContent.trim() !== "New file content") + throw new Error("File content mismatch"); + const addStatus = await framework.getGitStatus(); - if (!addStatus.includes('addition-test.txt')) throw new Error('Git should track new file'); - - console.log('✅ File addition test passed'); - tests.push({ name: 'File Addition', passed: true }); - + if (!addStatus.includes("addition-test.txt")) + throw new Error("Git should track new file"); + + console.log("✅ File addition test passed"); + tests.push({ name: "File Addition", passed: true }); + // Test 2: File Modification - console.log('\n🧪 Test 2: File Modification'); + console.log("\n🧪 Test 2: File Modification"); // Commit first so we can see modifications - await require('util').promisify(require('child_process').exec)( - `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add files for testing"` + await require("util").promisify(require("child_process").exec)( + `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add files for testing"`, ); - - await framework.modifyFile('addition-test.txt', 'Modified content'); - - const modContent = await framework.getShadowFileContent('addition-test.txt'); - if (modContent.trim() !== 'Modified content') throw new Error('File modification failed'); - + + await framework.modifyFile("addition-test.txt", "Modified content"); + + const modContent = + await framework.getShadowFileContent("addition-test.txt"); + if (modContent.trim() !== "Modified content") + throw new Error("File modification failed"); + const modStatus = await framework.getGitStatus(); - if (!modStatus.includes('M addition-test.txt')) throw new Error('Git should track modification'); - - console.log('✅ File modification test passed'); - tests.push({ name: 'File Modification', passed: true }); - + if (!modStatus.includes("M addition-test.txt")) + throw new Error("Git should track modification"); + + console.log("✅ File modification test passed"); + tests.push({ name: "File Modification", passed: true }); + // Test 3: File Deletion - console.log('\n🧪 Test 3: File Deletion'); - await framework.deleteFile('addition-test.txt'); - - const delExists = await framework.shadowFileExists('addition-test.txt'); - if (delExists) throw new Error('File deletion failed - file still exists'); - + console.log("\n🧪 Test 3: File Deletion"); + await framework.deleteFile("addition-test.txt"); + + const delExists = await framework.shadowFileExists("addition-test.txt"); + if (delExists) throw new Error("File deletion failed - file still exists"); + const delStatus = await framework.getGitStatus(); - if (!delStatus.includes('D addition-test.txt')) throw new Error('Git should track deletion'); - - console.log('✅ File deletion test passed'); - tests.push({ name: 'File Deletion', passed: true }); - + if (!delStatus.includes("D addition-test.txt")) + throw new Error("Git should track deletion"); + + console.log("✅ File deletion test passed"); + tests.push({ name: "File Deletion", passed: true }); + // Test 4: Directory Operations - console.log('\n🧪 Test 4: Directory Operations'); - await framework.createDirectory('test-dir'); - await framework.addFile('test-dir/nested-file.txt', 'Nested content'); - - const nestedExists = await framework.shadowFileExists('test-dir/nested-file.txt'); - if (!nestedExists) throw new Error('Nested file creation failed'); - - const nestedContent = await framework.getShadowFileContent('test-dir/nested-file.txt'); - if (nestedContent.trim() !== 'Nested content') throw new Error('Nested file content mismatch'); - - console.log('✅ Directory operations test passed'); - tests.push({ name: 'Directory Operations', passed: true }); - + console.log("\n🧪 Test 4: Directory Operations"); + await framework.createDirectory("test-dir"); + await framework.addFile("test-dir/nested-file.txt", "Nested content"); + + const nestedExists = await framework.shadowFileExists( + "test-dir/nested-file.txt", + ); + if (!nestedExists) throw new Error("Nested file creation failed"); + + const nestedContent = await framework.getShadowFileContent( + "test-dir/nested-file.txt", + ); + if (nestedContent.trim() !== "Nested content") + throw new Error("Nested file content mismatch"); + + console.log("✅ Directory operations test passed"); + tests.push({ name: "Directory Operations", passed: true }); + // Test 5: Web UI Notifications - console.log('\n🧪 Test 5: Web UI Notifications'); + console.log("\n🧪 Test 5: Web UI Notifications"); const initialEventCount = framework.receivedSyncEvents.length; - - await framework.addFile('notification-test.txt', 'Notification content'); + + await framework.addFile("notification-test.txt", "Notification content"); await framework.waitForSync(); - + const finalEventCount = framework.receivedSyncEvents.length; - if (finalEventCount <= initialEventCount) throw new Error('No sync events received'); - - const latestEvent = framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; - if (!latestEvent.data.hasChanges) throw new Error('Sync event should indicate changes'); - - console.log('✅ Web UI notifications test passed'); - tests.push({ name: 'Web UI Notifications', passed: true }); - + if (finalEventCount <= initialEventCount) + throw new Error("No sync events received"); + + const latestEvent = + framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; + if (!latestEvent.data.hasChanges) + throw new Error("Sync event should indicate changes"); + + console.log("✅ Web UI notifications test passed"); + tests.push({ name: "Web UI Notifications", passed: true }); } catch (error) { console.log(`❌ Test failed: ${error.message}`); - tests.push({ name: 'Current Test', passed: false, error: error.message }); + tests.push({ name: "Current Test", passed: false, error: error.message }); } finally { await framework.cleanup(); } - + // Results - console.log('\n' + '='.repeat(50)); - console.log('📊 Test Results:'); - - const passed = tests.filter(t => t.passed).length; - const failed = tests.filter(t => !t.passed).length; - - tests.forEach(test => { - const icon = test.passed ? '✅' : '❌'; + console.log("\n" + "=".repeat(50)); + console.log("📊 Test Results:"); + + const passed = tests.filter((t) => t.passed).length; + const failed = tests.filter((t) => !t.passed).length; + + tests.forEach((test) => { + const icon = test.passed ? "✅" : "❌"; console.log(`${icon} ${test.name}`); if (!test.passed && test.error) { console.log(` ${test.error}`); } }); - + console.log(`\n📈 Summary: ${passed} passed, ${failed} failed`); - + if (failed === 0) { - console.log('🎉 All core functionality tests passed!'); + console.log("🎉 All core functionality tests passed!"); return true; } else { - console.log('❌ Some tests failed'); + console.log("❌ Some tests failed"); return false; } } -runCoreTests().then((success) => { - process.exit(success ? 0 : 1); -}).catch((error) => { - console.error('❌ Test runner failed:', error); - process.exit(1); -}); \ No newline at end of file +runCoreTests() + .then((success) => { + process.exit(success ? 0 : 1); + }) + .catch((error) => { + console.error("❌ Test runner failed:", error); + process.exit(1); + }); diff --git a/test/e2e/dummy-repo/README.md b/test/e2e/dummy-repo/README.md index d445c7c..4cbc982 100644 --- a/test/e2e/dummy-repo/README.md +++ b/test/e2e/dummy-repo/README.md @@ -7,4 +7,4 @@ This is a dummy repository for testing claude-sandbox file synchronization funct - `src/main.js` - Main application file - `src/utils.js` - Utility functions - `package.json` - Package configuration -- `test/test.js` - Test file \ No newline at end of file +- `test/test.js` - Test file diff --git a/test/e2e/dummy-repo/package.json b/test/e2e/dummy-repo/package.json index ef49e2c..99b3ba6 100644 --- a/test/e2e/dummy-repo/package.json +++ b/test/e2e/dummy-repo/package.json @@ -7,7 +7,11 @@ "test": "node test/test.js", "start": "node src/main.js" }, - "keywords": ["test", "claude", "sandbox"], + "keywords": [ + "test", + "claude", + "sandbox" + ], "author": "Test", "license": "MIT" -} \ No newline at end of file +} diff --git a/test/e2e/dummy-repo/src/main.js b/test/e2e/dummy-repo/src/main.js index 4cc1c54..4ce38b3 100644 --- a/test/e2e/dummy-repo/src/main.js +++ b/test/e2e/dummy-repo/src/main.js @@ -1,14 +1,14 @@ -const utils = require('./utils'); +const utils = require("./utils"); function main() { - console.log('Starting test application...'); + console.log("Starting test application..."); const result = utils.calculate(10, 5); - console.log('Calculation result:', result); - console.log('Application finished.'); + console.log("Calculation result:", result); + console.log("Application finished."); } if (require.main === module) { main(); } -module.exports = { main }; \ No newline at end of file +module.exports = { main }; diff --git a/test/e2e/dummy-repo/src/utils.js b/test/e2e/dummy-repo/src/utils.js index c3c55a4..ada8d44 100644 --- a/test/e2e/dummy-repo/src/utils.js +++ b/test/e2e/dummy-repo/src/utils.js @@ -8,7 +8,7 @@ function multiply(a, b) { function divide(a, b) { if (b === 0) { - throw new Error('Division by zero'); + throw new Error("Division by zero"); } return a / b; } @@ -16,5 +16,5 @@ function divide(a, b) { module.exports = { calculate, multiply, - divide -}; \ No newline at end of file + divide, +}; diff --git a/test/e2e/dummy-repo/test/test.js b/test/e2e/dummy-repo/test/test.js index 9afd061..638cf22 100644 --- a/test/e2e/dummy-repo/test/test.js +++ b/test/e2e/dummy-repo/test/test.js @@ -1,24 +1,24 @@ -const utils = require('../src/utils'); +const utils = require("../src/utils"); function runTests() { - console.log('Running tests...'); - + console.log("Running tests..."); + // Test calculate function const result1 = utils.calculate(2, 3); - console.assert(result1 === 5, 'Calculate test failed'); - console.log('✓ Calculate test passed'); - + console.assert(result1 === 5, "Calculate test failed"); + console.log("✓ Calculate test passed"); + // Test multiply function const result2 = utils.multiply(4, 3); - console.assert(result2 === 12, 'Multiply test failed'); - console.log('✓ Multiply test passed'); - + console.assert(result2 === 12, "Multiply test failed"); + console.log("✓ Multiply test passed"); + // Test divide function const result3 = utils.divide(10, 2); - console.assert(result3 === 5, 'Divide test failed'); - console.log('✓ Divide test passed'); - - console.log('All tests passed!'); + console.assert(result3 === 5, "Divide test failed"); + console.log("✓ Divide test passed"); + + console.log("All tests passed!"); } -runTests(); \ No newline at end of file +runTests(); diff --git a/test/e2e/repo-to-container-sync-test.js b/test/e2e/repo-to-container-sync-test.js index 580b539..808c09c 100755 --- a/test/e2e/repo-to-container-sync-test.js +++ b/test/e2e/repo-to-container-sync-test.js @@ -1,31 +1,41 @@ #!/usr/bin/env node -const { SyncTestFramework } = require('./sync-test-framework'); +const { SyncTestFramework } = require("./sync-test-framework"); async function testRepoToContainerSync() { - console.log('🧪 Testing Repository to Container Sync'); - console.log('======================================'); - + console.log("🧪 Testing Repository to Container Sync"); + console.log("======================================"); + const framework = new SyncTestFramework(); - + try { await framework.setup(); - - console.log('\n🔍 Step 1: Verifying ONE-TO-ONE sync from test repo to container...'); - + + console.log( + "\n🔍 Step 1: Verifying ONE-TO-ONE sync from test repo to container...", + ); + // Get list of files in the original test repo const repoFiles = await framework.listRepoFiles(); - console.log(`📂 Test repository contains ${repoFiles.length} files:`, repoFiles); - + console.log( + `📂 Test repository contains ${repoFiles.length} files:`, + repoFiles, + ); + // Get list of files in the container const containerFiles = await framework.listContainerFiles(); - console.log(`🐳 Container contains ${containerFiles.length} files:`, containerFiles); - + console.log( + `🐳 Container contains ${containerFiles.length} files:`, + containerFiles, + ); + // CRITICAL: Check for exact one-to-one match if (containerFiles.length !== repoFiles.length) { - throw new Error(`File count mismatch! Repo has ${repoFiles.length} files but container has ${containerFiles.length} files`); + throw new Error( + `File count mismatch! Repo has ${repoFiles.length} files but container has ${containerFiles.length} files`, + ); } - + // Check that all repo files exist in container const missingInContainer = []; for (const file of repoFiles) { @@ -34,11 +44,13 @@ async function testRepoToContainerSync() { missingInContainer.push(file); } } - + if (missingInContainer.length > 0) { - throw new Error(`Files missing in container: ${missingInContainer.join(', ')}`); + throw new Error( + `Files missing in container: ${missingInContainer.join(", ")}`, + ); } - + // Check for extra files in container that aren't in repo const extraInContainer = []; for (const file of containerFiles) { @@ -46,134 +58,161 @@ async function testRepoToContainerSync() { extraInContainer.push(file); } } - + if (extraInContainer.length > 0) { - throw new Error(`Extra files found in container that aren't in test repo: ${extraInContainer.join(', ')}`); + throw new Error( + `Extra files found in container that aren't in test repo: ${extraInContainer.join(", ")}`, + ); } - - console.log('✅ Perfect one-to-one sync: All repo files exist in container, no extra files'); - - console.log('\n🔍 Step 2: Verifying file content integrity...'); - + + console.log( + "✅ Perfect one-to-one sync: All repo files exist in container, no extra files", + ); + + console.log("\n🔍 Step 2: Verifying file content integrity..."); + // Check content of key files - const testFiles = ['README.md', 'package.json', 'src/main.js', 'src/utils.js']; + const testFiles = [ + "README.md", + "package.json", + "src/main.js", + "src/utils.js", + ]; const contentMismatches = []; - + for (const file of testFiles) { if (repoFiles.includes(file)) { try { // Read from original repo - const repoContent = await require('fs').promises.readFile( - require('path').join(framework.testRepo, file), 'utf8' + const repoContent = await require("fs").promises.readFile( + require("path").join(framework.testRepo, file), + "utf8", ); - + // Read from container - const containerContent = await framework.getContainerFileContent(file); - + const containerContent = + await framework.getContainerFileContent(file); + if (repoContent.trim() !== containerContent.trim()) { contentMismatches.push({ file, repoLength: repoContent.length, - containerLength: containerContent.length + containerLength: containerContent.length, }); } } catch (error) { contentMismatches.push({ file, - error: error.message + error: error.message, }); } } } - + if (contentMismatches.length > 0) { - console.log('Content mismatches found:'); - contentMismatches.forEach(mismatch => { + console.log("Content mismatches found:"); + contentMismatches.forEach((mismatch) => { if (mismatch.error) { console.log(` ${mismatch.file}: ${mismatch.error}`); } else { - console.log(` ${mismatch.file}: repo ${mismatch.repoLength} bytes vs container ${mismatch.containerLength} bytes`); + console.log( + ` ${mismatch.file}: repo ${mismatch.repoLength} bytes vs container ${mismatch.containerLength} bytes`, + ); } }); - throw new Error('File content integrity check failed'); + throw new Error("File content integrity check failed"); } - - console.log('✅ File content integrity verified'); - - console.log('\n🔍 Step 3: Verifying no extra files from outside test repo...'); - + + console.log("✅ File content integrity verified"); + + console.log( + "\n🔍 Step 3: Verifying no extra files from outside test repo...", + ); + // List of common files that might accidentally be included but shouldn't be const shouldNotExist = [ - 'node_modules', - '.env', - 'dist', - 'build', - '.vscode', - '.idea', - 'coverage' + "node_modules", + ".env", + "dist", + "build", + ".vscode", + ".idea", + "coverage", ]; - + for (const file of shouldNotExist) { const exists = await framework.containerFileExists(file); if (exists) { - throw new Error(`Unexpected file/directory found in container: ${file} (should only contain test repo files)`); + throw new Error( + `Unexpected file/directory found in container: ${file} (should only contain test repo files)`, + ); } } - - console.log('✅ No unexpected files found - container only contains test repo files'); - - console.log('\n🔍 Step 4: Verifying git repository state...'); - - // Check that git repository exists in container - const gitExists = await framework.containerFileExists('.git/HEAD'); - if (!gitExists) { - throw new Error('Git repository not properly copied to container'); - } - - console.log('✅ Git repository state verified'); - - console.log('\n🔍 Step 5: Verifying working directory setup...'); - - // Check that we're on the correct branch - const { stdout: branchOutput } = await require('util').promisify(require('child_process').exec)( - `docker exec ${framework.containerId} git -C /workspace branch --show-current` + + console.log( + "✅ No unexpected files found - container only contains test repo files", ); - + + console.log("\n🔍 Step 4: Verifying git repository state..."); + + // Check that git repository exists in container + const gitExists = await framework.containerFileExists(".git/HEAD"); + if (!gitExists) { + throw new Error("Git repository not properly copied to container"); + } + + console.log("✅ Git repository state verified"); + + console.log("\n🔍 Step 5: Verifying working directory setup..."); + + // Check that we're on the correct branch + const { stdout: branchOutput } = await require("util").promisify( + require("child_process").exec, + )( + `docker exec ${framework.containerId} git -C /workspace branch --show-current`, + ); + const currentBranch = branchOutput.trim(); - if (!currentBranch.startsWith('claude/')) { - throw new Error(`Expected to be on a claude/ branch, but on: ${currentBranch}`); + if (!currentBranch.startsWith("claude/")) { + throw new Error( + `Expected to be on a claude/ branch, but on: ${currentBranch}`, + ); } - + console.log(`✅ Working on correct branch: ${currentBranch}`); - - console.log('\n🔍 Step 6: Testing file operations work correctly...'); - + + console.log("\n🔍 Step 6: Testing file operations work correctly..."); + // Test that we can create files in the container - await framework.addFile('test-creation.txt', 'Test content'); + await framework.addFile("test-creation.txt", "Test content"); await framework.waitForSync(); - - const createdExists = await framework.containerFileExists('test-creation.txt'); + + const createdExists = + await framework.containerFileExists("test-creation.txt"); if (!createdExists) { - throw new Error('File creation in container failed'); + throw new Error("File creation in container failed"); } - + // Test that the file also appears in shadow repo - const shadowExists = await framework.shadowFileExists('test-creation.txt'); + const shadowExists = await framework.shadowFileExists("test-creation.txt"); if (!shadowExists) { - throw new Error('File not synced to shadow repository'); + throw new Error("File not synced to shadow repository"); } - - console.log('✅ File operations working correctly'); - - console.log('\n🎉 SUCCESS: Repository to container sync is working perfectly!'); - - console.log('\n📊 Summary:'); - console.log(` 📁 ${repoFiles.length} files synced from test repository to container`); + + console.log("✅ File operations working correctly"); + + console.log( + "\n🎉 SUCCESS: Repository to container sync is working perfectly!", + ); + + console.log("\n📊 Summary:"); + console.log( + ` 📁 ${repoFiles.length} files synced from test repository to container`, + ); console.log(` ✅ One-to-one sync verified - no extra files`); console.log(` ✅ All files exist with correct content`); console.log(` 🌿 Git repository properly initialized`); console.log(` 🔄 Bidirectional sync operational`); - } catch (error) { console.log(`\n❌ FAILED: ${error.message}`); throw error; @@ -182,10 +221,17 @@ async function testRepoToContainerSync() { } } -testRepoToContainerSync().then(() => { - console.log('\n✅ Repository to container sync test completed successfully'); - process.exit(0); -}).catch((error) => { - console.error('\n❌ Repository to container sync test failed:', error.message); - process.exit(1); -}); \ No newline at end of file +testRepoToContainerSync() + .then(() => { + console.log( + "\n✅ Repository to container sync test completed successfully", + ); + process.exit(0); + }) + .catch((error) => { + console.error( + "\n❌ Repository to container sync test failed:", + error.message, + ); + process.exit(1); + }); diff --git a/test/e2e/simple-deletion-test.js b/test/e2e/simple-deletion-test.js index e98674b..1bd7645 100755 --- a/test/e2e/simple-deletion-test.js +++ b/test/e2e/simple-deletion-test.js @@ -1,62 +1,63 @@ #!/usr/bin/env node -const { SyncTestFramework } = require('./sync-test-framework'); +const { SyncTestFramework } = require("./sync-test-framework"); async function testDeletionFunctionality() { - console.log('🧪 Testing File Deletion Functionality'); - console.log('====================================='); - + console.log("🧪 Testing File Deletion Functionality"); + console.log("====================================="); + const framework = new SyncTestFramework(); - + try { // Setup await framework.setup(); - - console.log('\n📝 Step 1: Adding test files...'); - await framework.addFile('test1.txt', 'Content 1'); - await framework.addFile('test2.txt', 'Content 2'); - await framework.addFile('test3.txt', 'Content 3'); + + console.log("\n📝 Step 1: Adding test files..."); + await framework.addFile("test1.txt", "Content 1"); + await framework.addFile("test2.txt", "Content 2"); + await framework.addFile("test3.txt", "Content 3"); await framework.waitForSync(); - + // Commit the files so deletions can be tracked - const { stdout } = await require('util').promisify(require('child_process').exec)( - `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add test files"` + const { stdout } = await require("util").promisify( + require("child_process").exec, + )( + `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add test files"`, ); - console.log('✅ Files committed to git'); - - console.log('\n🗑️ Step 2: Deleting one file...'); - await framework.deleteFile('test2.txt'); + console.log("✅ Files committed to git"); + + console.log("\n🗑️ Step 2: Deleting one file..."); + await framework.deleteFile("test2.txt"); await framework.waitForSync(); - - console.log('\n🔍 Step 3: Verifying deletion...'); - + + console.log("\n🔍 Step 3: Verifying deletion..."); + // Check that file no longer exists in shadow repo - const exists = await framework.shadowFileExists('test2.txt'); + const exists = await framework.shadowFileExists("test2.txt"); if (exists) { - throw new Error('❌ File still exists in shadow repository'); + throw new Error("❌ File still exists in shadow repository"); } - console.log('✅ File removed from shadow repository'); - + console.log("✅ File removed from shadow repository"); + // Check git status shows deletion const gitStatus = await framework.getGitStatus(); - console.log('Git status:', gitStatus); - - if (!gitStatus.includes('D test2.txt')) { + console.log("Git status:", gitStatus); + + if (!gitStatus.includes("D test2.txt")) { throw new Error(`❌ Git status should show deletion: ${gitStatus}`); } - console.log('✅ Git properly tracks deletion'); - + console.log("✅ Git properly tracks deletion"); + // Check other files still exist - const exists1 = await framework.shadowFileExists('test1.txt'); - const exists3 = await framework.shadowFileExists('test3.txt'); - + const exists1 = await framework.shadowFileExists("test1.txt"); + const exists3 = await framework.shadowFileExists("test3.txt"); + if (!exists1 || !exists3) { - throw new Error('❌ Other files were incorrectly deleted'); + throw new Error("❌ Other files were incorrectly deleted"); } - console.log('✅ Other files preserved'); - - console.log('\n🎉 SUCCESS: File deletion tracking is working correctly!'); - + console.log("✅ Other files preserved"); + + console.log("\n🎉 SUCCESS: File deletion tracking is working correctly!"); } catch (error) { console.log(`\n❌ FAILED: ${error.message}`); throw error; @@ -65,10 +66,12 @@ async function testDeletionFunctionality() { } } -testDeletionFunctionality().then(() => { - console.log('\n✅ Deletion test completed successfully'); - process.exit(0); -}).catch((error) => { - console.error('\n❌ Deletion test failed:', error.message); - process.exit(1); -}); \ No newline at end of file +testDeletionFunctionality() + .then(() => { + console.log("\n✅ Deletion test completed successfully"); + process.exit(0); + }) + .catch((error) => { + console.error("\n❌ Deletion test failed:", error.message); + process.exit(1); + }); diff --git a/test/e2e/sync-test-framework.js b/test/e2e/sync-test-framework.js index 1dc1c99..7611aeb 100644 --- a/test/e2e/sync-test-framework.js +++ b/test/e2e/sync-test-framework.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -const { spawn, exec } = require('child_process'); -const fs = require('fs').promises; -const path = require('path'); -const http = require('http'); -const WebSocket = require('ws'); -const util = require('util'); +const { spawn, exec } = require("child_process"); +const fs = require("fs").promises; +const path = require("path"); +const http = require("http"); +const WebSocket = require("ws"); +const util = require("util"); const execAsync = util.promisify(exec); class SyncTestFramework { @@ -21,49 +21,51 @@ class SyncTestFramework { } async setup() { - console.log('🔧 Setting up test environment...'); - + console.log("🔧 Setting up test environment..."); + // Create temporary test repository - this.testRepo = path.join('/tmp', `test-repo-${Date.now()}`); + this.testRepo = path.join("/tmp", `test-repo-${Date.now()}`); await fs.mkdir(this.testRepo, { recursive: true }); - + // Copy dummy repo files to test repo - const dummyRepo = path.join(__dirname, 'dummy-repo'); + const dummyRepo = path.join(__dirname, "dummy-repo"); await execAsync(`cp -r ${dummyRepo}/* ${this.testRepo}/`); - + // Initialize as git repository - await execAsync(`cd ${this.testRepo} && git init && git add . && git commit -m "Initial commit"`); - + await execAsync( + `cd ${this.testRepo} && git init && git add . && git commit -m "Initial commit"`, + ); + console.log(`📁 Test repo created at: ${this.testRepo}`); - + // Start claude-sandbox from the test repo await this.startSandbox(); - + // Connect to web UI for monitoring sync events await this.connectToWebUI(); - - console.log('✅ Test environment ready'); + + console.log("✅ Test environment ready"); } async startSandbox() { return new Promise((resolve, reject) => { - console.log('🚀 Starting claude-sandbox...'); - - this.sandboxProcess = spawn('npx', ['claude-sandbox', 'start'], { + console.log("🚀 Starting claude-sandbox..."); + + this.sandboxProcess = spawn("npx", ["claude-sandbox", "start"], { cwd: this.testRepo, - stdio: 'pipe' + stdio: "pipe", }); let setupComplete = false; const timeout = setTimeout(() => { if (!setupComplete) { - reject(new Error('Sandbox startup timeout')); + reject(new Error("Sandbox startup timeout")); } }, 45000); - this.sandboxProcess.stdout.on('data', (data) => { + this.sandboxProcess.stdout.on("data", (data) => { const output = data.toString(); - console.log('SANDBOX:', output.trim()); + console.log("SANDBOX:", output.trim()); // Extract container ID const containerMatch = output.match(/Started container: ([a-f0-9]+)/); @@ -74,25 +76,27 @@ class SyncTestFramework { } // Extract web port - const portMatch = output.match(/Web UI server started at http:\/\/localhost:(\d+)/); + const portMatch = output.match( + /Web UI server started at http:\/\/localhost:(\d+)/, + ); if (portMatch) { this.webPort = parseInt(portMatch[1]); console.log(`🌐 Web UI port: ${this.webPort}`); } // Check for setup completion - if (output.includes('Files synced successfully') && !setupComplete) { + if (output.includes("Files synced successfully") && !setupComplete) { setupComplete = true; clearTimeout(timeout); setTimeout(() => resolve(), 2000); // Wait a bit more for full initialization } }); - this.sandboxProcess.stderr.on('data', (data) => { - console.error('SANDBOX ERROR:', data.toString()); + this.sandboxProcess.stderr.on("data", (data) => { + console.error("SANDBOX ERROR:", data.toString()); }); - this.sandboxProcess.on('close', (code) => { + this.sandboxProcess.on("close", (code) => { if (!setupComplete) { reject(new Error(`Sandbox process exited with code ${code}`)); } @@ -102,91 +106,100 @@ class SyncTestFramework { async connectToWebUI() { if (!this.webPort || !this.containerId) { - throw new Error('Web UI port or container ID not available'); + throw new Error("Web UI port or container ID not available"); } return new Promise((resolve, reject) => { const wsUrl = `ws://localhost:${this.webPort}/socket.io/?EIO=4&transport=websocket`; this.webSocket = new WebSocket(wsUrl); - this.webSocket.on('open', () => { - console.log('🔌 Connected to web UI'); - + this.webSocket.on("open", () => { + console.log("🔌 Connected to web UI"); + // Send initial connection message (Socket.IO protocol) - this.webSocket.send('40'); // Socket.IO connect message - + this.webSocket.send("40"); // Socket.IO connect message + setTimeout(() => { // Attach to container - this.webSocket.send(`42["attach",{"containerId":"${this.containerId}","cols":80,"rows":24}]`); + this.webSocket.send( + `42["attach",{"containerId":"${this.containerId}","cols":80,"rows":24}]`, + ); resolve(); }, 1000); }); - this.webSocket.on('message', (data) => { + this.webSocket.on("message", (data) => { const message = data.toString(); if (message.startsWith('42["sync-complete"')) { try { const eventData = JSON.parse(message.substring(2))[1]; this.receivedSyncEvents.push({ timestamp: Date.now(), - data: eventData + data: eventData, }); - console.log('📡 Received sync event:', eventData.summary); + console.log("📡 Received sync event:", eventData.summary); } catch (e) { // Ignore parsing errors } } }); - this.webSocket.on('error', (error) => { - console.error('WebSocket error:', error); + this.webSocket.on("error", (error) => { + console.error("WebSocket error:", error); reject(error); }); - setTimeout(() => reject(new Error('WebSocket connection timeout')), 10000); + setTimeout( + () => reject(new Error("WebSocket connection timeout")), + 10000, + ); }); } async waitForSync(expectedChanges = null, timeoutMs = 10000) { const startTime = Date.now(); const initialEventCount = this.receivedSyncEvents.length; - + // Wait for a new sync event or timeout while (Date.now() - startTime < timeoutMs) { // Check if we received a new sync event if (this.receivedSyncEvents.length > initialEventCount) { // Wait a bit more for the sync to fully complete - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); return this.receivedSyncEvents[this.receivedSyncEvents.length - 1]; } - + // Also wait for the actual file to appear in shadow repo if we're checking for additions if (expectedChanges && expectedChanges.filePath) { const exists = await this.shadowFileExists(expectedChanges.filePath); if (exists) { // File exists, sync completed - await new Promise(resolve => setTimeout(resolve, 500)); - return { data: { hasChanges: true, summary: 'Sync completed' } }; + await new Promise((resolve) => setTimeout(resolve, 500)); + return { data: { hasChanges: true, summary: "Sync completed" } }; } } - - await new Promise(resolve => setTimeout(resolve, 100)); + + await new Promise((resolve) => setTimeout(resolve, 100)); } - + // If no sync event was received, just wait a bit and return - await new Promise(resolve => setTimeout(resolve, 2000)); - return { data: { hasChanges: true, summary: 'Sync completed (timeout)' } }; + await new Promise((resolve) => setTimeout(resolve, 2000)); + return { data: { hasChanges: true, summary: "Sync completed (timeout)" } }; } async addFile(filePath, content) { const containerPath = `/workspace/${filePath}`; - await execAsync(`docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerPath}) && echo '${content}' > ${containerPath}"`); + await execAsync( + `docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerPath}) && echo '${content}' > ${containerPath}"`, + ); return this.waitForSync({ filePath }); } async modifyFile(filePath, newContent) { const containerPath = `/workspace/${filePath}`; - await execAsync(`docker exec ${this.containerId} bash -c "echo '${newContent}' > ${containerPath}"`); + await execAsync( + `docker exec ${this.containerId} bash -c "echo '${newContent}' > ${containerPath}"`, + ); return this.waitForSync(); } @@ -199,13 +212,17 @@ class SyncTestFramework { async moveFile(fromPath, toPath) { const containerFromPath = `/workspace/${fromPath}`; const containerToPath = `/workspace/${toPath}`; - await execAsync(`docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerToPath}) && mv ${containerFromPath} ${containerToPath}"`); + await execAsync( + `docker exec ${this.containerId} bash -c "mkdir -p $(dirname ${containerToPath}) && mv ${containerFromPath} ${containerToPath}"`, + ); return this.waitForSync(); } async createDirectory(dirPath) { const containerPath = `/workspace/${dirPath}`; - await execAsync(`docker exec ${this.containerId} mkdir -p ${containerPath}`); + await execAsync( + `docker exec ${this.containerId} mkdir -p ${containerPath}`, + ); } async deleteDirectory(dirPath) { @@ -216,7 +233,9 @@ class SyncTestFramework { async getGitStatus() { try { - const { stdout } = await execAsync(`git -C ${this.shadowPath} status --porcelain`); + const { stdout } = await execAsync( + `git -C ${this.shadowPath} status --porcelain`, + ); return stdout.trim(); } catch (error) { throw new Error(`Failed to get git status: ${error.message}`); @@ -226,9 +245,11 @@ class SyncTestFramework { async getShadowFileContent(filePath) { try { const fullPath = path.join(this.shadowPath, filePath); - return await fs.readFile(fullPath, 'utf8'); + return await fs.readFile(fullPath, "utf8"); } catch (error) { - throw new Error(`Failed to read shadow file ${filePath}: ${error.message}`); + throw new Error( + `Failed to read shadow file ${filePath}: ${error.message}`, + ); } } @@ -244,60 +265,80 @@ class SyncTestFramework { async getContainerFileContent(filePath) { try { - const { stdout } = await execAsync(`docker exec ${this.containerId} cat /workspace/${filePath}`); + const { stdout } = await execAsync( + `docker exec ${this.containerId} cat /workspace/${filePath}`, + ); return stdout; } catch (error) { - throw new Error(`Failed to read container file ${filePath}: ${error.message}`); + throw new Error( + `Failed to read container file ${filePath}: ${error.message}`, + ); } } async containerFileExists(filePath) { try { - await execAsync(`docker exec ${this.containerId} test -f /workspace/${filePath}`); + await execAsync( + `docker exec ${this.containerId} test -f /workspace/${filePath}`, + ); return true; } catch (error) { return false; } } - async listContainerFiles(directory = '') { + async listContainerFiles(directory = "") { try { - const containerPath = directory ? `/workspace/${directory}` : '/workspace'; - const { stdout } = await execAsync(`docker exec ${this.containerId} find ${containerPath} -type f -not -path "*/.*" | sed 's|^/workspace/||' | sort`); - return stdout.trim().split('\n').filter(f => f); + const containerPath = directory + ? `/workspace/${directory}` + : "/workspace"; + const { stdout } = await execAsync( + `docker exec ${this.containerId} find ${containerPath} -type f -not -path "*/.*" | sed 's|^/workspace/||' | sort`, + ); + return stdout + .trim() + .split("\n") + .filter((f) => f); } catch (error) { throw new Error(`Failed to list container files: ${error.message}`); } } - async listRepoFiles(directory = '') { + async listRepoFiles(directory = "") { try { - const repoPath = directory ? path.join(this.testRepo, directory) : this.testRepo; - const { stdout } = await execAsync(`find ${repoPath} -type f -not -path "*/.*" | sed 's|^${this.testRepo}/||' | sort`); - return stdout.trim().split('\n').filter(f => f); + const repoPath = directory + ? path.join(this.testRepo, directory) + : this.testRepo; + const { stdout } = await execAsync( + `find ${repoPath} -type f -not -path "*/.*" | sed 's|^${this.testRepo}/||' | sort`, + ); + return stdout + .trim() + .split("\n") + .filter((f) => f); } catch (error) { throw new Error(`Failed to list repo files: ${error.message}`); } } async cleanup() { - console.log('🧹 Cleaning up test environment...'); - + console.log("🧹 Cleaning up test environment..."); + if (this.webSocket) { this.webSocket.close(); } - + if (this.sandboxProcess) { - this.sandboxProcess.kill('SIGTERM'); + this.sandboxProcess.kill("SIGTERM"); } - + // Purge containers try { - await execAsync('npx claude-sandbox purge -y'); + await execAsync("npx claude-sandbox purge -y"); } catch (e) { // Ignore errors } - + // Clean up test repo if (this.testRepo) { try { @@ -306,9 +347,9 @@ class SyncTestFramework { // Ignore errors } } - - console.log('✅ Cleanup complete'); + + console.log("✅ Cleanup complete"); } } -module.exports = { SyncTestFramework }; \ No newline at end of file +module.exports = { SyncTestFramework }; diff --git a/test/e2e/test-suite.js b/test/e2e/test-suite.js index 78c8652..22f9770 100755 --- a/test/e2e/test-suite.js +++ b/test/e2e/test-suite.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const { SyncTestFramework } = require('./sync-test-framework'); +const { SyncTestFramework } = require("./sync-test-framework"); class TestRunner { constructor() { @@ -33,8 +33,8 @@ class TestRunner { } async runAll() { - console.log('🚀 Starting File Sync E2E Tests'); - console.log('='.repeat(50)); + console.log("🚀 Starting File Sync E2E Tests"); + console.log("=".repeat(50)); try { await this.framework.setup(); @@ -42,19 +42,20 @@ class TestRunner { for (const test of this.tests) { await this.runTest(test); } - } finally { await this.framework.cleanup(); } - console.log('\n' + '='.repeat(50)); - console.log(`📊 Test Results: ${this.passed} passed, ${this.failed} failed`); - + console.log("\n" + "=".repeat(50)); + console.log( + `📊 Test Results: ${this.passed} passed, ${this.failed} failed`, + ); + if (this.failed > 0) { - console.log('❌ Some tests failed'); + console.log("❌ Some tests failed"); process.exit(1); } else { - console.log('✅ All tests passed!'); + console.log("✅ All tests passed!"); process.exit(0); } } @@ -64,228 +65,245 @@ class TestRunner { const runner = new TestRunner(); // Test 1: File Addition -runner.addTest('File Addition', async (framework) => { - await framework.addFile('new-file.txt', 'Hello, World!'); - - const exists = await framework.shadowFileExists('new-file.txt'); +runner.addTest("File Addition", async (framework) => { + await framework.addFile("new-file.txt", "Hello, World!"); + + const exists = await framework.shadowFileExists("new-file.txt"); if (!exists) { - throw new Error('File was not synced to shadow repository'); + throw new Error("File was not synced to shadow repository"); } - - const content = await framework.getShadowFileContent('new-file.txt'); - if (content.trim() !== 'Hello, World!') { - throw new Error(`Content mismatch: expected "Hello, World!", got "${content.trim()}"`); + + const content = await framework.getShadowFileContent("new-file.txt"); + if (content.trim() !== "Hello, World!") { + throw new Error( + `Content mismatch: expected "Hello, World!", got "${content.trim()}"`, + ); } - + const gitStatus = await framework.getGitStatus(); - if (!gitStatus.includes('A new-file.txt')) { + if (!gitStatus.includes("A new-file.txt")) { throw new Error(`Git status should show addition: ${gitStatus}`); } }); // Test 2: File Modification -runner.addTest('File Modification', async (framework) => { +runner.addTest("File Modification", async (framework) => { // First, create and commit a file - await framework.addFile('modify-test.txt', 'Original content'); - const { stdout } = await require('util').promisify(require('child_process').exec)( - `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for modification test"` + await framework.addFile("modify-test.txt", "Original content"); + const { stdout } = await require("util").promisify( + require("child_process").exec, + )( + `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for modification test"`, ); - + // Now modify it - await framework.modifyFile('modify-test.txt', 'Modified content'); - - const content = await framework.getShadowFileContent('modify-test.txt'); - if (content.trim() !== 'Modified content') { + await framework.modifyFile("modify-test.txt", "Modified content"); + + const content = await framework.getShadowFileContent("modify-test.txt"); + if (content.trim() !== "Modified content") { throw new Error(`Content not modified: got "${content.trim()}"`); } - + const gitStatus = await framework.getGitStatus(); - if (!gitStatus.includes('M modify-test.txt')) { + if (!gitStatus.includes("M modify-test.txt")) { throw new Error(`Git status should show modification: ${gitStatus}`); } }); // Test 3: File Deletion -runner.addTest('File Deletion', async (framework) => { +runner.addTest("File Deletion", async (framework) => { // Create and commit a file first - await framework.addFile('delete-test.txt', 'To be deleted'); - const { stdout } = await require('util').promisify(require('child_process').exec)( - `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for deletion test"` + await framework.addFile("delete-test.txt", "To be deleted"); + const { stdout } = await require("util").promisify( + require("child_process").exec, + )( + `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for deletion test"`, ); - + // Delete the file - await framework.deleteFile('delete-test.txt'); - - const exists = await framework.shadowFileExists('delete-test.txt'); + await framework.deleteFile("delete-test.txt"); + + const exists = await framework.shadowFileExists("delete-test.txt"); if (exists) { - throw new Error('File still exists in shadow repository after deletion'); + throw new Error("File still exists in shadow repository after deletion"); } - + const gitStatus = await framework.getGitStatus(); - if (!gitStatus.includes('D delete-test.txt')) { + if (!gitStatus.includes("D delete-test.txt")) { throw new Error(`Git status should show deletion: ${gitStatus}`); } }); // Test 4: Multiple File Operations -runner.addTest('Multiple File Operations', async (framework) => { +runner.addTest("Multiple File Operations", async (framework) => { // Add multiple files - await framework.addFile('file1.txt', 'Content 1'); - await framework.addFile('file2.txt', 'Content 2'); - await framework.addFile('file3.txt', 'Content 3'); - + await framework.addFile("file1.txt", "Content 1"); + await framework.addFile("file2.txt", "Content 2"); + await framework.addFile("file3.txt", "Content 3"); + // Wait for sync await framework.waitForSync(); - + // Check that all files exist - const exists1 = await framework.shadowFileExists('file1.txt'); - const exists2 = await framework.shadowFileExists('file2.txt'); - const exists3 = await framework.shadowFileExists('file3.txt'); - + const exists1 = await framework.shadowFileExists("file1.txt"); + const exists2 = await framework.shadowFileExists("file2.txt"); + const exists3 = await framework.shadowFileExists("file3.txt"); + if (!exists1 || !exists2 || !exists3) { - throw new Error('Not all files were synced to shadow repository'); + throw new Error("Not all files were synced to shadow repository"); } - + const gitStatus = await framework.getGitStatus(); - if (!gitStatus.includes('file1.txt') || - !gitStatus.includes('file2.txt') || - !gitStatus.includes('file3.txt')) { + if ( + !gitStatus.includes("file1.txt") || + !gitStatus.includes("file2.txt") || + !gitStatus.includes("file3.txt") + ) { throw new Error(`All files should appear in git status: ${gitStatus}`); } }); // Test 5: Directory Operations -runner.addTest('Directory Operations', async (framework) => { +runner.addTest("Directory Operations", async (framework) => { // Create directory with files - await framework.createDirectory('new-dir'); - await framework.addFile('new-dir/nested-file.txt', 'Nested content'); - - const exists = await framework.shadowFileExists('new-dir/nested-file.txt'); + await framework.createDirectory("new-dir"); + await framework.addFile("new-dir/nested-file.txt", "Nested content"); + + const exists = await framework.shadowFileExists("new-dir/nested-file.txt"); if (!exists) { - throw new Error('Nested file was not synced'); + throw new Error("Nested file was not synced"); } - - const content = await framework.getShadowFileContent('new-dir/nested-file.txt'); - if (content.trim() !== 'Nested content') { + + const content = await framework.getShadowFileContent( + "new-dir/nested-file.txt", + ); + if (content.trim() !== "Nested content") { throw new Error(`Nested file content mismatch: got "${content.trim()}"`); } }); // Test 6: File Rename/Move -runner.addTest('File Rename/Move', async (framework) => { +runner.addTest("File Rename/Move", async (framework) => { // Create and commit a file - await framework.addFile('original-name.txt', 'Content to move'); - const { stdout } = await require('util').promisify(require('child_process').exec)( - `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for move test"` + await framework.addFile("original-name.txt", "Content to move"); + const { stdout } = await require("util").promisify( + require("child_process").exec, + )( + `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for move test"`, ); - + // Move the file - await framework.moveFile('original-name.txt', 'new-name.txt'); - - const originalExists = await framework.shadowFileExists('original-name.txt'); - const newExists = await framework.shadowFileExists('new-name.txt'); - + await framework.moveFile("original-name.txt", "new-name.txt"); + + const originalExists = await framework.shadowFileExists("original-name.txt"); + const newExists = await framework.shadowFileExists("new-name.txt"); + if (originalExists) { - throw new Error('Original file still exists after move'); + throw new Error("Original file still exists after move"); } - + if (!newExists) { - throw new Error('New file does not exist after move'); + throw new Error("New file does not exist after move"); } - - const content = await framework.getShadowFileContent('new-name.txt'); - if (content.trim() !== 'Content to move') { + + const content = await framework.getShadowFileContent("new-name.txt"); + if (content.trim() !== "Content to move") { throw new Error(`Moved file content mismatch: got "${content.trim()}"`); } }); // Test 7: Large File Handling -runner.addTest('Large File Handling', async (framework) => { - const largeContent = 'x'.repeat(10000); // 10KB file +runner.addTest("Large File Handling", async (framework) => { + const largeContent = "x".repeat(10000); // 10KB file // Use printf to avoid newline issues with echo const containerPath = `/workspace/large-file.txt`; - await require('util').promisify(require('child_process').exec)( - `docker exec ${framework.containerId} bash -c "printf '${largeContent}' > ${containerPath}"` + await require("util").promisify(require("child_process").exec)( + `docker exec ${framework.containerId} bash -c "printf '${largeContent}' > ${containerPath}"`, ); await framework.waitForSync(); - - const exists = await framework.shadowFileExists('large-file.txt'); + + const exists = await framework.shadowFileExists("large-file.txt"); if (!exists) { - throw new Error('Large file was not synced'); + throw new Error("Large file was not synced"); } - - const content = await framework.getShadowFileContent('large-file.txt'); + + const content = await framework.getShadowFileContent("large-file.txt"); if (content.length !== largeContent.length) { - throw new Error(`Large file size mismatch: expected ${largeContent.length}, got ${content.length}`); + throw new Error( + `Large file size mismatch: expected ${largeContent.length}, got ${content.length}`, + ); } }); // Test 8: Special Characters in Filenames -runner.addTest('Special Characters in Filenames', async (framework) => { - const specialFile = 'special-chars-test_file.txt'; - await framework.addFile(specialFile, 'Special content'); - +runner.addTest("Special Characters in Filenames", async (framework) => { + const specialFile = "special-chars-test_file.txt"; + await framework.addFile(specialFile, "Special content"); + const exists = await framework.shadowFileExists(specialFile); if (!exists) { - throw new Error('File with special characters was not synced'); + throw new Error("File with special characters was not synced"); } - + const content = await framework.getShadowFileContent(specialFile); - if (content.trim() !== 'Special content') { + if (content.trim() !== "Special content") { throw new Error(`Special file content mismatch: got "${content.trim()}"`); } }); // Test 9: Rapid File Changes -runner.addTest('Rapid File Changes', async (framework) => { +runner.addTest("Rapid File Changes", async (framework) => { // Create file - await framework.addFile('rapid-test.txt', 'Version 1'); - + await framework.addFile("rapid-test.txt", "Version 1"); + // Wait for initial sync await framework.waitForSync(); - + // Quickly modify it multiple times - await framework.modifyFile('rapid-test.txt', 'Version 2'); - await new Promise(resolve => setTimeout(resolve, 200)); - await framework.modifyFile('rapid-test.txt', 'Version 3'); - await new Promise(resolve => setTimeout(resolve, 200)); - await framework.modifyFile('rapid-test.txt', 'Final Version'); - + await framework.modifyFile("rapid-test.txt", "Version 2"); + await new Promise((resolve) => setTimeout(resolve, 200)); + await framework.modifyFile("rapid-test.txt", "Version 3"); + await new Promise((resolve) => setTimeout(resolve, 200)); + await framework.modifyFile("rapid-test.txt", "Final Version"); + // Wait for final sync await framework.waitForSync(null, 15000); - - const content = await framework.getShadowFileContent('rapid-test.txt'); - if (content.trim() !== 'Final Version') { + + const content = await framework.getShadowFileContent("rapid-test.txt"); + if (content.trim() !== "Final Version") { throw new Error(`Final content mismatch: got "${content.trim()}"`); } }); // Test 10: Web UI Sync Notifications -runner.addTest('Web UI Sync Notifications', async (framework) => { +runner.addTest("Web UI Sync Notifications", async (framework) => { const initialEventCount = framework.receivedSyncEvents.length; - - await framework.addFile('notification-test.txt', 'Test notification'); - + + await framework.addFile("notification-test.txt", "Test notification"); + // Check that we received sync events await framework.waitForSync(); - + const newEventCount = framework.receivedSyncEvents.length; if (newEventCount <= initialEventCount) { - throw new Error('No sync events received by web UI'); + throw new Error("No sync events received by web UI"); } - - const latestEvent = framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; + + const latestEvent = + framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1]; if (!latestEvent.data.hasChanges) { - throw new Error('Sync event should indicate changes'); + throw new Error("Sync event should indicate changes"); } - - if (!latestEvent.data.summary.includes('Added:')) { - throw new Error(`Sync event should show addition: ${latestEvent.data.summary}`); + + if (!latestEvent.data.summary.includes("Added:")) { + throw new Error( + `Sync event should show addition: ${latestEvent.data.summary}`, + ); } }); // Run all tests runner.runAll().catch((error) => { - console.error('❌ Test runner failed:', error); + console.error("❌ Test runner failed:", error); process.exit(1); -}); \ No newline at end of file +});