diff --git a/features/execute_phase_context_assembler_coverage.feature b/features/execute_phase_context_assembler_coverage.feature index f9f26069d..660a79392 100644 --- a/features/execute_phase_context_assembler_coverage.feature +++ b/features/execute_phase_context_assembler_coverage.feature @@ -46,7 +46,9 @@ Feature: Execute-phase context assembler coverage When epcov I check path matching for "src/foo.py" with exclude "src/secret*" Then epcov the path should match -@tdd_issue @tdd_issue_10972 + # ---- Absolute path matching with relative globs (issue #10972) ---- + + @tdd_issue @tdd_issue_10972 Scenario: epcov path matches absolute path against relative include glob When epcov I check path matching for "/app/.opencode/skills/SKILL.md" with include ".opencode/**" Then epcov the path should match @@ -71,6 +73,26 @@ Feature: Execute-phase context assembler coverage When epcov I check path matching for "build/debug/output.log" with exclude "build/**" Then epcov the path should not match + @tdd_issue @tdd_issue_10972 + Scenario: epcov absolute path matched against relative exclude glob + When epcov I check path matching for "/app/.opencode/skills/secret.md" with exclude ".opencode/**" + Then epcov the path should not match + + @tdd_issue @tdd_issue_10972 + Scenario: epcov absolute path deep subdirectory matches relative glob + When epcov I check path matching for "/app/docs/api/endpoint.md" with include "docs/**" + Then epcov the path should match + + @tdd_issue @tdd_issue_10972 + Scenario: epcov absolute path excluded by nested relative glob + When epcov I check path matching for "/app/vendor/lib/external.py" with exclude "vendor/**" + Then epcov the path should not match + + @tdd_issue @tdd_issue_10972 + Scenario: epcov absolute pattern (anchored with /) passed through unchanged + When epcov I check path matching for "/src/main.py" with include "/src/**" + Then epcov the path should match + # ---- _resource_matches static method ---- Scenario: epcov resource matches with no rules passes all diff --git a/features/project_context_phase_analysis.feature b/features/project_context_phase_analysis.feature index 5d751c366..361bae573 100644 --- a/features/project_context_phase_analysis.feature +++ b/features/project_context_phase_analysis.feature @@ -30,9 +30,18 @@ Feature: Project context phase analysis summaries Then execute phase should have fewer tokens than strategize phase And apply phase should have fewer or equal tokens than execute phase + # ---- Absolute path matching regression tests (issue #10972) ---- + @tdd_issue @tdd_issue_10972 Scenario: Absolute path fragments are correctly excluded by relative exclude globs Given a phase analysis policy with opencode exclude paths And an absolute path fragment for phase analysis When I compute project context phase analysis with budget 2000 Then strategize phase should exclude the absolute path fragment + + @tdd_issue @tdd_issue_10972 + Scenario: absolute path excluded by relative exclude glob in phase analysis + Given a phase analysis policy with narrow include_paths "src/**" and strict exclude_paths ".opencode/**" + And an absolute-path fragment for phase analysis at "/app/.opencode/skills/SKILL.md" + When I compute project context phase analysis with budget 2000 + Then the fragment should be excluded because its path does not match include paths diff --git a/features/steps/project_context_phase_analysis_steps.py b/features/steps/project_context_phase_analysis_steps.py index 77e80bef9..7cda8f14d 100644 --- a/features/steps/project_context_phase_analysis_steps.py +++ b/features/steps/project_context_phase_analysis_steps.py @@ -208,7 +208,7 @@ def step_policy_opencode_exclude(context: Any) -> None: @given("an absolute path fragment for phase analysis") -def step_absolute_path_fragment(context: Any) -> None: +def step_absolute_path_fragment_opencode(context: Any) -> None: """Fragment with an absolute path that should be excluded by relative globs.""" context.phase_fragments = [ TieredFragment( @@ -226,6 +226,35 @@ def step_absolute_path_fragment(context: Any) -> None: ] +@given( + 'a phase analysis policy with narrow include_paths "{include}" and strict exclude_paths "{exclude}"' +) +def step_strict_path_policy(context: Any, include: str, exclude: str) -> None: + context.phase_policy = ProjectContextPolicy( + default_view=ContextView(include_resources=["local/*"]), + strategize_view=ContextView( + include_paths=[include], + exclude_paths=[exclude], + ), + ) + + +@given('an absolute-path fragment for phase analysis at "{path}"') +def step_absolute_path_fragment_at(context: Any, path: str) -> None: + """Create a fragment with an absolute path for testing issue #10972.""" + context.phase_fragments = [ + TieredFragment( + fragment_id="abs-paths", + content="x" * 50, + tier=ContextTier.HOT, + resource_id="local/repo-a", + project_name="local/ctx-app", + token_count=60, + metadata={"path": path, "byte_size": 50_000}, + ) + ] + + @then("strategize phase should exclude the absolute path fragment") def step_strat_excludes_absolute(context: Any) -> None: """Verify that the absolute path fragment is excluded by relative glob patterns.""" @@ -234,3 +263,11 @@ def step_strat_excludes_absolute(context: Any) -> None: f"Expected 0 fragments (absolute path should be excluded by relative glob), " f"got {strat['fragment_count']}" ) + + +@then("the fragment should be excluded because its path does not match include paths") +def step_fragment_excluded(context: Any) -> None: + strat = context.phase_result["phases"]["strategize"] + assert strat["fragment_count"] == 0, ( + f"Expected 0 fragments in strategize phase but had {strat['fragment_count']}" + )