diff --git a/.kiro/specs/controller-peers-env-fallback/tasks.md b/.kiro/specs/controller-peers-env-fallback/tasks.md index c441354f..7dd7f4b5 100644 --- a/.kiro/specs/controller-peers-env-fallback/tasks.md +++ b/.kiro/specs/controller-peers-env-fallback/tasks.md @@ -1,7 +1,7 @@ # Implementation Plan - [ ] 1. Foundation: shared libxce primitives and the regression script -- [ ] 1.1 Add the environment-variable reader for the default candidate list +- [x] 1.1 Add the environment-variable reader for the default candidate list - Write the failing libxce unit tests first: variable unset gives no value, set to an empty string gives no value, set to a non-empty string returns it unchanged - Add the reader to the shared cluster module and declare it next to the existing peers parser, so every C daemon and the harness can call it with no new build dependency - Each test restores the variable to unset afterwards, so later libxce tests are unaffected diff --git a/src/include/xce/cluster.h b/src/include/xce/cluster.h index a0e6565c..c41585e5 100644 --- a/src/include/xce/cluster.h +++ b/src/include/xce/cluster.h @@ -137,6 +137,17 @@ void xce_reconnect_fini(xce_reconnect_ctx_t* rctx); */ int xce_parse_peers(const char* peers_str, xce_cluster_config_t* cfg); +/** + * xce_controller_peers_env() — default controller candidate list from the + * XCE_CONTROLLER_PEERS environment variable. + * + * Returns the variable's value unchanged (same format as --peers, not parsed + * here) when it is set and non-empty; NULL when it is unset or empty, so an + * empty value behaves exactly like an unset one. The returned pointer is + * owned by the process environment; do not free or modify it. + */ +const char* xce_controller_peers_env(void); + /* Test seam: set to non-NULL to override the real connect (unit tests only) */ typedef void* (*xce_connect_fn_t)(const char* host, int port); void xce_cluster_set_connect_fn(xce_connect_fn_t fn); diff --git a/src/lib/cluster.c b/src/lib/cluster.c index 9568eee7..0d54e66b 100644 --- a/src/lib/cluster.c +++ b/src/lib/cluster.c @@ -59,6 +59,12 @@ int xce_parse_peers(const char* peers_str, xce_cluster_config_t* cfg) return cfg->candidates_count > 0 ? 0 : -1; } +const char* xce_controller_peers_env(void) +{ + const char* v = getenv("XCE_CONTROLLER_PEERS"); + return (v && v[0] != '\0') ? v : NULL; +} + /* ── Test seam ─────────────────────────────────────────────────── */ static xce_connect_fn_t s_connect_fn = NULL; diff --git a/src/lib/testsuite/test_cluster.c b/src/lib/testsuite/test_cluster.c index 15985685..edbc77ad 100644 --- a/src/lib/testsuite/test_cluster.c +++ b/src/lib/testsuite/test_cluster.c @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -290,6 +291,41 @@ static void test_convert_does_not_set_identity_fields(void) printf("test_convert_does_not_set_identity_fields: PASS\n"); } +/* ── xce_controller_peers_env() ─────────────────────────────────── */ + +/* Req 1.2/1.3: an unset variable supplies no candidate list. */ +static void test_peers_env_unset_returns_null(void) +{ + unsetenv("XCE_CONTROLLER_PEERS"); + TEST_ASSERT_NULL(xce_controller_peers_env()); + printf("test_peers_env_unset_returns_null: PASS\n"); +} + +/* Req 1.3/1.7: an empty variable is treated exactly like an unset one. */ +static void test_peers_env_empty_returns_null(void) +{ + TEST_ASSERT_EQUAL_INT(0, setenv("XCE_CONTROLLER_PEERS", "", 1)); + const char* v = xce_controller_peers_env(); + unsetenv("XCE_CONTROLLER_PEERS"); + TEST_ASSERT_NULL(v); + printf("test_peers_env_empty_returns_null: PASS\n"); +} + +/* Req 1.2: a non-empty variable is returned unchanged (no parsing here). */ +static void test_peers_env_nonempty_returned_unchanged(void) +{ + const char* val = "c1=host1:9991,host2:9992"; + TEST_ASSERT_EQUAL_INT(0, setenv("XCE_CONTROLLER_PEERS", val, 1)); + const char* v = xce_controller_peers_env(); + char copy[64] = ""; + if (v) + snprintf(copy, sizeof(copy), "%s", v); + unsetenv("XCE_CONTROLLER_PEERS"); + TEST_ASSERT_NOT_NULL(v); + TEST_ASSERT_EQUAL_STRING(val, copy); + printf("test_peers_env_nonempty_returned_unchanged: PASS\n"); +} + /* ── Test group entry point ─────────────────────────────────────── */ void test_cluster(void) { @@ -310,4 +346,7 @@ void test_cluster(void) RUN_TEST(test_convert_rejects_zero_candidates); RUN_TEST(test_convert_truncates_over_max_candidates); RUN_TEST(test_convert_does_not_set_identity_fields); + RUN_TEST(test_peers_env_unset_returns_null); + RUN_TEST(test_peers_env_empty_returns_null); + RUN_TEST(test_peers_env_nonempty_returned_unchanged); }