mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Enable tool call type coersion (#477)
* feat: enable tool call type coercion * fix: tests for type coercion --------- Co-authored-by: Mingholy <mingholy.lmh@gmail.com>
This commit is contained in:
@@ -62,6 +62,9 @@ describe('GlobTool', () => {
|
|||||||
// Ensure a noticeable difference in modification time
|
// Ensure a noticeable difference in modification time
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
await fs.writeFile(path.join(tempRootDir, 'newer.sortme'), 'newer_content');
|
await fs.writeFile(path.join(tempRootDir, 'newer.sortme'), 'newer_content');
|
||||||
|
|
||||||
|
// For type coercion testing
|
||||||
|
await fs.mkdir(path.join(tempRootDir, '123'));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -279,26 +282,20 @@ describe('GlobTool', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if path is provided but is not a string (schema validation)', () => {
|
it('should pass if path is provided but is not a string (type coercion)', () => {
|
||||||
const params = {
|
const params = {
|
||||||
pattern: '*.ts',
|
pattern: '*.ts',
|
||||||
path: 123,
|
path: 123,
|
||||||
};
|
} as unknown as GlobToolParams; // Force incorrect type
|
||||||
// @ts-expect-error - We're intentionally creating invalid params for testing
|
expect(globTool.validateToolParams(params)).toBeNull();
|
||||||
expect(globTool.validateToolParams(params)).toBe(
|
|
||||||
'params/path must be string',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error if case_sensitive is provided but is not a boolean (schema validation)', () => {
|
it('should pass if case_sensitive is provided but is not a boolean (type coercion)', () => {
|
||||||
const params = {
|
const params = {
|
||||||
pattern: '*.ts',
|
pattern: '*.ts',
|
||||||
case_sensitive: 'true',
|
case_sensitive: 'true',
|
||||||
};
|
} as unknown as GlobToolParams; // Force incorrect type
|
||||||
// @ts-expect-error - We're intentionally creating invalid params for testing
|
expect(globTool.validateToolParams(params)).toBeNull();
|
||||||
expect(globTool.validateToolParams(params)).toBe(
|
|
||||||
'params/case_sensitive must be boolean',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error if search path resolves outside the tool's root directory", () => {
|
it("should return error if search path resolves outside the tool's root directory", () => {
|
||||||
|
|||||||
@@ -191,14 +191,12 @@ describe('ReadManyFilesTool', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if include array contains non-string elements', () => {
|
it('should coerce non-string elements in include array', () => {
|
||||||
const params = {
|
const params = {
|
||||||
paths: ['file1.txt'],
|
paths: ['file1.txt'],
|
||||||
include: ['*.ts', 123] as string[],
|
include: ['*.ts', 123] as string[],
|
||||||
};
|
};
|
||||||
expect(() => tool.build(params)).toThrow(
|
expect(() => tool.build(params)).toBeDefined();
|
||||||
'params/include/1 must be string',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if exclude array contains non-string elements', () => {
|
it('should throw error if exclude array contains non-string elements', () => {
|
||||||
|
|||||||
@@ -220,14 +220,12 @@ describe('WriteFileTool', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the content is null', () => {
|
it('should coerce null content into an empty string', () => {
|
||||||
const dirAsFilePath = path.join(rootDir, 'a_directory');
|
|
||||||
fs.mkdirSync(dirAsFilePath);
|
|
||||||
const params = {
|
const params = {
|
||||||
file_path: dirAsFilePath,
|
file_path: path.join(rootDir, 'test.txt'),
|
||||||
content: null,
|
content: null,
|
||||||
} as unknown as WriteFileToolParams; // Intentionally non-conforming
|
} as unknown as WriteFileToolParams; // Intentionally non-conforming
|
||||||
expect(() => tool.build(params)).toThrow('params/content must be string');
|
expect(() => tool.build(params)).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if the file_path is empty', () => {
|
it('should throw error if the file_path is empty', () => {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import * as addFormats from 'ajv-formats';
|
|||||||
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const AjvClass = (AjvPkg as any).default || AjvPkg;
|
const AjvClass = (AjvPkg as any).default || AjvPkg;
|
||||||
const ajValidator = new AjvClass();
|
const ajValidator = new AjvClass({ coerceTypes: true });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const addFormatsFunc = (addFormats as any).default || addFormats;
|
const addFormatsFunc = (addFormats as any).default || addFormats;
|
||||||
addFormatsFunc(ajValidator);
|
addFormatsFunc(ajValidator);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple utility to validate objects against JSON Schemas
|
* Simple utility to validate objects against JSON Schemas
|
||||||
*/
|
*/
|
||||||
@@ -32,8 +33,27 @@ export class SchemaValidator {
|
|||||||
const validate = ajValidator.compile(schema);
|
const validate = ajValidator.compile(schema);
|
||||||
const valid = validate(data);
|
const valid = validate(data);
|
||||||
if (!valid && validate.errors) {
|
if (!valid && validate.errors) {
|
||||||
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
// Find any True or False values and lowercase them
|
||||||
|
fixBooleanCasing(data as Record<string, unknown>);
|
||||||
|
|
||||||
|
const validate = ajValidator.compile(schema);
|
||||||
|
const valid = validate(data);
|
||||||
|
|
||||||
|
if (!valid && validate.errors) {
|
||||||
|
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fixBooleanCasing(data: Record<string, unknown>) {
|
||||||
|
for (const key of Object.keys(data)) {
|
||||||
|
if (!(key in data)) continue;
|
||||||
|
|
||||||
|
if (typeof data[key] === 'object') {
|
||||||
|
fixBooleanCasing(data[key] as Record<string, unknown>);
|
||||||
|
} else if (data[key] === 'True') data[key] = 'true';
|
||||||
|
else if (data[key] === 'False') data[key] = 'false';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user