Files
cleveragents-core/features/config_service_coverage.feature
T
freemo 55aee7cf22
CI / lint (pull_request) Successful in 18s
CI / quality (pull_request) Successful in 21s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 14s
CI / security (pull_request) Successful in 49s
CI / typecheck (pull_request) Successful in 56s
CI / integration_tests (pull_request) Successful in 2m35s
CI / unit_tests (pull_request) Successful in 14m30s
CI / docker (pull_request) Successful in 55s
CI / benchmark-regression (pull_request) Successful in 21m11s
CI / coverage (pull_request) Successful in 34m22s
CI / lint (push) Successful in 12s
CI / build (push) Successful in 16s
CI / quality (push) Successful in 17s
CI / security (push) Successful in 29s
CI / typecheck (push) Successful in 1m0s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 4m54s
CI / benchmark-publish (push) Successful in 11m48s
CI / unit_tests (push) Successful in 16m30s
CI / docker (push) Successful in 28s
CI / coverage (push) Successful in 25m45s
fix(test): commit after each add_skill to prevent session GC rollback, and improved coverage.
The step_register_skills_table step called add_skill in a loop but only
committed once at the end. Because SkillRepository.create() obtains a
new session per call and only flushes (never commits), the intermediate
sessions could be garbage-collected before the final commit, rolling
back their transactions on the shared SQLite :memory: connection. Moving
_commit_pending inside the loop ensures each skill is durably committed
before the next session is created.

ISSUES CLOSED: #418
2026-02-24 12:19:04 -05:00

278 lines
13 KiB
Gherkin

Feature: ConfigService full coverage
As a developer
I want every line of config_service.py tested
So that the module has complete test coverage
# ---------- ConfigLevel enum ----------
Scenario: ConfigLevel enum has five precedence levels
Then ConfigLevel should have values CLI_FLAG ENV_VAR PROJECT GLOBAL DEFAULT
# ---------- ConfigEntry and ResolvedValue dataclasses ----------
Scenario: ConfigEntry dataclass stores all metadata fields
Given a ConfigEntry with key "test.key" and type str
Then the ConfigEntry fields should match the provided values
Scenario: ResolvedValue dataclass stores resolution result
Given a ResolvedValue with key "test.key" and source DEFAULT
Then the ResolvedValue fields should match including an empty chain by default
# ---------- _env_name helper ----------
Scenario: _env_name builds uppercase CLEVERAGENTS env var name
When I call _env_name with section "core" and key "log_level"
Then the env name should be "CLEVERAGENTS_CORE_LOG_LEVEL"
# ---------- _register and _build_catalog ----------
Scenario: _register adds an entry to the global registry
When I register a custom key "custom.test_key" with type int and default 42
Then the registry should contain "custom.test_key" with default 42
Scenario: _register uses explicit env_var when provided
When I register a key "custom.explicit_env" with explicit env_var "MY_CUSTOM_VAR"
Then the entry "custom.explicit_env" should have env_var "MY_CUSTOM_VAR"
Scenario: _build_catalog populates all expected config sections
Then the registry should contain keys from sections core plan provider sandbox context index
Scenario: _build_catalog registers the expected number of keys
Then the registry should contain at least 20 keys
# ---------- ConfigService instantiation ----------
Scenario: ConfigService uses custom config_dir and config_path
Given a temporary directory for config service
When I create a ConfigService with custom paths
Then the service should use the custom config directory and path
Scenario: ConfigService uses defaults when no paths provided
When I create a ConfigService with default paths
Then the service config path should end with ".cleveragents/config.toml"
# ---------- registry(), get_entry(), registered_keys() ----------
Scenario: registry returns a copy of all registered entries
When I call ConfigService.registry
Then it should return a dict with the same keys as the internal registry
Scenario: get_entry returns the entry for a known key
When I call ConfigService.get_entry with "core.log_level"
Then it should return a ConfigEntry with key "core.log_level"
Scenario: get_entry returns None for an unknown key
When I call ConfigService.get_entry with "nonexistent.key"
Then it should return None
Scenario: registered_keys returns a sorted list of all keys
When I call ConfigService.registered_keys
Then it should return a sorted list containing "core.log_level"
# ---------- read_config ----------
Scenario: read_config returns empty dict when file does not exist
Given a temporary directory for config service
When I create a ConfigService pointing to a missing config file
Then read_config should return an empty dict
Scenario: read_config parses an existing TOML file
Given a temporary directory for config service
And a TOML config file with key "core.log_level" set to "DEBUG"
When I create a ConfigService pointing to that TOML file
Then read_config should return a dict with "core.log_level" equal to "DEBUG"
# ---------- write_config ----------
Scenario: write_config creates directories and writes a new TOML file
Given a temporary directory for config service
When I create a ConfigService with a nested non-existent config directory
And I call write_config with key "greeting" value "hello"
Then the TOML file should exist and contain key "greeting" with value "hello"
Scenario: write_config merges data into an existing TOML file
Given a temporary directory for config service
And a TOML config file with key "existing" set to "value1"
When I create a ConfigService pointing to that TOML file
And I call write_config with key "new_key" value "value2"
Then read_config should return a dict containing both "existing" and "new_key"
# ---------- set_value ----------
Scenario: set_value persists a single key-value pair
Given a temporary directory for config service
When I create a ConfigService with custom paths
And I call set_value with key "mykey" and value "myval"
Then read_config should return a dict with "mykey" equal to "myval"
# ---------- validate_key ----------
Scenario: validate_key returns entry for a known key
When I call validate_key with "core.log_level"
Then it should return the ConfigEntry for "core.log_level" without error
Scenario: validate_key raises ValueError for an unknown key
When I call validate_key with "totally.unknown"
Then a ValueError should be raised mentioning "Unknown configuration key"
# ---------- validate_type ----------
Scenario: validate_type returns value unchanged when type already matches
When I call validate_type with key "core.log_level" and value "INFO"
Then it should return "INFO" unchanged
Scenario: validate_type coerces a string to int
When I call validate_type with key "core.server_port" and string value "9090"
Then it should return the integer 9090
Scenario: validate_type coerces a string to float
When I call validate_type with key "provider.temperature" and string value "0.5"
Then it should return the float 0.5
Scenario: validate_type coerces non-string to str
When I call validate_type with key "core.log_level" and integer value 123
Then it should return the string "123"
Scenario: validate_type coerces "true" string to bool True
When I call validate_type with key "core.debug_enabled" and string value "true"
Then it should return boolean True
Scenario: validate_type coerces "1" string to bool True
When I call validate_type with key "core.debug_enabled" and string value "1"
Then it should return boolean True
Scenario: validate_type coerces "yes" string to bool True
When I call validate_type with key "core.debug_enabled" and string value "yes"
Then it should return boolean True
Scenario: validate_type coerces "false" string to bool False
When I call validate_type with key "core.debug_enabled" and string value "false"
Then it should return boolean False
Scenario: validate_type coerces "0" string to bool False
When I call validate_type with key "core.debug_enabled" and string value "0"
Then it should return boolean False
Scenario: validate_type coerces "no" string to bool False
When I call validate_type with key "core.debug_enabled" and string value "no"
Then it should return boolean False
Scenario: validate_type raises TypeError for invalid bool string
When I call validate_type with key "core.debug_enabled" and string value "maybe"
Then a TypeError should be raised mentioning "Cannot convert"
Scenario: validate_type raises TypeError for non-coercible int value
When I call validate_type with key "core.server_port" and string value "not_a_number"
Then a TypeError should be raised mentioning "Type mismatch"
Scenario: validate_type raises TypeError for non-bool non-string to bool key
When I call validate_type with key "core.debug_enabled" and a list value
Then a TypeError should be raised mentioning "Type mismatch"
Scenario: validate_type raises ValueError for completely unknown key
When I call validate_type with unknown key "fake.unknown" and value "x"
Then a ValueError should be raised mentioning "Cannot validate type for unknown key"
# ---------- resolve: five-level chain ----------
Scenario: resolve returns default value when no overrides exist
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I resolve key "core.log_level" with no overrides
Then the resolved value should be "INFO" from source DEFAULT
Scenario: resolve returns CLI flag value when provided
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I resolve key "core.log_level" with cli_value "TRACE"
Then the resolved value should be "TRACE" from source CLI_FLAG
Scenario: resolve returns environment variable when no CLI flag
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
And the env var "CLEVERAGENTS_CORE_LOG_LEVEL" is injected with "WARNING"
When I resolve key "core.log_level" with no overrides
Then the resolved value should be "WARNING" from source ENV_VAR
Scenario: CLI flag takes precedence over environment variable
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
And the env var "CLEVERAGENTS_CORE_LOG_LEVEL" is injected with "WARNING"
When I resolve key "core.log_level" with cli_value "TRACE"
Then the resolved value should be "TRACE" from source CLI_FLAG
Scenario: resolve returns project-scoped value when set
Given a temporary directory for config service
And a ConfigService with project-scoped config for key "core.log_level" value "PROJECT_DEBUG" under project "myproj"
When I resolve key "core.log_level" with project_name "myproj"
Then the resolved value should be "PROJECT_DEBUG" from source PROJECT
Scenario: resolve returns global config value when no higher overrides
Given a temporary directory for config service
And a ConfigService with global config key "core.log_level" set to "GLOBAL_WARN"
When I resolve key "core.log_level" with no overrides
Then the resolved value should be "GLOBAL_WARN" from source GLOBAL
Scenario: resolve populates chain when verbose is True
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I resolve key "core.log_level" with verbose True and no overrides
Then the resolved chain should have 5 entries covering all levels
Scenario: resolve verbose chain includes env_name for ENV_VAR level
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I resolve key "core.log_level" with verbose True and no overrides
Then the verbose chain ENV_VAR entry should include env_name "CLEVERAGENTS_CORE_LOG_LEVEL"
Scenario: resolve verbose chain includes path for GLOBAL level
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I resolve key "core.log_level" with verbose True and no overrides
Then the verbose chain GLOBAL entry should include the config file path
Scenario: resolve verbose chain shows project value when project-scoped wins
Given a temporary directory for config service
And a ConfigService with project-scoped config for key "core.log_level" value "PROJECT_DEBUG" under project "myproj"
When I resolve key "core.log_level" with project_name "myproj" and verbose True
Then the resolved value should be "PROJECT_DEBUG" from source PROJECT
And the verbose chain PROJECT entry should have value "PROJECT_DEBUG"
Scenario: resolve verbose chain shows global value when global config wins
Given a temporary directory for config service
And a ConfigService with global config key "core.log_level" set to "GLOBAL_WARN"
When I resolve key "core.log_level" with verbose True and no overrides
Then the resolved value should be "GLOBAL_WARN" from source GLOBAL
And the verbose chain GLOBAL entry should have value "GLOBAL_WARN"
Scenario: resolve skips project scope for non-scopable keys
Given a temporary directory for config service
And a ConfigService with project-scoped config for key "core.database_url" value "pg://proj" under project "myproj"
When I resolve key "core.database_url" with project_name "myproj"
Then the resolved value should not be "pg://proj"
And the resolved source should be DEFAULT or GLOBAL
# ---------- env_var_for_key ----------
Scenario: env_var_for_key returns the env var name for a valid key
When I call env_var_for_key with "core.log_level"
Then it should return "CLEVERAGENTS_CORE_LOG_LEVEL"
Scenario: env_var_for_key raises ValueError for an unknown key
When I call env_var_for_key with "nonexistent.key"
Then a ValueError should be raised mentioning "Unknown configuration key"
# ---------- resolve_all ----------
Scenario: resolve_all returns a resolved value for every registered key
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I call resolve_all with no overrides
Then the result should contain an entry for every registered key
Scenario: resolve_all applies CLI overrides to matching keys
Given a temporary directory for config service
And a ConfigService with empty config for resolve tests
When I call resolve_all with cli_override "core.log_level" set to "FATAL"
Then the resolve_all result for "core.log_level" should have value "FATAL" and source CLI_FLAG