diff --git a/src/cli/client_adapter.c b/src/cli/client_adapter.c index 83a4042764..cfd2719c78 100644 --- a/src/cli/client_adapter.c +++ b/src/cli/client_adapter.c @@ -148,38 +148,36 @@ static void append_typebox_type(adapter_sb_t *sb, yyjson_val *val, const char *d if (type_str && strcmp(type_str, "string") == 0 && enum_val && yyjson_is_arr(enum_val) && yyjson_arr_size(enum_val) > 0) { - sb_append(sb, "Type.Union(["); size_t enum_idx = 0; size_t enum_max = yyjson_arr_size(enum_val); - bool first_enum = true; + size_t valid_count = 0; for (enum_idx = 0; enum_idx < enum_max; enum_idx++) { - yyjson_val *enum_item = yyjson_arr_get(enum_val, enum_idx); - const char *enum_str = yyjson_get_str(enum_item); - if (!enum_str) { - continue; + if (yyjson_get_str(yyjson_arr_get(enum_val, enum_idx))) { + valid_count++; } - if (!first_enum) { - sb_append(sb, ", "); - } - first_enum = false; - sb_append(sb, "Type.Literal("); - sb_append_js_string(sb, enum_str); - sb_append(sb, ")"); } - if (first_enum) { - sb_append(sb, "Type.String()"); - } else { + if (valid_count > 0) { + sb_append(sb, "Type.String({ enum: ["); + bool first_enum = true; + for (enum_idx = 0; enum_idx < enum_max; enum_idx++) { + const char *enum_str = yyjson_get_str(yyjson_arr_get(enum_val, enum_idx)); + if (!enum_str) { + continue; + } + if (!first_enum) { + sb_append(sb, ", "); + } + first_enum = false; + sb_append_js_string(sb, enum_str); + } sb_append(sb, "]"); if (include_description && description) { - sb_append(sb, ", { description: "); + sb_append(sb, ", description: "); sb_append_js_string(sb, description); - sb_append(sb, " }"); } - sb_append(sb, ")"); + sb_append(sb, " })"); return; } - sb_append(sb, ")"); - return; } const char *typebox_name = "Any"; diff --git a/tests/test_agent_clients.c b/tests/test_agent_clients.c index f1bc3699e1..0d57723a07 100644 --- a/tests/test_agent_clients.c +++ b/tests/test_agent_clients.c @@ -1177,6 +1177,10 @@ TEST(client_adapter_pi_emits_parameters_and_execute) { /* TypeBox parameters schema is embedded with Type.Object syntax. */ ASSERT_NOT_NULL(strstr(js, "Type.Object(")); ASSERT_NOT_NULL(strstr(js, "Type.String(")); + /* #2787: String enums must use Type.String({ enum: [...] }) without anyOf/const */ + ASSERT_NOT_NULL(strstr(js, "Type.String({ enum: [")); + ASSERT_NULL(strstr(js, "Type.Union([Type.Literal")); + ASSERT_NULL(strstr(js, "Type.Literal(")); /* A numeric registry property must remain numeric in the generated schema. */ ASSERT_NOT_NULL(strstr(js, "depth: Type.Optional(Type.Integer(")); /* TypeBox import should be present. */ @@ -1244,6 +1248,23 @@ TEST(client_adapter_pi_wraps_result_and_honors_abort) { PASS(); } +/* #2787: Pi extension must emit string enums as Type.String({ enum: [...] }) + * rather than Type.Union([Type.Literal(...)]) which TypeBox serializes as + * anyOf/const, breaking Gemini / Antigravity function declaration validation. */ +TEST(client_adapter_pi_emits_string_enum_issue2787) { + char *js = cbm_client_adapter_pi("/usr/local/bin/codebase-memory-mcp"); + ASSERT_NOT_NULL(js); + ASSERT_NULL(strstr(js, "Type.Union([Type.Literal")); + ASSERT_NULL(strstr(js, "Type.Literal(")); + ASSERT_NOT_NULL(strstr(js, "Type.String({ enum: [")); + ASSERT_NOT_NULL(strstr(js, "'full'")); + ASSERT_NOT_NULL(strstr(js, "'moderate'")); + ASSERT_NOT_NULL(strstr(js, "'fast'")); + ASSERT_NOT_NULL(strstr(js, "'cross-repo-intelligence'")); + free(js); + PASS(); +} + /* #616 rejected `"` in its path template but not `\`, so a Windows home like * C:\Users\urs\bin produced `\u` — an invalid unicode escape — and the whole * auto-loaded plugin failed to parse. A broken plugin in an auto-load directory @@ -1405,6 +1426,7 @@ SUITE(agent_clients) { RUN_TEST(client_adapter_pi_emits_parameters_and_execute); RUN_TEST(client_adapter_pi_sends_arguments_over_stdin_issue1834); RUN_TEST(client_adapter_pi_wraps_result_and_honors_abort); + RUN_TEST(client_adapter_pi_emits_string_enum_issue2787); RUN_TEST(client_adapter_escapes_windows_paths_and_quotes); RUN_TEST(client_adapter_opencode_sends_the_required_hook_event); RUN_TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction);