Skip to content

Commit ccfc890

Browse files
author
Mohammod Al Amin Ashik
committed
ci: Add pre-commit hooks for Rust and TypeScript validation
- Create pre-commit hook that validates before each commit - Runs cargo check for Rust files - Runs pnpm typecheck for TypeScript files - Auto-formats Rust code with cargo fmt - Add validate script: pnpm validate - Add prepare script to set hook permissions - Document git hooks setup in .github/GIT_HOOKS.md This prevents CI failures by catching issues locally.
1 parent 52ec9de commit ccfc890

7 files changed

Lines changed: 430 additions & 344 deletions

File tree

.github/GIT_HOOKS.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Git Hooks
2+
3+
This project uses Git hooks to maintain code quality and prevent CI failures.
4+
5+
## Pre-commit Hook
6+
7+
The pre-commit hook automatically runs before each commit to validate:
8+
9+
### **Rust Validation**
10+
- Formats Rust code with `cargo fmt`
11+
- Runs `cargo check --workspace` to verify compilation
12+
- Automatically adds formatted files to the commit
13+
14+
### **TypeScript Validation**
15+
- Runs `pnpm typecheck` to verify type correctness
16+
- Checks all TypeScript files in the workspace
17+
18+
## Setup
19+
20+
The hook is automatically set up when you:
21+
1. Clone the repository
22+
2. Run `pnpm install` (triggers `prepare` script)
23+
24+
### Manual Setup
25+
26+
If the hook isn't working, run:
27+
28+
```bash
29+
# Make hook executable (Linux/Mac)
30+
chmod +x .git/hooks/pre-commit
31+
32+
# Windows (PowerShell)
33+
icacls ".git\hooks\pre-commit" /grant Everyone:RX
34+
```
35+
36+
## Testing the Hook
37+
38+
To test the validation without committing:
39+
40+
```bash
41+
# Run both checks
42+
pnpm validate
43+
44+
# Or individually
45+
cargo check --workspace
46+
pnpm typecheck
47+
```
48+
49+
## Bypassing the Hook (Not Recommended)
50+
51+
In rare cases where you need to bypass validation:
52+
53+
```bash
54+
git commit --no-verify -m "your message"
55+
```
56+
57+
**Note**: Only use `--no-verify` when absolutely necessary, as it skips important validations that prevent CI failures.
58+
59+
## Troubleshooting
60+
61+
### Hook not running
62+
- Ensure `.git/hooks/pre-commit` exists
63+
- Check it's executable: `ls -la .git/hooks/pre-commit`
64+
- Try manual setup commands above
65+
66+
### Hook fails on Windows
67+
- The hook uses bash scripting (requires Git Bash or WSL)
68+
- Alternatively, use the PowerShell version: `.git/hooks/pre-commit.ps1`
69+
- Configure Git to use PowerShell hooks:
70+
```powershell
71+
git config core.hooksPath .git/hooks
72+
```
73+
74+
### Slow validation
75+
- The hook only validates files in your commit (staged changes)
76+
- If you have many Rust crates, consider using `cargo check -p <crate-name>`
77+
- TypeScript check runs on entire workspace (necessary for type consistency)
78+
79+
## CI Integration
80+
81+
These same checks run in CI:
82+
- GitHub Actions runs `cargo check` and `pnpm typecheck`
83+
- Pre-commit hooks help catch issues early before pushing

apps/desktop/src-tauri/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ url.workspace = true
4040
urlencoding = "2.1"
4141
open = "5.3"
4242
dotenvy.workspace = true
43-
notify = { version = "7", default-features = false, features = ["macos_fsevent"] }
43+
notify = { version = "7", default-features = false, features = [
44+
"macos_fsevent",
45+
] }
4446
notify-debouncer-mini = "0.5"
4547

4648
# Internal crates (path-only, no version needed)
4749
mcpmux-core.workspace = true
4850
mcpmux-gateway.workspace = true
4951
mcpmux-storage.workspace = true
50-

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"setup": "pwsh -ExecutionPolicy Bypass -File scripts/setup-dev.ps1",
8+
"prepare": "node -e \"try{require('fs').chmodSync('.git/hooks/pre-commit',0o755)}catch(e){}\"",
89
"dev": "pnpm --filter @mcpmux/desktop dev",
910
"dev:web": "pnpm --filter @mcpmux/desktop dev:web",
1011
"build": "pnpm --filter @mcpmux/desktop build",
@@ -30,6 +31,7 @@
3031
"format": "prettier --write . && cargo fmt --all",
3132
"format:check": "prettier --check . && cargo fmt --all --check",
3233
"typecheck": "pnpm -r typecheck",
34+
"validate": "cargo check --workspace && pnpm typecheck",
3335
"clean": "pnpm -r clean && cargo clean"
3436
},
3537
"devDependencies": {

packages/ui/src/components/common/Switch.test.tsx

Lines changed: 123 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -7,127 +7,127 @@ import { render, screen, fireEvent } from '@testing-library/react';
77
import { Switch } from './Switch';
88

99
describe('Switch', () => {
10-
it('renders with unchecked state', () => {
11-
const mockHandler = vi.fn();
12-
render(<Switch checked={false} onCheckedChange={mockHandler} />);
13-
14-
const button = screen.getByRole('switch');
15-
expect(button).toBeInTheDocument();
16-
expect(button).toHaveAttribute('aria-checked', 'false');
17-
});
18-
19-
it('renders with checked state', () => {
20-
const mockHandler = vi.fn();
21-
render(<Switch checked={true} onCheckedChange={mockHandler} />);
22-
23-
const button = screen.getByRole('switch');
24-
expect(button).toHaveAttribute('aria-checked', 'true');
25-
});
26-
27-
it('calls onCheckedChange when clicked', () => {
28-
const mockHandler = vi.fn();
29-
render(<Switch checked={false} onCheckedChange={mockHandler} />);
30-
31-
const button = screen.getByRole('switch');
32-
fireEvent.click(button);
33-
34-
expect(mockHandler).toHaveBeenCalledWith(true);
35-
expect(mockHandler).toHaveBeenCalledTimes(1);
36-
});
37-
38-
it('toggles from checked to unchecked', () => {
39-
const mockHandler = vi.fn();
40-
render(<Switch checked={true} onCheckedChange={mockHandler} />);
41-
42-
const button = screen.getByRole('switch');
43-
fireEvent.click(button);
44-
45-
expect(mockHandler).toHaveBeenCalledWith(false);
46-
});
47-
48-
it('does not call handler when disabled', () => {
49-
const mockHandler = vi.fn();
50-
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
51-
52-
const button = screen.getByRole('switch');
53-
fireEvent.click(button);
54-
55-
expect(mockHandler).not.toHaveBeenCalled();
56-
});
57-
58-
it('applies disabled attribute when disabled', () => {
59-
const mockHandler = vi.fn();
60-
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
61-
62-
const button = screen.getByRole('switch');
63-
expect(button).toBeDisabled();
64-
});
65-
66-
it('applies custom className', () => {
67-
const mockHandler = vi.fn();
68-
render(<Switch checked={false} onCheckedChange={mockHandler} className="custom-class" />);
69-
70-
const button = screen.getByRole('switch');
71-
expect(button).toHaveClass('custom-class');
72-
});
73-
74-
it('applies data-testid when provided', () => {
75-
const mockHandler = vi.fn();
76-
render(<Switch checked={false} onCheckedChange={mockHandler} data-testid="test-switch" />);
77-
78-
const button = screen.getByTestId('test-switch');
79-
expect(button).toBeInTheDocument();
80-
});
81-
82-
it('has correct styles for checked state', () => {
83-
const mockHandler = vi.fn();
84-
render(<Switch checked={true} onCheckedChange={mockHandler} />);
85-
86-
const button = screen.getByRole('switch');
87-
expect(button.className).toMatch(/bg-\[rgb\(var\(--primary\)\)\]/);
88-
});
89-
90-
it('has correct styles for unchecked state', () => {
91-
const mockHandler = vi.fn();
92-
render(<Switch checked={false} onCheckedChange={mockHandler} />);
93-
94-
const button = screen.getByRole('switch');
95-
expect(button.className).toMatch(/bg-gray-300/);
96-
});
97-
98-
it('has disabled styling when disabled', () => {
99-
const mockHandler = vi.fn();
100-
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
101-
102-
const button = screen.getByRole('switch');
103-
expect(button.className).toMatch(/opacity-50/);
104-
});
105-
106-
it('can be toggled multiple times', () => {
107-
const mockHandler = vi.fn();
108-
const { rerender } = render(<Switch checked={false} onCheckedChange={mockHandler} />);
109-
110-
const button = screen.getByRole('switch');
111-
112-
// First click - should call with true
113-
fireEvent.click(button);
114-
expect(mockHandler).toHaveBeenCalledWith(true);
115-
expect(mockHandler).toHaveBeenCalledTimes(1);
116-
117-
// Simulate parent updating the prop
118-
rerender(<Switch checked={true} onCheckedChange={mockHandler} />);
119-
120-
// Second click - should call with false
121-
fireEvent.click(button);
122-
expect(mockHandler).toHaveBeenCalledWith(false);
123-
expect(mockHandler).toHaveBeenCalledTimes(2);
124-
125-
// Simulate parent updating the prop again
126-
rerender(<Switch checked={false} onCheckedChange={mockHandler} />);
127-
128-
// Third click - should call with true again
129-
fireEvent.click(button);
130-
expect(mockHandler).toHaveBeenCalledWith(true);
131-
expect(mockHandler).toHaveBeenCalledTimes(3);
132-
});
10+
it('renders with unchecked state', () => {
11+
const mockHandler = vi.fn();
12+
render(<Switch checked={false} onCheckedChange={mockHandler} />);
13+
14+
const button = screen.getByRole('switch');
15+
expect(button).toBeInTheDocument();
16+
expect(button).toHaveAttribute('aria-checked', 'false');
17+
});
18+
19+
it('renders with checked state', () => {
20+
const mockHandler = vi.fn();
21+
render(<Switch checked={true} onCheckedChange={mockHandler} />);
22+
23+
const button = screen.getByRole('switch');
24+
expect(button).toHaveAttribute('aria-checked', 'true');
25+
});
26+
27+
it('calls onCheckedChange when clicked', () => {
28+
const mockHandler = vi.fn();
29+
render(<Switch checked={false} onCheckedChange={mockHandler} />);
30+
31+
const button = screen.getByRole('switch');
32+
fireEvent.click(button);
33+
34+
expect(mockHandler).toHaveBeenCalledWith(true);
35+
expect(mockHandler).toHaveBeenCalledTimes(1);
36+
});
37+
38+
it('toggles from checked to unchecked', () => {
39+
const mockHandler = vi.fn();
40+
render(<Switch checked={true} onCheckedChange={mockHandler} />);
41+
42+
const button = screen.getByRole('switch');
43+
fireEvent.click(button);
44+
45+
expect(mockHandler).toHaveBeenCalledWith(false);
46+
});
47+
48+
it('does not call handler when disabled', () => {
49+
const mockHandler = vi.fn();
50+
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
51+
52+
const button = screen.getByRole('switch');
53+
fireEvent.click(button);
54+
55+
expect(mockHandler).not.toHaveBeenCalled();
56+
});
57+
58+
it('applies disabled attribute when disabled', () => {
59+
const mockHandler = vi.fn();
60+
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
61+
62+
const button = screen.getByRole('switch');
63+
expect(button).toBeDisabled();
64+
});
65+
66+
it('applies custom className', () => {
67+
const mockHandler = vi.fn();
68+
render(<Switch checked={false} onCheckedChange={mockHandler} className="custom-class" />);
69+
70+
const button = screen.getByRole('switch');
71+
expect(button).toHaveClass('custom-class');
72+
});
73+
74+
it('applies data-testid when provided', () => {
75+
const mockHandler = vi.fn();
76+
render(<Switch checked={false} onCheckedChange={mockHandler} data-testid="test-switch" />);
77+
78+
const button = screen.getByTestId('test-switch');
79+
expect(button).toBeInTheDocument();
80+
});
81+
82+
it('has correct styles for checked state', () => {
83+
const mockHandler = vi.fn();
84+
render(<Switch checked={true} onCheckedChange={mockHandler} />);
85+
86+
const button = screen.getByRole('switch');
87+
expect(button.className).toMatch(/bg-\[rgb\(var\(--primary\)\)\]/);
88+
});
89+
90+
it('has correct styles for unchecked state', () => {
91+
const mockHandler = vi.fn();
92+
render(<Switch checked={false} onCheckedChange={mockHandler} />);
93+
94+
const button = screen.getByRole('switch');
95+
expect(button.className).toMatch(/bg-gray-300/);
96+
});
97+
98+
it('has disabled styling when disabled', () => {
99+
const mockHandler = vi.fn();
100+
render(<Switch checked={false} onCheckedChange={mockHandler} disabled={true} />);
101+
102+
const button = screen.getByRole('switch');
103+
expect(button.className).toMatch(/opacity-50/);
104+
});
105+
106+
it('can be toggled multiple times', () => {
107+
const mockHandler = vi.fn();
108+
const { rerender } = render(<Switch checked={false} onCheckedChange={mockHandler} />);
109+
110+
const button = screen.getByRole('switch');
111+
112+
// First click - should call with true
113+
fireEvent.click(button);
114+
expect(mockHandler).toHaveBeenCalledWith(true);
115+
expect(mockHandler).toHaveBeenCalledTimes(1);
116+
117+
// Simulate parent updating the prop
118+
rerender(<Switch checked={true} onCheckedChange={mockHandler} />);
119+
120+
// Second click - should call with false
121+
fireEvent.click(button);
122+
expect(mockHandler).toHaveBeenCalledWith(false);
123+
expect(mockHandler).toHaveBeenCalledTimes(2);
124+
125+
// Simulate parent updating the prop again
126+
rerender(<Switch checked={false} onCheckedChange={mockHandler} />);
127+
128+
// Third click - should call with true again
129+
fireEvent.click(button);
130+
expect(mockHandler).toHaveBeenCalledWith(true);
131+
expect(mockHandler).toHaveBeenCalledTimes(3);
132+
});
133133
});

packages/ui/src/components/common/Switch.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export function Switch({
3636
data-testid={testId}
3737
className={cn(
3838
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
39-
checked
40-
? 'bg-[rgb(var(--primary))] border-transparent'
39+
checked
40+
? 'bg-[rgb(var(--primary))] border-transparent'
4141
: 'bg-gray-300 dark:bg-gray-600 border-gray-400 dark:border-gray-500',
4242
className
4343
)}

0 commit comments

Comments
 (0)