diff --git a/features/config_cli_safety_net_coverage.feature b/features/config_cli_safety_net_coverage.feature index 0a2ae7158..09d0800e4 100644 --- a/features/config_cli_safety_net_coverage.feature +++ b/features/config_cli_safety_net_coverage.feature @@ -138,11 +138,11 @@ Feature: Config CLI safety-net coverage # _resolution_chain (L177-209) # ===================================================================== - Scenario: safety-net _resolution_chain returns six-entry list + Scenario: safety-net _resolution_chain returns five-entry list Given a safety-net isolated temp config directory When the safety-net chain builder builds chain for key "core.log.level" - Then the safety-net chain should have exactly 6 entries - And the safety-net chain sources should be "cli_flag, env_var, local, project, global, default" + Then the safety-net chain should have exactly 5 entries + And the safety-net chain sources should be "cli_flag, env_var, project, global, default" # ===================================================================== # config_set - type coercion paths (L241-248) diff --git a/features/config_cli_scope_coverage.feature b/features/config_cli_scope_coverage.feature index e0091426e..8f7d1b96e 100644 --- a/features/config_cli_scope_coverage.feature +++ b/features/config_cli_scope_coverage.feature @@ -1,19 +1,19 @@ Feature: Config CLI scope coverage (cfcov3) - Supplementary scenarios targeting uncovered lines 224-229 and 252-259 - in cleveragents/cli/commands/config.py — the --scope flag handling + Supplementary scenarios targeting uncovered lines in + cleveragents/cli/commands/config.py — the --scope flag handling in the config_set command. Background: Given a cfcov3 isolated temp config directory - # ── Lines 224-229: invalid scope raises typer.BadParameter ────────── + # ── Invalid scope raises typer.BadParameter ────────────────────────── Scenario: config set with invalid --scope raises BadParameter When I invoke cfcov3 config set "plan.concurrency" "8" with scope "invalid" Then the cfcov3 CLI result should have a non-zero exit code And the cfcov3 CLI output should contain "Invalid scope" - # ── Lines 252-253, 258-259: --scope global branch ─────────────────── + # ── --scope global branch ───────────────────────────────────────────── Scenario: config set with --scope global writes via set_value When I invoke cfcov3 config set "plan.concurrency" "16" with scope "global" @@ -21,7 +21,7 @@ Feature: Config CLI scope coverage (cfcov3) And the cfcov3 CLI output should contain "plan.concurrency" And the cfcov3 CLI output should contain "global" - # ── Lines 254-255, 258-259: --scope project branch ───────────────── + # ── --scope project branch ──────────────────────────────────────────── Scenario: config set with --scope project writes via set_value When I invoke cfcov3 config set "plan.concurrency" "12" with scope "project" @@ -29,15 +29,7 @@ Feature: Config CLI scope coverage (cfcov3) And the cfcov3 CLI output should contain "plan.concurrency" And the cfcov3 CLI output should contain "project" - # ── Lines 257-259: --scope local branch ───────────────────────────── - - Scenario: config set with --scope local writes via set_value - When I invoke cfcov3 config set "plan.concurrency" "20" with scope "local" - Then the cfcov3 CLI result should have exit code 0 - And the cfcov3 CLI output should contain "plan.concurrency" - And the cfcov3 CLI output should contain "local" - - # ── Lines 252-253 with pre-existing global value ──────────────────── + # ── --scope global with pre-existing value ──────────────────────────── Scenario: config set --scope global shows previous value from config_data Given the cfcov3 global config has "plan.concurrency" set to 4 @@ -45,7 +37,7 @@ Feature: Config CLI scope coverage (cfcov3) Then the cfcov3 CLI result should have exit code 0 And the cfcov3 set_value mock should have been called with scope GLOBAL - # ── Lines 254-255 reading previous from project config ────────────── + # ── --scope project reads previous from project config ──────────────── Scenario: config set --scope project reads previous from project config Given the cfcov3 project config returns "plan.concurrency" as 7 @@ -53,10 +45,9 @@ Feature: Config CLI scope coverage (cfcov3) Then the cfcov3 CLI result should have exit code 0 And the cfcov3 read_project_config mock should have been called - # ── Lines 257-259 reading previous from local config ──────────────── + # ── --scope local is rejected as invalid ───────────────────────────── - Scenario: config set --scope local reads previous from local config - Given the cfcov3 local config returns "plan.concurrency" as 3 - When I invoke cfcov3 config set "plan.concurrency" "30" with scope "local" - Then the cfcov3 CLI result should have exit code 0 - And the cfcov3 read_local_config mock should have been called + Scenario: config set with --scope local is rejected as invalid scope + When I invoke cfcov3 config set "plan.concurrency" "20" with scope "local" + Then the cfcov3 CLI result should have a non-zero exit code + And the cfcov3 CLI output should contain "Invalid scope" diff --git a/features/config_service_coverage_r3.feature b/features/config_service_coverage_r3.feature index e4bea7cdf..ac2f1d418 100644 --- a/features/config_service_coverage_r3.feature +++ b/features/config_service_coverage_r3.feature @@ -1,13 +1,11 @@ -Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, set_value, and resolve local +Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, set_value Exercises uncovered lines in config_service.py: - Line 1195: project_root property - Line 1201: project_config_path returns None - - Line 1208: local_config_path returns None - Lines 1284-1285, 1289: write_scoped_config ValueError for missing project root - Lines 1297-1298: write_scoped_config reads existing target file - Lines 1336-1337: set_value redacts sensitive keys - Lines 1351-1352: set_value catches event_bus emit failure - - Lines 1507-1508, 1510-1511: resolve verbose with local config winning Background: Given cfscov3 a fresh temporary directory @@ -28,12 +26,6 @@ Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, Given cfscov3 a ConfigService with no project root Then cfscov3 project_config_path should return None - # -- local_config_path returns None (line 1208) -- - - Scenario: local_config_path returns None when project root is absent - Given cfscov3 a ConfigService with no project root - Then cfscov3 local_config_path should return None - # -- write_scoped_config raises ValueError (lines 1284-1285, 1289) -- Scenario: write_scoped_config raises ValueError for PROJECT scope without project root @@ -41,11 +33,6 @@ Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, When cfscov3 I call write_scoped_config with PROJECT scope Then cfscov3 a ValueError mentioning "no project root" should be stored - Scenario: write_scoped_config raises ValueError for LOCAL scope without project root - Given cfscov3 a ConfigService with no project root - When cfscov3 I call write_scoped_config with LOCAL scope - Then cfscov3 a ValueError mentioning "no project root" should be stored - # -- write_scoped_config reads existing target (lines 1297-1298) -- Scenario: write_scoped_config merges into existing project config.toml @@ -53,11 +40,6 @@ Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, When cfscov3 I call write_scoped_config with PROJECT scope adding a new key Then cfscov3 the project config.toml should contain both the old and new keys - Scenario: write_scoped_config merges into existing local config.local.toml - Given cfscov3 a ConfigService with a project root containing an existing config.local.toml - When cfscov3 I call write_scoped_config with LOCAL scope adding a new key - Then cfscov3 the local config.local.toml should contain both the old and new keys - # -- set_value redacts sensitive keys (lines 1336-1337) -- Scenario: set_value redacts old and new values for a sensitive key @@ -71,11 +53,3 @@ Feature: ConfigService coverage round 3 — path accessors, write_scoped_config, Given cfscov3 a ConfigService with a failing event bus When cfscov3 I call set_value with key "core.log.level" and value "DEBUG" Then cfscov3 no error should have been raised - - # -- resolve verbose with local config winning (lines 1507-1508, 1510-1511) -- - - Scenario: resolve in verbose mode returns local config value with chain entry - Given cfscov3 a ConfigService with a local config containing "core.log.level" set to "TRACE" - When cfscov3 I resolve "core.log.level" with verbose True - Then cfscov3 the resolved value should be "TRACE" from LOCAL source - And cfscov3 the chain should contain a local entry with value "TRACE" and a path diff --git a/features/config_three_scope.feature b/features/config_three_scope.feature deleted file mode 100644 index 087c31ec2..000000000 --- a/features/config_three_scope.feature +++ /dev/null @@ -1,119 +0,0 @@ -Feature: Three-scope configuration resolution - As a developer working on a CleverAgents project - I want configuration to resolve across global, project, and local scopes - So that I can have developer-specific overrides without affecting shared config - - Background: - Given a clean three-scope config environment - - # ── Scope precedence ────────────────────────────────────────────────── - - Scenario: Local scope overrides project scope - Given a project config value "core.automation-profile" = "supervised" - And a local config value "core.automation-profile" = "manual" - When I resolve three-scope key "core.automation-profile" - Then the three-scope resolved value should be "manual" - And the three-scope resolved source should be "local" - - Scenario: Project scope overrides global scope - Given a global config value for three-scope "core.automation-profile" = "full-auto" - And a project config value "core.automation-profile" = "supervised" - When I resolve three-scope key "core.automation-profile" - Then the three-scope resolved value should be "supervised" - And the three-scope resolved source should be "project" - - Scenario: Local scope overrides both project and global scopes - Given a global config value for three-scope "core.automation-profile" = "full-auto" - And a project config value "core.automation-profile" = "supervised" - And a local config value "core.automation-profile" = "manual" - When I resolve three-scope key "core.automation-profile" - Then the three-scope resolved value should be "manual" - And the three-scope resolved source should be "local" - - Scenario: Global scope used when no project or local override exists - Given a global config value for three-scope "core.automation-profile" = "supervised" - When I resolve three-scope key "core.automation-profile" - Then the three-scope resolved value should be "supervised" - And the three-scope resolved source should be "global" - - Scenario: Default used when no scope provides a value - When I resolve three-scope key "core.automation-profile" - Then the three-scope resolved source should be "default" - - # ── Deep merge ──────────────────────────────────────────────────────── - - Scenario: Deep merge combines nested keys from all scopes - Given a global config with nested data "section_a.key1" = "global_val1" - And a project config with nested data "section_a.key2" = "project_val2" - And a local config with nested data "section_a.key3" = "local_val3" - When I read the three-scope merged config - Then the merged config "section_a" should contain key "key1" with value "global_val1" - And the merged config "section_a" should contain key "key2" with value "project_val2" - And the merged config "section_a" should contain key "key3" with value "local_val3" - - Scenario: Deep merge local overrides project on conflict - Given a project config with nested data "section_a.key1" = "project_val" - And a local config with nested data "section_a.key1" = "local_val" - When I read the three-scope merged config - Then the merged config "section_a" should contain key "key1" with value "local_val" - - # ── Project root discovery ──────────────────────────────────────────── - - Scenario: Project root discovered via cleveragents.toml marker - Given a directory tree with "cleveragents.toml" marker at the root - When I discover the project root from a subdirectory - Then the discovered project root should match the marker directory - - Scenario: Project root discovered via .cleveragents directory marker - Given a directory tree with ".cleveragents" directory marker at the root - When I discover the project root from a subdirectory - Then the discovered project root should match the marker directory - - Scenario: No project root returns None when no markers found - Given a directory tree with no project markers - When I discover the project root - Then the discovered project root should be None - - # ── config.local.toml file loading ──────────────────────────────────── - - Scenario: Local config file is loaded when present - Given a local config file with "plan.concurrency" = "16" - When I resolve three-scope key "plan.concurrency" - Then the three-scope resolved value should be "16" - And the three-scope resolved source should be "local" - - Scenario: Missing local config file is handled gracefully - When I read the local config - Then the local config should be an empty dict - - # ── Scoped write ────────────────────────────────────────────────────── - - Scenario: Set value with local scope writes to config.local.toml - When I set three-scope value "core.automation-profile" to "manual" with scope "local" - Then the local config file should contain "core.automation-profile" = "manual" - - Scenario: Set value with project scope writes to project config.toml - When I set three-scope value "core.automation-profile" to "supervised" with scope "project" - Then the project config file should contain "core.automation-profile" = "supervised" - - Scenario: Set value with global scope writes to global config.toml - When I set three-scope value "core.automation-profile" to "full-auto" with scope "global" - Then the global config file should contain "core.automation-profile" = "full-auto" - - # ── ConfigScope enum ────────────────────────────────────────────────── - - Scenario: ConfigScope enum has all three values - Then ConfigScope should have value "global" - And ConfigScope should have value "project" - And ConfigScope should have value "local" - - # ── ConfigLevel enum includes LOCAL ─────────────────────────────────── - - Scenario: ConfigLevel enum includes LOCAL between ENV_VAR and PROJECT - Then ConfigLevel should have value "local" - And ConfigLevel "local" should exist between "env_var" and "project" - - # ── .gitignore template includes config.local.toml ──────────────────── - - Scenario: Default ignore patterns include config.local.toml - Then the default ignore patterns should include "config.local.toml" diff --git a/features/config_two_scope.feature b/features/config_two_scope.feature new file mode 100644 index 000000000..a3b30e5ee --- /dev/null +++ b/features/config_two_scope.feature @@ -0,0 +1,89 @@ +Feature: Two-scope configuration resolution + As a developer working on a CleverAgents project + I want configuration to resolve across global and project scopes + So that I can have project-specific overrides without affecting global config + + Background: + Given a clean two-scope config environment + + # ── Scope precedence ────────────────────────────────────────────────── + + Scenario: Project scope overrides global scope + Given a global config value for two-scope "core.automation-profile" = "full-auto" + And a project config value "core.automation-profile" = "supervised" + When I resolve two-scope key "core.automation-profile" + Then the two-scope resolved value should be "supervised" + And the two-scope resolved source should be "project" + + Scenario: Global scope used when no project override exists + Given a global config value for two-scope "core.automation-profile" = "supervised" + When I resolve two-scope key "core.automation-profile" + Then the two-scope resolved value should be "supervised" + And the two-scope resolved source should be "global" + + Scenario: Default used when no scope provides a value + When I resolve two-scope key "core.automation-profile" + Then the two-scope resolved source should be "default" + + # ── Deep merge ──────────────────────────────────────────────────────── + + Scenario: Deep merge combines nested keys from global and project scopes + Given a global config with nested data "section_a.key1" = "global_val1" + And a project config with nested data "section_a.key2" = "project_val2" + When I read the two-scope merged config + Then the merged config "section_a" should contain key "key1" with value "global_val1" + And the merged config "section_a" should contain key "key2" with value "project_val2" + + Scenario: Deep merge project overrides global on conflict + Given a global config with nested data "section_a.key1" = "global_val" + And a project config with nested data "section_a.key1" = "project_val" + When I read the two-scope merged config + Then the merged config "section_a" should contain key "key1" with value "project_val" + + # ── Project root discovery ──────────────────────────────────────────── + + Scenario: Project root discovered via cleveragents.toml marker + Given a directory tree with "cleveragents.toml" marker at the root + When I discover the project root from a subdirectory + Then the discovered project root should match the marker directory + + Scenario: Project root discovered via .cleveragents directory marker + Given a directory tree with ".cleveragents" directory marker at the root + When I discover the project root from a subdirectory + Then the discovered project root should match the marker directory + + Scenario: No project root returns None when no markers found + Given a directory tree with no project markers + When I discover the project root + Then the discovered project root should be None + + # ── Scoped write ────────────────────────────────────────────────────── + + Scenario: Set value with project scope writes to project config.toml + When I set two-scope value "core.automation-profile" to "supervised" with scope "project" + Then the project config file should contain "core.automation-profile" = "supervised" + + Scenario: Set value with global scope writes to global config.toml + When I set two-scope value "core.automation-profile" to "full-auto" with scope "global" + Then the global config file should contain "core.automation-profile" = "full-auto" + + # ── ConfigScope enum ────────────────────────────────────────────────── + + Scenario: ConfigScope enum has exactly two spec-defined values + Then ConfigScope should have value "global" + And ConfigScope should have value "project" + + # ── ConfigLevel enum matches spec 5-level chain ─────────────────────── + + Scenario: ConfigLevel enum has exactly five spec-defined levels + Then ConfigLevel should have value "cli_flag" + And ConfigLevel should have value "env_var" + And ConfigLevel should have value "project" + And ConfigLevel should have value "global" + And ConfigLevel should have value "default" + + Scenario: ConfigLevel enum does not include undocumented local level + Then ConfigLevel should not have value "local" + + Scenario: ConfigScope enum does not include undocumented local scope + Then ConfigScope should not have value "local" diff --git a/features/consolidated_config.feature b/features/consolidated_config.feature index 4f9955cd5..5c33b0610 100644 --- a/features/consolidated_config.feature +++ b/features/consolidated_config.feature @@ -172,10 +172,9 @@ Feature: Consolidated Config Scenario: Verbose mode returns full resolution chain Given a clean config service test environment When I resolve config key "core.log.level" with verbose mode - Then the resolution chain should have 6 entries + Then the resolution chain should have 5 entries And the chain should include source "cli_flag" And the chain should include source "env_var" - And the chain should include source "local" And the chain should include source "project" And the chain should include source "global" And the chain should include source "default" @@ -294,8 +293,8 @@ Feature: Consolidated Config # Feature: ConfigService full coverage # ============================================================ - Scenario: ConfigLevel enum has six precedence levels - Then ConfigLevel should have values CLI_FLAG ENV_VAR LOCAL PROJECT GLOBAL DEFAULT + Scenario: ConfigLevel enum has five spec-defined precedence levels + Then ConfigLevel should have values CLI_FLAG ENV_VAR PROJECT GLOBAL DEFAULT # ---------- ConfigEntry and ResolvedValue dataclasses ---------- @@ -547,7 +546,7 @@ Feature: Consolidated Config 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 6 entries covering all levels + Then the resolved chain should have 5 entries covering all levels Scenario: resolve verbose chain includes env_name for ENV_VAR level diff --git a/features/steps/config_cli_scope_coverage_steps.py b/features/steps/config_cli_scope_coverage_steps.py index 7dfaba107..ce0ec8a9a 100644 --- a/features/steps/config_cli_scope_coverage_steps.py +++ b/features/steps/config_cli_scope_coverage_steps.py @@ -1,11 +1,10 @@ """Step definitions for config_cli_scope_coverage.feature (cfcov3). Targets uncovered lines in cleveragents/cli/commands/config.py: - - Lines 224-229: invalid --scope triggers ValueError → typer.BadParameter - - Lines 252-253: --scope global reads previous from config_data - - Lines 254-255: --scope project reads previous from read_project_config() - - Lines 257-259: --scope local reads previous from read_local_config(), - then calls svc.set_value() and sets scope_display + - Invalid --scope triggers ValueError → typer.BadParameter + - --scope global reads previous from config_data + - --scope project reads previous from read_project_config() + - --scope local is rejected as invalid (not in spec) All step text uses a 'cfcov3' prefix to avoid collisions with other step definition files. @@ -68,7 +67,6 @@ def _teardown_cfcov3_temp(context: Context) -> None: def _build_mock_svc( config_data: dict[str, Any] | None = None, project_config: dict[str, Any] | None = None, - local_config: dict[str, Any] | None = None, ) -> MagicMock: """Build a MagicMock that behaves like ConfigService for config_set.""" mock_svc = MagicMock() @@ -76,9 +74,6 @@ def _build_mock_svc( mock_svc.read_project_config.return_value = ( project_config if project_config is not None else {} ) - mock_svc.read_local_config.return_value = ( - local_config if local_config is not None else {} - ) # validate_type should pass through the value, coercing to int when possible def _validate_type(key: str, value: Any) -> Any: @@ -110,7 +105,7 @@ def step_cfcov3_temp_dir(context: Context) -> None: # =================================================================== -# Lines 224-229: invalid scope +# Scope invocation # =================================================================== @@ -162,7 +157,7 @@ def step_cfcov3_output_contains(context: Context, text: str) -> None: # =================================================================== -# Lines 252-253: --scope global with pre-existing value +# --scope global with pre-existing value # =================================================================== @@ -185,7 +180,7 @@ def step_cfcov3_set_value_global(context: Context) -> None: # =================================================================== -# Lines 254-255: --scope project reads from read_project_config +# --scope project reads from read_project_config # =================================================================== @@ -200,21 +195,3 @@ def step_cfcov3_read_project_called(context: Context) -> None: mock_svc = context.cfcov3_mock_svc assert mock_svc is not None, "No mock service was set up" mock_svc.read_project_config.assert_called_once() - - -# =================================================================== -# Lines 257-259: --scope local reads from read_local_config -# =================================================================== - - -@given('the cfcov3 local config returns "{key}" as {value:d}') -def step_cfcov3_local_preset(context: Context, key: str, value: int) -> None: - mock_svc = _build_mock_svc(local_config={key: value}) - context.cfcov3_mock_svc = mock_svc - - -@then("the cfcov3 read_local_config mock should have been called") -def step_cfcov3_read_local_called(context: Context) -> None: - mock_svc = context.cfcov3_mock_svc - assert mock_svc is not None, "No mock service was set up" - mock_svc.read_local_config.assert_called_once() diff --git a/features/steps/config_service_coverage_r3_steps.py b/features/steps/config_service_coverage_r3_steps.py index 01c0a01c9..f1e70e239 100644 --- a/features/steps/config_service_coverage_r3_steps.py +++ b/features/steps/config_service_coverage_r3_steps.py @@ -3,12 +3,10 @@ Targets uncovered lines in config_service.py: - Line 1195: project_root property - Line 1201: project_config_path returns None - - Line 1208: local_config_path returns None - Lines 1284-1285, 1289: write_scoped_config ValueError (no project root) - Lines 1297-1298: write_scoped_config reads existing target file - Lines 1336-1337: set_value redacts sensitive keys - Lines 1351-1352: set_value catches event_bus emit failure - - Lines 1507-1508, 1510-1511: resolve verbose with local config winning """ from __future__ import annotations @@ -17,13 +15,12 @@ import tempfile import tomllib from pathlib import Path from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import tomlkit from behave import given, then, when from cleveragents.application.services.config_service import ( - ConfigLevel, ConfigScope, ConfigService, ) @@ -92,28 +89,6 @@ def step_cfscov3_svc_with_existing_project_config(context: Any) -> None: ) -@given( - "cfscov3 a ConfigService with a project root containing an existing config.local.toml" -) -def step_cfscov3_svc_with_existing_local_config(context: Any) -> None: - project_root = context.cfscov3_tmpdir / "proj_existing_local" - project_root.mkdir(parents=True, exist_ok=True) - context.cfscov3_project_root = project_root - - # Write an initial config.local.toml with an existing key - target = project_root / "config.local.toml" - doc = tomlkit.document() - doc["local_existing"] = "local_value" - with open(target, "w") as fh: - tomlkit.dump(doc, fh) - - context.cfscov3_svc = ConfigService( - config_dir=context.cfscov3_config_dir, - config_path=context.cfscov3_config_path, - project_root=project_root, - ) - - @given("cfscov3 a ConfigService with a mock event bus") def step_cfscov3_svc_with_mock_event_bus(context: Any) -> None: bus = MagicMock() @@ -139,28 +114,6 @@ def step_cfscov3_svc_with_failing_event_bus(context: Any) -> None: ) -@given( - 'cfscov3 a ConfigService with a local config containing "core.log.level" set to "TRACE"' -) -def step_cfscov3_svc_with_local_config_log_level(context: Any) -> None: - project_root = context.cfscov3_tmpdir / "proj_local_resolve" - project_root.mkdir(parents=True, exist_ok=True) - context.cfscov3_project_root = project_root - - # Write config.local.toml with the key - local_cfg = project_root / "config.local.toml" - doc = tomlkit.document() - doc["core.log.level"] = "TRACE" - with open(local_cfg, "w") as fh: - tomlkit.dump(doc, fh) - - context.cfscov3_svc = ConfigService( - config_dir=context.cfscov3_config_dir, - config_path=context.cfscov3_config_path, - project_root=project_root, - ) - - # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @@ -174,14 +127,6 @@ def step_cfscov3_write_scoped_project(context: Any) -> None: context.cfscov3_error = exc -@when("cfscov3 I call write_scoped_config with LOCAL scope") -def step_cfscov3_write_scoped_local(context: Any) -> None: - try: - context.cfscov3_svc.write_scoped_config({"key": "val"}, ConfigScope.LOCAL) - except (ValueError, TypeError) as exc: - context.cfscov3_error = exc - - @when("cfscov3 I call write_scoped_config with PROJECT scope adding a new key") def step_cfscov3_write_scoped_project_add_key(context: Any) -> None: try: @@ -192,16 +137,6 @@ def step_cfscov3_write_scoped_project_add_key(context: Any) -> None: context.cfscov3_error = exc -@when("cfscov3 I call write_scoped_config with LOCAL scope adding a new key") -def step_cfscov3_write_scoped_local_add_key(context: Any) -> None: - try: - context.cfscov3_svc.write_scoped_config( - {"new_key": "new_value"}, ConfigScope.LOCAL - ) - except (ValueError, TypeError) as exc: - context.cfscov3_error = exc - - @when('cfscov3 I call set_value with sensitive key "{key}" and value "{value}"') def step_cfscov3_set_value_sensitive(context: Any, key: str, value: str) -> None: try: @@ -218,24 +153,6 @@ def step_cfscov3_set_value(context: Any, key: str, value: str) -> None: context.cfscov3_error = exc -@when('cfscov3 I resolve "{key}" with verbose True') -def step_cfscov3_resolve_verbose(context: Any, key: str) -> None: - # Ensure no env var interferes with resolution - entry = ConfigService.get_entry(key) - env_var_name = entry.env_var if entry else "" - patcher = patch.dict("os.environ", {}, clear=False) - patcher.start() - context.add_cleanup(patcher.stop) - # Remove the specific env var if present to ensure local config wins - import os - - saved = os.environ.pop(env_var_name, None) - if saved is not None: - context.add_cleanup(lambda: os.environ.__setitem__(env_var_name, saved)) - - context.cfscov3_result = context.cfscov3_svc.resolve(key, verbose=True) - - # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @@ -260,12 +177,6 @@ def step_cfscov3_project_config_path_none(context: Any) -> None: assert actual is None, f"Expected project_config_path=None, got {actual}" -@then("cfscov3 local_config_path should return None") -def step_cfscov3_local_config_path_none(context: Any) -> None: - actual = context.cfscov3_svc.local_config_path - assert actual is None, f"Expected local_config_path=None, got {actual}" - - @then('cfscov3 a ValueError mentioning "no project root" should be stored') def step_cfscov3_valueerror_no_project_root(context: Any) -> None: assert context.cfscov3_error is not None, ( @@ -299,25 +210,6 @@ def step_cfscov3_verify_merged_project_config(context: Any) -> None: assert data["new_key"] == "new_value" -@then("cfscov3 the local config.local.toml should contain both the old and new keys") -def step_cfscov3_verify_merged_local_config(context: Any) -> None: - assert context.cfscov3_error is None, ( - f"write_scoped_config raised: {context.cfscov3_error}" - ) - target = context.cfscov3_project_root / "config.local.toml" - assert target.exists(), "config.local.toml should exist" - with open(target, "rb") as fh: - data = tomllib.load(fh) - assert "local_existing" in data, ( - f"Expected 'local_existing' in config, got keys: {list(data.keys())}" - ) - assert data["local_existing"] == "local_value" - assert "new_key" in data, ( - f"Expected 'new_key' in config, got keys: {list(data.keys())}" - ) - assert data["new_key"] == "new_value" - - @then("cfscov3 the event bus should have received redacted values") def step_cfscov3_verify_redacted_event(context: Any) -> None: assert context.cfscov3_error is None, f"set_value raised: {context.cfscov3_error}" @@ -339,31 +231,3 @@ def step_cfscov3_no_error(context: Any) -> None: assert context.cfscov3_error is None, ( f"Expected no error, but got: {context.cfscov3_error}" ) - - -@then('cfscov3 the resolved value should be "{expected}" from LOCAL source') -def step_cfscov3_resolved_local(context: Any, expected: str) -> None: - result = context.cfscov3_result - assert result.value == expected, ( - f"Expected resolved value '{expected}', got '{result.value}'" - ) - assert result.source == ConfigLevel.LOCAL, ( - f"Expected source LOCAL, got {result.source}" - ) - - -@then( - 'cfscov3 the chain should contain a local entry with value "{expected}" and a path' -) -def step_cfscov3_chain_local_entry(context: Any, expected: str) -> None: - chain = context.cfscov3_result.chain - local_entries = [e for e in chain if e["source"] == ConfigLevel.LOCAL.value] - assert len(local_entries) == 1, ( - f"Expected 1 LOCAL chain entry, got {len(local_entries)}: {chain}" - ) - entry = local_entries[0] - assert entry["value"] == expected, ( - f"Expected LOCAL chain value '{expected}', got '{entry['value']}'" - ) - assert "path" in entry, "Expected 'path' key in LOCAL chain entry" - assert entry["path"], "Expected non-empty path in LOCAL chain entry" diff --git a/features/steps/config_service_coverage_steps.py b/features/steps/config_service_coverage_steps.py index 262bda4ab..2ba1bc4aa 100644 --- a/features/steps/config_service_coverage_steps.py +++ b/features/steps/config_service_coverage_steps.py @@ -25,15 +25,17 @@ from cleveragents.application.services.config_service import ( # --------------------------------------------------------------------------- -@then("ConfigLevel should have values CLI_FLAG ENV_VAR LOCAL PROJECT GLOBAL DEFAULT") +@then("ConfigLevel should have values CLI_FLAG ENV_VAR PROJECT GLOBAL DEFAULT") def step_config_level_enum(context: Any) -> None: assert ConfigLevel.CLI_FLAG.value == "cli_flag" assert ConfigLevel.ENV_VAR.value == "env_var" - assert ConfigLevel.LOCAL.value == "local" assert ConfigLevel.PROJECT.value == "project" assert ConfigLevel.GLOBAL.value == "global" assert ConfigLevel.DEFAULT.value == "default" - assert len(ConfigLevel) == 6 + assert len(ConfigLevel) == 5 + # Verify LOCAL is not present (spec defines only 5 levels) + values = [m.value for m in ConfigLevel] + assert "local" not in values, f"LOCAL should not be in ConfigLevel: {values}" # --------------------------------------------------------------------------- @@ -680,15 +682,14 @@ def step_resolve_verbose(context: Any) -> None: context.result = context.svc.resolve("core.log.level", verbose=True) -@then("the resolved chain should have 6 entries covering all levels") +@then("the resolved chain should have 5 entries covering all levels") def step_verify_verbose_chain_length(context: Any) -> None: chain = context.result.chain - assert len(chain) == 6, f"Expected 6 chain entries, got {len(chain)}" + assert len(chain) == 5, f"Expected 5 chain entries, got {len(chain)}" sources = [entry["source"] for entry in chain] assert sources == [ ConfigLevel.CLI_FLAG.value, ConfigLevel.ENV_VAR.value, - ConfigLevel.LOCAL.value, ConfigLevel.PROJECT.value, ConfigLevel.GLOBAL.value, ConfigLevel.DEFAULT.value, diff --git a/features/steps/config_three_scope_steps.py b/features/steps/config_two_scope_steps.py similarity index 68% rename from features/steps/config_three_scope_steps.py rename to features/steps/config_two_scope_steps.py index a37422844..87f36b48f 100644 --- a/features/steps/config_three_scope_steps.py +++ b/features/steps/config_two_scope_steps.py @@ -1,4 +1,4 @@ -"""Step definitions for config_three_scope.feature — three-scope resolution.""" +"""Step definitions for config_two_scope.feature — two-scope resolution.""" from __future__ import annotations @@ -27,7 +27,7 @@ from cleveragents.application.services.config_service import ( def _svc(context: Context) -> ConfigService: - return context._three_scope_service + return context._two_scope_service def _write_toml(path: Path, data: dict[str, Any]) -> None: @@ -48,7 +48,7 @@ def _read_toml(path: Path) -> dict[str, Any]: def _cleanup(context: Context) -> None: - tmpdir = getattr(context, "_three_scope_tmpdir", None) + tmpdir = getattr(context, "_two_scope_tmpdir", None) if tmpdir and os.path.isdir(tmpdir): shutil.rmtree(tmpdir, ignore_errors=True) @@ -58,10 +58,10 @@ def _cleanup(context: Context) -> None: # --------------------------------------------------------------------------- -@given("a clean three-scope config environment") -def step_clean_three_scope_env(context: Context) -> None: +@given("a clean two-scope config environment") +def step_clean_two_scope_env(context: Context) -> None: tmpdir = tempfile.mkdtemp() - context._three_scope_tmpdir = tmpdir + context._two_scope_tmpdir = tmpdir base = Path(tmpdir) # Global config dir @@ -73,10 +73,10 @@ def step_clean_three_scope_env(context: Context) -> None: project_root.mkdir() (project_root / "cleveragents.toml").touch() - context._three_scope_global_dir = global_dir - context._three_scope_project_root = project_root + context._two_scope_global_dir = global_dir + context._two_scope_project_root = project_root - context._three_scope_service = ConfigService( + context._two_scope_service = ConfigService( config_dir=global_dir, config_path=global_dir / "config.toml", project_root=project_root, @@ -92,7 +92,7 @@ def step_clean_three_scope_env(context: Context) -> None: # --------------------------------------------------------------------------- -@given('a global config value for three-scope "{key}" = "{value}"') +@given('a global config value for two-scope "{key}" = "{value}"') def step_global_value(context: Context, key: str, value: str) -> None: svc = _svc(context) data = svc.read_config() @@ -102,15 +102,7 @@ def step_global_value(context: Context, key: str, value: str) -> None: @given('a project config value "{key}" = "{value}"') def step_project_value(context: Context, key: str, value: str) -> None: - path = context._three_scope_project_root / "config.toml" - data = _read_toml(path) - data[key] = value - _write_toml(path, data) - - -@given('a local config value "{key}" = "{value}"') -def step_local_value(context: Context, key: str, value: str) -> None: - path = context._three_scope_project_root / "config.local.toml" + path = context._two_scope_project_root / "config.toml" data = _read_toml(path) data[key] = value _write_toml(path, data) @@ -128,16 +120,7 @@ def step_global_nested(context: Context, dotted: str, value: str) -> None: @given('a project config with nested data "{dotted}" = "{value}"') def step_project_nested(context: Context, dotted: str, value: str) -> None: - path = context._three_scope_project_root / "config.toml" - data = _read_toml(path) - section, key = dotted.split(".", 1) - data.setdefault(section, {})[key] = value - _write_toml(path, data) - - -@given('a local config with nested data "{dotted}" = "{value}"') -def step_local_nested(context: Context, dotted: str, value: str) -> None: - path = context._three_scope_project_root / "config.local.toml" + path = context._two_scope_project_root / "config.toml" data = _read_toml(path) section, key = dotted.split(".", 1) data.setdefault(section, {})[key] = value @@ -194,30 +177,21 @@ def step_dir_tree_no_markers(context: Context) -> None: ) -# Given — local config file -@given('a local config file with "{key}" = "{value}"') -def step_local_file_with_key(context: Context, key: str, value: str) -> None: - path = context._three_scope_project_root / "config.local.toml" - data = _read_toml(path) - data[key] = value - _write_toml(path, data) - - # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- -@when('I resolve three-scope key "{key}"') -def step_resolve_three_scope(context: Context, key: str) -> None: +@when('I resolve two-scope key "{key}"') +def step_resolve_two_scope(context: Context, key: str) -> None: svc = _svc(context) - context._three_scope_resolved = svc.resolve(key) + context._two_scope_resolved = svc.resolve(key) -@when("I read the three-scope merged config") +@when("I read the two-scope merged config") def step_read_merged(context: Context) -> None: svc = _svc(context) - context._three_scope_merged = svc.read_merged_config() + context._two_scope_merged = svc.read_merged_config() @when("I discover the project root from a subdirectory") @@ -230,13 +204,7 @@ def step_discover_project_root(context: Context) -> None: context._discovered_root = discover_project_root(context._discovery_sub) -@when("I read the local config") -def step_read_local(context: Context) -> None: - svc = _svc(context) - context._local_config = svc.read_local_config() - - -@when('I set three-scope value "{key}" to "{value}" with scope "{scope}"') +@when('I set two-scope value "{key}" to "{value}" with scope "{scope}"') def step_set_scoped(context: Context, key: str, value: str, scope: str) -> None: svc = _svc(context) config_scope = ConfigScope(scope) @@ -248,15 +216,15 @@ def step_set_scoped(context: Context, key: str, value: str, scope: str) -> None: # --------------------------------------------------------------------------- -@then('the three-scope resolved value should be "{value}"') +@then('the two-scope resolved value should be "{value}"') def step_check_value(context: Context, value: str) -> None: - actual = str(context._three_scope_resolved.value) + actual = str(context._two_scope_resolved.value) assert actual == value, f"Expected '{value}', got '{actual}'" -@then('the three-scope resolved source should be "{source}"') +@then('the two-scope resolved source should be "{source}"') def step_check_source(context: Context, source: str) -> None: - actual = context._three_scope_resolved.source.value + actual = context._two_scope_resolved.source.value assert actual == source, f"Expected source '{source}', got '{actual}'" @@ -264,7 +232,7 @@ def step_check_source(context: Context, source: str) -> None: def step_merged_section_key( context: Context, section: str, key: str, value: str ) -> None: - merged = context._three_scope_merged + merged = context._two_scope_merged assert section in merged, f"Section '{section}' not in merged config: {merged}" sec = merged[section] assert key in sec, f"Key '{key}' not in section '{section}': {sec}" @@ -288,24 +256,9 @@ def step_root_none(context: Context) -> None: ) -@then("the local config should be an empty dict") -def step_local_empty(context: Context) -> None: - assert context._local_config == {}, ( - f"Expected empty dict, got {context._local_config}" - ) - - -@then('the local config file should contain "{key}" = "{value}"') -def step_local_file_contains(context: Context, key: str, value: str) -> None: - path = context._three_scope_project_root / "config.local.toml" - data = _read_toml(path) - assert key in data, f"Key '{key}' not in local config: {data}" - assert str(data[key]) == value, f"Expected '{value}', got '{data[key]}'" - - @then('the project config file should contain "{key}" = "{value}"') def step_project_file_contains(context: Context, key: str, value: str) -> None: - path = context._three_scope_project_root / "config.toml" + path = context._two_scope_project_root / "config.toml" data = _read_toml(path) assert key in data, f"Key '{key}' not in project config: {data}" assert str(data[key]) == value, f"Expected '{value}', got '{data[key]}'" @@ -325,29 +278,23 @@ def step_scope_enum_value(context: Context, value: str) -> None: assert scope.value == value, f"Expected ConfigScope.{value}, got {scope}" +@then('ConfigScope should not have value "{value}"') +def step_scope_enum_not_have_value(context: Context, value: str) -> None: + values = [s.value for s in ConfigScope] + assert value not in values, ( + f"ConfigScope should NOT have value '{value}', but found it in {values}" + ) + + @then('ConfigLevel should have value "{value}"') def step_level_enum_value(context: Context, value: str) -> None: level = ConfigLevel(value) assert level.value == value, f"Expected ConfigLevel.{value}, got {level}" -@then('ConfigLevel "{value}" should exist between "{before}" and "{after}"') -def step_level_ordering(context: Context, value: str, before: str, after: str) -> None: - members = [m.value for m in ConfigLevel] - idx_before = members.index(before) - idx_value = members.index(value) - idx_after = members.index(after) - assert idx_before < idx_value < idx_after, ( - f"Expected {before} < {value} < {after} in {members}" - ) - - -@then('the default ignore patterns should include "{pattern}"') -def step_ignore_patterns(context: Context, pattern: str) -> None: - from cleveragents.application.services.context_service import ( - DEFAULT_IGNORE_PATTERNS, - ) - - assert pattern in DEFAULT_IGNORE_PATTERNS, ( - f"'{pattern}' not in DEFAULT_IGNORE_PATTERNS: {DEFAULT_IGNORE_PATTERNS}" +@then('ConfigLevel should not have value "{value}"') +def step_level_enum_not_have_value(context: Context, value: str) -> None: + values = [m.value for m in ConfigLevel] + assert value not in values, ( + f"ConfigLevel should NOT have value '{value}', but found it in {values}" ) diff --git a/robot/config_resolution.robot b/robot/config_resolution.robot index b5ff0339c..4354efef5 100644 --- a/robot/config_resolution.robot +++ b/robot/config_resolution.robot @@ -59,7 +59,7 @@ Config Resolution Project Scope Should Contain ${result.stdout} config-resolution-project-ok Config Resolution Verbose Chain - [Documentation] Verify that verbose=True returns all 6 chain entries + [Documentation] Verify that verbose=True returns all 5 chain entries ${result}= Run Process ${PYTHON} ${HELPER} resolve-verbose-chain cwd=${WORKSPACE} Log ${result.stdout} Log ${result.stderr} diff --git a/robot/helper_config_resolution.py b/robot/helper_config_resolution.py index 6effd4b9c..745fb5065 100644 --- a/robot/helper_config_resolution.py +++ b/robot/helper_config_resolution.py @@ -182,17 +182,16 @@ def resolve_project() -> None: def resolve_verbose_chain() -> None: - """Verify verbose=True populates all 6 chain entries.""" + """Verify verbose=True populates all 5 chain entries.""" svc, tmpdir = _make_service() try: result = svc.resolve("core.log.level", verbose=True) chain_len = len(result.chain) - if chain_len == 6: + if chain_len == 5: # Verify the sources in order expected_sources = [ ConfigLevel.CLI_FLAG.value, ConfigLevel.ENV_VAR.value, - ConfigLevel.LOCAL.value, ConfigLevel.PROJECT.value, ConfigLevel.GLOBAL.value, ConfigLevel.DEFAULT.value, @@ -208,7 +207,7 @@ def resolve_verbose_chain() -> None: sys.exit(1) else: print( - f"FAIL: expected 6 chain entries, got {chain_len}", + f"FAIL: expected 5 chain entries, got {chain_len}", file=sys.stderr, ) sys.exit(1) diff --git a/src/cleveragents/application/services/config_service.py b/src/cleveragents/application/services/config_service.py index 92fa0316f..31826d31f 100644 --- a/src/cleveragents/application/services/config_service.py +++ b/src/cleveragents/application/services/config_service.py @@ -1,7 +1,8 @@ """Config service with multi-level resolution chain and typed key registry. Provides a resolution layer on top of ``Settings`` (pydantic-settings) that -manages TOML file persistence and implements a five-level precedence chain: +manages TOML file persistence and implements the spec-defined five-level +precedence chain: CLI flag > environment variable > project-scoped > global config > default @@ -36,22 +37,27 @@ _logger = structlog.get_logger(__name__) class ConfigLevel(Enum): - """Precedence levels from highest to lowest priority.""" + """Precedence levels from highest to lowest priority. + + Matches the five-level chain defined in the specification: + CLI flag > env var > project-scoped > global config > default. + """ CLI_FLAG = "cli_flag" ENV_VAR = "env_var" - LOCAL = "local" PROJECT = "project" GLOBAL = "global" DEFAULT = "default" class ConfigScope(Enum): - """File-based configuration scopes for ``--scope`` CLI flag.""" + """File-based configuration scopes for ``--scope`` CLI flag. + + Only ``global`` and ``project`` scopes are defined in the specification. + """ GLOBAL = "global" PROJECT = "project" - LOCAL = "local" @dataclass(frozen=True, slots=True) @@ -1230,13 +1236,6 @@ class ConfigService: return None return self._project_root / "config.toml" - @property - def local_config_path(self) -> Path | None: - """Return the path to the local-scoped ``config.local.toml``.""" - if self._project_root is None: - return None - return self._project_root / "config.local.toml" - # -- TOML file management ------------------------------------------------- def read_config(self) -> dict[str, Any]: @@ -1254,23 +1253,14 @@ class ConfigService: with open(path, "rb") as fh: return tomllib.load(fh) - def read_local_config(self) -> dict[str, Any]: - """Read the local-scoped ``config.local.toml`` from the project root.""" - path = self.local_config_path - if path is None or not path.exists(): - return {} - with open(path, "rb") as fh: - return tomllib.load(fh) - def read_merged_config(self) -> dict[str, Any]: - """Return the three-scope deep-merged configuration. + """Return the two-scope deep-merged configuration. Merge order (lowest to highest priority): - ``global < project < local`` + ``global < project`` """ merged = self.read_config() merged = _deep_merge(merged, self.read_project_config()) - merged = _deep_merge(merged, self.read_local_config()) return merged def write_config(self, data: dict[str, Any]) -> None: @@ -1302,8 +1292,7 @@ class ConfigService: Raises ------ ValueError - If the project root is not discovered and scope is - ``PROJECT`` or ``LOCAL``. + If the project root is not discovered and scope is ``PROJECT``. """ if scope == ConfigScope.GLOBAL: self.write_config(data) @@ -1317,10 +1306,7 @@ class ConfigService: ) raise ValueError(msg) - if scope == ConfigScope.PROJECT: - target = self._project_root / "config.toml" - else: - target = self._project_root / "config.local.toml" + target = self._project_root / "config.toml" if target.exists(): with open(target) as fh: @@ -1397,10 +1383,8 @@ class ConfigService: if effective_scope == ConfigScope.GLOBAL: data = self.read_config() - elif effective_scope == ConfigScope.PROJECT: - data = self.read_project_config() else: - data = self.read_local_config() + data = self.read_project_config() old_value = data.get(key) data[key] = value @@ -1490,7 +1474,7 @@ class ConfigService: project_name: str | None = None, verbose: bool = False, ) -> ResolvedValue: - """Resolve *key* through the six-level precedence chain. + """Resolve *key* through the five-level precedence chain. Parameters ---------- @@ -1511,8 +1495,7 @@ class ConfigService: Precedence (highest → lowest):: - CLI flag > env var > local (config.local.toml) - > project (config.toml) > global > default + CLI flag > env var > project (config.toml) > global > default """ entry = self.validate_key(key) chain: list[dict[str, Any]] = [] @@ -1553,33 +1536,7 @@ class ConfigService: } ) - # Level 3: Local config (config.local.toml in project root) - local_val: Any | None = None - if winner_source is None: - local_data = self.read_local_config() - local_val = local_data.get(key) - if local_val is not None and winner_source is None: - coerced = self.validate_type(key, local_val) - if verbose: - chain.append( - { - "source": ConfigLevel.LOCAL.value, - "value": coerced, - "path": str(self.local_config_path or ""), - } - ) - winner_value = coerced - winner_source = ConfigLevel.LOCAL - elif verbose: - chain.append( - { - "source": ConfigLevel.LOCAL.value, - "value": None, - "path": str(self.local_config_path or ""), - } - ) - - # Level 4: Project-scoped config (config.toml in project root, + # Level 3: Project-scoped config (config.toml in project root, # or legacy per-project section in global config) project_val: Any | None = None if winner_source is None: @@ -1617,7 +1574,7 @@ class ConfigService: } ) - # Level 5: Global config file + # Level 4: Global config file global_val: Any | None = None if winner_source is None: config_data = self.read_config() @@ -1643,7 +1600,7 @@ class ConfigService: } ) - # Level 6: Default + # Level 5: Default if verbose: chain.append({"source": ConfigLevel.DEFAULT.value, "value": entry.default}) if winner_source is None: diff --git a/src/cleveragents/application/services/context_service.py b/src/cleveragents/application/services/context_service.py index f8d95086b..48de4ddb8 100644 --- a/src/cleveragents/application/services/context_service.py +++ b/src/cleveragents/application/services/context_service.py @@ -41,7 +41,6 @@ DEFAULT_IGNORE_PATTERNS = [ "venv", ".env", ".agentsignore", - "config.local.toml", "*.pyc", "*.pyo", "*.pyd", diff --git a/src/cleveragents/cli/commands/config.py b/src/cleveragents/cli/commands/config.py index 9ef828889..f2d743266 100644 --- a/src/cleveragents/cli/commands/config.py +++ b/src/cleveragents/cli/commands/config.py @@ -189,7 +189,7 @@ def config_set( typer.Option( "--scope", "-s", - help="Config scope: global, project, or local (default: global)", + help="Config scope: global or project (default: global)", ), ] = None, project: Annotated[ @@ -202,13 +202,12 @@ def config_set( Writes the value to the config file for the specified scope. Scopes: ``global`` (~/.cleveragents/config.toml), - ``project`` (/config.toml), - ``local`` (/config.local.toml). + ``project`` (/config.toml). Examples:: agents config set core.log.level DEBUG - agents config set plan.concurrency 8 --scope local + agents config set plan.concurrency 8 --scope project agents config set core.automation-profile manual --project local/prod """ normalized = _validate_key(key) @@ -225,7 +224,7 @@ def config_set( config_scope = ConfigScope(scope.lower()) except ValueError as exc: raise typer.BadParameter( - f"Invalid scope: '{scope}'. Use 'global', 'project', or 'local'." + f"Invalid scope: '{scope}'. Use 'global' or 'project'." ) from exc # Read previous value @@ -248,13 +247,11 @@ def config_set( svc.write_config(config_data) scope_display = f"project:{project}" elif config_scope is not None: - # New three-scope set (--scope flag) + # Two-scope set (--scope flag) if config_scope == ConfigScope.GLOBAL: previous = config_data.get(normalized) - elif config_scope == ConfigScope.PROJECT: - previous = svc.read_project_config().get(normalized) else: - previous = svc.read_local_config().get(normalized) + previous = svc.read_project_config().get(normalized) svc.set_value(normalized, coerced, scope=config_scope) scope_display = config_scope.value else: