Exclude .DS_Store files, run prettier

This commit is contained in:
Onur Solmaz 2025-06-04 21:16:09 +02:00
parent 56c120e7a8
commit f815760d73
18 changed files with 700 additions and 545 deletions

View file

@ -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']
};
ignorePatterns: ["dist/", "node_modules/", "coverage/", "*.js"],
};

View file

@ -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
- [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly

View file

@ -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;

View file

@ -1,39 +1,36 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/test'],
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/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: '<rootDir>/coverage',
coverageReporters: ['text', 'lcov', 'html'],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/'
"src/**/*.{ts,tsx}",
"!src/**/*.d.ts",
"!src/**/*.test.{ts,tsx}",
"!src/**/*.spec.{ts,tsx}",
],
coverageDirectory: "<rootDir>/coverage",
coverageReporters: ["text", "lcov", "html"],
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
"^@/(.*)$": "<rootDir>/src/$1",
},
globals: {
'ts-jest': {
"ts-jest": {
tsconfig: {
esModuleInterop: true,
allowJs: true
}
}
}
};
allowJs: true,
},
},
},
};

View file

@ -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);

View file

@ -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} \

View file

@ -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`
Run E2E tests with: `npm run test:e2e`

View file

@ -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.
The tests provide proper exit codes (0 for success, 1 for failure) and detailed logging for debugging purposes.

View file

@ -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);
});
runCoreTests()
.then((success) => {
process.exit(success ? 0 : 1);
})
.catch((error) => {
console.error("❌ Test runner failed:", error);
process.exit(1);
});

View file

@ -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
- `test/test.js` - Test file

View file

@ -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"
}
}

View file

@ -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 };
module.exports = { main };

View file

@ -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
};
divide,
};

View file

@ -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();
runTests();

View file

@ -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);
});
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);
});

View file

@ -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);
});
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);
});

View file

@ -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 };
module.exports = { SyncTestFramework };

View file

@ -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);
});
});