Skip to content

Commit 105ad74

Browse files
authored
Merge pull request #1284 from tt-a1i/fix/boolean-string-coercion
fix(core): coerce string boolean values in schema validation
2 parents e27e9a5 + ac3f7cb commit 105ad74

File tree

3 files changed

+150
-11
lines changed

3 files changed

+150
-11
lines changed

packages/core/src/tools/shell.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,44 @@ describe('ShellTool', () => {
169169
});
170170
expect(invocation.getDescription()).not.toContain('[background]');
171171
});
172+
173+
describe('is_background parameter coercion', () => {
174+
it('should accept string "true" as boolean true', () => {
175+
const invocation = shellTool.build({
176+
command: 'npm run dev',
177+
is_background: 'true' as unknown as boolean,
178+
});
179+
expect(invocation).toBeDefined();
180+
expect(invocation.getDescription()).toContain('[background]');
181+
});
182+
183+
it('should accept string "false" as boolean false', () => {
184+
const invocation = shellTool.build({
185+
command: 'npm run build',
186+
is_background: 'false' as unknown as boolean,
187+
});
188+
expect(invocation).toBeDefined();
189+
expect(invocation.getDescription()).not.toContain('[background]');
190+
});
191+
192+
it('should accept string "True" as boolean true', () => {
193+
const invocation = shellTool.build({
194+
command: 'npm run dev',
195+
is_background: 'True' as unknown as boolean,
196+
});
197+
expect(invocation).toBeDefined();
198+
expect(invocation.getDescription()).toContain('[background]');
199+
});
200+
201+
it('should accept string "False" as boolean false', () => {
202+
const invocation = shellTool.build({
203+
command: 'npm run build',
204+
is_background: 'False' as unknown as boolean,
205+
});
206+
expect(invocation).toBeDefined();
207+
expect(invocation.getDescription()).not.toContain('[background]');
208+
});
209+
});
172210
});
173211

174212
describe('execute', () => {

packages/core/src/utils/schemaValidator.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,91 @@ describe('SchemaValidator', () => {
122122
};
123123
expect(SchemaValidator.validate(schema, params)).not.toBeNull();
124124
});
125+
126+
describe('boolean string coercion', () => {
127+
const booleanSchema = {
128+
type: 'object',
129+
properties: {
130+
is_background: {
131+
type: 'boolean',
132+
},
133+
},
134+
required: ['is_background'],
135+
};
136+
137+
it('should coerce string "true" to boolean true', () => {
138+
const params = { is_background: 'true' };
139+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
140+
expect(params.is_background).toBe(true);
141+
});
142+
143+
it('should coerce string "True" to boolean true', () => {
144+
const params = { is_background: 'True' };
145+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
146+
expect(params.is_background).toBe(true);
147+
});
148+
149+
it('should coerce string "TRUE" to boolean true', () => {
150+
const params = { is_background: 'TRUE' };
151+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
152+
expect(params.is_background).toBe(true);
153+
});
154+
155+
it('should coerce string "false" to boolean false', () => {
156+
const params = { is_background: 'false' };
157+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
158+
expect(params.is_background).toBe(false);
159+
});
160+
161+
it('should coerce string "False" to boolean false', () => {
162+
const params = { is_background: 'False' };
163+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
164+
expect(params.is_background).toBe(false);
165+
});
166+
167+
it('should coerce string "FALSE" to boolean false', () => {
168+
const params = { is_background: 'FALSE' };
169+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
170+
expect(params.is_background).toBe(false);
171+
});
172+
173+
it('should handle nested objects with string booleans', () => {
174+
const nestedSchema = {
175+
type: 'object',
176+
properties: {
177+
options: {
178+
type: 'object',
179+
properties: {
180+
enabled: { type: 'boolean' },
181+
},
182+
},
183+
},
184+
};
185+
const params = { options: { enabled: 'true' } };
186+
expect(SchemaValidator.validate(nestedSchema, params)).toBeNull();
187+
expect((params.options as unknown as { enabled: boolean }).enabled).toBe(
188+
true,
189+
);
190+
});
191+
192+
it('should not affect non-boolean strings', () => {
193+
const mixedSchema = {
194+
type: 'object',
195+
properties: {
196+
name: { type: 'string' },
197+
is_active: { type: 'boolean' },
198+
},
199+
};
200+
const params = { name: 'trueman', is_active: 'true' };
201+
expect(SchemaValidator.validate(mixedSchema, params)).toBeNull();
202+
expect(params.name).toBe('trueman');
203+
expect(params.is_active).toBe(true);
204+
});
205+
206+
it('should pass through actual boolean values unchanged', () => {
207+
const params = { is_background: true };
208+
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
209+
expect(params.is_background).toBe(true);
210+
});
211+
});
125212
});

packages/core/src/utils/schemaValidator.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,12 @@ export class SchemaValidator {
4141
return 'Value of params must be an object';
4242
}
4343
const validate = ajValidator.compile(schema);
44-
const valid = validate(data);
44+
let valid = validate(data);
4545
if (!valid && validate.errors) {
46-
// Find any True or False values and lowercase them
47-
fixBooleanCasing(data as Record<string, unknown>);
48-
49-
const validate = ajValidator.compile(schema);
50-
const valid = validate(data);
46+
// Coerce string boolean values ("true"/"false") to actual booleans
47+
fixBooleanValues(data as Record<string, unknown>);
5148

49+
valid = validate(data);
5250
if (!valid && validate.errors) {
5351
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
5452
}
@@ -57,13 +55,29 @@ export class SchemaValidator {
5755
}
5856
}
5957

60-
function fixBooleanCasing(data: Record<string, unknown>) {
58+
/**
59+
* Coerces string boolean values to actual booleans.
60+
* This handles cases where LLMs return "true"/"false" strings instead of boolean values,
61+
* which is common with self-hosted LLMs.
62+
*
63+
* Converts:
64+
* - "true", "True", "TRUE" -> true
65+
* - "false", "False", "FALSE" -> false
66+
*/
67+
function fixBooleanValues(data: Record<string, unknown>) {
6168
for (const key of Object.keys(data)) {
6269
if (!(key in data)) continue;
70+
const value = data[key];
6371

64-
if (typeof data[key] === 'object') {
65-
fixBooleanCasing(data[key] as Record<string, unknown>);
66-
} else if (data[key] === 'True') data[key] = 'true';
67-
else if (data[key] === 'False') data[key] = 'false';
72+
if (typeof value === 'object' && value !== null) {
73+
fixBooleanValues(value as Record<string, unknown>);
74+
} else if (typeof value === 'string') {
75+
const lower = value.toLowerCase();
76+
if (lower === 'true') {
77+
data[key] = true;
78+
} else if (lower === 'false') {
79+
data[key] = false;
80+
}
81+
}
6882
}
6983
}

0 commit comments

Comments
 (0)