Skip to content

Fix detection of deletions #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: './tsconfig.json'
},
env: {
node: true,
es2022: true,
jest: true
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'off',
'semi': ['error', 'always'],
'quotes': ['error', 'single'],
'comma-dangle': ['error', 'never']
},
ignorePatterns: ['dist/', 'node_modules/', 'coverage/', '*.js']
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

e2e-tests/dummy-repo/
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
- [ ] How should we deal with it if the local shadow repo falls behind the remote branch? Options:
- a. Let user merge changes from remote to local: We would need to implement a conflict resolver somehow.
- b. If conflicts arise, we could just block the operation and let user dump the current state in order not to lose work. This is the simplest option.
- Either way, we need to think about how to apply new commits from the remote, because changes currently only flow from the sandbox to the shadow repo.
- 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
39 changes: 39 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: [
'**/test/**/*.test.ts',
'**/test/**/*.test.js',
'**/test/**/*.spec.ts',
'**/test/**/*.spec.js'
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.jsx?$': 'babel-jest'
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}'
],
coverageDirectory: '<rootDir>/coverage',
coverageReporters: ['text', 'lcov', 'html'],
testPathIgnorePatterns: [
'/node_modules/',
'/dist/'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
globals: {
'ts-jest': {
tsconfig: {
esModuleInterop: true,
allowJs: true
}
}
}
};
93 changes: 68 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"start": "node dist/cli.js",
"lint": "eslint src/**/*.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:unit": "jest test/unit",
"test:integration": "jest test/integration",
"test:e2e": "cd test/e2e && ./run-tests.sh",
"test:coverage": "jest --coverage",
"purge-containers": "docker ps -a --filter \"ancestor=claude-code-sandbox:latest\" -q | xargs -r docker rm -f && docker rmi claude-code-sandbox:latest"
},
"keywords": [
Expand All @@ -37,6 +42,7 @@
"simple-git": "^3.22.0",
"socket.io": "^4.8.1",
"tar-stream": "^3.1.7",
"ws": "^8.18.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-web-links": "^0.9.0"
Expand Down
15 changes: 15 additions & 0 deletions src/git/shadow-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ export class ShadowRepository {
try {
await execAsync("git add .", { cwd: this.shadowPath });
console.log(chalk.gray(" Staged all files for tracking"));

// Create initial commit to ensure deletions can be tracked
await execAsync('git commit -m "Initial snapshot of working directory" --allow-empty', { cwd: this.shadowPath });
console.log(chalk.gray(" Created initial commit for change tracking"));
} catch (stageError: any) {
console.log(chalk.gray(" Could not stage files:", stageError.message));
}
Expand Down Expand Up @@ -342,6 +346,13 @@ export class ShadowRepository {
await this.syncWithDockerCp(containerId, containerPath);
}

// Stage all changes including deletions
try {
await execAsync("git add -A", { cwd: this.shadowPath });
} catch (stageError) {
console.log(chalk.gray(" Could not stage changes:", stageError));
}

console.log(chalk.green("✓ Files synced successfully"));
}

Expand Down Expand Up @@ -413,6 +424,10 @@ export class ShadowRepository {
`docker cp ${this.rsyncExcludeFile} ${containerId}:${containerExcludeFile}`,
);

// Rsync directly from container to shadow repo with proper deletion handling
// First, clear the shadow repo (except .git) to ensure deletions are reflected
await execAsync(`find ${this.shadowPath} -mindepth 1 -not -path '${this.shadowPath}/.git*' -delete`);

// Rsync within container to staging area using exclude file
const rsyncCmd = `docker exec ${containerId} rsync -av --delete \
--exclude-from=${containerExcludeFile} \
Expand Down
64 changes: 55 additions & 9 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,69 @@
# Claude Code Sandbox Tests
# Test Directory Structure

This directory contains tests for the Claude Code Sandbox project.
This directory contains all tests for the Claude Code Sandbox project.

## Test Structure
## Directory Layout

- `unit/` - Unit tests for individual components
- `integration/` - Integration tests for testing multiple components together
- `e2e/` - End-to-end tests for testing full workflows
```
test/
├── unit/ # Unit tests for individual modules
├── integration/ # Integration tests for module interactions
├── e2e/ # End-to-end tests for full workflow scenarios
├── fixtures/ # Test data, mock responses, sample files
└── helpers/ # Shared test utilities and helpers
```

## Running Tests

```bash
# Run all tests
npm test

# Run specific test file
npm test test/unit/container.test.js
# Run tests in watch mode
npm run test:watch

# Run only unit tests
npm run test:unit

# Run only integration tests
npm run test:integration

# Run only E2E tests
npm run test:e2e

# Run tests with coverage
npm run test:coverage
```

## Test Naming Conventions

- Unit tests: `*.test.ts` or `*.spec.ts`
- Test files should mirror the source structure
- Example: `test/unit/container.test.ts` tests `src/container.ts`

## Writing Tests

Tests should be written using a testing framework like Jest or Mocha. Each test file should be self-contained and test a specific component or feature.
Tests are written using Jest with TypeScript support. The Jest configuration is in `jest.config.js` at the project root.

### Example Unit Test

```typescript
import { someFunction } from '../../src/someModule';

describe('someFunction', () => {
it('should do something', () => {
const result = someFunction('input');
expect(result).toBe('expected output');
});
});
```

## E2E Tests

End-to-end tests are located in `test/e2e/` and test the complete workflow of the CLI tool. These tests:
- Create actual Docker containers
- Run Claude commands
- Verify git operations
- Test the full user experience

Run E2E tests with: `npm run test:e2e`
Loading