diff --git a/src/daemon/application.c b/src/daemon/application.c index 12543808e..69ff52ec7 100644 --- a/src/daemon/application.c +++ b/src/daemon/application.c @@ -374,6 +374,11 @@ static bool application_regular_db_exists(const char *project) { return stat(path, &status) == 0 && S_ISREG(status.st_mode); } +static bool application_canonical_directory_exists(const char *path) { + cbm_path_info_t info = {0}; + return cbm_path_info_utf8(path, &info) == 0 && info.is_directory; +} + static cbm_daemon_application_watch_t *application_find_watch_locked( cbm_daemon_application_t *application, const char *project) { for (cbm_daemon_application_watch_t *watch = application->watches; watch; watch = watch->next) { @@ -2354,9 +2359,7 @@ static cbm_daemon_runtime_application_status_t application_set_context( if (canonical && allowed_present) { canonical = cbm_canonical_path(allowed, canonical_allowed, sizeof(canonical_allowed)); } - struct stat root_status; - canonical = - canonical && stat(canonical_root, &root_status) == 0 && S_ISDIR(root_status.st_mode); + canonical = canonical && application_canonical_directory_exists(canonical_root); bool set = canonical && cbm_mcp_server_set_session_context(session->mcp, canonical_root, allowed_present ? canonical_allowed : NULL); @@ -3369,9 +3372,8 @@ static int application_background_index(cbm_daemon_application_t *application, return -1; } char canonical_root[APPLICATION_PATH_CAP]; - struct stat root_status; if (!cbm_canonical_path(root_path, canonical_root, sizeof(canonical_root)) || - stat(canonical_root, &root_status) != 0 || !S_ISDIR(root_status.st_mode)) { + !application_canonical_directory_exists(canonical_root)) { return -1; } yyjson_mut_doc *document = yyjson_mut_doc_new(NULL); diff --git a/tests/test_daemon_application.c b/tests/test_daemon_application.c index e0ef6b459..fceeb7130 100644 --- a/tests/test_daemon_application.c +++ b/tests/test_daemon_application.c @@ -714,6 +714,45 @@ TEST(daemon_application_requires_immutable_explicit_context) { PASS(); } +TEST(daemon_application_accepts_utf8_session_context_root) { + char root[APP_TEST_PATH_CAP]; + snprintf(root, sizeof(root), "%s/cbm-app-context-caf\xC3\xA9-XXXXXX", cbm_tmpdir()); + bool root_ok = cbm_mkdtemp(root) != NULL; + cbm_daemon_application_t *application = cbm_daemon_application_new(NULL); + cbm_daemon_runtime_application_callbacks_t callbacks = + cbm_daemon_application_runtime_callbacks(application); + cbm_daemon_runtime_application_session_t *session = app_test_open(&callbacks, 311); + uint8_t *context = NULL; + uint32_t context_length = 0; + uint8_t *response = NULL; + uint32_t response_length = 0; + bool context_ok = root_ok && app_test_context_request(root, root, &context, &context_length); + cbm_daemon_runtime_application_status_t status = + context_ok ? app_test_request(&callbacks, session, context, context_length, &response, + &response_length) + : CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR; + + free(context); + free(response); + if (session) { + callbacks.session_close(callbacks.context, session); + } + bool stopped = application && cbm_daemon_application_shutdown(application, APP_TEST_TIMEOUT_MS); + cbm_daemon_application_free(application); + if (root_ok) { + (void)cbm_rmdir(root); + } + + ASSERT_TRUE(root_ok); + ASSERT_NOT_NULL(application); + ASSERT_NOT_NULL(session); + ASSERT_TRUE(context_ok); + ASSERT_EQ(status, CBM_DAEMON_RUNTIME_APPLICATION_OK); + ASSERT_EQ(response_length, 0); + ASSERT_TRUE(stopped); + PASS(); +} + TEST(daemon_application_mcp_notification_has_no_response) { cbm_daemon_application_t *application = cbm_daemon_application_new(NULL); cbm_daemon_runtime_application_callbacks_t callbacks = @@ -5348,6 +5387,7 @@ SUITE(daemon_application) { RUN_TEST(daemon_application_new_session_does_not_retain_initial_store); RUN_TEST(daemon_application_request_cancel_is_scoped_to_exact_token); RUN_TEST(daemon_application_requires_immutable_explicit_context); + RUN_TEST(daemon_application_accepts_utf8_session_context_root); RUN_TEST(daemon_application_ui_config_updates_are_masked_and_serialized); RUN_TEST(daemon_application_ui_config_rejects_noncanonical_frames); RUN_TEST(daemon_application_ui_readiness_proof_is_generation_bound_before_context); diff --git a/tests/windows/test_non_ascii_path.py b/tests/windows/test_non_ascii_path.py index bd7de22a4..bb181688f 100644 --- a/tests/windows/test_non_ascii_path.py +++ b/tests/windows/test_non_ascii_path.py @@ -358,9 +358,9 @@ def no_project_error(index_txt, repo, cache): def index_and_count(binary, repo, cache): - """Index `repo` into an isolated cache and return label-resolved counts.""" + """Start in and index `repo`, then return label-resolved counts.""" os.makedirs(cache, exist_ok=True) - with McpServer(binary, cache_dir=cache) as s: + with McpServer(binary, cache_dir=cache, cwd=repo) as s: s.initialize() resp = s.call_tool("index_repository", {"repo_path": repo}, timeout=180) index_txt, err = s.tool_text(resp)