Files
temp/features/steps/project_context_phase_analysis_steps.py
aditya ef8f9640fa feat(acms): context analysis produces meaningful summaries
Add phase-wise context analysis summaries for strategize, execute, and apply, and integrate them into `agents project context inspect` and `agents project context simulate` with per-phase resource counts, size and token utilization, and narrowing diagnostics.\n\nAdd Behave and Robot coverage for empty, single-resource, multi-resource, and budget-constrained context analysis, and tighten the M5 smoke coverage after review feedback with exact summary assertions and a missing-path regression scenario for `agents context add`.\n\nISSUES CLOSED: #849
2026-03-30 08:42:48 +00:00

186 lines
6.0 KiB
Python

"""Behave steps for project_context_phase_analysis.feature."""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from cleveragents.application.services.context_phase_analysis import (
analyze_phase_summaries,
)
from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment
from cleveragents.domain.models.core.context_policy import (
ContextView,
ProjectContextPolicy,
)
def _base_policy() -> ProjectContextPolicy:
return ProjectContextPolicy(
default_view=ContextView(
include_resources=["local/*"],
max_file_size=2_000_000,
max_total_size=5_000_000,
),
strategize_view=ContextView(
include_resources=["local/*"],
include_paths=["src/**/*.py", "docs/**/*.md"],
exclude_paths=["**/test_*.py"],
max_file_size=1_000_000,
max_total_size=2_000_000,
),
execute_view=ContextView(
include_resources=["local/*"],
include_paths=["src/**/*.py"],
exclude_paths=["**/test_*.py", "**/conftest.py"],
max_file_size=500_000,
max_total_size=700_000,
),
apply_view=ContextView(
include_resources=["local/*"],
include_paths=["src/**/*.py"],
exclude_paths=["docs/**"],
max_file_size=250_000,
max_total_size=400_000,
),
)
@given("a phase analysis policy with narrowing limits")
def step_policy(context: Any) -> None:
context.phase_policy = _base_policy()
@given("no tiered fragments for phase analysis")
def step_no_fragments(context: Any) -> None:
context.phase_fragments = []
@given("a single resource fragment for phase analysis")
def step_single_fragment(context: Any) -> None:
context.phase_fragments = [
TieredFragment(
fragment_id="single",
content="x" * 100,
tier=ContextTier.HOT,
resource_id="local/repo-a",
project_name="local/ctx-app",
token_count=120,
metadata={"path": "src/main.py", "byte_size": 100_000},
)
]
@given("multiple resource fragments for phase analysis")
def step_multiple_resources(context: Any) -> None:
context.phase_fragments = [
TieredFragment(
fragment_id="a",
content="x" * 80,
tier=ContextTier.HOT,
resource_id="local/repo-a",
project_name="local/ctx-app",
token_count=100,
metadata={"path": "src/a.py", "byte_size": 80_000},
),
TieredFragment(
fragment_id="b",
content="x" * 90,
tier=ContextTier.WARM,
resource_id="local/repo-b",
project_name="local/ctx-app",
token_count=110,
metadata={"path": "src/b.py", "byte_size": 90_000},
),
]
@given("budget constrained fragments for phase analysis")
def step_budget_fragments(context: Any) -> None:
context.phase_fragments = [
TieredFragment(
fragment_id="f1",
content="x" * 400,
tier=ContextTier.HOT,
resource_id="local/repo-a",
project_name="local/ctx-app",
token_count=450,
metadata={"path": "src/core.py", "byte_size": 400_000},
),
TieredFragment(
fragment_id="f2",
content="x" * 380,
tier=ContextTier.WARM,
resource_id="local/repo-a",
project_name="local/ctx-app",
token_count=430,
metadata={"path": "src/engine.py", "byte_size": 380_000},
),
TieredFragment(
fragment_id="f3",
content="x" * 200,
tier=ContextTier.COLD,
resource_id="local/repo-a",
project_name="local/ctx-app",
token_count=220,
metadata={"path": "docs/design.md", "byte_size": 200_000},
),
]
@when("I compute project context phase analysis with budget {budget:d}")
def step_compute(context: Any, budget: int) -> None:
context.phase_result = analyze_phase_summaries(
context.phase_policy,
context.phase_fragments,
budget,
)
@then("each phase summary should include resource and size metrics")
def step_metrics_present(context: Any) -> None:
for phase in ("strategize", "execute", "apply"):
row = context.phase_result["phases"][phase]
assert "resource_count" in row
assert "total_bytes" in row
assert "budget_used_ratio" in row
@then("phase analysis should report monotonic narrowing")
def step_narrowing(context: Any) -> None:
narrowing = context.phase_result["narrowing"]
assert narrowing["overall"] is True
@then("each phase summary should include human-readable text")
def step_summary_text(context: Any) -> None:
for phase in ("strategize", "execute", "apply"):
summary = context.phase_result["phases"][phase]["summary"]
assert isinstance(summary, str)
assert phase in summary
@then("strategize phase should include at least one fragment")
def step_strat_non_empty(context: Any) -> None:
assert context.phase_result["phases"]["strategize"]["fragment_count"] >= 1
@then("strategize phase resource count should be 2")
def step_resource_count_two(context: Any) -> None:
assert context.phase_result["phases"]["strategize"]["resource_count"] == 2
@then("execute phase should have fewer tokens than strategize phase")
def step_execute_less(context: Any) -> None:
strat_tokens = context.phase_result["phases"]["strategize"]["total_tokens"]
exec_tokens = context.phase_result["phases"]["execute"]["total_tokens"]
assert exec_tokens < strat_tokens
@then("apply phase should have fewer or equal tokens than execute phase")
def step_apply_less_or_equal(context: Any) -> None:
exec_tokens = context.phase_result["phases"]["execute"]["total_tokens"]
apply_tokens = context.phase_result["phases"]["apply"]["total_tokens"]
assert apply_tokens <= exec_tokens