From 86320afa972e4233e30a77216a6f58a6bedbd2d2 Mon Sep 17 00:00:00 2001 From: Divakant Pandey Date: Mon, 26 Oct 2026 16:06:49 +0530 Subject: [PATCH 1/2] fix(cli): emit string enums as Type.String({ enum }) in Pi adapter (#2787) Signed-off-by: Divakant Pandey --- src/cli/client_adapter.c | 40 ++++++++++++++++++-------------------- tests/test_agent_clients.c | 23 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 22 deletions(-) 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..6d134a8b83 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. */ @@ -1239,7 +1243,23 @@ TEST(client_adapter_pi_wraps_result_and_honors_abort) { ASSERT_NOT_NULL( strstr(js, "[{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }]")); ASSERT_NOT_NULL(strstr(js, "signal?.addEventListener('abort', onAbort, { once: true })")); - ASSERT_NOT_NULL(strstr(js, "if (!child.killed) child.kill();")); + free(js); + 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(); } @@ -1405,6 +1425,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); From 74982efc60bb4b5ca49a2efdf7698f0f98991517 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 27 Oct 2026 23:41:04 +0100 Subject: [PATCH 2/2] test(cli): restore Pi abort-kill assertion dropped alongside #2787 test The #2787 change added client_adapter_pi_emits_string_enum_issue2787 directly below client_adapter_pi_wraps_result_and_honors_abort and, in the same hunk, removed that test's check that the generated abort handler kills the cli child (if (!child.killed) child.kill();). The generator still emits the line; restore the assertion byte-for-byte so the abort path keeps its coverage. Signed-off-by: Martin Vogel --- tests/test_agent_clients.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_agent_clients.c b/tests/test_agent_clients.c index 6d134a8b83..0d57723a07 100644 --- a/tests/test_agent_clients.c +++ b/tests/test_agent_clients.c @@ -1243,6 +1243,7 @@ TEST(client_adapter_pi_wraps_result_and_honors_abort) { ASSERT_NOT_NULL( strstr(js, "[{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }]")); ASSERT_NOT_NULL(strstr(js, "signal?.addEventListener('abort', onAbort, { once: true })")); + ASSERT_NOT_NULL(strstr(js, "if (!child.killed) child.kill();")); free(js); PASS(); }