forked from HAL9000/cleveragents-core
ef8f9640fa
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
186 lines
6.0 KiB
Python
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
|