test(e2e): add M5 ACMS + context suites
CI / lint (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 21s
CI / security (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 41s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / integration_tests (pull_request) Successful in 2m47s
CI / benchmark-regression (pull_request) Successful in 28m30s
CI / unit_tests (pull_request) Failing after 33m23s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 53m42s
CI / lint (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 21s
CI / security (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 41s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / integration_tests (pull_request) Successful in 2m47s
CI / benchmark-regression (pull_request) Successful in 28m30s
CI / unit_tests (pull_request) Failing after 33m23s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 53m42s
This commit is contained in:
@@ -0,0 +1,148 @@
|
|||||||
|
"""ASV benchmarks for M5 ACMS pipeline and context smoke suite runtime.
|
||||||
|
|
||||||
|
Measures the performance of:
|
||||||
|
- Context policy resolution across ACMS phases
|
||||||
|
- Budget enforcement checks (file size and total size)
|
||||||
|
- Fixture loading overhead
|
||||||
|
- Context view construction with various configurations
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||||
|
if _SRC not in sys.path:
|
||||||
|
sys.path.insert(0, _SRC)
|
||||||
|
|
||||||
|
import cleveragents # noqa: E402
|
||||||
|
|
||||||
|
importlib.reload(cleveragents)
|
||||||
|
|
||||||
|
from cleveragents.domain.models.core.context_policy import ( # noqa: E402
|
||||||
|
ContextView,
|
||||||
|
ProjectContextPolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" / "m5"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_fixture(name: str) -> dict:
|
||||||
|
"""Load a fixture JSON file."""
|
||||||
|
return json.loads((_FIXTURES_DIR / name).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _make_policy_with_overrides() -> ProjectContextPolicy:
|
||||||
|
"""Create a policy with all phase overrides."""
|
||||||
|
return ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
exclude_paths=["**/__pycache__/**"],
|
||||||
|
max_file_size=524288,
|
||||||
|
max_total_size=10485760,
|
||||||
|
),
|
||||||
|
strategize_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py", "docs/**/*.md"],
|
||||||
|
max_file_size=262144,
|
||||||
|
max_total_size=5242880,
|
||||||
|
),
|
||||||
|
execute_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
max_file_size=524288,
|
||||||
|
),
|
||||||
|
apply_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py", "tests/**/*.py"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyResolutionSuite:
|
||||||
|
"""Benchmark context policy view resolution across phases."""
|
||||||
|
|
||||||
|
params: ClassVar[list[str]] = ["default", "strategize", "execute", "apply"]
|
||||||
|
param_names: ClassVar[list[str]] = ["phase"]
|
||||||
|
|
||||||
|
def setup(self, phase: str) -> None:
|
||||||
|
self.policy = _make_policy_with_overrides()
|
||||||
|
|
||||||
|
def time_resolve_view(self, phase: str) -> None:
|
||||||
|
self.policy.resolve_view(phase)
|
||||||
|
|
||||||
|
|
||||||
|
class BudgetEnforcementSuite:
|
||||||
|
"""Benchmark budget enforcement checks."""
|
||||||
|
|
||||||
|
params: ClassVar[list[int]] = [100, 1000, 10000]
|
||||||
|
param_names: ClassVar[list[str]] = ["file_count"]
|
||||||
|
|
||||||
|
def setup(self, file_count: int) -> None:
|
||||||
|
self.view = ContextView(max_file_size=8192, max_total_size=1048576)
|
||||||
|
self.files = [
|
||||||
|
{"path": f"src/file_{i}.py", "size": (i % 20) * 1024}
|
||||||
|
for i in range(file_count)
|
||||||
|
]
|
||||||
|
|
||||||
|
def time_check_file_budget(self, file_count: int) -> None:
|
||||||
|
max_size = self.view.max_file_size
|
||||||
|
assert max_size is not None
|
||||||
|
for f in self.files:
|
||||||
|
_ = f["size"] <= max_size
|
||||||
|
|
||||||
|
def time_check_total_budget(self, file_count: int) -> None:
|
||||||
|
total = sum(f["size"] for f in self.files)
|
||||||
|
max_total = self.view.max_total_size
|
||||||
|
assert max_total is not None
|
||||||
|
_ = total <= max_total
|
||||||
|
|
||||||
|
|
||||||
|
class FixtureLoadSuite:
|
||||||
|
"""Benchmark fixture file loading."""
|
||||||
|
|
||||||
|
def time_load_policy_fixture(self) -> None:
|
||||||
|
_load_fixture("acms_context_policy.json")
|
||||||
|
|
||||||
|
def time_load_large_project_fixture(self) -> None:
|
||||||
|
_load_fixture("large_project_context.json")
|
||||||
|
|
||||||
|
def time_load_analysis_fixture(self) -> None:
|
||||||
|
_load_fixture("context_analysis_results.json")
|
||||||
|
|
||||||
|
def time_load_all_fixtures(self) -> None:
|
||||||
|
_load_fixture("acms_context_policy.json")
|
||||||
|
_load_fixture("large_project_context.json")
|
||||||
|
_load_fixture("context_analysis_results.json")
|
||||||
|
|
||||||
|
|
||||||
|
class ContextViewConstructionSuite:
|
||||||
|
"""Benchmark ContextView and ProjectContextPolicy construction."""
|
||||||
|
|
||||||
|
params: ClassVar[list[int]] = [1, 10, 50]
|
||||||
|
param_names: ClassVar[list[str]] = ["path_count"]
|
||||||
|
|
||||||
|
def setup(self, path_count: int) -> None:
|
||||||
|
self.include_paths = [f"src/module_{i}/**/*.py" for i in range(path_count)]
|
||||||
|
self.exclude_paths = [f"**/__cache_{i}__/**" for i in range(path_count)]
|
||||||
|
|
||||||
|
def time_construct_view(self, path_count: int) -> None:
|
||||||
|
ContextView(
|
||||||
|
include_paths=self.include_paths,
|
||||||
|
exclude_paths=self.exclude_paths,
|
||||||
|
max_file_size=524288,
|
||||||
|
max_total_size=10485760,
|
||||||
|
)
|
||||||
|
|
||||||
|
def time_construct_full_policy(self, path_count: int) -> None:
|
||||||
|
view = ContextView(
|
||||||
|
include_paths=self.include_paths,
|
||||||
|
exclude_paths=self.exclude_paths,
|
||||||
|
)
|
||||||
|
ProjectContextPolicy(
|
||||||
|
default_view=view,
|
||||||
|
strategize_view=view,
|
||||||
|
execute_view=view,
|
||||||
|
apply_view=view,
|
||||||
|
)
|
||||||
@@ -1295,3 +1295,90 @@ nox -s benchmark
|
|||||||
- **Coverage drops**: The M4 smoke tests cover correction CLI commands,
|
- **Coverage drops**: The M4 smoke tests cover correction CLI commands,
|
||||||
SubplanFailureHandler logic, and fixture loading. Check
|
SubplanFailureHandler logic, and fixture loading. Check
|
||||||
`build/htmlcov/index.html` for uncovered lines in correction/plan CLI code.
|
`build/htmlcov/index.html` for uncovered lines in correction/plan CLI code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M5 ACMS Pipeline + Large-Project Context Smoke Tests
|
||||||
|
|
||||||
|
The M5 smoke suites verify the ACMS context pipeline foundation: context
|
||||||
|
assembly, budget enforcement, multi-project context, context analysis,
|
||||||
|
and project context policy resolution.
|
||||||
|
|
||||||
|
### Behave: `features/m5_acms_smoke.feature`
|
||||||
|
|
||||||
|
27 scenarios covering:
|
||||||
|
|
||||||
|
| Area | Scenarios |
|
||||||
|
|------|-----------|
|
||||||
|
| Fixture loading | Load ACMS context policy, large project context, analysis results |
|
||||||
|
| Context policy resolution | Default view, inheritance chain, strategize/execute/apply overrides |
|
||||||
|
| Budget enforcement | max_file_size, max_total_size, zero/negative validation, unlimited |
|
||||||
|
| Context assembly (CLI) | list, add, show, clear via mocked ContextService |
|
||||||
|
| Project context policy CLI | show policy, inspect/simulate stubs (NotImplementedError) |
|
||||||
|
| Context analysis agent | Summary, dependencies, relevance scores (mocked) |
|
||||||
|
| Multi-project context | Independent context per project |
|
||||||
|
| Exclusion patterns | Glob-based path filtering |
|
||||||
|
|
||||||
|
Step definitions: `features/steps/m5_acms_smoke_steps.py`
|
||||||
|
Fixtures: `features/fixtures/m5/`
|
||||||
|
|
||||||
|
- `acms_context_policy.json` -- Phase-specific context policy with size limits
|
||||||
|
- `large_project_context.json` -- Simulated 500-file project with tier metadata
|
||||||
|
- `context_analysis_results.json` -- Pre-computed analysis with dependencies and
|
||||||
|
relevance scores
|
||||||
|
|
||||||
|
### Robot: `robot/m5_acms_smoke.robot`
|
||||||
|
|
||||||
|
| Test Case | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Load ACMS Context Policy Fixture | Validates fixture structure |
|
||||||
|
| Load Large Project Context Fixture | Validates file count and entries |
|
||||||
|
| Resolve Default View From Empty Policy | Empty policy includes all paths |
|
||||||
|
| Resolve Strategize Inherits From Default | Phase inheritance works |
|
||||||
|
| Resolve Strategize With Override | Override takes precedence |
|
||||||
|
| Budget Max File Size Enforcement | File-level budget check |
|
||||||
|
| Budget Max Total Size Enforcement | Aggregate budget check |
|
||||||
|
| Invalid Phase Raises Error | Error handling for invalid phases |
|
||||||
|
| Context Analysis Fixture Has Required Fields | Analysis result structure |
|
||||||
|
| Multi Project Independent Context | Independent context per project |
|
||||||
|
|
||||||
|
Helper script: `robot/helper_m5_acms_smoke.py`
|
||||||
|
|
||||||
|
### ASV Benchmarks: `benchmarks/m5_smoke_bench.py`
|
||||||
|
|
||||||
|
Four benchmark suites measuring M5 suite runtime:
|
||||||
|
|
||||||
|
- **`PolicyResolutionSuite`** -- `time_resolve_view` across all 4 phases
|
||||||
|
- **`BudgetEnforcementSuite`** -- `time_check_file_budget`,
|
||||||
|
`time_check_total_budget` with 100/1000/10000 files
|
||||||
|
- **`FixtureLoadSuite`** -- `time_load_policy_fixture`,
|
||||||
|
`time_load_large_project_fixture`, `time_load_analysis_fixture`,
|
||||||
|
`time_load_all_fixtures`
|
||||||
|
- **`ContextViewConstructionSuite`** -- `time_construct_view`,
|
||||||
|
`time_construct_full_policy` with 1/10/50 path patterns
|
||||||
|
|
||||||
|
### Running the M5 ACMS Smoke Suites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Behave only (M5 smoke feature)
|
||||||
|
nox -s unit_tests -- features/m5_acms_smoke.feature
|
||||||
|
|
||||||
|
# Robot only (M5 smoke suite)
|
||||||
|
nox -s integration_tests -- --suite robot/m5_acms_smoke.robot
|
||||||
|
|
||||||
|
# Benchmarks
|
||||||
|
nox -s benchmark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failure Triage Tips
|
||||||
|
|
||||||
|
- **`AmbiguousStep` errors**: All M5 smoke steps are prefixed with `m5 smoke`.
|
||||||
|
If ambiguous, check that no other step file defines a conflicting pattern.
|
||||||
|
- **Fixture file not found**: Verify `features/fixtures/m5/` contains all three
|
||||||
|
fixture files (`acms_context_policy.json`, `large_project_context.json`,
|
||||||
|
`context_analysis_results.json`).
|
||||||
|
- **`context_inspect`/`context_simulate` stubs**: These commands raise
|
||||||
|
`NotImplementedError` by design until ACMS wiring is complete.
|
||||||
|
- **Coverage drops**: The M5 smoke tests cover context policy resolution, budget
|
||||||
|
enforcement, and CLI context commands. Check `build/htmlcov/index.html` for
|
||||||
|
uncovered lines in context-related modules.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"default_view": {
|
||||||
|
"include_resources": [],
|
||||||
|
"exclude_resources": [],
|
||||||
|
"include_paths": ["src/**/*.py", "tests/**/*.py"],
|
||||||
|
"exclude_paths": ["**/__pycache__/**", "*.pyc"],
|
||||||
|
"max_file_size": 524288,
|
||||||
|
"max_total_size": 10485760
|
||||||
|
},
|
||||||
|
"strategize_view": {
|
||||||
|
"include_resources": [],
|
||||||
|
"exclude_resources": [],
|
||||||
|
"include_paths": ["src/**/*.py", "docs/**/*.md", "README.md"],
|
||||||
|
"exclude_paths": ["**/__pycache__/**"],
|
||||||
|
"max_file_size": 262144,
|
||||||
|
"max_total_size": 5242880
|
||||||
|
},
|
||||||
|
"execute_view": {
|
||||||
|
"include_resources": [],
|
||||||
|
"exclude_resources": [],
|
||||||
|
"include_paths": ["src/**/*.py"],
|
||||||
|
"exclude_paths": ["**/__pycache__/**", "**/test_*"],
|
||||||
|
"max_file_size": 524288,
|
||||||
|
"max_total_size": 10485760
|
||||||
|
},
|
||||||
|
"apply_view": null
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"summary": "The project contains 5 Python modules with a dependency chain from module_0 through module_4.",
|
||||||
|
"dependencies": {
|
||||||
|
"src/module_0.py": ["src/module_1.py"],
|
||||||
|
"src/module_1.py": ["src/module_2.py"],
|
||||||
|
"src/module_2.py": [],
|
||||||
|
"src/deep/nested/module_3.py": ["src/module_0.py", "src/module_1.py"],
|
||||||
|
"src/deep/nested/module_4.py": []
|
||||||
|
},
|
||||||
|
"relevance_scores": {
|
||||||
|
"src/module_0.py": 0.95,
|
||||||
|
"src/module_1.py": 0.87,
|
||||||
|
"src/module_2.py": 0.72,
|
||||||
|
"src/deep/nested/module_3.py": 0.65,
|
||||||
|
"src/deep/nested/module_4.py": 0.30
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"project_name": "local/m5-large-project",
|
||||||
|
"description": "Simulated large project with many context files",
|
||||||
|
"file_count": 500,
|
||||||
|
"total_size_bytes": 2097152,
|
||||||
|
"file_entries": [
|
||||||
|
{"path": "src/module_0.py", "size": 4096, "hash": "aabbccdd00"},
|
||||||
|
{"path": "src/module_1.py", "size": 8192, "hash": "aabbccdd01"},
|
||||||
|
{"path": "src/module_2.py", "size": 2048, "hash": "aabbccdd02"},
|
||||||
|
{"path": "src/deep/nested/module_3.py", "size": 16384, "hash": "aabbccdd03"},
|
||||||
|
{"path": "src/deep/nested/module_4.py", "size": 1024, "hash": "aabbccdd04"}
|
||||||
|
],
|
||||||
|
"context_tiers": {
|
||||||
|
"hot": {"max_tokens": 4000, "file_count": 10},
|
||||||
|
"warm": {"max_tokens": 8000, "file_count": 50},
|
||||||
|
"cold": {"max_tokens": 16000, "file_count": 500}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
Feature: M5 ACMS pipeline and large-project context smoke tests
|
||||||
|
As a developer working with the CleverAgents M5 milestone
|
||||||
|
I want to verify the ACMS context pipeline end-to-end
|
||||||
|
So that context assembly, budget enforcement, and multi-project context work correctly
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given a m5 smoke test runner
|
||||||
|
And a m5 smoke mocked context service
|
||||||
|
|
||||||
|
# --- Fixture loading ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke load ACMS context policy fixture
|
||||||
|
When I m5 smoke load the ACMS context policy fixture
|
||||||
|
Then the m5 smoke policy fixture should have a default view
|
||||||
|
And the m5 smoke policy fixture should have a strategize view
|
||||||
|
And the m5 smoke policy fixture should have an execute view
|
||||||
|
|
||||||
|
Scenario: M5 smoke load large project context fixture
|
||||||
|
When I m5 smoke load the large project context fixture
|
||||||
|
Then the m5 smoke large project fixture should have file entries
|
||||||
|
And the m5 smoke large project fixture should have context tiers
|
||||||
|
|
||||||
|
Scenario: M5 smoke load context analysis results fixture
|
||||||
|
When I m5 smoke load the context analysis results fixture
|
||||||
|
Then the m5 smoke analysis fixture should have a summary
|
||||||
|
And the m5 smoke analysis fixture should have dependencies
|
||||||
|
And the m5 smoke analysis fixture should have relevance scores
|
||||||
|
|
||||||
|
# --- Context policy resolution ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke resolve default view from empty policy
|
||||||
|
Given a m5 smoke empty context policy
|
||||||
|
When I m5 smoke resolve the view for phase "default"
|
||||||
|
Then the m5 smoke resolved view should include all paths
|
||||||
|
|
||||||
|
Scenario: M5 smoke resolve strategize view inherits from default
|
||||||
|
Given a m5 smoke policy with only default view
|
||||||
|
When I m5 smoke resolve the view for phase "strategize"
|
||||||
|
Then the m5 smoke resolved view should match the default view
|
||||||
|
|
||||||
|
Scenario: M5 smoke resolve strategize view with override
|
||||||
|
Given a m5 smoke policy with strategize override
|
||||||
|
When I m5 smoke resolve the view for phase "strategize"
|
||||||
|
Then the m5 smoke resolved view should use the strategize override
|
||||||
|
|
||||||
|
Scenario: M5 smoke resolve execute view inherits from strategize
|
||||||
|
Given a m5 smoke policy with strategize override
|
||||||
|
When I m5 smoke resolve the view for phase "execute"
|
||||||
|
Then the m5 smoke resolved view should use the strategize override
|
||||||
|
|
||||||
|
Scenario: M5 smoke resolve apply view falls through to default
|
||||||
|
Given a m5 smoke policy with only default view
|
||||||
|
When I m5 smoke resolve the view for phase "apply"
|
||||||
|
Then the m5 smoke resolved view should match the default view
|
||||||
|
|
||||||
|
Scenario: M5 smoke invalid phase raises ValueError
|
||||||
|
Given a m5 smoke empty context policy
|
||||||
|
When I m5 smoke resolve the view for invalid phase "invalid"
|
||||||
|
Then a m5 smoke ValueError should be raised
|
||||||
|
|
||||||
|
# --- Budget enforcement ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke max file size rejects oversized files
|
||||||
|
Given a m5 smoke context view with max_file_size 1024
|
||||||
|
Then a m5 smoke file of size 2048 should exceed the budget
|
||||||
|
And a m5 smoke file of size 512 should be within the budget
|
||||||
|
|
||||||
|
Scenario: M5 smoke max total size limits aggregate context
|
||||||
|
Given a m5 smoke context view with max_total_size 8192
|
||||||
|
Then a m5 smoke aggregate of size 10000 should exceed the budget
|
||||||
|
And a m5 smoke aggregate of size 4096 should be within the budget
|
||||||
|
|
||||||
|
Scenario: M5 smoke zero max file size is invalid
|
||||||
|
When I m5 smoke create a context view with max_file_size 0
|
||||||
|
Then a m5 smoke validation error should be raised
|
||||||
|
|
||||||
|
Scenario: M5 smoke negative max total size is invalid
|
||||||
|
When I m5 smoke create a context view with max_total_size -1
|
||||||
|
Then a m5 smoke validation error should be raised
|
||||||
|
|
||||||
|
Scenario: M5 smoke None size limits allow unlimited
|
||||||
|
Given a m5 smoke context view with no size limits
|
||||||
|
Then a m5 smoke file of size 999999999 should be within the budget
|
||||||
|
And a m5 smoke aggregate of size 999999999 should be within the budget
|
||||||
|
|
||||||
|
# --- Context assembly via CLI ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke context list with empty project
|
||||||
|
Given a m5 smoke project with no context
|
||||||
|
When I m5 smoke invoke context list
|
||||||
|
Then the m5 smoke context list should succeed
|
||||||
|
And the m5 smoke context list output should be empty
|
||||||
|
|
||||||
|
Scenario: M5 smoke context add file to project
|
||||||
|
Given a m5 smoke project with mocked context service
|
||||||
|
When I m5 smoke invoke context add with path "src/main.py"
|
||||||
|
Then the m5 smoke context add should succeed
|
||||||
|
|
||||||
|
Scenario: M5 smoke context show displays file content
|
||||||
|
Given a m5 smoke project with context entries
|
||||||
|
When I m5 smoke invoke context show for path "src/main.py"
|
||||||
|
Then the m5 smoke context show should succeed
|
||||||
|
And the m5 smoke context show output should contain content
|
||||||
|
|
||||||
|
Scenario: M5 smoke context clear removes all entries
|
||||||
|
Given a m5 smoke project with context entries
|
||||||
|
When I m5 smoke invoke context clear
|
||||||
|
Then the m5 smoke context clear should succeed
|
||||||
|
|
||||||
|
# --- Project context policy CLI ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke project context show displays policy
|
||||||
|
Given a m5 smoke project with a saved context policy
|
||||||
|
When I m5 smoke invoke project context show
|
||||||
|
Then the m5 smoke project context show should succeed
|
||||||
|
And the m5 smoke project context output should contain phase views
|
||||||
|
|
||||||
|
Scenario: M5 smoke project context inspect is not yet wired
|
||||||
|
When I m5 smoke invoke project context inspect
|
||||||
|
Then a m5 smoke NotImplementedError should be raised mentioning "ACMS"
|
||||||
|
|
||||||
|
Scenario: M5 smoke project context simulate is not yet wired
|
||||||
|
When I m5 smoke invoke project context simulate
|
||||||
|
Then a m5 smoke NotImplementedError should be raised mentioning "ACMS"
|
||||||
|
|
||||||
|
# --- Context analysis agent ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke context analysis produces summary
|
||||||
|
Given a m5 smoke mocked context analysis agent
|
||||||
|
When I m5 smoke invoke context analysis
|
||||||
|
Then the m5 smoke analysis result should have a non-empty summary
|
||||||
|
|
||||||
|
Scenario: M5 smoke context analysis produces dependencies
|
||||||
|
Given a m5 smoke mocked context analysis agent
|
||||||
|
When I m5 smoke invoke context analysis
|
||||||
|
Then the m5 smoke analysis result should have dependency entries
|
||||||
|
|
||||||
|
Scenario: M5 smoke context analysis produces relevance scores
|
||||||
|
Given a m5 smoke mocked context analysis agent
|
||||||
|
When I m5 smoke invoke context analysis
|
||||||
|
Then the m5 smoke analysis result should have relevance scores
|
||||||
|
And all m5 smoke relevance scores should be between 0 and 1
|
||||||
|
|
||||||
|
# --- Multi-project context ---
|
||||||
|
|
||||||
|
Scenario: M5 smoke multiple projects have independent context
|
||||||
|
Given m5 smoke project "proj-a" with 3 context files
|
||||||
|
And m5 smoke project "proj-b" with 5 context files
|
||||||
|
Then m5 smoke project "proj-a" should have 3 context entries
|
||||||
|
And m5 smoke project "proj-b" should have 5 context entries
|
||||||
|
|
||||||
|
Scenario: M5 smoke context exclusion patterns filter correctly
|
||||||
|
Given a m5 smoke context view excluding "**/__pycache__/**"
|
||||||
|
Then the m5 smoke path "__pycache__/module.pyc" should be excluded
|
||||||
|
And the m5 smoke path "src/module.py" should not be excluded
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
"""Step definitions for M5 ACMS pipeline and large-project context smoke tests.
|
||||||
|
|
||||||
|
All step names are prefixed with ``m5 smoke`` to avoid ``AmbiguousStep``
|
||||||
|
conflicts with existing steps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from behave import given, then, when
|
||||||
|
from behave.runner import Context
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
from cleveragents.cli.commands.context import app as context_app
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_inspect,
|
||||||
|
context_simulate,
|
||||||
|
)
|
||||||
|
from cleveragents.domain.models.core.context import Context as ContextModel
|
||||||
|
from cleveragents.domain.models.core.context_policy import (
|
||||||
|
ContextView,
|
||||||
|
ProjectContextPolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "m5"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Background
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke test runner")
|
||||||
|
def step_m5_smoke_runner(context: Context) -> None:
|
||||||
|
"""Set up the CLI runner for M5 smoke tests."""
|
||||||
|
context.runner = CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke mocked context service")
|
||||||
|
def step_m5_smoke_mocked_service(context: Context) -> None:
|
||||||
|
"""Set up mocked context service for M5 smoke tests."""
|
||||||
|
context.mock_context_service = MagicMock()
|
||||||
|
context.m5_error = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixture loading
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke load the ACMS context policy fixture")
|
||||||
|
def step_m5_load_policy_fixture(context: Context) -> None:
|
||||||
|
fixture_path = _FIXTURES_DIR / "acms_context_policy.json"
|
||||||
|
context.m5_policy_fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke policy fixture should have a default view")
|
||||||
|
def step_m5_policy_has_default(context: Context) -> None:
|
||||||
|
assert "default_view" in context.m5_policy_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke policy fixture should have a strategize view")
|
||||||
|
def step_m5_policy_has_strategize(context: Context) -> None:
|
||||||
|
assert "strategize_view" in context.m5_policy_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke policy fixture should have an execute view")
|
||||||
|
def step_m5_policy_has_execute(context: Context) -> None:
|
||||||
|
assert "execute_view" in context.m5_policy_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke load the large project context fixture")
|
||||||
|
def step_m5_load_large_project(context: Context) -> None:
|
||||||
|
fixture_path = _FIXTURES_DIR / "large_project_context.json"
|
||||||
|
context.m5_large_project = json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke large project fixture should have file entries")
|
||||||
|
def step_m5_large_project_has_files(context: Context) -> None:
|
||||||
|
assert "file_entries" in context.m5_large_project
|
||||||
|
assert len(context.m5_large_project["file_entries"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke large project fixture should have context tiers")
|
||||||
|
def step_m5_large_project_has_tiers(context: Context) -> None:
|
||||||
|
tiers = context.m5_large_project.get("context_tiers", {})
|
||||||
|
assert "hot" in tiers
|
||||||
|
assert "warm" in tiers
|
||||||
|
assert "cold" in tiers
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke load the context analysis results fixture")
|
||||||
|
def step_m5_load_analysis(context: Context) -> None:
|
||||||
|
fixture_path = _FIXTURES_DIR / "context_analysis_results.json"
|
||||||
|
context.m5_analysis_fixture = json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis fixture should have a summary")
|
||||||
|
def step_m5_analysis_has_summary(context: Context) -> None:
|
||||||
|
assert context.m5_analysis_fixture.get("summary")
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis fixture should have dependencies")
|
||||||
|
def step_m5_analysis_has_deps(context: Context) -> None:
|
||||||
|
assert "dependencies" in context.m5_analysis_fixture
|
||||||
|
assert len(context.m5_analysis_fixture["dependencies"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis fixture should have relevance scores")
|
||||||
|
def step_m5_analysis_has_scores(context: Context) -> None:
|
||||||
|
assert "relevance_scores" in context.m5_analysis_fixture
|
||||||
|
assert len(context.m5_analysis_fixture["relevance_scores"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Context policy resolution
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke empty context policy")
|
||||||
|
def step_m5_empty_policy(context: Context) -> None:
|
||||||
|
context.m5_policy = ProjectContextPolicy()
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke policy with only default view")
|
||||||
|
def step_m5_default_only_policy(context: Context) -> None:
|
||||||
|
context.m5_policy = ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
exclude_paths=["**/__pycache__/**"],
|
||||||
|
max_file_size=262144,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke policy with strategize override")
|
||||||
|
def step_m5_strategize_override(context: Context) -> None:
|
||||||
|
context.m5_policy = ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
),
|
||||||
|
strategize_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py", "docs/**/*.md"],
|
||||||
|
max_file_size=131072,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I m5 smoke resolve the view for phase "{phase}"')
|
||||||
|
def step_m5_resolve_view(context: Context, phase: str) -> None:
|
||||||
|
context.m5_resolved_view = context.m5_policy.resolve_view(phase)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I m5 smoke resolve the view for invalid phase "{phase}"')
|
||||||
|
def step_m5_resolve_invalid_phase(context: Context, phase: str) -> None:
|
||||||
|
try:
|
||||||
|
context.m5_policy.resolve_view(phase)
|
||||||
|
context.m5_error = None
|
||||||
|
except ValueError as exc:
|
||||||
|
context.m5_error = exc
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke resolved view should include all paths")
|
||||||
|
def step_m5_view_includes_all(context: Context) -> None:
|
||||||
|
assert context.m5_resolved_view.include_paths == []
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke resolved view should match the default view")
|
||||||
|
def step_m5_view_matches_default(context: Context) -> None:
|
||||||
|
default_view = context.m5_policy.default_view
|
||||||
|
assert context.m5_resolved_view == default_view
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke resolved view should use the strategize override")
|
||||||
|
def step_m5_view_uses_strategize(context: Context) -> None:
|
||||||
|
assert context.m5_resolved_view == context.m5_policy.strategize_view
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke ValueError should be raised")
|
||||||
|
def step_m5_value_error_raised(context: Context) -> None:
|
||||||
|
assert context.m5_error is not None
|
||||||
|
assert isinstance(context.m5_error, ValueError)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Budget enforcement
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke context view with max_file_size {size:d}")
|
||||||
|
def step_m5_view_max_file_size(context: Context, size: int) -> None:
|
||||||
|
context.m5_view = ContextView(max_file_size=size)
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke context view with max_total_size {size:d}")
|
||||||
|
def step_m5_view_max_total_size(context: Context, size: int) -> None:
|
||||||
|
context.m5_view = ContextView(max_total_size=size)
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke context view with no size limits")
|
||||||
|
def step_m5_view_no_limits(context: Context) -> None:
|
||||||
|
context.m5_view = ContextView()
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke file of size {size:d} should exceed the budget")
|
||||||
|
def step_m5_file_exceeds(context: Context, size: int) -> None:
|
||||||
|
assert context.m5_view.max_file_size is not None
|
||||||
|
assert size > context.m5_view.max_file_size
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke file of size {size:d} should be within the budget")
|
||||||
|
def step_m5_file_within(context: Context, size: int) -> None:
|
||||||
|
if context.m5_view.max_file_size is None:
|
||||||
|
return # No limit means always within budget
|
||||||
|
assert size <= context.m5_view.max_file_size
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke aggregate of size {size:d} should exceed the budget")
|
||||||
|
def step_m5_aggregate_exceeds(context: Context, size: int) -> None:
|
||||||
|
assert context.m5_view.max_total_size is not None
|
||||||
|
assert size > context.m5_view.max_total_size
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke aggregate of size {size:d} should be within the budget")
|
||||||
|
def step_m5_aggregate_within(context: Context, size: int) -> None:
|
||||||
|
if context.m5_view.max_total_size is None:
|
||||||
|
return # No limit means always within budget
|
||||||
|
assert size <= context.m5_view.max_total_size
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke create a context view with max_file_size {size:d}")
|
||||||
|
def step_m5_create_view_file_size(context: Context, size: int) -> None:
|
||||||
|
try:
|
||||||
|
ContextView(max_file_size=size)
|
||||||
|
context.m5_error = None
|
||||||
|
except ValidationError as exc:
|
||||||
|
context.m5_error = exc
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke create a context view with max_total_size {size:d}")
|
||||||
|
def step_m5_create_view_total_size(context: Context, size: int) -> None:
|
||||||
|
try:
|
||||||
|
ContextView(max_total_size=size)
|
||||||
|
context.m5_error = None
|
||||||
|
except ValidationError as exc:
|
||||||
|
context.m5_error = exc
|
||||||
|
|
||||||
|
|
||||||
|
@then("a m5 smoke validation error should be raised")
|
||||||
|
def step_m5_validation_error(context: Context) -> None:
|
||||||
|
assert context.m5_error is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Context assembly via CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke project with no context")
|
||||||
|
def step_m5_project_no_context(context: Context) -> None:
|
||||||
|
context.mock_context_service.list_context.return_value = []
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke project with mocked context service")
|
||||||
|
def step_m5_project_mocked_service(context: Context) -> None:
|
||||||
|
context.mock_context_service.add_to_context.return_value = (
|
||||||
|
[Path("src/main.py")],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke project with context entries")
|
||||||
|
def step_m5_project_with_entries(context: Context) -> None:
|
||||||
|
mock_entry = MagicMock(spec=ContextModel)
|
||||||
|
mock_entry.path = "src/main.py"
|
||||||
|
mock_entry.size = 1024
|
||||||
|
mock_entry.type = "FILE"
|
||||||
|
context.mock_context_service.list_context.return_value = [mock_entry]
|
||||||
|
context.mock_context_service.show_context_content.return_value = {
|
||||||
|
"src/main.py": "print('hello')"
|
||||||
|
}
|
||||||
|
context.mock_context_service.get_context_content.return_value = "print('hello')"
|
||||||
|
context.mock_context_service.clear_context.return_value = 1
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke context list")
|
||||||
|
def step_m5_invoke_context_list(context: Context) -> None:
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context.ContextService",
|
||||||
|
return_value=context.mock_context_service,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context._resolve_project",
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = context.runner.invoke(context_app, ["list", "--format", "json"])
|
||||||
|
context.m5_result = result
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context list should succeed")
|
||||||
|
def step_m5_context_list_success(context: Context) -> None:
|
||||||
|
assert context.m5_result.exit_code == 0, (
|
||||||
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context list output should be empty")
|
||||||
|
def step_m5_context_list_empty(context: Context) -> None:
|
||||||
|
# Empty context list may show "[]" or "No context" message
|
||||||
|
output = context.m5_result.output.strip()
|
||||||
|
assert output == "[]" or "no context" in output.lower() or output == ""
|
||||||
|
|
||||||
|
|
||||||
|
@when('I m5 smoke invoke context add with path "{path}"')
|
||||||
|
def step_m5_invoke_context_add(context: Context, path: str) -> None:
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context.ContextService",
|
||||||
|
return_value=context.mock_context_service,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context._resolve_project",
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = context.runner.invoke(context_app, ["add", path])
|
||||||
|
context.m5_result = result
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context add should succeed")
|
||||||
|
def step_m5_context_add_success(context: Context) -> None:
|
||||||
|
assert context.m5_result.exit_code == 0, (
|
||||||
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I m5 smoke invoke context show for path "{path}"')
|
||||||
|
def step_m5_invoke_context_show(context: Context, path: str) -> None:
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context.ContextService",
|
||||||
|
return_value=context.mock_context_service,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context._resolve_project",
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = context.runner.invoke(context_app, ["show", path])
|
||||||
|
context.m5_result = result
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context show should succeed")
|
||||||
|
def step_m5_context_show_success(context: Context) -> None:
|
||||||
|
assert context.m5_result.exit_code == 0, (
|
||||||
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context show output should contain content")
|
||||||
|
def step_m5_context_show_has_content(context: Context) -> None:
|
||||||
|
assert len(context.m5_result.output.strip()) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke context clear")
|
||||||
|
def step_m5_invoke_context_clear(context: Context) -> None:
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context.ContextService",
|
||||||
|
return_value=context.mock_context_service,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"cleveragents.cli.commands.context._resolve_project",
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = context.runner.invoke(context_app, ["clear", "--yes"])
|
||||||
|
context.m5_result = result
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke context clear should succeed")
|
||||||
|
def step_m5_context_clear_success(context: Context) -> None:
|
||||||
|
assert context.m5_result.exit_code == 0, (
|
||||||
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Project context policy CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke project with a saved context policy")
|
||||||
|
def step_m5_project_with_policy(context: Context) -> None:
|
||||||
|
context.m5_saved_policy = ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
max_file_size=262144,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke project context show")
|
||||||
|
def step_m5_invoke_project_context_show(context: Context) -> None:
|
||||||
|
mock_project = MagicMock()
|
||||||
|
mock_project.context_policy = context.m5_saved_policy
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"cleveragents.cli.commands.project_context._get_namespaced_project_repo",
|
||||||
|
return_value=(mock_project, MagicMock()),
|
||||||
|
):
|
||||||
|
from cleveragents.cli.commands.project_context import app as pc_app
|
||||||
|
|
||||||
|
result = context.runner.invoke(pc_app, ["show", "local/m5-proj"])
|
||||||
|
context.m5_result = result
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke project context show should succeed")
|
||||||
|
def step_m5_project_context_show_success(context: Context) -> None:
|
||||||
|
assert context.m5_result.exit_code == 0, (
|
||||||
|
f"Exit code: {context.m5_result.exit_code}\n{context.m5_result.output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke project context output should contain phase views")
|
||||||
|
def step_m5_project_context_has_phases(context: Context) -> None:
|
||||||
|
output = context.m5_result.output
|
||||||
|
assert "default" in output.lower() or "view" in output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke project context inspect")
|
||||||
|
def step_m5_invoke_context_inspect(context: Context) -> None:
|
||||||
|
try:
|
||||||
|
context_inspect(project="local/m5-proj")
|
||||||
|
context.m5_error = None
|
||||||
|
except NotImplementedError as exc:
|
||||||
|
context.m5_error = exc
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke project context simulate")
|
||||||
|
def step_m5_invoke_context_simulate(context: Context) -> None:
|
||||||
|
try:
|
||||||
|
context_simulate(project="local/m5-proj")
|
||||||
|
context.m5_error = None
|
||||||
|
except NotImplementedError as exc:
|
||||||
|
context.m5_error = exc
|
||||||
|
|
||||||
|
|
||||||
|
@then('a m5 smoke NotImplementedError should be raised mentioning "{text}"')
|
||||||
|
def step_m5_not_implemented_mentioning(context: Context, text: str) -> None:
|
||||||
|
assert context.m5_error is not None, "Expected NotImplementedError was not raised"
|
||||||
|
assert isinstance(context.m5_error, NotImplementedError)
|
||||||
|
assert text.lower() in str(context.m5_error).lower(), (
|
||||||
|
f"Expected '{text}' in: {context.m5_error}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Context analysis agent
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given("a m5 smoke mocked context analysis agent")
|
||||||
|
def step_m5_mocked_analysis_agent(context: Context) -> None:
|
||||||
|
fixture_path = _FIXTURES_DIR / "context_analysis_results.json"
|
||||||
|
fixture_data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
context.m5_mock_analysis = {
|
||||||
|
"file_paths": list(fixture_data["dependencies"].keys()),
|
||||||
|
"documents": [],
|
||||||
|
"dependencies": fixture_data["dependencies"],
|
||||||
|
"summary": fixture_data["summary"],
|
||||||
|
"relevance_scores": fixture_data["relevance_scores"],
|
||||||
|
"chunks": [],
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@when("I m5 smoke invoke context analysis")
|
||||||
|
def step_m5_invoke_analysis(context: Context) -> None:
|
||||||
|
context.m5_analysis_result = context.m5_mock_analysis
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis result should have a non-empty summary")
|
||||||
|
def step_m5_analysis_has_nonempty_summary(context: Context) -> None:
|
||||||
|
assert context.m5_analysis_result["summary"]
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis result should have dependency entries")
|
||||||
|
def step_m5_analysis_has_dep_entries(context: Context) -> None:
|
||||||
|
assert len(context.m5_analysis_result["dependencies"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@then("the m5 smoke analysis result should have relevance scores")
|
||||||
|
def step_m5_analysis_has_rel_scores(context: Context) -> None:
|
||||||
|
assert len(context.m5_analysis_result["relevance_scores"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
@then("all m5 smoke relevance scores should be between 0 and 1")
|
||||||
|
def step_m5_scores_bounded(context: Context) -> None:
|
||||||
|
for path, score in context.m5_analysis_result["relevance_scores"].items():
|
||||||
|
assert 0.0 <= score <= 1.0, f"Score for {path} out of range: {score}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Multi-project context
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given('m5 smoke project "{name}" with {count:d} context files')
|
||||||
|
def step_m5_project_with_files(context: Context, name: str, count: int) -> None:
|
||||||
|
if not hasattr(context, "m5_projects"):
|
||||||
|
context.m5_projects = {}
|
||||||
|
context.m5_projects[name] = [
|
||||||
|
{"path": f"src/file_{i}.py", "size": 1024} for i in range(count)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@then('m5 smoke project "{name}" should have {count:d} context entries')
|
||||||
|
def step_m5_project_entry_count(context: Context, name: str, count: int) -> None:
|
||||||
|
assert len(context.m5_projects[name]) == count, (
|
||||||
|
f"Expected {count} entries for {name}, got {len(context.m5_projects[name])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Context exclusion patterns
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given('a m5 smoke context view excluding "{pattern}"')
|
||||||
|
def step_m5_view_excluding(context: Context, pattern: str) -> None:
|
||||||
|
context.m5_view = ContextView(exclude_paths=[pattern])
|
||||||
|
context.m5_exclude_pattern = pattern
|
||||||
|
|
||||||
|
|
||||||
|
@then('the m5 smoke path "{path}" should be excluded')
|
||||||
|
def step_m5_path_excluded(context: Context, path: str) -> None:
|
||||||
|
assert fnmatch.fnmatch(path, context.m5_exclude_pattern), (
|
||||||
|
f"Expected '{path}' to match exclude pattern '{context.m5_exclude_pattern}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the m5 smoke path "{path}" should not be excluded')
|
||||||
|
def step_m5_path_not_excluded(context: Context, path: str) -> None:
|
||||||
|
assert not fnmatch.fnmatch(path, context.m5_exclude_pattern), (
|
||||||
|
f"Expected '{path}' NOT to match exclude pattern"
|
||||||
|
)
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Robot Framework helper for M5 ACMS pipeline smoke tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from cleveragents.domain.models.core.context_policy import (
|
||||||
|
ContextView,
|
||||||
|
ProjectContextPolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "features" / "fixtures" / "m5"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Robot keywords
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def load_acms_context_policy_fixture() -> dict[str, Any]:
|
||||||
|
"""Load the ACMS context policy fixture file."""
|
||||||
|
fixture_path = _FIXTURES_DIR / "acms_context_policy.json"
|
||||||
|
return json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_large_project_context_fixture() -> dict[str, Any]:
|
||||||
|
"""Load the large project context fixture file."""
|
||||||
|
fixture_path = _FIXTURES_DIR / "large_project_context.json"
|
||||||
|
return json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_context_analysis_fixture() -> dict[str, Any]:
|
||||||
|
"""Load the context analysis results fixture file."""
|
||||||
|
fixture_path = _FIXTURES_DIR / "context_analysis_results.json"
|
||||||
|
return json.loads(fixture_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_view_for_phase(phase: str) -> dict[str, Any]:
|
||||||
|
"""Resolve a view from an empty policy for the given phase."""
|
||||||
|
policy = ProjectContextPolicy()
|
||||||
|
view = policy.resolve_view(phase)
|
||||||
|
return {
|
||||||
|
"include_paths": view.include_paths,
|
||||||
|
"exclude_paths": view.exclude_paths,
|
||||||
|
"include_resources": view.include_resources,
|
||||||
|
"exclude_resources": view.exclude_resources,
|
||||||
|
"max_file_size": view.max_file_size,
|
||||||
|
"max_total_size": view.max_total_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_strategize_inheriting_default() -> dict[str, Any]:
|
||||||
|
"""Resolve strategize view that inherits from default."""
|
||||||
|
policy = ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
exclude_paths=["**/__pycache__/**"],
|
||||||
|
max_file_size=262144,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
view = policy.resolve_view("strategize")
|
||||||
|
return {
|
||||||
|
"include_paths": view.include_paths,
|
||||||
|
"max_file_size": view.max_file_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_strategize_with_override() -> dict[str, Any]:
|
||||||
|
"""Resolve strategize view with explicit override."""
|
||||||
|
policy = ProjectContextPolicy(
|
||||||
|
default_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py"],
|
||||||
|
max_file_size=262144,
|
||||||
|
),
|
||||||
|
strategize_view=ContextView(
|
||||||
|
include_paths=["src/**/*.py", "docs/**/*.md"],
|
||||||
|
max_file_size=131072,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
view = policy.resolve_view("strategize")
|
||||||
|
return {
|
||||||
|
"include_paths": view.include_paths,
|
||||||
|
"max_file_size": view.max_file_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_budget_enforcement(
|
||||||
|
max_size: int,
|
||||||
|
oversized_file: int,
|
||||||
|
within_file: int,
|
||||||
|
) -> dict[str, bool]:
|
||||||
|
"""Check if files exceed or fit within file size budget."""
|
||||||
|
view = ContextView(max_file_size=int(max_size))
|
||||||
|
limit = view.max_file_size
|
||||||
|
assert limit is not None
|
||||||
|
return {
|
||||||
|
"oversized": int(oversized_file) > limit,
|
||||||
|
"within_budget": int(within_file) <= limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_total_budget_enforcement(
|
||||||
|
max_total: int,
|
||||||
|
oversized_total: int,
|
||||||
|
within_total: int,
|
||||||
|
) -> dict[str, bool]:
|
||||||
|
"""Check if aggregate context exceeds total size budget."""
|
||||||
|
view = ContextView(max_total_size=int(max_total))
|
||||||
|
limit = view.max_total_size
|
||||||
|
assert limit is not None
|
||||||
|
return {
|
||||||
|
"oversized": int(oversized_total) > limit,
|
||||||
|
"within_budget": int(within_total) <= limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def attempt_resolve_invalid_phase(phase: str) -> dict[str, str]:
|
||||||
|
"""Try resolving an invalid phase and capture the error."""
|
||||||
|
policy = ProjectContextPolicy()
|
||||||
|
try:
|
||||||
|
policy.resolve_view(phase)
|
||||||
|
return {"error": "False", "message": ""}
|
||||||
|
except ValueError as exc:
|
||||||
|
return {"error": "True", "message": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
def create_multi_project_context(
|
||||||
|
count_a: int,
|
||||||
|
count_b: int,
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""Simulate independent context for two projects."""
|
||||||
|
proj_a = [{"path": f"src/a_{i}.py", "size": 1024} for i in range(int(count_a))]
|
||||||
|
proj_b = [{"path": f"src/b_{i}.py", "size": 1024} for i in range(int(count_b))]
|
||||||
|
return {
|
||||||
|
"proj_a_count": len(proj_a),
|
||||||
|
"proj_b_count": len(proj_b),
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation M5 ACMS pipeline and context integration tests
|
||||||
|
Library OperatingSystem
|
||||||
|
Library Collections
|
||||||
|
Library helper_m5_acms_smoke.py
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
|
||||||
|
Load ACMS Context Policy Fixture
|
||||||
|
[Documentation] Load and validate M5 ACMS context policy fixture
|
||||||
|
${policy}= Load Acms Context Policy Fixture
|
||||||
|
Dictionary Should Contain Key ${policy} default_view
|
||||||
|
Dictionary Should Contain Key ${policy} strategize_view
|
||||||
|
Dictionary Should Contain Key ${policy} execute_view
|
||||||
|
|
||||||
|
Load Large Project Context Fixture
|
||||||
|
[Documentation] Load and validate large project context fixture
|
||||||
|
${project}= Load Large Project Context Fixture
|
||||||
|
Should Be Equal As Integers ${project}[file_count] 500
|
||||||
|
Length Should Be ${project}[file_entries] 5
|
||||||
|
|
||||||
|
Resolve Default View From Empty Policy
|
||||||
|
[Documentation] Empty policy resolves to default view with no filtering
|
||||||
|
${view}= Resolve View For Phase default
|
||||||
|
Length Should Be ${view}[include_paths] 0
|
||||||
|
Length Should Be ${view}[exclude_paths] 0
|
||||||
|
|
||||||
|
Resolve Strategize Inherits From Default
|
||||||
|
[Documentation] Strategize phase inherits from default when not overridden
|
||||||
|
${view}= Resolve Strategize Inheriting Default
|
||||||
|
Should Be Equal As Integers ${view}[max_file_size] 262144
|
||||||
|
|
||||||
|
Resolve Strategize With Override
|
||||||
|
[Documentation] Strategize override takes precedence over default
|
||||||
|
${view}= Resolve Strategize With Override
|
||||||
|
Should Be Equal As Integers ${view}[max_file_size] 131072
|
||||||
|
|
||||||
|
Budget Max File Size Enforcement
|
||||||
|
[Documentation] Files exceeding max_file_size are detected
|
||||||
|
${result}= Check Budget Enforcement 1024 2048 512
|
||||||
|
Should Be True ${result}[oversized]
|
||||||
|
Should Be True ${result}[within_budget]
|
||||||
|
|
||||||
|
Budget Max Total Size Enforcement
|
||||||
|
[Documentation] Aggregate context exceeding max_total_size is detected
|
||||||
|
${result}= Check Total Budget Enforcement 8192 10000 4096
|
||||||
|
Should Be True ${result}[oversized]
|
||||||
|
Should Be True ${result}[within_budget]
|
||||||
|
|
||||||
|
Invalid Phase Raises Error
|
||||||
|
[Documentation] Resolving an invalid phase produces an error
|
||||||
|
${result}= Attempt Resolve Invalid Phase invalid
|
||||||
|
Should Be Equal ${result}[error] True
|
||||||
|
|
||||||
|
Context Analysis Fixture Has Required Fields
|
||||||
|
[Documentation] Analysis results fixture has summary, dependencies, scores
|
||||||
|
${analysis}= Load Context Analysis Fixture
|
||||||
|
Should Not Be Empty ${analysis}[summary]
|
||||||
|
Should Not Be Empty ${analysis}[dependencies]
|
||||||
|
Should Not Be Empty ${analysis}[relevance_scores]
|
||||||
|
|
||||||
|
Multi Project Independent Context
|
||||||
|
[Documentation] Two projects maintain independent context entries
|
||||||
|
${result}= Create Multi Project Context 3 5
|
||||||
|
Should Be Equal As Integers ${result}[proj_a_count] 3
|
||||||
|
Should Be Equal As Integers ${result}[proj_b_count] 5
|
||||||
Reference in New Issue
Block a user