mirror of
https://github.com/textcortex/claude-code-sandbox.git
synced 2025-07-07 21:35: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 = {
|
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"],
|
||||||
};
|
};
|
||||||
|
|
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.
|
- 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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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} \
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue