|
4 | 4 | * Covers: JSON-RPC parsing, MCP protocol, tool dispatch, tool handlers. |
5 | 5 | */ |
6 | 6 | #include "../src/foundation/compat.h" |
| 7 | +#include <sqlite3.h> |
7 | 8 | #include "../src/foundation/compat_fs.h" /* cbm_unlink / cbm_rmdir */ |
8 | 9 | #include "../src/foundation/constants.h" |
9 | 10 | #include "../src/foundation/log.h" |
@@ -4964,6 +4965,97 @@ static int idx773_double_index_check(const char *dir_a, const char *dir_b) { |
4964 | 4965 | } |
4965 | 4966 | #endif /* !_WIN32 */ |
4966 | 4967 |
|
| 4968 | +/* #898: the SEQUENTIAL pipeline emitted malformed JSON for brokered |
| 4969 | + * ASYNC_CALLS edges ("broker":"bullmq} — missing closing quote) and stored |
| 4970 | + * the RAW broker/method string as the synthesized Route node's properties |
| 4971 | + * (literally `bullmq` instead of {"broker":"bullmq"}). json_extract over |
| 4972 | + * those rows errors, generated-column indexes fail, and PRAGMA quick_check |
| 4973 | + * aborts with "malformed JSON" — which since the artifact deep-integrity |
| 4974 | + * check also means such caches are refused at import. The parallel path |
| 4975 | + * was correct; both pipelines must emit identical, valid JSON. */ |
| 4976 | +TEST(sequential_service_edge_props_are_valid_json_issue898) { |
| 4977 | + char tmp[CBM_SZ_256]; |
| 4978 | + snprintf(tmp, sizeof(tmp), "/tmp/cbm_seq898_XXXXXX"); |
| 4979 | + if (!cbm_mkdtemp(tmp)) { |
| 4980 | + FAIL("mkdtemp failed"); |
| 4981 | + } |
| 4982 | + char src_path[CBM_SZ_512]; |
| 4983 | + snprintf(src_path, sizeof(src_path), "%s/queue.py", tmp); |
| 4984 | + FILE *f = fopen(src_path, "w"); |
| 4985 | + ASSERT_NOT_NULL(f); |
| 4986 | + /* celery.Celery("tasks") resolves through the import map to a QN the |
| 4987 | + * service-pattern table classifies as ASYNC with broker "celery". */ |
| 4988 | + fputs("import celery\n" |
| 4989 | + "\n" |
| 4990 | + "def enqueue():\n" |
| 4991 | + " celery.Celery(\"tasks\")\n", |
| 4992 | + f); |
| 4993 | + fclose(f); |
| 4994 | + |
| 4995 | + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); |
| 4996 | + ASSERT_NOT_NULL(srv); |
| 4997 | + char args[CBM_SZ_512]; |
| 4998 | + snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", tmp); |
| 4999 | + char *resp = cbm_mcp_handle_tool(srv, "index_repository", args); |
| 5000 | + ASSERT_NOT_NULL(resp); |
| 5001 | + ASSERT_NOT_NULL(strstr(resp, "indexed")); |
| 5002 | + free(resp); |
| 5003 | + |
| 5004 | + cbm_store_t *store = cbm_mcp_server_store(srv); |
| 5005 | + ASSERT_NOT_NULL(store); |
| 5006 | + struct sqlite3 *db = cbm_store_get_db(store); |
| 5007 | + ASSERT_NOT_NULL(db); |
| 5008 | + |
| 5009 | + /* Non-vacuous: the fixture must actually produce a brokered edge. */ |
| 5010 | + sqlite3_stmt *stmt = NULL; |
| 5011 | + ASSERT_EQ(sqlite3_prepare_v2(db, "SELECT count(*) FROM edges WHERE type='ASYNC_CALLS';", -1, |
| 5012 | + &stmt, NULL), |
| 5013 | + SQLITE_OK); |
| 5014 | + ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); |
| 5015 | + int async_edges = sqlite3_column_int(stmt, 0); |
| 5016 | + sqlite3_finalize(stmt); |
| 5017 | + ASSERT_TRUE(async_edges >= 1); |
| 5018 | + |
| 5019 | + /* THE BUG: malformed properties on edges (broker quote) and Route nodes |
| 5020 | + * (raw string). Every properties blob must be valid JSON. */ |
| 5021 | + ASSERT_EQ(sqlite3_prepare_v2(db, |
| 5022 | + "SELECT count(*) FROM edges WHERE properties IS NOT NULL " |
| 5023 | + "AND properties != '' AND json_valid(properties)=0;", |
| 5024 | + -1, &stmt, NULL), |
| 5025 | + SQLITE_OK); |
| 5026 | + ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); |
| 5027 | + int bad_edges = sqlite3_column_int(stmt, 0); |
| 5028 | + sqlite3_finalize(stmt); |
| 5029 | + ASSERT_EQ(bad_edges, 0); |
| 5030 | + |
| 5031 | + ASSERT_EQ(sqlite3_prepare_v2(db, |
| 5032 | + "SELECT count(*) FROM nodes WHERE properties IS NOT NULL " |
| 5033 | + "AND properties != '' AND json_valid(properties)=0;", |
| 5034 | + -1, &stmt, NULL), |
| 5035 | + SQLITE_OK); |
| 5036 | + ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); |
| 5037 | + int bad_nodes = sqlite3_column_int(stmt, 0); |
| 5038 | + sqlite3_finalize(stmt); |
| 5039 | + ASSERT_EQ(bad_nodes, 0); |
| 5040 | + |
| 5041 | + /* Pipeline parity: the broker must be extractable exactly like the |
| 5042 | + * parallel path emits it. */ |
| 5043 | + ASSERT_EQ(sqlite3_prepare_v2(db, |
| 5044 | + "SELECT count(*) FROM edges WHERE type='ASYNC_CALLS' AND " |
| 5045 | + "json_extract(properties,'$.broker')='celery';", |
| 5046 | + -1, &stmt, NULL), |
| 5047 | + SQLITE_OK); |
| 5048 | + ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); |
| 5049 | + int brokered = sqlite3_column_int(stmt, 0); |
| 5050 | + sqlite3_finalize(stmt); |
| 5051 | + ASSERT_TRUE(brokered >= 1); |
| 5052 | + |
| 5053 | + cbm_mcp_server_free(srv); |
| 5054 | + unlink(src_path); |
| 5055 | + cbm_rmdir(tmp); |
| 5056 | + PASS(); |
| 5057 | +} |
| 5058 | + |
4967 | 5059 | TEST(index_second_inprocess_run_survives_issue773) { |
4968 | 5060 | #ifdef _WIN32 |
4969 | 5061 | SKIP_PLATFORM("fork-isolated crash guard (POSIX-only)"); |
@@ -5615,6 +5707,7 @@ SUITE(mcp) { |
5615 | 5707 | RUN_TEST(index_repository_cli_name_override_issue823); |
5616 | 5708 | RUN_TEST(index_supervisor_gate_requires_marked_host_issue845); |
5617 | 5709 | RUN_TEST(index_bg_paths_route_through_supervisor_issue832); |
| 5710 | + RUN_TEST(sequential_service_edge_props_are_valid_json_issue898); |
5618 | 5711 | RUN_TEST(index_second_inprocess_run_survives_issue773); |
5619 | 5712 | RUN_TEST(index_recovery_parallel_quarantines_crasher); |
5620 | 5713 | RUN_TEST(tool_manage_adr_not_found_rich_error); |
|
0 commit comments