Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/daemon/application.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions tests/test_daemon_application.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions tests/windows/test_non_ascii_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading