From da0c5f14b54247e07ae7cf39de6497f74d2255f0 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 5 Apr 2026 07:15:48 +0000 Subject: [PATCH] fix(config): add server_url, server_token, format, and default_estimation_actor fields to Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add four spec-required environment variable fields to the Settings class that were previously missing, causing AttributeError when accessed: - server_url: str | None — maps to CLEVERAGENTS_SERVER_URL (spec: server.url) - server_token: str | None — maps to CLEVERAGENTS_SERVER_TOKEN (spec: server.token) - format: str | None — maps to CLEVERAGENTS_FORMAT (spec: core.format) - default_estimation_actor: str | None — maps to CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR (spec: core.default_estimation_actor) All fields default to None when the corresponding env var is unset, and are fully statically typed (no type: ignore). BDD scenarios cover env var binding, default value, type validation, and model_fields presence for each new field. ISSUES CLOSED: #2866 --- features/settings_missing_spec_fields.feature | 89 ++++++++++ .../settings_missing_spec_fields_steps.py | 155 ++++++++++++++++++ src/cleveragents/config/settings.py | 34 ++++ 3 files changed, 278 insertions(+) create mode 100644 features/settings_missing_spec_fields.feature create mode 100644 features/steps/settings_missing_spec_fields_steps.py diff --git a/features/settings_missing_spec_fields.feature b/features/settings_missing_spec_fields.feature new file mode 100644 index 000000000..245182e72 --- /dev/null +++ b/features/settings_missing_spec_fields.feature @@ -0,0 +1,89 @@ +Feature: Settings class spec-required env var fields + The Settings class must expose fields for all spec-required environment + variables: CLEVERAGENTS_SERVER_URL, CLEVERAGENTS_SERVER_TOKEN, + CLEVERAGENTS_FORMAT, and CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR. + + Background: + Given no CleverAgents spec-field env vars are set + + # ── server_url ────────────────────────────────────────────────────────────── + + Scenario: server_url defaults to None when env var is absent + When I instantiate Settings for spec fields + Then settings.server_url should be None + + Scenario: server_url reads CLEVERAGENTS_SERVER_URL from environment + Given the environment variable "CLEVERAGENTS_SERVER_URL" is set to "https://ca.example.com" + When I instantiate Settings for spec fields + Then settings.server_url should be "https://ca.example.com" + + Scenario: server_url accepts any string value + Given the environment variable "CLEVERAGENTS_SERVER_URL" is set to "http://localhost:9000" + When I instantiate Settings for spec fields + Then settings.server_url should be "http://localhost:9000" + + # ── server_token ───────────────────────────────────────────────────────────── + + Scenario: server_token defaults to None when env var is absent + When I instantiate Settings for spec fields + Then settings.server_token should be None + + Scenario: server_token reads CLEVERAGENTS_SERVER_TOKEN from environment + Given the environment variable "CLEVERAGENTS_SERVER_TOKEN" is set to "secret-token-abc" + When I instantiate Settings for spec fields + Then settings.server_token should be "secret-token-abc" + + Scenario: server_token accepts any string value + Given the environment variable "CLEVERAGENTS_SERVER_TOKEN" is set to "bearer-xyz-789" + When I instantiate Settings for spec fields + Then settings.server_token should be "bearer-xyz-789" + + # ── format ─────────────────────────────────────────────────────────────────── + + Scenario: format defaults to None when env var is absent + When I instantiate Settings for spec fields + Then settings.format should be None + + Scenario: format reads CLEVERAGENTS_FORMAT from environment + Given the environment variable "CLEVERAGENTS_FORMAT" is set to "json" + When I instantiate Settings for spec fields + Then settings.format should be "json" + + Scenario: format accepts any string value + Given the environment variable "CLEVERAGENTS_FORMAT" is set to "markdown" + When I instantiate Settings for spec fields + Then settings.format should be "markdown" + + # ── default_estimation_actor ───────────────────────────────────────────────── + + Scenario: default_estimation_actor defaults to None when env var is absent + When I instantiate Settings for spec fields + Then settings.default_estimation_actor should be None + + Scenario: default_estimation_actor reads CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR from environment + Given the environment variable "CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR" is set to "gpt-estimator" + When I instantiate Settings for spec fields + Then settings.default_estimation_actor should be "gpt-estimator" + + Scenario: default_estimation_actor accepts any string value + Given the environment variable "CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR" is set to "claude-estimator" + When I instantiate Settings for spec fields + Then settings.default_estimation_actor should be "claude-estimator" + + # ── model_fields presence ──────────────────────────────────────────────────── + + Scenario: all four spec fields appear in Settings.model_fields + When I inspect Settings.model_fields + Then "server_url" should be present in model_fields + And "server_token" should be present in model_fields + And "format" should be present in model_fields + And "default_estimation_actor" should be present in model_fields + + # ── type annotation ────────────────────────────────────────────────────────── + + Scenario: all four spec fields are typed as str or None + When I instantiate Settings for spec fields + Then settings.server_url should be of type str or None + And settings.server_token should be of type str or None + And settings.format should be of type str or None + And settings.default_estimation_actor should be of type str or None diff --git a/features/steps/settings_missing_spec_fields_steps.py b/features/steps/settings_missing_spec_fields_steps.py new file mode 100644 index 000000000..678d6209a --- /dev/null +++ b/features/steps/settings_missing_spec_fields_steps.py @@ -0,0 +1,155 @@ +"""Step definitions for Settings spec-required env var fields (issue #2866).""" + +from __future__ import annotations + +import os + +from behave import given, then, when + +from cleveragents.config.settings import Settings + +_SPEC_FIELD_ENV_VARS = [ + "CLEVERAGENTS_SERVER_URL", + "CLEVERAGENTS_SERVER_TOKEN", + "CLEVERAGENTS_FORMAT", + "CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR", +] + + +@given("no CleverAgents spec-field env vars are set") +def step_clear_spec_field_env_vars(context): + """Remove all spec-field env vars so Settings starts from a clean slate.""" + for key in _SPEC_FIELD_ENV_VARS: + os.environ.pop(key, None) + if not hasattr(context, "env_vars_to_clean"): + context.env_vars_to_clean = [] + context.env_vars_to_clean.extend(_SPEC_FIELD_ENV_VARS) + + +@when("I instantiate Settings for spec fields") +def step_instantiate_settings_for_spec_fields(context): + """Create a fresh Settings instance (bypassing the singleton).""" + Settings._instance = None + context.settings = Settings() + + +@when("I inspect Settings.model_fields") +def step_inspect_model_fields(context): + """Capture the model_fields mapping for assertion.""" + context.model_fields = Settings.model_fields + + +# ── server_url assertions ──────────────────────────────────────────────────── + + +@then("settings.server_url should be None") +def step_server_url_is_none(context): + assert context.settings.server_url is None, ( + f"Expected server_url to be None, got {context.settings.server_url!r}" + ) + + +@then('settings.server_url should be "{expected}"') +def step_server_url_equals(context, expected): + assert context.settings.server_url == expected, ( + f"Expected server_url={expected!r}, got {context.settings.server_url!r}" + ) + + +# ── server_token assertions ────────────────────────────────────────────────── + + +@then("settings.server_token should be None") +def step_server_token_is_none(context): + assert context.settings.server_token is None, ( + f"Expected server_token to be None, got {context.settings.server_token!r}" + ) + + +@then('settings.server_token should be "{expected}"') +def step_server_token_equals(context, expected): + assert context.settings.server_token == expected, ( + f"Expected server_token={expected!r}, got {context.settings.server_token!r}" + ) + + +# ── format assertions ──────────────────────────────────────────────────────── + + +@then("settings.format should be None") +def step_format_is_none(context): + assert context.settings.format is None, ( + f"Expected format to be None, got {context.settings.format!r}" + ) + + +@then('settings.format should be "{expected}"') +def step_format_equals(context, expected): + assert context.settings.format == expected, ( + f"Expected format={expected!r}, got {context.settings.format!r}" + ) + + +# ── default_estimation_actor assertions ────────────────────────────────────── + + +@then("settings.default_estimation_actor should be None") +def step_default_estimation_actor_is_none(context): + assert context.settings.default_estimation_actor is None, ( + "Expected default_estimation_actor to be None, " + f"got {context.settings.default_estimation_actor!r}" + ) + + +@then('settings.default_estimation_actor should be "{expected}"') +def step_default_estimation_actor_equals(context, expected): + assert context.settings.default_estimation_actor == expected, ( + f"Expected default_estimation_actor={expected!r}, " + f"got {context.settings.default_estimation_actor!r}" + ) + + +# ── model_fields presence assertions ───────────────────────────────────────── + + +@then('"{field_name}" should be present in model_fields') +def step_field_in_model_fields(context, field_name): + assert field_name in context.model_fields, ( + f"Expected '{field_name}' to be in Settings.model_fields, " + f"but only found: {sorted(context.model_fields.keys())}" + ) + + +# ── type annotation assertions ─────────────────────────────────────────────── + + +@then("settings.server_url should be of type str or None") +def step_server_url_type(context): + value = context.settings.server_url + assert value is None or isinstance(value, str), ( + f"Expected server_url to be str | None, got {type(value)}" + ) + + +@then("settings.server_token should be of type str or None") +def step_server_token_type(context): + value = context.settings.server_token + assert value is None or isinstance(value, str), ( + f"Expected server_token to be str | None, got {type(value)}" + ) + + +@then("settings.format should be of type str or None") +def step_format_type(context): + value = context.settings.format + assert value is None or isinstance(value, str), ( + f"Expected format to be str | None, got {type(value)}" + ) + + +@then("settings.default_estimation_actor should be of type str or None") +def step_default_estimation_actor_type(context): + value = context.settings.default_estimation_actor + assert value is None or isinstance(value, str), ( + f"Expected default_estimation_actor to be str | None, got {type(value)}" + ) diff --git a/src/cleveragents/config/settings.py b/src/cleveragents/config/settings.py index 80a8e69db..ce39cf2d5 100644 --- a/src/cleveragents/config/settings.py +++ b/src/cleveragents/config/settings.py @@ -111,11 +111,45 @@ class Settings(BaseSettings): default=False, validation_alias=AliasChoices("CLEVERAGENTS_SERVER_RELOAD"), ) + server_url: str | None = Field( + default=None, + validation_alias=AliasChoices("CLEVERAGENTS_SERVER_URL"), + description=( + "URL for the CleverAgents server endpoint, enabling server mode. " + "Maps to the spec's server.url config key." + ), + ) + server_token: str | None = Field( + default=None, + validation_alias=AliasChoices("CLEVERAGENTS_SERVER_TOKEN"), + description=( + "Authentication token for connecting to the CleverAgents server. " + "Maps to the spec's server.token config key." + ), + ) debug_enabled: bool = Field( default=False, validation_alias=AliasChoices("CLEVERAGENTS_DEBUG_ENABLED"), ) + # Core output and estimation configuration + format: str | None = Field( + default=None, + validation_alias=AliasChoices("CLEVERAGENTS_FORMAT"), + description=( + "Default output format (e.g. 'json', 'markdown', 'text'). " + "Overrides the core.format config key when set." + ), + ) + default_estimation_actor: str | None = Field( + default=None, + validation_alias=AliasChoices("CLEVERAGENTS_DEFAULT_ESTIMATION_ACTOR"), + description=( + "Default actor used for estimation tasks. " + "Maps to the spec's core.default_estimation_actor config key." + ), + ) + # Logging/paths log_level: str = Field( default="INFO",