mirror of
https://github.com/we-promise/sure.git
synced 2026-09-03 05:41:18 +00:00
* fix(ai): drop empty data-driven enums from assistant function schemas
Enum values in tool schemas are built from family data (account names,
categories, merchants, tags, tickers). A family with none of these gets
enum: [], which is invalid JSON Schema. OpenAI tolerates it, but strict
OpenAI-compatible providers reject the entire request, breaking chat for
fresh families until they create a tag or merchant.
Prune empty enums in build_schema, falling back to a plain string. One
choke point covers every function and both consumers: chat tool
definitions for all providers, and the /mcp endpoint's tools/list.
* fix(ai): address review feedback on enum pruning
Stop recursion at populated enum values: enum members are literal
values, not subschemas, so a literal like enum: [{ enum: [] }] must be
preserved verbatim rather than rewritten.
Also cover PREVIEW_FUNCTION_CLASSES in the registry regression test by
enabling the preview preference on the test user, with a guard assertion
so the test fails if preview functions ever silently drop out.
109 lines
3.2 KiB
Ruby
109 lines
3.2 KiB
Ruby
require "test_helper"
|
|
|
|
class Assistant::FunctionTest < ActiveSupport::TestCase
|
|
class EmptyEnumFunction < Assistant::Function
|
|
class << self
|
|
def name
|
|
"empty_enum_function"
|
|
end
|
|
|
|
def description
|
|
"Test function with data-driven enums"
|
|
end
|
|
end
|
|
|
|
def call(params = {})
|
|
{}
|
|
end
|
|
|
|
def params_schema
|
|
build_schema(
|
|
required: [ "name" ],
|
|
properties: {
|
|
name: {
|
|
type: "string",
|
|
description: "Property-level enum built from empty user data",
|
|
enum: []
|
|
},
|
|
accounts: {
|
|
type: "array",
|
|
description: "Items-level enum built from empty user data",
|
|
items: { enum: [] },
|
|
minItems: 1,
|
|
uniqueItems: true
|
|
},
|
|
order: {
|
|
enum: [ "asc", "desc" ],
|
|
description: "Static enum that must be preserved"
|
|
}
|
|
}
|
|
)
|
|
end
|
|
end
|
|
|
|
setup do
|
|
@function = EmptyEnumFunction.new(users(:family_admin))
|
|
end
|
|
|
|
test "drops empty enums so strict providers accept the schema" do
|
|
properties = @function.to_definition[:params_schema][:properties]
|
|
|
|
refute properties[:name].key?(:enum)
|
|
assert_equal "string", properties[:name][:type]
|
|
|
|
refute properties[:accounts][:items].key?(:enum)
|
|
assert_equal "string", properties[:accounts][:items][:type]
|
|
assert_equal 1, properties[:accounts][:minItems]
|
|
end
|
|
|
|
test "preserves populated enums and surrounding schema" do
|
|
schema = @function.to_definition[:params_schema]
|
|
|
|
assert_equal [ "asc", "desc" ], schema[:properties][:order][:enum]
|
|
assert_equal [ "name" ], schema[:required]
|
|
assert_equal "object", schema[:type]
|
|
end
|
|
|
|
test "keeps populated enum values verbatim, including literal objects and arrays" do
|
|
schema = {
|
|
status: { enum: [ { enum: [] }, [ "nested" ] ] },
|
|
order: { enum: [ "asc" ] }
|
|
}
|
|
|
|
pruned = @function.send(:prune_empty_enums, schema)
|
|
|
|
assert_equal [ { enum: [] }, [ "nested" ] ], pruned[:status][:enum]
|
|
assert_equal [ "asc" ], pruned[:order][:enum]
|
|
end
|
|
|
|
test "no registered function emits an empty enum" do
|
|
user = users(:family_admin)
|
|
user.update!(preferences: (user.preferences || {}).merge("preview_features_enabled" => true))
|
|
|
|
function_classes = Assistant.function_classes(user)
|
|
assert_empty Assistant::PREVIEW_FUNCTION_CLASSES - function_classes,
|
|
"expected preview functions to be included in the assertion"
|
|
|
|
function_classes.each do |function_class|
|
|
definition = function_class.new(user).to_definition
|
|
assert_no_empty_enums definition[:params_schema], function_class.name
|
|
end
|
|
end
|
|
|
|
private
|
|
def assert_no_empty_enums(node, function_name, path = "params_schema")
|
|
case node
|
|
when Hash
|
|
node.each do |key, value|
|
|
refute key.to_sym == :enum && value == [],
|
|
"#{function_name} emits an empty enum at #{path}.#{key}, which is invalid JSON Schema"
|
|
assert_no_empty_enums(value, function_name, "#{path}.#{key}")
|
|
end
|
|
when Array
|
|
node.each_with_index do |value, index|
|
|
assert_no_empty_enums(value, function_name, "#{path}[#{index}]")
|
|
end
|
|
end
|
|
end
|
|
end
|