@@ -2911,6 +2911,59 @@ TEST(cypher_multi_prop_projection_no_alias) {
29112911 * forked child so a stack overwrite (ASan abort, or a raw segfault) shows up
29122912 * as a killing signal instead of taking down the whole runner; the bounded
29132913 * path returns an ordinary error and the child exits cleanly. */
2914+ /* Property projection must return the WHOLE value of composite properties.
2915+ * json_extract_prop() scanned a non-string value up to the first ',' — so an
2916+ * array/object property was truncated at its first INTERNAL comma. Real-world
2917+ * hit: a NestJS handler's decorators
2918+ * ["@Roles('OWNER', 'ADMIN')","@Get()"]
2919+ * projected as ["@Roles('OWNER' — unusable for route/authz queries. */
2920+ TEST (cypher_exec_prop_array_with_internal_commas ) {
2921+ cbm_store_t * s = cbm_store_open_memory ();
2922+ cbm_store_upsert_project (s , "test" , "/tmp/test" );
2923+ cbm_node_t n = {.project = "test" ,
2924+ .label = "Method" ,
2925+ .name = "findAll" ,
2926+ .qualified_name = "test.PacienteController.findAll" ,
2927+ .file_path = "paciente.controller.ts" ,
2928+ .properties_json =
2929+ "{\"decorators\":[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"],\"lines\":3}" };
2930+ cbm_store_upsert_node (s , & n );
2931+
2932+ cbm_cypher_result_t r = {0 };
2933+ int rc = cbm_cypher_execute (s , "MATCH (m:Method) RETURN m.decorators, m.lines" , "test" , 0 , & r );
2934+ ASSERT_EQ (rc , 0 );
2935+ ASSERT_EQ (r .row_count , 1 );
2936+ /* whole array, commas and all — was ["@Roles('OWNER' before the fix */
2937+ ASSERT_STR_EQ (r .rows [0 ][0 ], "[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"]" );
2938+ ASSERT_STR_EQ (r .rows [0 ][1 ], "3" ); /* scalar sibling still parses */
2939+ cbm_cypher_result_free (& r );
2940+ cbm_store_close (s );
2941+ PASS ();
2942+ }
2943+
2944+ /* A string property must not end at an ESCAPED quote: the scan stopped at the
2945+ * first '"' regardless of a preceding backslash, cutting the value short. */
2946+ TEST (cypher_exec_prop_string_with_escaped_quote ) {
2947+ cbm_store_t * s = cbm_store_open_memory ();
2948+ cbm_store_upsert_project (s , "test" , "/tmp/test" );
2949+ cbm_node_t n = {.project = "test" ,
2950+ .label = "Function" ,
2951+ .name = "parse" ,
2952+ .qualified_name = "test.parse" ,
2953+ .file_path = "p.ts" ,
2954+ .properties_json = "{\"signature\":\"(sep: \\\"a,b\\\") => void\"}" };
2955+ cbm_store_upsert_node (s , & n );
2956+
2957+ cbm_cypher_result_t r = {0 };
2958+ int rc = cbm_cypher_execute (s , "MATCH (f:Function) RETURN f.signature" , "test" , 0 , & r );
2959+ ASSERT_EQ (rc , 0 );
2960+ ASSERT_EQ (r .row_count , 1 );
2961+ ASSERT_STR_EQ (r .rows [0 ][0 ], "(sep: \\\"a,b\\\") => void" ); /* was: (sep: \ */
2962+ cbm_cypher_result_free (& r );
2963+ cbm_store_close (s );
2964+ PASS ();
2965+ }
2966+
29142967TEST (cypher_wide_return_projection_bounded ) {
29152968#ifdef _WIN32
29162969 SKIP_PLATFORM ("fork crash-isolation is POSIX-only; the parse-time bound is platform-agnostic" );
@@ -3116,4 +3169,7 @@ SUITE(cypher) {
31163169 RUN_TEST (cypher_parse_unwind );
31173170 RUN_TEST (cypher_parse_unwind_var );
31183171 RUN_TEST (cypher_wide_return_projection_bounded );
3172+ /* Composite property projection (arrays/objects, escaped quotes) */
3173+ RUN_TEST (cypher_exec_prop_array_with_internal_commas );
3174+ RUN_TEST (cypher_exec_prop_string_with_escaped_quote );
31193175}
0 commit comments