@@ -6170,6 +6170,246 @@ TEST(daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daem
61706170 PASS ();
61716171}
61726172
6173+ /* ── #2134 x #2144: the unnamed status poll resolves the start's key ────────
6174+ *
6175+ * An unnamed index_repository of a root that another indexed project already
6176+ * owns adopts that owner's name (#2134), so the daemon keys the async job by
6177+ * it. The async reply tells the client to poll "with the same repo_path and
6178+ * status: true"; that unnamed poll must resolve the same key (owner first,
6179+ * then path-derived; several owners fail loudly), or it never finds the job
6180+ * it just started. Owners are real project databases in a private cache; the
6181+ * held fake worker never writes one, so the seeded state is all there is. */
6182+
6183+ enum { APP_OWNER_CANONICAL_CAP = 4096 }; /* cbm_canonical_path needs >= 4096 */
6184+
6185+ typedef struct {
6186+ char cache [APP_TEST_PATH_CAP ];
6187+ char canonical_root [APP_OWNER_CANONICAL_CAP ];
6188+ char * saved_cache ;
6189+ bool had_cache ;
6190+ bool cache_set ;
6191+ app_async_fixture_t fixture ;
6192+ cbm_daemon_runtime_application_session_t * session ;
6193+ } app_owner_fixture_t ;
6194+
6195+ typedef struct {
6196+ char * started ;
6197+ char * running ;
6198+ char * finished ;
6199+ bool worker_running ;
6200+ bool drained ;
6201+ } app_owner_run_t ;
6202+
6203+ static bool app_owner_fixture_init (app_owner_fixture_t * owner , const char * label ) {
6204+ memset (owner , 0 , sizeof (* owner ));
6205+ const char * old_cache = getenv ("CBM_CACHE_DIR" );
6206+ owner -> had_cache = old_cache != NULL ;
6207+ owner -> saved_cache = old_cache ? strdup (old_cache ) : NULL ;
6208+ (void )snprintf (owner -> cache , sizeof (owner -> cache ), "%s/cbm-app-owner-cache-XXXXXX" ,
6209+ cbm_tmpdir ());
6210+ if ((owner -> had_cache && !owner -> saved_cache ) || !cbm_mkdtemp (owner -> cache )) {
6211+ owner -> cache [0 ] = '\0' ;
6212+ return false;
6213+ }
6214+ owner -> cache_set = cbm_setenv ("CBM_CACHE_DIR" , owner -> cache , 1 ) == 0 ;
6215+ if (!owner -> cache_set || !app_async_fixture_init (& owner -> fixture , label , true) ||
6216+ !cbm_canonical_path (owner -> fixture .root , owner -> canonical_root ,
6217+ sizeof (owner -> canonical_root ))) {
6218+ return false;
6219+ }
6220+ /* Stored root_path values use forward slashes on every platform. */
6221+ cbm_normalize_path_sep (owner -> canonical_root );
6222+ return true;
6223+ }
6224+
6225+ /* One indexed project whose stored root is this fixture's root. */
6226+ static bool app_owner_seed (const app_owner_fixture_t * owner , const char * project ) {
6227+ char path [APP_TEST_PATH_CAP ];
6228+ int written = snprintf (path , sizeof (path ), "%s/%s.db" , owner -> cache , project );
6229+ if (written <= 0 || (size_t )written >= sizeof (path )) {
6230+ return false;
6231+ }
6232+ cbm_store_t * store = cbm_store_open_path (path );
6233+ bool seeded =
6234+ store && cbm_store_upsert_project (store , project , owner -> canonical_root ) == CBM_STORE_OK ;
6235+ cbm_store_close (store );
6236+ return seeded ;
6237+ }
6238+
6239+ /* An unnamed async start, then unnamed status polls naming the same
6240+ * repo_path: one while the held worker runs, one after it drained. */
6241+ static void app_owner_run (app_owner_fixture_t * owner , app_owner_run_t * run ) {
6242+ memset (run , 0 , sizeof (* run ));
6243+ app_async_fixture_t * fixture = & owner -> fixture ;
6244+ run -> started = app_async_call_now (fixture , owner -> session , ",\"async\":true" );
6245+ run -> worker_running = run -> started && !app_async_contains (run -> started , "\"isError\":true" ) &&
6246+ app_wait_for_atomic_int (& fixture -> fake .starts , 1 );
6247+ run -> running = app_async_call_now (fixture , owner -> session , ",\"status\":true" );
6248+ atomic_store (& fixture -> fake .allow_completion , true);
6249+ run -> drained = run -> worker_running && app_wait_for_active_jobs (fixture -> application , 0 );
6250+ run -> finished =
6251+ run -> drained ? app_async_call_now (fixture , owner -> session , ",\"status\":true" ) : NULL ;
6252+ }
6253+
6254+ static void app_owner_run_free (app_owner_run_t * run ) {
6255+ free (run -> started );
6256+ free (run -> running );
6257+ free (run -> finished );
6258+ memset (run , 0 , sizeof (* run ));
6259+ }
6260+
6261+ static bool app_owner_fixture_finish (app_owner_fixture_t * owner ) {
6262+ if (owner -> session ) {
6263+ owner -> fixture .callbacks .session_close (owner -> fixture .callbacks .context , owner -> session );
6264+ owner -> session = NULL ;
6265+ }
6266+ bool stopped = app_async_fixture_finish (& owner -> fixture );
6267+ if (owner -> cache_set ) {
6268+ if (owner -> had_cache ) {
6269+ (void )cbm_setenv ("CBM_CACHE_DIR" , owner -> saved_cache , 1 );
6270+ } else {
6271+ (void )cbm_unsetenv ("CBM_CACHE_DIR" );
6272+ }
6273+ }
6274+ free (owner -> saved_cache );
6275+ owner -> saved_cache = NULL ;
6276+ if (owner -> cache [0 ]) {
6277+ (void )th_rmtree (owner -> cache );
6278+ }
6279+ return stopped ;
6280+ }
6281+
6282+ static bool app_owner_names_project (const char * response , const char * project ) {
6283+ char needle [APP_TEST_PATH_CAP ];
6284+ int written = snprintf (needle , sizeof (needle ), "\"project\":\"%s\"" , project );
6285+ return written > 0 && (size_t )written < sizeof (needle ) && app_async_contains (response , needle );
6286+ }
6287+
6288+ TEST (daemon_application_unnamed_status_finds_async_job_of_root_owner_issue2134 ) {
6289+ app_owner_fixture_t owner ;
6290+ bool setup =
6291+ app_owner_fixture_init (& owner , "owner-poll" ) && app_owner_seed (& owner , "owned-2134s" );
6292+ owner .session = setup ? app_async_session (& owner .fixture , 7801 ) : NULL ;
6293+ bool session_opened = owner .session != NULL ;
6294+ app_owner_run_t run ;
6295+ memset (& run , 0 , sizeof (run ));
6296+ if (session_opened ) {
6297+ app_owner_run (& owner , & run );
6298+ }
6299+ int cancels = atomic_load (& owner .fixture .fake .cancels );
6300+ bool stopped = app_owner_fixture_finish (& owner );
6301+ if (!app_async_contains (run .running , "\"state\":\"running\"" )) {
6302+ fprintf (stderr , " [2134x2144] unnamed status missed the owner's job: %.400s\n" ,
6303+ run .running ? run .running : "(null)" );
6304+ }
6305+
6306+ ASSERT_TRUE (setup );
6307+ ASSERT_TRUE (session_opened );
6308+ /* #2134: the unnamed start adopted the owner; the daemon keyed its job so. */
6309+ ASSERT_TRUE (app_owner_names_project (run .started , "owned-2134s" ));
6310+ ASSERT_TRUE (run .worker_running );
6311+ /* The unnamed poll with the same repo_path reports that live job ... */
6312+ ASSERT_FALSE (app_async_contains (run .running , "\"isError\":true" ));
6313+ ASSERT_TRUE (app_owner_names_project (run .running , "owned-2134s" ));
6314+ ASSERT_TRUE (app_async_contains (run .running , "\"state\":\"running\"" ));
6315+ /* ... and, once drained, the outcome recorded under the same key. */
6316+ ASSERT_TRUE (run .drained );
6317+ ASSERT_TRUE (app_owner_names_project (run .finished , "owned-2134s" ));
6318+ ASSERT_TRUE (app_async_contains (run .finished , "\"state\":\"succeeded\"" ));
6319+ ASSERT_EQ (cancels , 0 );
6320+ ASSERT_TRUE (stopped );
6321+ app_owner_run_free (& run );
6322+ PASS ();
6323+ }
6324+
6325+ /* Control: the path-derived key is untouched where #2134 keeps it - a root no
6326+ * other project owns, and a root whose path-derived project exists even though
6327+ * a named twin shares it. Start and poll name the same project either way. */
6328+ TEST (daemon_application_unnamed_status_keeps_path_derived_key_issue2134 ) {
6329+ app_owner_fixture_t fresh ;
6330+ bool fresh_setup = app_owner_fixture_init (& fresh , "owner-fresh" );
6331+ fresh .session = fresh_setup ? app_async_session (& fresh .fixture , 7811 ) : NULL ;
6332+ char * fresh_project = fresh .fixture .project ? strdup (fresh .fixture .project ) : NULL ;
6333+ app_owner_run_t fresh_run ;
6334+ memset (& fresh_run , 0 , sizeof (fresh_run ));
6335+ if (fresh .session ) {
6336+ app_owner_run (& fresh , & fresh_run );
6337+ }
6338+ bool fresh_stopped = app_owner_fixture_finish (& fresh );
6339+
6340+ app_owner_fixture_t twin ;
6341+ bool twin_setup = app_owner_fixture_init (& twin , "owner-twin" ) && twin .fixture .project &&
6342+ app_owner_seed (& twin , twin .fixture .project ) &&
6343+ app_owner_seed (& twin , "twin-2134s" );
6344+ twin .session = twin_setup ? app_async_session (& twin .fixture , 7812 ) : NULL ;
6345+ char * twin_project = twin .fixture .project ? strdup (twin .fixture .project ) : NULL ;
6346+ app_owner_run_t twin_run ;
6347+ memset (& twin_run , 0 , sizeof (twin_run ));
6348+ if (twin .session ) {
6349+ app_owner_run (& twin , & twin_run );
6350+ }
6351+ bool twin_stopped = app_owner_fixture_finish (& twin );
6352+
6353+ ASSERT_TRUE (fresh_setup );
6354+ ASSERT_NOT_NULL (fresh_project );
6355+ ASSERT_TRUE (app_owner_names_project (fresh_run .started , fresh_project ));
6356+ ASSERT_TRUE (fresh_run .worker_running );
6357+ ASSERT_TRUE (app_owner_names_project (fresh_run .running , fresh_project ));
6358+ ASSERT_TRUE (app_async_contains (fresh_run .running , "\"state\":\"running\"" ));
6359+ ASSERT_TRUE (app_async_contains (fresh_run .finished , "\"state\":\"succeeded\"" ));
6360+ ASSERT_TRUE (fresh_stopped );
6361+
6362+ ASSERT_TRUE (twin_setup );
6363+ ASSERT_NOT_NULL (twin_project );
6364+ ASSERT_TRUE (app_owner_names_project (twin_run .started , twin_project ));
6365+ ASSERT_FALSE (app_owner_names_project (twin_run .started , "twin-2134s" ));
6366+ ASSERT_TRUE (twin_run .worker_running );
6367+ ASSERT_TRUE (app_owner_names_project (twin_run .running , twin_project ));
6368+ ASSERT_TRUE (app_async_contains (twin_run .running , "\"state\":\"running\"" ));
6369+ ASSERT_TRUE (app_async_contains (twin_run .finished , "\"state\":\"succeeded\"" ));
6370+ ASSERT_TRUE (twin_stopped );
6371+ free (fresh_project );
6372+ free (twin_project );
6373+ app_owner_run_free (& fresh_run );
6374+ app_owner_run_free (& twin_run );
6375+ PASS ();
6376+ }
6377+
6378+ /* Control: several owners and no path-derived project is ambiguous. The start
6379+ * still refuses and names them without spawning a worker, and the poll gives
6380+ * the same loud error instead of guessing (or pointing at a path-derived
6381+ * project nobody indexes). */
6382+ TEST (daemon_application_unnamed_async_and_status_refuse_ambiguous_root_issue2134 ) {
6383+ app_owner_fixture_t owner ;
6384+ bool setup = app_owner_fixture_init (& owner , "owner-ambiguous" ) &&
6385+ app_owner_seed (& owner , "first-2134s" ) && app_owner_seed (& owner , "second-2134s" );
6386+ owner .session = setup ? app_async_session (& owner .fixture , 7821 ) : NULL ;
6387+ bool session_opened = owner .session != NULL ;
6388+ app_owner_run_t run ;
6389+ memset (& run , 0 , sizeof (run ));
6390+ if (session_opened ) {
6391+ app_owner_run (& owner , & run );
6392+ }
6393+ int starts = atomic_load (& owner .fixture .fake .starts );
6394+ bool stopped = app_owner_fixture_finish (& owner );
6395+
6396+ ASSERT_TRUE (setup );
6397+ ASSERT_TRUE (session_opened );
6398+ ASSERT_TRUE (app_async_contains (run .started , "\"isError\":true" ));
6399+ ASSERT_TRUE (app_async_contains (run .started , "several indexed projects share root_path" ));
6400+ ASSERT_TRUE (app_async_contains (run .started , "first-2134s" ));
6401+ ASSERT_TRUE (app_async_contains (run .started , "second-2134s" ));
6402+ ASSERT_FALSE (run .worker_running );
6403+ ASSERT_EQ (starts , 0 );
6404+ ASSERT_TRUE (app_async_contains (run .running , "\"isError\":true" ));
6405+ ASSERT_TRUE (app_async_contains (run .running , "several indexed projects share root_path" ));
6406+ ASSERT_TRUE (app_async_contains (run .running , "first-2134s" ));
6407+ ASSERT_TRUE (app_async_contains (run .running , "second-2134s" ));
6408+ ASSERT_TRUE (stopped );
6409+ app_owner_run_free (& run );
6410+ PASS ();
6411+ }
6412+
61736413SUITE (daemon_application ) {
61746414 RUN_TEST (daemon_application_oversized_reply_is_a_jsonrpc_error_not_a_death );
61756415 RUN_TEST (daemon_application_new_session_does_not_retain_initial_store );
@@ -6197,6 +6437,9 @@ SUITE(daemon_application) {
61976437 RUN_TEST (daemon_application_cancelled_sync_index_reply_offers_async );
61986438 RUN_TEST (daemon_application_cut_short_sync_index_advice_reaches_next_call );
61996439 RUN_TEST (daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daemon );
6440+ RUN_TEST (daemon_application_unnamed_status_finds_async_job_of_root_owner_issue2134 );
6441+ RUN_TEST (daemon_application_unnamed_status_keeps_path_derived_key_issue2134 );
6442+ RUN_TEST (daemon_application_unnamed_async_and_status_refuse_ambiguous_root_issue2134 );
62006443 RUN_TEST (daemon_application_programmatic_index_injects_resource_policy );
62016444 RUN_TEST (daemon_application_auto_index_honors_tracked_file_limit );
62026445 RUN_TEST (daemon_application_auto_index_file_count_handles_literal_metacharacter_path );
0 commit comments