mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-05 18:16:28 +00:00
fix(cliproxy): handle all invalid prefixes and reverse-order collisions
forceValidChars() now ensures leading char is letter or underscore (catches dot, hyphen, colon, slash — not just digits). registerTools() always registers in mapping so collisions are detected regardless of tool registration order.
This commit is contained in:
@@ -300,5 +300,36 @@ describe('Tool Name Mapper', () => {
|
|||||||
assert.ok(collisions[0].originals.includes('x__y__y'));
|
assert.ok(collisions[0].originals.includes('x__y__y'));
|
||||||
assert.ok(collisions[0].originals.includes('x__y'));
|
assert.ok(collisions[0].originals.includes('x__y'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('detects collision when unchanged tool appears before sanitized one', () => {
|
||||||
|
const mapper = new ToolNameMapper();
|
||||||
|
const tools = [
|
||||||
|
{ name: 'abc__def' }, // unchanged, but registered in mapping
|
||||||
|
{ name: 'abc__def__def' }, // sanitizes to 'abc__def', collides
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapper.registerTools(tools);
|
||||||
|
|
||||||
|
assert.strictEqual(result[0].name, 'abc__def');
|
||||||
|
assert.strictEqual(result[1].name, 'abc__def_2');
|
||||||
|
assert.strictEqual(mapper.hasCollisions(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores correctly for reverse-order collision', () => {
|
||||||
|
const mapper = new ToolNameMapper();
|
||||||
|
const tools = [{ name: 'abc__def' }, { name: 'abc__def__def' }];
|
||||||
|
|
||||||
|
mapper.registerTools(tools);
|
||||||
|
|
||||||
|
const content = [
|
||||||
|
{ type: 'tool_use', id: '1', name: 'abc__def', input: {} },
|
||||||
|
{ type: 'tool_use', id: '2', name: 'abc__def_2', input: {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
const restored = mapper.restoreToolUse(content);
|
||||||
|
|
||||||
|
assert.strictEqual(restored[0].name, 'abc__def');
|
||||||
|
assert.strictEqual(restored[1].name, 'abc__def__def');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -203,5 +203,29 @@ describe('Tool Name Sanitizer', () => {
|
|||||||
assert.ok(isValidToolName(result.sanitized), `Expected valid name, got: ${result.sanitized}`);
|
assert.ok(isValidToolName(result.sanitized), `Expected valid name, got: ${result.sanitized}`);
|
||||||
assert.ok(result.sanitized.length <= 64);
|
assert.ok(result.sanitized.length <= 64);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forces valid prefix for dot-starting name', () => {
|
||||||
|
const result = sanitizeToolName('.tool');
|
||||||
|
|
||||||
|
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
|
||||||
|
assert.ok(isValidToolName(result.sanitized));
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forces valid prefix for hyphen-starting name', () => {
|
||||||
|
const result = sanitizeToolName('-tool');
|
||||||
|
|
||||||
|
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
|
||||||
|
assert.ok(isValidToolName(result.sanitized));
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forces valid prefix for colon-starting name', () => {
|
||||||
|
const result = sanitizeToolName(':tool');
|
||||||
|
|
||||||
|
assert.ok(result.sanitized.startsWith('_'), `Expected _ prefix, got: ${result.sanitized}`);
|
||||||
|
assert.ok(isValidToolName(result.sanitized));
|
||||||
|
assert.strictEqual(result.changed, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -107,9 +107,12 @@ export class ToolNameMapper {
|
|||||||
finalName = disambiguated;
|
finalName = disambiguated;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store mapping if name changed or was disambiguated
|
// Always register in mapping so later tools can detect collisions
|
||||||
|
// even when this tool's name was unchanged
|
||||||
|
this.mapping.set(finalName, tool.name);
|
||||||
|
|
||||||
|
// Record change only if name actually changed
|
||||||
if (finalName !== tool.name) {
|
if (finalName !== tool.name) {
|
||||||
this.mapping.set(finalName, tool.name);
|
|
||||||
this.changes.push({
|
this.changes.push({
|
||||||
original: tool.name,
|
original: tool.name,
|
||||||
sanitized: finalName,
|
sanitized: finalName,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function smartTruncate(name: string, maxLen: number = GEMINI_MAX_TOOL_NAM
|
|||||||
/**
|
/**
|
||||||
* Force a string to comply with Gemini tool name character rules.
|
* Force a string to comply with Gemini tool name character rules.
|
||||||
* Replaces invalid characters with underscores and ensures the name
|
* Replaces invalid characters with underscores and ensures the name
|
||||||
* starts with a letter or underscore.
|
* starts with a letter or underscore (not a digit, dot, colon, etc.).
|
||||||
*/
|
*/
|
||||||
function forceValidChars(name: string): string {
|
function forceValidChars(name: string): string {
|
||||||
// Replace any character not in [a-zA-Z0-9_.:/-] with underscore
|
// Replace any character not in [a-zA-Z0-9_.:/-] with underscore
|
||||||
@@ -112,8 +112,8 @@ function forceValidChars(name: string): string {
|
|||||||
// Collapse multiple consecutive underscores from replacement
|
// Collapse multiple consecutive underscores from replacement
|
||||||
fixed = fixed.replace(/_+/g, '_');
|
fixed = fixed.replace(/_+/g, '_');
|
||||||
|
|
||||||
// Ensure starts with letter or underscore
|
// Ensure starts with letter or underscore (not digit, dot, colon, hyphen, slash)
|
||||||
if (fixed.length > 0 && /^[0-9]/.test(fixed)) {
|
if (fixed.length > 0 && !/^[a-zA-Z_]/.test(fixed)) {
|
||||||
fixed = '_' + fixed;
|
fixed = '_' + fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user