mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 08:18:23 +00:00
Merge pull request #452 from kaitranntt/kai/fix/450-hook-duplication
fix(hooks): deduplicate WebSearch hooks when saving via Dashboard
This commit is contained in:
@@ -48,5 +48,11 @@ export function deduplicateCcsHooks(settings: Record<string, unknown>): boolean
|
||||
return false; // Remove subsequent duplicates
|
||||
});
|
||||
|
||||
return hooks.PreToolUse.length < originalLength;
|
||||
const newLength = hooks.PreToolUse.length;
|
||||
if (process.env.CCS_DEBUG && newLength < originalLength) {
|
||||
const removedCount = originalLength - newLength;
|
||||
console.error(`Removed ${removedCount} duplicate CCS WebSearch hook(s)`);
|
||||
}
|
||||
|
||||
return newLength < originalLength;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
resetAuthToDefaults,
|
||||
} from '../../cliproxy';
|
||||
import { regenerateConfig } from '../../cliproxy/config-generator';
|
||||
import { deduplicateCcsHooks } from '../../utils/websearch/hook-utils';
|
||||
import type { Settings } from '../../types/config';
|
||||
|
||||
const router = Router();
|
||||
@@ -136,6 +137,10 @@ router.put('/:profile', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate CCS hooks to prevent accumulation (fixes #450)
|
||||
// This handles cases where duplicate hooks were added by previous versions
|
||||
deduplicateCcsHooks(settings as Record<string, unknown>);
|
||||
|
||||
const ccsDir = getCcsDir();
|
||||
|
||||
// Check for missing required fields (warning, not blocking - runtime fills defaults)
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* Tests for exponential backoff retry behavior on 429 rate limit errors
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { describe, it, expect, beforeEach, afterEach, setDefaultTimeout } from 'bun:test';
|
||||
|
||||
// Increase timeout for CI - dynamic imports and proxy creation are slow on CI runners
|
||||
setDefaultTimeout(30000);
|
||||
|
||||
// Store original env vars
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
@@ -289,4 +289,83 @@ describe('deduplicateCcsHooks', () => {
|
||||
expect(result).toBe(false);
|
||||
expect(settings.hooks.PreToolUse).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Stress test: 15 duplicate CCS WebSearch hooks', () => {
|
||||
const ccsHook = {
|
||||
matcher: 'WebSearch',
|
||||
hooks: [
|
||||
{
|
||||
command: 'node /home/user/.ccs/hooks/websearch-transformer/index.js',
|
||||
},
|
||||
],
|
||||
};
|
||||
const settings = {
|
||||
hooks: {
|
||||
PreToolUse: Array(15).fill(ccsHook),
|
||||
},
|
||||
};
|
||||
const result = deduplicateCcsHooks(settings);
|
||||
expect(result).toBe(true);
|
||||
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
||||
expect(settings.hooks.PreToolUse[0]).toEqual(ccsHook);
|
||||
});
|
||||
|
||||
test('Leaves PostToolUse and PreToolCall untouched', () => {
|
||||
const postToolUseHook1 = {
|
||||
matcher: 'CustomMatcher1',
|
||||
hooks: [{ command: 'custom-command-1' }],
|
||||
};
|
||||
const postToolUseHook2 = {
|
||||
matcher: 'CustomMatcher2',
|
||||
hooks: [{ command: 'custom-command-2' }],
|
||||
};
|
||||
const preToolCallHook = {
|
||||
matcher: 'CustomMatcher3',
|
||||
hooks: [{ command: 'custom-command-3' }],
|
||||
};
|
||||
const settings = {
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'WebSearch',
|
||||
hooks: [
|
||||
{
|
||||
command: 'node /path1/.ccs/hooks/websearch-transformer/index.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: 'WebSearch',
|
||||
hooks: [
|
||||
{
|
||||
command: 'node /path2/.ccs/hooks/websearch-transformer/index.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matcher: 'WebSearch',
|
||||
hooks: [
|
||||
{
|
||||
command: 'node /path3/.ccs/hooks/websearch-transformer/index.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
PostToolUse: [postToolUseHook1, postToolUseHook2],
|
||||
PreToolCall: [preToolCallHook],
|
||||
},
|
||||
};
|
||||
const result = deduplicateCcsHooks(settings);
|
||||
expect(result).toBe(true);
|
||||
// PreToolUse should be deduplicated to 1 hook
|
||||
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
||||
expect(settings.hooks.PreToolUse[0].matcher).toBe('WebSearch');
|
||||
// PostToolUse should remain unchanged with 2 hooks
|
||||
expect(settings.hooks.PostToolUse).toHaveLength(2);
|
||||
expect(settings.hooks.PostToolUse[0]).toEqual(postToolUseHook1);
|
||||
expect(settings.hooks.PostToolUse[1]).toEqual(postToolUseHook2);
|
||||
// PreToolCall should remain unchanged with 1 hook
|
||||
expect(settings.hooks.PreToolCall).toHaveLength(1);
|
||||
expect(settings.hooks.PreToolCall[0]).toEqual(preToolCallHook);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user