mirror of
https://github.com/textcortex/claude-code-sandbox.git
synced 2025-07-07 13:25:10 +00:00
Exclude .DS_Store files, run prettier
This commit is contained in:
parent
56c120e7a8
commit
f815760d73
18 changed files with 700 additions and 545 deletions
33
.eslintrc.js
33
.eslintrc.js
|
@ -1,28 +1,25 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json'
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true,
|
||||
jest: true
|
||||
jest: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'no-console': 'off',
|
||||
'semi': ['error', 'always'],
|
||||
'quotes': ['error', 'single'],
|
||||
'comma-dangle': ['error', 'never']
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
"no-console": "off",
|
||||
semi: ["error", "always"],
|
||||
quotes: ["error", "single"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
},
|
||||
ignorePatterns: ['dist/', 'node_modules/', 'coverage/', '*.js']
|
||||
};
|
||||
ignorePatterns: ["dist/", "node_modules/", "coverage/", "*.js"],
|
||||
};
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -12,4 +12,4 @@
|
|||
- a. Let user merge changes from remote to local: We would need to implement a conflict resolver somehow.
|
||||
- b. If conflicts arise, we could just block the operation and let user dump the current state in order not to lose work. This is the simplest option.
|
||||
- Either way, we need to think about how to apply new commits from the remote, because changes currently only flow from the sandbox to the shadow repo.
|
||||
- [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly
|
||||
- [ ] rsync, inotifywait, etc. should be included in the image, not installed in the fly
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} \
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue