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 = { module.exports = {
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
extends: [ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
parserOptions: { parserOptions: {
ecmaVersion: 2022, ecmaVersion: 2022,
sourceType: 'module', sourceType: "module",
project: './tsconfig.json' project: "./tsconfig.json",
}, },
env: { env: {
node: true, node: true,
es2022: true, es2022: true,
jest: true jest: true,
}, },
plugins: ['@typescript-eslint'], plugins: ["@typescript-eslint"],
rules: { rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off', "@typescript-eslint/explicit-module-boundary-types": "off",
'@typescript-eslint/no-explicit-any': 'warn', "@typescript-eslint/no-explicit-any": "warn",
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
'no-console': 'off', "no-console": "off",
'semi': ['error', 'always'], semi: ["error", "always"],
'quotes': ['error', 'single'], quotes: ["error", "single"],
'comma-dangle': ['error', 'never'] "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. - 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. - 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. - 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)** 2. **Shadow Repository Setup (Optimized)**
```typescript ```typescript
class ShadowRepository { class ShadowRepository {
private shadowPath: string; private shadowPath: string;

View file

@ -1,39 +1,36 @@
module.exports = { module.exports = {
preset: 'ts-jest', preset: "ts-jest",
testEnvironment: 'node', testEnvironment: "node",
roots: ['<rootDir>/test'], roots: ["<rootDir>/test"],
testMatch: [ testMatch: [
'**/test/**/*.test.ts', "**/test/**/*.test.ts",
'**/test/**/*.test.js', "**/test/**/*.test.js",
'**/test/**/*.spec.ts', "**/test/**/*.spec.ts",
'**/test/**/*.spec.js' "**/test/**/*.spec.js",
], ],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transform: { transform: {
'^.+\\.tsx?$': 'ts-jest', "^.+\\.tsx?$": "ts-jest",
'^.+\\.jsx?$': 'babel-jest' "^.+\\.jsx?$": "babel-jest",
}, },
collectCoverageFrom: [ collectCoverageFrom: [
'src/**/*.{ts,tsx}', "src/**/*.{ts,tsx}",
'!src/**/*.d.ts', "!src/**/*.d.ts",
'!src/**/*.test.{ts,tsx}', "!src/**/*.test.{ts,tsx}",
'!src/**/*.spec.{ts,tsx}' "!src/**/*.spec.{ts,tsx}",
],
coverageDirectory: '<rootDir>/coverage',
coverageReporters: ['text', 'lcov', 'html'],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/'
], ],
coverageDirectory: "<rootDir>/coverage",
coverageReporters: ["text", "lcov", "html"],
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1' "^@/(.*)$": "<rootDir>/src/$1",
}, },
globals: { globals: {
'ts-jest': { "ts-jest": {
tsconfig: { tsconfig: {
esModuleInterop: true, 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 // Also copy .git directory to preserve git history
console.log(chalk.blue("• Copying git history...")); console.log(chalk.blue("• Copying git history..."));
const gitTarFile = `/tmp/claude-sandbox-git-${Date.now()}.tar`; const gitTarFile = `/tmp/claude-sandbox-git-${Date.now()}.tar`;
// Exclude macOS resource fork files when creating git archive // Exclude macOS resource fork files and .DS_Store when creating git archive
execSync(`tar -cf "${gitTarFile}" --exclude="._*" .git`, { execSync(
cwd: workDir, `tar -cf "${gitTarFile}" --exclude="._*" --exclude=".DS_Store" .git`,
stdio: "pipe", {
}); cwd: workDir,
stdio: "pipe",
},
);
try { try {
const gitStream = fs.createReadStream(gitTarFile); const gitStream = fs.createReadStream(gitTarFile);

View file

@ -166,9 +166,12 @@ export class ShadowRepository {
try { try {
await execAsync("git add .", { cwd: this.shadowPath }); await execAsync("git add .", { cwd: this.shadowPath });
console.log(chalk.gray(" Staged all files for tracking")); console.log(chalk.gray(" Staged all files for tracking"));
// Create initial commit to ensure deletions can be tracked // 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")); console.log(chalk.gray(" Created initial commit for change tracking"));
} catch (stageError: any) { } catch (stageError: any) {
console.log(chalk.gray(" Could not stage files:", stageError.message)); 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 // Rsync directly from container to shadow repo with proper deletion handling
// First, clear the shadow repo (except .git) to ensure deletions are reflected // 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 // Rsync within container to staging area using exclude file
const rsyncCmd = `docker exec ${containerId} rsync -av --delete \ const rsyncCmd = `docker exec ${containerId} rsync -av --delete \
--exclude-from=${containerExcludeFile} \ --exclude-from=${containerExcludeFile} \

View file

@ -48,12 +48,12 @@ Tests are written using Jest with TypeScript support. The Jest configuration is
### Example Unit Test ### Example Unit Test
```typescript ```typescript
import { someFunction } from '../../src/someModule'; import { someFunction } from "../../src/someModule";
describe('someFunction', () => { describe("someFunction", () => {
it('should do something', () => { it("should do something", () => {
const result = someFunction('input'); const result = someFunction("input");
expect(result).toBe('expected output'); expect(result).toBe("expected output");
}); });
}); });
``` ```
@ -61,9 +61,10 @@ describe('someFunction', () => {
## E2E Tests ## E2E Tests
End-to-end tests are located in `test/e2e/` and test the complete workflow of the CLI tool. These 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 - Create actual Docker containers
- Run Claude commands - Run Claude commands
- Verify git operations - Verify git operations
- Test the full user experience - 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 ## Running Tests
### Quick Test (Recommended) ### Quick Test (Recommended)
```bash ```bash
# Run core functionality tests # Run core functionality tests
node core-functionality-test.js node core-functionality-test.js
``` ```
### Individual Tests ### Individual Tests
```bash ```bash
# Test repository to container sync # Test repository to container sync
node repo-to-container-sync-test.js node repo-to-container-sync-test.js
@ -53,6 +55,7 @@ node test-suite.js
``` ```
### Automated Test Runner ### Automated Test Runner
```bash ```bash
# Run all tests with cleanup # Run all tests with cleanup
./run-tests.sh ./run-tests.sh
@ -67,11 +70,13 @@ node test-suite.js
## Test Process ## Test Process
1. **Setup Phase** 1. **Setup Phase**
- Creates temporary git repository with dummy files - Creates temporary git repository with dummy files
- Starts claude-sandbox instance - Starts claude-sandbox instance
- Connects to web UI for monitoring sync events - Connects to web UI for monitoring sync events
2. **Test Execution** 2. **Test Execution**
- Performs file operations inside the container - Performs file operations inside the container
- Waits for synchronization to complete - Waits for synchronization to complete
- Verifies shadow repository state - Verifies shadow repository state
@ -84,6 +89,7 @@ node test-suite.js
## Key Features Tested ## Key Features Tested
### Repository to Container Sync ### Repository to Container Sync
- ✅ One-to-one file mapping from test repo to container - ✅ One-to-one file mapping from test repo to container
- ✅ No extra files in container (only test repo files) - ✅ No extra files in container (only test repo files)
- ✅ File content integrity verification - ✅ File content integrity verification
@ -91,6 +97,7 @@ node test-suite.js
- ✅ Correct branch creation - ✅ Correct branch creation
### File Synchronization ### File Synchronization
- ✅ New file creation and content sync - ✅ New file creation and content sync
- ✅ File modification and content updates - ✅ File modification and content updates
- ✅ File deletion and proper removal - ✅ File deletion and proper removal
@ -99,12 +106,14 @@ node test-suite.js
- ✅ Special characters in filenames - ✅ Special characters in filenames
### Git Integration ### Git Integration
- ✅ Staging of additions (`A` status) - ✅ Staging of additions (`A` status)
- ✅ Tracking of modifications (`M` status) - ✅ Tracking of modifications (`M` status)
- ✅ Detection of deletions (`D` status) - ✅ Detection of deletions (`D` status)
- ✅ Proper git commit workflow - ✅ Proper git commit workflow
### Web UI Integration ### Web UI Integration
- ✅ Real-time sync event notifications - ✅ Real-time sync event notifications
- ✅ Change summary reporting - ✅ Change summary reporting
- ✅ WebSocket communication - ✅ WebSocket communication
@ -114,25 +123,30 @@ node test-suite.js
### Common Issues ### Common Issues
**Container startup timeout** **Container startup timeout**
- Increase timeout values in test framework - Increase timeout values in test framework
- Check Docker daemon is running - Check Docker daemon is running
- Verify claude-sandbox image exists - Verify claude-sandbox image exists
**Git lock conflicts** **Git lock conflicts**
- Tests automatically handle concurrent git operations - Tests automatically handle concurrent git operations
- Temporary `.git/index.lock` files are cleaned up - Temporary `.git/index.lock` files are cleaned up
**Port conflicts** **Port conflicts**
- Tests use dynamic port allocation - Tests use dynamic port allocation
- Multiple tests can run sequentially - Multiple tests can run sequentially
**WebSocket connection issues** **WebSocket connection issues**
- Framework includes connection retry logic - Framework includes connection retry logic
- Fallback to polling if WebSocket fails - Fallback to polling if WebSocket fails
### Test Failure Analysis ### Test Failure Analysis
Tests provide detailed error messages indicating: Tests provide detailed error messages indicating:
- Which specific operation failed - Which specific operation failed
- Expected vs actual file states - Expected vs actual file states
- Git status differences - Git status differences
@ -145,33 +159,36 @@ Tests provide detailed error messages indicating:
1. Create test function in appropriate test file 1. Create test function in appropriate test file
2. Use framework methods for file operations: 2. Use framework methods for file operations:
```javascript ```javascript
await framework.addFile('path/file.txt', 'content'); await framework.addFile("path/file.txt", "content");
await framework.modifyFile('path/file.txt', 'new content'); await framework.modifyFile("path/file.txt", "new content");
await framework.deleteFile('path/file.txt'); await framework.deleteFile("path/file.txt");
``` ```
3. Verify results using assertion methods: 3. Verify results using assertion methods:
```javascript ```javascript
const exists = await framework.shadowFileExists('file.txt'); const exists = await framework.shadowFileExists("file.txt");
const content = await framework.getShadowFileContent('file.txt'); const content = await framework.getShadowFileContent("file.txt");
const gitStatus = await framework.getGitStatus(); const gitStatus = await framework.getGitStatus();
``` ```
### Framework API ### Framework API
**File Operations** **File Operations**
- `addFile(path, content)` - Create new file - `addFile(path, content)` - Create new file
- `modifyFile(path, content)` - Update existing file - `modifyFile(path, content)` - Update existing file
- `deleteFile(path)` - Remove file - `deleteFile(path)` - Remove file
- `moveFile(from, to)` - Rename/move file - `moveFile(from, to)` - Rename/move file
- `createDirectory(path)` - Create directory - `createDirectory(path)` - Create directory
**Verification Methods** **Verification Methods**
- `shadowFileExists(path)` - Check file existence - `shadowFileExists(path)` - Check file existence
- `getShadowFileContent(path)` - Read file content - `getShadowFileContent(path)` - Read file content
- `getGitStatus()` - Get git status output - `getGitStatus()` - Get git status output
- `waitForSync()` - Wait for synchronization - `waitForSync()` - Wait for synchronization
**Event Monitoring** **Event Monitoring**
- `receivedSyncEvents` - Array of sync notifications - `receivedSyncEvents` - Array of sync notifications
- WebSocket connection automatically established - WebSocket connection automatically established
@ -188,4 +205,4 @@ These tests are designed to run in automated environments:
./run-tests.sh ./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 #!/usr/bin/env node
const { SyncTestFramework } = require('./sync-test-framework'); const { SyncTestFramework } = require("./sync-test-framework");
async function runCoreTests() { async function runCoreTests() {
console.log('🚀 Core File Sync Functionality Tests'); console.log("🚀 Core File Sync Functionality Tests");
console.log('====================================='); console.log("=====================================");
const framework = new SyncTestFramework(); const framework = new SyncTestFramework();
const tests = []; const tests = [];
try { try {
await framework.setup(); await framework.setup();
// Test 0: Initial Repository Sync // 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 // Verify that repository files are properly synced to container
const repoFiles = await framework.listRepoFiles(); const repoFiles = await framework.listRepoFiles();
const containerFiles = await framework.listContainerFiles(); const containerFiles = await framework.listContainerFiles();
// Check that key files exist in container // 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) { for (const file of expectedFiles) {
const exists = await framework.containerFileExists(file); const exists = await framework.containerFileExists(file);
if (!exists) { if (!exists) {
throw new Error(`Expected file ${file} not found in container`); throw new Error(`Expected file ${file} not found in container`);
} }
// Verify content matches // Verify content matches
const repoContent = await require('fs').promises.readFile( const repoContent = await require("fs").promises.readFile(
require('path').join(framework.testRepo, file), 'utf8' require("path").join(framework.testRepo, file),
"utf8",
); );
const containerContent = await framework.getContainerFileContent(file); const containerContent = await framework.getContainerFileContent(file);
if (repoContent.trim() !== containerContent.trim()) { if (repoContent.trim() !== containerContent.trim()) {
throw new Error(`Content mismatch for ${file}`); throw new Error(`Content mismatch for ${file}`);
} }
} }
console.log('✅ Initial repository sync test passed'); console.log("✅ Initial repository sync test passed");
tests.push({ name: 'Initial Repository Sync', passed: true }); tests.push({ name: "Initial Repository Sync", passed: true });
// Test 1: File Addition // Test 1: File Addition
console.log('\n🧪 Test 1: File Addition'); console.log("\n🧪 Test 1: File Addition");
await framework.addFile('addition-test.txt', 'New file content'); await framework.addFile("addition-test.txt", "New file content");
const addExists = await framework.shadowFileExists('addition-test.txt'); const addExists = await framework.shadowFileExists("addition-test.txt");
if (!addExists) throw new Error('File addition failed'); if (!addExists) throw new Error("File addition failed");
const addContent = await framework.getShadowFileContent('addition-test.txt'); const addContent =
if (addContent.trim() !== 'New file content') throw new Error('File content mismatch'); await framework.getShadowFileContent("addition-test.txt");
if (addContent.trim() !== "New file content")
throw new Error("File content mismatch");
const addStatus = await framework.getGitStatus(); const addStatus = await framework.getGitStatus();
if (!addStatus.includes('addition-test.txt')) throw new Error('Git should track new file'); 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 }); console.log("✅ File addition test passed");
tests.push({ name: "File Addition", passed: true });
// Test 2: File Modification // 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 // Commit first so we can see modifications
await require('util').promisify(require('child_process').exec)( await require("util").promisify(require("child_process").exec)(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add files for testing"` `git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add files for testing"`,
); );
await framework.modifyFile('addition-test.txt', 'Modified content'); await framework.modifyFile("addition-test.txt", "Modified content");
const modContent = await framework.getShadowFileContent('addition-test.txt'); const modContent =
if (modContent.trim() !== 'Modified content') throw new Error('File modification failed'); await framework.getShadowFileContent("addition-test.txt");
if (modContent.trim() !== "Modified content")
throw new Error("File modification failed");
const modStatus = await framework.getGitStatus(); const modStatus = await framework.getGitStatus();
if (!modStatus.includes('M addition-test.txt')) throw new Error('Git should track modification'); 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 }); console.log("✅ File modification test passed");
tests.push({ name: "File Modification", passed: true });
// Test 3: File Deletion // Test 3: File Deletion
console.log('\n🧪 Test 3: File Deletion'); console.log("\n🧪 Test 3: File Deletion");
await framework.deleteFile('addition-test.txt'); await framework.deleteFile("addition-test.txt");
const delExists = await framework.shadowFileExists('addition-test.txt'); const delExists = await framework.shadowFileExists("addition-test.txt");
if (delExists) throw new Error('File deletion failed - file still exists'); if (delExists) throw new Error("File deletion failed - file still exists");
const delStatus = await framework.getGitStatus(); const delStatus = await framework.getGitStatus();
if (!delStatus.includes('D addition-test.txt')) throw new Error('Git should track deletion'); 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 }); console.log("✅ File deletion test passed");
tests.push({ name: "File Deletion", passed: true });
// Test 4: Directory Operations // Test 4: Directory Operations
console.log('\n🧪 Test 4: Directory Operations'); console.log("\n🧪 Test 4: Directory Operations");
await framework.createDirectory('test-dir'); await framework.createDirectory("test-dir");
await framework.addFile('test-dir/nested-file.txt', 'Nested content'); await framework.addFile("test-dir/nested-file.txt", "Nested content");
const nestedExists = await framework.shadowFileExists('test-dir/nested-file.txt'); const nestedExists = await framework.shadowFileExists(
if (!nestedExists) throw new Error('Nested file creation failed'); "test-dir/nested-file.txt",
);
const nestedContent = await framework.getShadowFileContent('test-dir/nested-file.txt'); if (!nestedExists) throw new Error("Nested file creation failed");
if (nestedContent.trim() !== 'Nested content') throw new Error('Nested file content mismatch');
const nestedContent = await framework.getShadowFileContent(
console.log('✅ Directory operations test passed'); "test-dir/nested-file.txt",
tests.push({ name: 'Directory Operations', passed: true }); );
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 // 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; const initialEventCount = framework.receivedSyncEvents.length;
await framework.addFile('notification-test.txt', 'Notification content'); await framework.addFile("notification-test.txt", "Notification content");
await framework.waitForSync(); await framework.waitForSync();
const finalEventCount = framework.receivedSyncEvents.length; const finalEventCount = framework.receivedSyncEvents.length;
if (finalEventCount <= initialEventCount) throw new Error('No sync events received'); 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'); const latestEvent =
framework.receivedSyncEvents[framework.receivedSyncEvents.length - 1];
console.log('✅ Web UI notifications test passed'); if (!latestEvent.data.hasChanges)
tests.push({ name: 'Web UI Notifications', passed: true }); 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) { } catch (error) {
console.log(`❌ Test failed: ${error.message}`); 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 { } finally {
await framework.cleanup(); await framework.cleanup();
} }
// Results // Results
console.log('\n' + '='.repeat(50)); console.log("\n" + "=".repeat(50));
console.log('📊 Test Results:'); console.log("📊 Test Results:");
const passed = tests.filter(t => t.passed).length; const passed = tests.filter((t) => t.passed).length;
const failed = tests.filter(t => !t.passed).length; const failed = tests.filter((t) => !t.passed).length;
tests.forEach(test => { tests.forEach((test) => {
const icon = test.passed ? '✅' : '❌'; const icon = test.passed ? "✅" : "❌";
console.log(`${icon} ${test.name}`); console.log(`${icon} ${test.name}`);
if (!test.passed && test.error) { if (!test.passed && test.error) {
console.log(` ${test.error}`); console.log(` ${test.error}`);
} }
}); });
console.log(`\n📈 Summary: ${passed} passed, ${failed} failed`); console.log(`\n📈 Summary: ${passed} passed, ${failed} failed`);
if (failed === 0) { if (failed === 0) {
console.log('🎉 All core functionality tests passed!'); console.log("🎉 All core functionality tests passed!");
return true; return true;
} else { } else {
console.log('❌ Some tests failed'); console.log("❌ Some tests failed");
return false; return false;
} }
} }
runCoreTests().then((success) => { runCoreTests()
process.exit(success ? 0 : 1); .then((success) => {
}).catch((error) => { process.exit(success ? 0 : 1);
console.error('❌ Test runner failed:', error); })
process.exit(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/main.js` - Main application file
- `src/utils.js` - Utility functions - `src/utils.js` - Utility functions
- `package.json` - Package configuration - `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", "test": "node test/test.js",
"start": "node src/main.js" "start": "node src/main.js"
}, },
"keywords": ["test", "claude", "sandbox"], "keywords": [
"test",
"claude",
"sandbox"
],
"author": "Test", "author": "Test",
"license": "MIT" "license": "MIT"
} }

View file

@ -1,14 +1,14 @@
const utils = require('./utils'); const utils = require("./utils");
function main() { function main() {
console.log('Starting test application...'); console.log("Starting test application...");
const result = utils.calculate(10, 5); const result = utils.calculate(10, 5);
console.log('Calculation result:', result); console.log("Calculation result:", result);
console.log('Application finished.'); console.log("Application finished.");
} }
if (require.main === module) { if (require.main === module) {
main(); main();
} }
module.exports = { main }; module.exports = { main };

View file

@ -8,7 +8,7 @@ function multiply(a, b) {
function divide(a, b) { function divide(a, b) {
if (b === 0) { if (b === 0) {
throw new Error('Division by zero'); throw new Error("Division by zero");
} }
return a / b; return a / b;
} }
@ -16,5 +16,5 @@ function divide(a, b) {
module.exports = { module.exports = {
calculate, calculate,
multiply, multiply,
divide divide,
}; };

View file

@ -1,24 +1,24 @@
const utils = require('../src/utils'); const utils = require("../src/utils");
function runTests() { function runTests() {
console.log('Running tests...'); console.log("Running tests...");
// Test calculate function // Test calculate function
const result1 = utils.calculate(2, 3); const result1 = utils.calculate(2, 3);
console.assert(result1 === 5, 'Calculate test failed'); console.assert(result1 === 5, "Calculate test failed");
console.log('✓ Calculate test passed'); console.log("✓ Calculate test passed");
// Test multiply function // Test multiply function
const result2 = utils.multiply(4, 3); const result2 = utils.multiply(4, 3);
console.assert(result2 === 12, 'Multiply test failed'); console.assert(result2 === 12, "Multiply test failed");
console.log('✓ Multiply test passed'); console.log("✓ Multiply test passed");
// Test divide function // Test divide function
const result3 = utils.divide(10, 2); const result3 = utils.divide(10, 2);
console.assert(result3 === 5, 'Divide test failed'); console.assert(result3 === 5, "Divide test failed");
console.log('✓ Divide test passed'); console.log("✓ Divide test passed");
console.log('All tests passed!'); console.log("All tests passed!");
} }
runTests(); runTests();

View file

@ -1,31 +1,41 @@
#!/usr/bin/env node #!/usr/bin/env node
const { SyncTestFramework } = require('./sync-test-framework'); const { SyncTestFramework } = require("./sync-test-framework");
async function testRepoToContainerSync() { async function testRepoToContainerSync() {
console.log('🧪 Testing Repository to Container Sync'); console.log("🧪 Testing Repository to Container Sync");
console.log('======================================'); console.log("======================================");
const framework = new SyncTestFramework(); const framework = new SyncTestFramework();
try { try {
await framework.setup(); 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 // Get list of files in the original test repo
const repoFiles = await framework.listRepoFiles(); 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 // Get list of files in the container
const containerFiles = await framework.listContainerFiles(); 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 // CRITICAL: Check for exact one-to-one match
if (containerFiles.length !== repoFiles.length) { 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 // Check that all repo files exist in container
const missingInContainer = []; const missingInContainer = [];
for (const file of repoFiles) { for (const file of repoFiles) {
@ -34,11 +44,13 @@ async function testRepoToContainerSync() {
missingInContainer.push(file); missingInContainer.push(file);
} }
} }
if (missingInContainer.length > 0) { 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 // Check for extra files in container that aren't in repo
const extraInContainer = []; const extraInContainer = [];
for (const file of containerFiles) { for (const file of containerFiles) {
@ -46,134 +58,161 @@ async function testRepoToContainerSync() {
extraInContainer.push(file); extraInContainer.push(file);
} }
} }
if (extraInContainer.length > 0) { 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(
"✅ 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("\n🔍 Step 2: Verifying file content integrity...");
// Check content of key files // 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 = []; const contentMismatches = [];
for (const file of testFiles) { for (const file of testFiles) {
if (repoFiles.includes(file)) { if (repoFiles.includes(file)) {
try { try {
// Read from original repo // Read from original repo
const repoContent = await require('fs').promises.readFile( const repoContent = await require("fs").promises.readFile(
require('path').join(framework.testRepo, file), 'utf8' require("path").join(framework.testRepo, file),
"utf8",
); );
// Read from container // Read from container
const containerContent = await framework.getContainerFileContent(file); const containerContent =
await framework.getContainerFileContent(file);
if (repoContent.trim() !== containerContent.trim()) { if (repoContent.trim() !== containerContent.trim()) {
contentMismatches.push({ contentMismatches.push({
file, file,
repoLength: repoContent.length, repoLength: repoContent.length,
containerLength: containerContent.length containerLength: containerContent.length,
}); });
} }
} catch (error) { } catch (error) {
contentMismatches.push({ contentMismatches.push({
file, file,
error: error.message error: error.message,
}); });
} }
} }
} }
if (contentMismatches.length > 0) { if (contentMismatches.length > 0) {
console.log('Content mismatches found:'); console.log("Content mismatches found:");
contentMismatches.forEach(mismatch => { contentMismatches.forEach((mismatch) => {
if (mismatch.error) { if (mismatch.error) {
console.log(` ${mismatch.file}: ${mismatch.error}`); console.log(` ${mismatch.file}: ${mismatch.error}`);
} else { } 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("✅ File content integrity verified");
console.log('\n🔍 Step 3: Verifying no extra files from outside test repo...'); 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 // List of common files that might accidentally be included but shouldn't be
const shouldNotExist = [ const shouldNotExist = [
'node_modules', "node_modules",
'.env', ".env",
'dist', "dist",
'build', "build",
'.vscode', ".vscode",
'.idea', ".idea",
'coverage' "coverage",
]; ];
for (const file of shouldNotExist) { for (const file of shouldNotExist) {
const exists = await framework.containerFileExists(file); const exists = await framework.containerFileExists(file);
if (exists) { 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(
"✅ 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("\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(); const currentBranch = branchOutput.trim();
if (!currentBranch.startsWith('claude/')) { if (!currentBranch.startsWith("claude/")) {
throw new Error(`Expected to be on a claude/ branch, but on: ${currentBranch}`); throw new Error(
`Expected to be on a claude/ branch, but on: ${currentBranch}`,
);
} }
console.log(`✅ Working on correct branch: ${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 // 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(); await framework.waitForSync();
const createdExists = await framework.containerFileExists('test-creation.txt'); const createdExists =
await framework.containerFileExists("test-creation.txt");
if (!createdExists) { 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 // 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) { 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("✅ File operations working correctly");
console.log('\n🎉 SUCCESS: Repository to container sync is working perfectly!'); 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("\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(` ✅ One-to-one sync verified - no extra files`);
console.log(` ✅ All files exist with correct content`); console.log(` ✅ All files exist with correct content`);
console.log(` 🌿 Git repository properly initialized`); console.log(` 🌿 Git repository properly initialized`);
console.log(` 🔄 Bidirectional sync operational`); console.log(` 🔄 Bidirectional sync operational`);
} catch (error) { } catch (error) {
console.log(`\n❌ FAILED: ${error.message}`); console.log(`\n❌ FAILED: ${error.message}`);
throw error; throw error;
@ -182,10 +221,17 @@ async function testRepoToContainerSync() {
} }
} }
testRepoToContainerSync().then(() => { testRepoToContainerSync()
console.log('\n✅ Repository to container sync test completed successfully'); .then(() => {
process.exit(0); console.log(
}).catch((error) => { "\n✅ Repository to container sync test completed successfully",
console.error('\n❌ Repository to container sync test failed:', error.message); );
process.exit(1); 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 #!/usr/bin/env node
const { SyncTestFramework } = require('./sync-test-framework'); const { SyncTestFramework } = require("./sync-test-framework");
async function testDeletionFunctionality() { async function testDeletionFunctionality() {
console.log('🧪 Testing File Deletion Functionality'); console.log("🧪 Testing File Deletion Functionality");
console.log('====================================='); console.log("=====================================");
const framework = new SyncTestFramework(); const framework = new SyncTestFramework();
try { try {
// Setup // Setup
await framework.setup(); await framework.setup();
console.log('\n📝 Step 1: Adding test files...'); console.log("\n📝 Step 1: Adding test files...");
await framework.addFile('test1.txt', 'Content 1'); await framework.addFile("test1.txt", "Content 1");
await framework.addFile('test2.txt', 'Content 2'); await framework.addFile("test2.txt", "Content 2");
await framework.addFile('test3.txt', 'Content 3'); await framework.addFile("test3.txt", "Content 3");
await framework.waitForSync(); await framework.waitForSync();
// Commit the files so deletions can be tracked // Commit the files so deletions can be tracked
const { stdout } = await require('util').promisify(require('child_process').exec)( const { stdout } = await require("util").promisify(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add test files"` 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("✅ Files committed to git");
console.log('\n🗑 Step 2: Deleting one file...'); console.log("\n🗑 Step 2: Deleting one file...");
await framework.deleteFile('test2.txt'); await framework.deleteFile("test2.txt");
await framework.waitForSync(); 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 // 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) { 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 // Check git status shows deletion
const gitStatus = await framework.getGitStatus(); const gitStatus = await framework.getGitStatus();
console.log('Git status:', gitStatus); console.log("Git status:", gitStatus);
if (!gitStatus.includes('D test2.txt')) { if (!gitStatus.includes("D test2.txt")) {
throw new Error(`❌ Git status should show deletion: ${gitStatus}`); 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 // Check other files still exist
const exists1 = await framework.shadowFileExists('test1.txt'); const exists1 = await framework.shadowFileExists("test1.txt");
const exists3 = await framework.shadowFileExists('test3.txt'); const exists3 = await framework.shadowFileExists("test3.txt");
if (!exists1 || !exists3) { 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("✅ Other files preserved");
console.log('\n🎉 SUCCESS: File deletion tracking is working correctly!'); console.log("\n🎉 SUCCESS: File deletion tracking is working correctly!");
} catch (error) { } catch (error) {
console.log(`\n❌ FAILED: ${error.message}`); console.log(`\n❌ FAILED: ${error.message}`);
throw error; throw error;
@ -65,10 +66,12 @@ async function testDeletionFunctionality() {
} }
} }
testDeletionFunctionality().then(() => { testDeletionFunctionality()
console.log('\n✅ Deletion test completed successfully'); .then(() => {
process.exit(0); console.log("\n✅ Deletion test completed successfully");
}).catch((error) => { process.exit(0);
console.error('\n❌ Deletion test failed:', error.message); })
process.exit(1); .catch((error) => {
}); console.error("\n❌ Deletion test failed:", error.message);
process.exit(1);
});

View file

@ -1,11 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
const { spawn, exec } = require('child_process'); const { spawn, exec } = require("child_process");
const fs = require('fs').promises; const fs = require("fs").promises;
const path = require('path'); const path = require("path");
const http = require('http'); const http = require("http");
const WebSocket = require('ws'); const WebSocket = require("ws");
const util = require('util'); const util = require("util");
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
class SyncTestFramework { class SyncTestFramework {
@ -21,49 +21,51 @@ class SyncTestFramework {
} }
async setup() { async setup() {
console.log('🔧 Setting up test environment...'); console.log("🔧 Setting up test environment...");
// Create temporary test repository // 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 }); await fs.mkdir(this.testRepo, { recursive: true });
// Copy dummy repo files to test repo // 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}/`); await execAsync(`cp -r ${dummyRepo}/* ${this.testRepo}/`);
// Initialize as git repository // 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}`); console.log(`📁 Test repo created at: ${this.testRepo}`);
// Start claude-sandbox from the test repo // Start claude-sandbox from the test repo
await this.startSandbox(); await this.startSandbox();
// Connect to web UI for monitoring sync events // Connect to web UI for monitoring sync events
await this.connectToWebUI(); await this.connectToWebUI();
console.log('✅ Test environment ready'); console.log("✅ Test environment ready");
} }
async startSandbox() { async startSandbox() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log('🚀 Starting claude-sandbox...'); console.log("🚀 Starting claude-sandbox...");
this.sandboxProcess = spawn('npx', ['claude-sandbox', 'start'], { this.sandboxProcess = spawn("npx", ["claude-sandbox", "start"], {
cwd: this.testRepo, cwd: this.testRepo,
stdio: 'pipe' stdio: "pipe",
}); });
let setupComplete = false; let setupComplete = false;
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (!setupComplete) { if (!setupComplete) {
reject(new Error('Sandbox startup timeout')); reject(new Error("Sandbox startup timeout"));
} }
}, 45000); }, 45000);
this.sandboxProcess.stdout.on('data', (data) => { this.sandboxProcess.stdout.on("data", (data) => {
const output = data.toString(); const output = data.toString();
console.log('SANDBOX:', output.trim()); console.log("SANDBOX:", output.trim());
// Extract container ID // Extract container ID
const containerMatch = output.match(/Started container: ([a-f0-9]+)/); const containerMatch = output.match(/Started container: ([a-f0-9]+)/);
@ -74,25 +76,27 @@ class SyncTestFramework {
} }
// Extract web port // 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) { if (portMatch) {
this.webPort = parseInt(portMatch[1]); this.webPort = parseInt(portMatch[1]);
console.log(`🌐 Web UI port: ${this.webPort}`); console.log(`🌐 Web UI port: ${this.webPort}`);
} }
// Check for setup completion // Check for setup completion
if (output.includes('Files synced successfully') && !setupComplete) { if (output.includes("Files synced successfully") && !setupComplete) {
setupComplete = true; setupComplete = true;
clearTimeout(timeout); clearTimeout(timeout);
setTimeout(() => resolve(), 2000); // Wait a bit more for full initialization setTimeout(() => resolve(), 2000); // Wait a bit more for full initialization
} }
}); });
this.sandboxProcess.stderr.on('data', (data) => { this.sandboxProcess.stderr.on("data", (data) => {
console.error('SANDBOX ERROR:', data.toString()); console.error("SANDBOX ERROR:", data.toString());
}); });
this.sandboxProcess.on('close', (code) => { this.sandboxProcess.on("close", (code) => {
if (!setupComplete) { if (!setupComplete) {
reject(new Error(`Sandbox process exited with code ${code}`)); reject(new Error(`Sandbox process exited with code ${code}`));
} }
@ -102,91 +106,100 @@ class SyncTestFramework {
async connectToWebUI() { async connectToWebUI() {
if (!this.webPort || !this.containerId) { 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) => { return new Promise((resolve, reject) => {
const wsUrl = `ws://localhost:${this.webPort}/socket.io/?EIO=4&transport=websocket`; const wsUrl = `ws://localhost:${this.webPort}/socket.io/?EIO=4&transport=websocket`;
this.webSocket = new WebSocket(wsUrl); this.webSocket = new WebSocket(wsUrl);
this.webSocket.on('open', () => { this.webSocket.on("open", () => {
console.log('🔌 Connected to web UI'); console.log("🔌 Connected to web UI");
// Send initial connection message (Socket.IO protocol) // Send initial connection message (Socket.IO protocol)
this.webSocket.send('40'); // Socket.IO connect message this.webSocket.send("40"); // Socket.IO connect message
setTimeout(() => { setTimeout(() => {
// Attach to container // 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(); resolve();
}, 1000); }, 1000);
}); });
this.webSocket.on('message', (data) => { this.webSocket.on("message", (data) => {
const message = data.toString(); const message = data.toString();
if (message.startsWith('42["sync-complete"')) { if (message.startsWith('42["sync-complete"')) {
try { try {
const eventData = JSON.parse(message.substring(2))[1]; const eventData = JSON.parse(message.substring(2))[1];
this.receivedSyncEvents.push({ this.receivedSyncEvents.push({
timestamp: Date.now(), timestamp: Date.now(),
data: eventData data: eventData,
}); });
console.log('📡 Received sync event:', eventData.summary); console.log("📡 Received sync event:", eventData.summary);
} catch (e) { } catch (e) {
// Ignore parsing errors // Ignore parsing errors
} }
} }
}); });
this.webSocket.on('error', (error) => { this.webSocket.on("error", (error) => {
console.error('WebSocket error:', error); console.error("WebSocket error:", error);
reject(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) { async waitForSync(expectedChanges = null, timeoutMs = 10000) {
const startTime = Date.now(); const startTime = Date.now();
const initialEventCount = this.receivedSyncEvents.length; const initialEventCount = this.receivedSyncEvents.length;
// Wait for a new sync event or timeout // Wait for a new sync event or timeout
while (Date.now() - startTime < timeoutMs) { while (Date.now() - startTime < timeoutMs) {
// Check if we received a new sync event // Check if we received a new sync event
if (this.receivedSyncEvents.length > initialEventCount) { if (this.receivedSyncEvents.length > initialEventCount) {
// Wait a bit more for the sync to fully complete // 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]; return this.receivedSyncEvents[this.receivedSyncEvents.length - 1];
} }
// Also wait for the actual file to appear in shadow repo if we're checking for additions // Also wait for the actual file to appear in shadow repo if we're checking for additions
if (expectedChanges && expectedChanges.filePath) { if (expectedChanges && expectedChanges.filePath) {
const exists = await this.shadowFileExists(expectedChanges.filePath); const exists = await this.shadowFileExists(expectedChanges.filePath);
if (exists) { if (exists) {
// File exists, sync completed // File exists, sync completed
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
return { data: { hasChanges: true, summary: 'Sync completed' } }; 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 // If no sync event was received, just wait a bit and return
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
return { data: { hasChanges: true, summary: 'Sync completed (timeout)' } }; return { data: { hasChanges: true, summary: "Sync completed (timeout)" } };
} }
async addFile(filePath, content) { async addFile(filePath, content) {
const containerPath = `/workspace/${filePath}`; 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 }); return this.waitForSync({ filePath });
} }
async modifyFile(filePath, newContent) { async modifyFile(filePath, newContent) {
const containerPath = `/workspace/${filePath}`; 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(); return this.waitForSync();
} }
@ -199,13 +212,17 @@ class SyncTestFramework {
async moveFile(fromPath, toPath) { async moveFile(fromPath, toPath) {
const containerFromPath = `/workspace/${fromPath}`; const containerFromPath = `/workspace/${fromPath}`;
const containerToPath = `/workspace/${toPath}`; 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(); return this.waitForSync();
} }
async createDirectory(dirPath) { async createDirectory(dirPath) {
const containerPath = `/workspace/${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) { async deleteDirectory(dirPath) {
@ -216,7 +233,9 @@ class SyncTestFramework {
async getGitStatus() { async getGitStatus() {
try { try {
const { stdout } = await execAsync(`git -C ${this.shadowPath} status --porcelain`); const { stdout } = await execAsync(
`git -C ${this.shadowPath} status --porcelain`,
);
return stdout.trim(); return stdout.trim();
} catch (error) { } catch (error) {
throw new Error(`Failed to get git status: ${error.message}`); throw new Error(`Failed to get git status: ${error.message}`);
@ -226,9 +245,11 @@ class SyncTestFramework {
async getShadowFileContent(filePath) { async getShadowFileContent(filePath) {
try { try {
const fullPath = path.join(this.shadowPath, filePath); const fullPath = path.join(this.shadowPath, filePath);
return await fs.readFile(fullPath, 'utf8'); return await fs.readFile(fullPath, "utf8");
} catch (error) { } 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) { async getContainerFileContent(filePath) {
try { 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; return stdout;
} catch (error) { } 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) { async containerFileExists(filePath) {
try { try {
await execAsync(`docker exec ${this.containerId} test -f /workspace/${filePath}`); await execAsync(
`docker exec ${this.containerId} test -f /workspace/${filePath}`,
);
return true; return true;
} catch (error) { } catch (error) {
return false; return false;
} }
} }
async listContainerFiles(directory = '') { async listContainerFiles(directory = "") {
try { try {
const containerPath = directory ? `/workspace/${directory}` : '/workspace'; const containerPath = directory
const { stdout } = await execAsync(`docker exec ${this.containerId} find ${containerPath} -type f -not -path "*/.*" | sed 's|^/workspace/||' | sort`); ? `/workspace/${directory}`
return stdout.trim().split('\n').filter(f => f); : "/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) { } catch (error) {
throw new Error(`Failed to list container files: ${error.message}`); throw new Error(`Failed to list container files: ${error.message}`);
} }
} }
async listRepoFiles(directory = '') { async listRepoFiles(directory = "") {
try { try {
const repoPath = directory ? path.join(this.testRepo, directory) : this.testRepo; const repoPath = directory
const { stdout } = await execAsync(`find ${repoPath} -type f -not -path "*/.*" | sed 's|^${this.testRepo}/||' | sort`); ? path.join(this.testRepo, directory)
return stdout.trim().split('\n').filter(f => f); : 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) { } catch (error) {
throw new Error(`Failed to list repo files: ${error.message}`); throw new Error(`Failed to list repo files: ${error.message}`);
} }
} }
async cleanup() { async cleanup() {
console.log('🧹 Cleaning up test environment...'); console.log("🧹 Cleaning up test environment...");
if (this.webSocket) { if (this.webSocket) {
this.webSocket.close(); this.webSocket.close();
} }
if (this.sandboxProcess) { if (this.sandboxProcess) {
this.sandboxProcess.kill('SIGTERM'); this.sandboxProcess.kill("SIGTERM");
} }
// Purge containers // Purge containers
try { try {
await execAsync('npx claude-sandbox purge -y'); await execAsync("npx claude-sandbox purge -y");
} catch (e) { } catch (e) {
// Ignore errors // Ignore errors
} }
// Clean up test repo // Clean up test repo
if (this.testRepo) { if (this.testRepo) {
try { try {
@ -306,9 +347,9 @@ class SyncTestFramework {
// Ignore errors // 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 #!/usr/bin/env node
const { SyncTestFramework } = require('./sync-test-framework'); const { SyncTestFramework } = require("./sync-test-framework");
class TestRunner { class TestRunner {
constructor() { constructor() {
@ -33,8 +33,8 @@ class TestRunner {
} }
async runAll() { async runAll() {
console.log('🚀 Starting File Sync E2E Tests'); console.log("🚀 Starting File Sync E2E Tests");
console.log('='.repeat(50)); console.log("=".repeat(50));
try { try {
await this.framework.setup(); await this.framework.setup();
@ -42,19 +42,20 @@ class TestRunner {
for (const test of this.tests) { for (const test of this.tests) {
await this.runTest(test); await this.runTest(test);
} }
} finally { } finally {
await this.framework.cleanup(); await this.framework.cleanup();
} }
console.log('\n' + '='.repeat(50)); console.log("\n" + "=".repeat(50));
console.log(`📊 Test Results: ${this.passed} passed, ${this.failed} failed`); console.log(
`📊 Test Results: ${this.passed} passed, ${this.failed} failed`,
);
if (this.failed > 0) { if (this.failed > 0) {
console.log('❌ Some tests failed'); console.log("❌ Some tests failed");
process.exit(1); process.exit(1);
} else { } else {
console.log('✅ All tests passed!'); console.log("✅ All tests passed!");
process.exit(0); process.exit(0);
} }
} }
@ -64,228 +65,245 @@ class TestRunner {
const runner = new TestRunner(); const runner = new TestRunner();
// Test 1: File Addition // Test 1: File Addition
runner.addTest('File Addition', async (framework) => { runner.addTest("File Addition", async (framework) => {
await framework.addFile('new-file.txt', 'Hello, World!'); await framework.addFile("new-file.txt", "Hello, World!");
const exists = await framework.shadowFileExists('new-file.txt'); const exists = await framework.shadowFileExists("new-file.txt");
if (!exists) { 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'); const content = await framework.getShadowFileContent("new-file.txt");
if (content.trim() !== 'Hello, World!') { if (content.trim() !== "Hello, World!") {
throw new Error(`Content mismatch: expected "Hello, World!", got "${content.trim()}"`); throw new Error(
`Content mismatch: expected "Hello, World!", got "${content.trim()}"`,
);
} }
const gitStatus = await framework.getGitStatus(); 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}`); throw new Error(`Git status should show addition: ${gitStatus}`);
} }
}); });
// Test 2: File Modification // Test 2: File Modification
runner.addTest('File Modification', async (framework) => { runner.addTest("File Modification", async (framework) => {
// First, create and commit a file // First, create and commit a file
await framework.addFile('modify-test.txt', 'Original content'); await framework.addFile("modify-test.txt", "Original content");
const { stdout } = await require('util').promisify(require('child_process').exec)( const { stdout } = await require("util").promisify(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for modification test"` require("child_process").exec,
)(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for modification test"`,
); );
// Now modify it // Now modify it
await framework.modifyFile('modify-test.txt', 'Modified content'); await framework.modifyFile("modify-test.txt", "Modified content");
const content = await framework.getShadowFileContent('modify-test.txt'); const content = await framework.getShadowFileContent("modify-test.txt");
if (content.trim() !== 'Modified content') { if (content.trim() !== "Modified content") {
throw new Error(`Content not modified: got "${content.trim()}"`); throw new Error(`Content not modified: got "${content.trim()}"`);
} }
const gitStatus = await framework.getGitStatus(); 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}`); throw new Error(`Git status should show modification: ${gitStatus}`);
} }
}); });
// Test 3: File Deletion // Test 3: File Deletion
runner.addTest('File Deletion', async (framework) => { runner.addTest("File Deletion", async (framework) => {
// Create and commit a file first // Create and commit a file first
await framework.addFile('delete-test.txt', 'To be deleted'); await framework.addFile("delete-test.txt", "To be deleted");
const { stdout } = await require('util').promisify(require('child_process').exec)( const { stdout } = await require("util").promisify(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for deletion test"` require("child_process").exec,
)(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for deletion test"`,
); );
// Delete the file // Delete the file
await framework.deleteFile('delete-test.txt'); await framework.deleteFile("delete-test.txt");
const exists = await framework.shadowFileExists('delete-test.txt'); const exists = await framework.shadowFileExists("delete-test.txt");
if (exists) { 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(); 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}`); throw new Error(`Git status should show deletion: ${gitStatus}`);
} }
}); });
// Test 4: Multiple File Operations // Test 4: Multiple File Operations
runner.addTest('Multiple File Operations', async (framework) => { runner.addTest("Multiple File Operations", async (framework) => {
// Add multiple files // Add multiple files
await framework.addFile('file1.txt', 'Content 1'); await framework.addFile("file1.txt", "Content 1");
await framework.addFile('file2.txt', 'Content 2'); await framework.addFile("file2.txt", "Content 2");
await framework.addFile('file3.txt', 'Content 3'); await framework.addFile("file3.txt", "Content 3");
// Wait for sync // Wait for sync
await framework.waitForSync(); await framework.waitForSync();
// Check that all files exist // Check that all files exist
const exists1 = await framework.shadowFileExists('file1.txt'); const exists1 = await framework.shadowFileExists("file1.txt");
const exists2 = await framework.shadowFileExists('file2.txt'); const exists2 = await framework.shadowFileExists("file2.txt");
const exists3 = await framework.shadowFileExists('file3.txt'); const exists3 = await framework.shadowFileExists("file3.txt");
if (!exists1 || !exists2 || !exists3) { 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(); const gitStatus = await framework.getGitStatus();
if (!gitStatus.includes('file1.txt') || if (
!gitStatus.includes('file2.txt') || !gitStatus.includes("file1.txt") ||
!gitStatus.includes('file3.txt')) { !gitStatus.includes("file2.txt") ||
!gitStatus.includes("file3.txt")
) {
throw new Error(`All files should appear in git status: ${gitStatus}`); throw new Error(`All files should appear in git status: ${gitStatus}`);
} }
}); });
// Test 5: Directory Operations // Test 5: Directory Operations
runner.addTest('Directory Operations', async (framework) => { runner.addTest("Directory Operations", async (framework) => {
// Create directory with files // Create directory with files
await framework.createDirectory('new-dir'); await framework.createDirectory("new-dir");
await framework.addFile('new-dir/nested-file.txt', 'Nested content'); await framework.addFile("new-dir/nested-file.txt", "Nested content");
const exists = await framework.shadowFileExists('new-dir/nested-file.txt'); const exists = await framework.shadowFileExists("new-dir/nested-file.txt");
if (!exists) { 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'); const content = await framework.getShadowFileContent(
if (content.trim() !== 'Nested content') { "new-dir/nested-file.txt",
);
if (content.trim() !== "Nested content") {
throw new Error(`Nested file content mismatch: got "${content.trim()}"`); throw new Error(`Nested file content mismatch: got "${content.trim()}"`);
} }
}); });
// Test 6: File Rename/Move // Test 6: File Rename/Move
runner.addTest('File Rename/Move', async (framework) => { runner.addTest("File Rename/Move", async (framework) => {
// Create and commit a file // Create and commit a file
await framework.addFile('original-name.txt', 'Content to move'); await framework.addFile("original-name.txt", "Content to move");
const { stdout } = await require('util').promisify(require('child_process').exec)( const { stdout } = await require("util").promisify(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for move test"` require("child_process").exec,
)(
`git -C ${framework.shadowPath} add -A && git -C ${framework.shadowPath} commit -m "Add file for move test"`,
); );
// Move the file // Move the file
await framework.moveFile('original-name.txt', 'new-name.txt'); await framework.moveFile("original-name.txt", "new-name.txt");
const originalExists = await framework.shadowFileExists('original-name.txt'); const originalExists = await framework.shadowFileExists("original-name.txt");
const newExists = await framework.shadowFileExists('new-name.txt'); const newExists = await framework.shadowFileExists("new-name.txt");
if (originalExists) { if (originalExists) {
throw new Error('Original file still exists after move'); throw new Error("Original file still exists after move");
} }
if (!newExists) { 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'); const content = await framework.getShadowFileContent("new-name.txt");
if (content.trim() !== 'Content to move') { if (content.trim() !== "Content to move") {
throw new Error(`Moved file content mismatch: got "${content.trim()}"`); throw new Error(`Moved file content mismatch: got "${content.trim()}"`);
} }
}); });
// Test 7: Large File Handling // Test 7: Large File Handling
runner.addTest('Large File Handling', async (framework) => { runner.addTest("Large File Handling", async (framework) => {
const largeContent = 'x'.repeat(10000); // 10KB file const largeContent = "x".repeat(10000); // 10KB file
// Use printf to avoid newline issues with echo // Use printf to avoid newline issues with echo
const containerPath = `/workspace/large-file.txt`; const containerPath = `/workspace/large-file.txt`;
await require('util').promisify(require('child_process').exec)( await require("util").promisify(require("child_process").exec)(
`docker exec ${framework.containerId} bash -c "printf '${largeContent}' > ${containerPath}"` `docker exec ${framework.containerId} bash -c "printf '${largeContent}' > ${containerPath}"`,
); );
await framework.waitForSync(); await framework.waitForSync();
const exists = await framework.shadowFileExists('large-file.txt'); const exists = await framework.shadowFileExists("large-file.txt");
if (!exists) { 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) { 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 // Test 8: Special Characters in Filenames
runner.addTest('Special Characters in Filenames', async (framework) => { runner.addTest("Special Characters in Filenames", async (framework) => {
const specialFile = 'special-chars-test_file.txt'; const specialFile = "special-chars-test_file.txt";
await framework.addFile(specialFile, 'Special content'); await framework.addFile(specialFile, "Special content");
const exists = await framework.shadowFileExists(specialFile); const exists = await framework.shadowFileExists(specialFile);
if (!exists) { 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); 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()}"`); throw new Error(`Special file content mismatch: got "${content.trim()}"`);
} }
}); });
// Test 9: Rapid File Changes // Test 9: Rapid File Changes
runner.addTest('Rapid File Changes', async (framework) => { runner.addTest("Rapid File Changes", async (framework) => {
// Create file // Create file
await framework.addFile('rapid-test.txt', 'Version 1'); await framework.addFile("rapid-test.txt", "Version 1");
// Wait for initial sync // Wait for initial sync
await framework.waitForSync(); await framework.waitForSync();
// Quickly modify it multiple times // Quickly modify it multiple times
await framework.modifyFile('rapid-test.txt', 'Version 2'); await framework.modifyFile("rapid-test.txt", "Version 2");
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
await framework.modifyFile('rapid-test.txt', 'Version 3'); await framework.modifyFile("rapid-test.txt", "Version 3");
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
await framework.modifyFile('rapid-test.txt', 'Final Version'); await framework.modifyFile("rapid-test.txt", "Final Version");
// Wait for final sync // Wait for final sync
await framework.waitForSync(null, 15000); await framework.waitForSync(null, 15000);
const content = await framework.getShadowFileContent('rapid-test.txt'); const content = await framework.getShadowFileContent("rapid-test.txt");
if (content.trim() !== 'Final Version') { if (content.trim() !== "Final Version") {
throw new Error(`Final content mismatch: got "${content.trim()}"`); throw new Error(`Final content mismatch: got "${content.trim()}"`);
} }
}); });
// Test 10: Web UI Sync Notifications // 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; 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 // Check that we received sync events
await framework.waitForSync(); await framework.waitForSync();
const newEventCount = framework.receivedSyncEvents.length; const newEventCount = framework.receivedSyncEvents.length;
if (newEventCount <= initialEventCount) { 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) { 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:')) { if (!latestEvent.data.summary.includes("Added:")) {
throw new Error(`Sync event should show addition: ${latestEvent.data.summary}`); throw new Error(
`Sync event should show addition: ${latestEvent.data.summary}`,
);
} }
}); });
// Run all tests // Run all tests
runner.runAll().catch((error) => { runner.runAll().catch((error) => {
console.error('❌ Test runner failed:', error); console.error("❌ Test runner failed:", error);
process.exit(1); process.exit(1);
}); });