fix(cli): address PR #6626 review blockers for context set output
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 44s
CI / lint (pull_request) Failing after 1m45s
CI / build (pull_request) Successful in 1m45s
CI / helm (pull_request) Successful in 1m12s
CI / benchmark-regression (pull_request) Failing after 1m46s
CI / quality (pull_request) Successful in 2m3s
CI / typecheck (pull_request) Successful in 2m17s
CI / security (pull_request) Successful in 3m0s
CI / integration_tests (pull_request) Successful in 4m47s
CI / e2e_tests (pull_request) Successful in 5m48s
CI / unit_tests (pull_request) Successful in 7m22s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 44s
CI / lint (pull_request) Failing after 1m45s
CI / build (pull_request) Successful in 1m45s
CI / helm (pull_request) Successful in 1m12s
CI / benchmark-regression (pull_request) Failing after 1m46s
CI / quality (pull_request) Successful in 2m3s
CI / typecheck (pull_request) Successful in 2m17s
CI / security (pull_request) Successful in 3m0s
CI / integration_tests (pull_request) Successful in 4m47s
CI / e2e_tests (pull_request) Successful in 5m48s
CI / unit_tests (pull_request) Successful in 7m22s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
- Fix ruff import ordering in project_context_cli_steps.py (yaml moved to third-party section) - Remove unused noqa: F401 suppression in rendering/__init__.py - Add *args, **kwargs to _test_format_output stub in coverage boost steps (fixes TypeError) - Extract context-set step definitions into dedicated module (project_context_set_steps.py) to bring project_context_cli_steps.py under 500-line limit (469 -> 316 lines) - Revert spec changes for context show/inspect/simulate message format (bare strings) to keep PR focused on issue #6319 (context set only),
This commit is contained in:
+6
-18
@@ -4177,9 +4177,7 @@ Show the active context policy for a project.
|
|||||||
"timing": {
|
"timing": {
|
||||||
"duration_ms": 38
|
"duration_ms": 38
|
||||||
},
|
},
|
||||||
"messages": [
|
"messages": ["Context policy loaded"]
|
||||||
{"level": "ok", "text": "Context policy loaded"}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -4218,8 +4216,7 @@ Show the active context policy for a project.
|
|||||||
timing:
|
timing:
|
||||||
duration_ms: 38
|
duration_ms: 38
|
||||||
messages:
|
messages:
|
||||||
- level: ok
|
- Context policy loaded
|
||||||
text: Context policy loaded
|
|
||||||
```
|
```
|
||||||
|
|
||||||
###### agents project context inspect
|
###### agents project context inspect
|
||||||
@@ -4368,9 +4365,7 @@ Inspect the current state of the ACMS context assembly for a project. Shows the
|
|||||||
"timing": {
|
"timing": {
|
||||||
"duration_ms": 185
|
"duration_ms": 185
|
||||||
},
|
},
|
||||||
"messages": [
|
"messages": ["Context inspection complete"]
|
||||||
{"level": "ok", "text": "Context inspection complete"}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -4437,8 +4432,7 @@ Inspect the current state of the ACMS context assembly for a project. Shows the
|
|||||||
timing:
|
timing:
|
||||||
duration_ms: 185
|
duration_ms: 185
|
||||||
messages:
|
messages:
|
||||||
- level: ok
|
- Context inspection complete
|
||||||
text: Context inspection complete
|
|
||||||
```
|
```
|
||||||
|
|
||||||
###### agents project context simulate
|
###### agents project context simulate
|
||||||
@@ -4602,12 +4596,7 @@ Simulate a context assembly run without invoking an actor. Produces a dry-run re
|
|||||||
"timing": {
|
"timing": {
|
||||||
"duration_ms": 310
|
"duration_ms": 310
|
||||||
},
|
},
|
||||||
"messages": [
|
"messages": ["Simulation complete (dry run -- no context was assembled)"]
|
||||||
{
|
|
||||||
"level": "ok",
|
|
||||||
"text": "Simulation complete (dry run -- no context was assembled)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -4675,8 +4664,7 @@ Simulate a context assembly run without invoking an actor. Produces a dry-run re
|
|||||||
timing:
|
timing:
|
||||||
duration_ms: 310
|
duration_ms: 310
|
||||||
messages:
|
messages:
|
||||||
- level: ok
|
- "Simulation complete (dry run -- no context was assembled)"
|
||||||
text: Simulation complete (dry run -- no context was assembled)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### agents actor
|
#### agents actor
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def _run(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
|
|||||||
soft_wrap=True,
|
soft_wrap=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _test_format_output(data, format_type):
|
def _test_format_output(data, format_type, *args, **kwargs):
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
from io import StringIO as _SIO
|
from io import StringIO as _SIO
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ def _run(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
|
|||||||
_old = _sys.stdout
|
_old = _sys.stdout
|
||||||
_sys.stdout = _b
|
_sys.stdout = _b
|
||||||
try:
|
try:
|
||||||
_r = _fo(data, format_type)
|
_r = _fo(data, format_type, *args, **kwargs)
|
||||||
finally:
|
finally:
|
||||||
_sys.stdout = _old
|
_sys.stdout = _old
|
||||||
return _r or _b.getvalue().rstrip("\n")
|
return _r or _b.getvalue().rstrip("\n")
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ CLI commands using an in-memory SQLite database.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import yaml
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# third-party
|
||||||
from behave import given, then, when # type: ignore[import-untyped]
|
from behave import given, then, when # type: ignore[import-untyped]
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
@@ -186,164 +186,11 @@ def _run_with_container(context: Any, func: Any, *args: Any, **kwargs: Any) -> N
|
|||||||
|
|
||||||
context.ctx_output = buf.getvalue()
|
context.ctx_output = buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# context set
|
# context set
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@when(
|
|
||||||
'I run context set on "{project}" with view "{view}" and include-resource "{res}"'
|
|
||||||
)
|
|
||||||
def step_ctx_set_include_res(context: Any, project: str, view: str, res: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
include_resource=[res],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when(
|
|
||||||
'I run context set on "{project}" with view "{view}" and exclude-resource "{res}"'
|
|
||||||
)
|
|
||||||
def step_ctx_set_exclude_res(context: Any, project: str, view: str, res: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
exclude_resource=[res],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with view "{view}" and include-path "{path}"')
|
|
||||||
def step_ctx_set_include_path(context: Any, project: str, view: str, path: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
include_path=[path],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with view "{view}" and exclude-path "{path}"')
|
|
||||||
def step_ctx_set_exclude_path(context: Any, project: str, view: str, path: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
exclude_path=[path],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set spec example on "{project}" with format "{fmt}"')
|
|
||||||
def step_ctx_set_spec_example(context: Any, project: str, fmt: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view="strategize",
|
|
||||||
include_resource=["repo"],
|
|
||||||
exclude_path=["**/node_modules/**"],
|
|
||||||
hot_max_tokens=12_000,
|
|
||||||
warm_max_decisions=50,
|
|
||||||
cold_max_decisions=200,
|
|
||||||
query_limit=20,
|
|
||||||
max_file_size=1_048_576,
|
|
||||||
max_total_size=50 * 1_048_576,
|
|
||||||
summarize=True,
|
|
||||||
summary_max_tokens=800,
|
|
||||||
output_format=fmt,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with view "{view}" and max-file-size {size:d}')
|
|
||||||
def step_ctx_set_max_file_size(
|
|
||||||
context: Any, project: str, view: str, size: int
|
|
||||||
) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
max_file_size=size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with view "{view}" and max-total-size {size:d}')
|
|
||||||
def step_ctx_set_max_total_size(
|
|
||||||
context: Any, project: str, view: str, size: int
|
|
||||||
) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
max_total_size=size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with invalid view "{view}"')
|
|
||||||
def step_ctx_set_invalid_view(context: Any, project: str, view: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@when('I run context set on "{project}" with view "{view}" and clear flag')
|
|
||||||
def step_ctx_set_clear(context: Any, project: str, view: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view=view,
|
|
||||||
clear=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# context show
|
# context show
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -404,198 +251,6 @@ def step_ctx_simulate(context: Any, project: str) -> None:
|
|||||||
_run_with_container(context, context_simulate, project=project)
|
_run_with_container(context, context_simulate, project=project)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Given steps (pre-conditions)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@given('I have set a strategize view on "{project}" with defaults')
|
|
||||||
def step_given_strategize_view(context: Any, project: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view="strategize",
|
|
||||||
include_resource=["strat-default"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given('I have set a default view on "{project}" with include-resource "{res}"')
|
|
||||||
def step_given_default_view(context: Any, project: str, res: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view="default",
|
|
||||||
include_resource=[res],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given('I have set a strategize view on "{project}" with include-resource "{res}"')
|
|
||||||
def step_given_strategize_view_res(context: Any, project: str, res: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view="strategize",
|
|
||||||
include_resource=[res],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@given('I have set an execute view on "{project}" with include-resource "{res}"')
|
|
||||||
def step_given_execute_view_res(context: Any, project: str, res: str) -> None:
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
context_set,
|
|
||||||
)
|
|
||||||
|
|
||||||
_run_with_container(
|
|
||||||
context,
|
|
||||||
context_set,
|
|
||||||
project=project,
|
|
||||||
view="execute",
|
|
||||||
include_resource=[res],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Then assertions
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context set command should succeed")
|
|
||||||
def step_ctx_set_ok(context: Any) -> None:
|
|
||||||
assert context.ctx_exit_code == 0, (
|
|
||||||
f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context set command should fail")
|
|
||||||
def step_ctx_set_fail(context: Any) -> None:
|
|
||||||
assert context.ctx_exit_code != 0, (
|
|
||||||
f"Expected non-zero exit, got {context.ctx_exit_code}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context set JSON output should match the spec envelope")
|
|
||||||
def step_ctx_set_json_spec(context: Any) -> None:
|
|
||||||
output = context.ctx_output.strip()
|
|
||||||
assert output, "No CLI output captured"
|
|
||||||
payload = json.loads(output)
|
|
||||||
|
|
||||||
assert payload.get("command") == "project context set"
|
|
||||||
assert payload.get("status") == "ok"
|
|
||||||
assert payload.get("exit_code") == 0
|
|
||||||
|
|
||||||
timing = payload.get("timing", {})
|
|
||||||
assert isinstance(timing.get("duration_ms"), int)
|
|
||||||
|
|
||||||
messages = payload.get("messages", [])
|
|
||||||
assert messages and messages[0].get("text") == "Context policy updated"
|
|
||||||
|
|
||||||
data = payload.get("data", {})
|
|
||||||
|
|
||||||
context_policy = data.get("context_policy", {})
|
|
||||||
assert context_policy.get("project") == "local/ctx-app"
|
|
||||||
assert context_policy.get("view") == "strategize"
|
|
||||||
assert context_policy.get("include_resources") == ["repo"]
|
|
||||||
assert context_policy.get("exclude_paths") == ["**/node_modules/**"]
|
|
||||||
|
|
||||||
limits = data.get("limits", {})
|
|
||||||
assert limits.get("hot_max_tokens") == 12_000
|
|
||||||
assert limits.get("warm_max_decisions") == 50
|
|
||||||
assert limits.get("cold_max_decisions") == 200
|
|
||||||
assert limits.get("query_limit") == 20
|
|
||||||
assert limits.get("max_file_size") == "1 MB"
|
|
||||||
assert limits.get("max_total_size") == "50 MB"
|
|
||||||
|
|
||||||
summarization = data.get("summarization", {})
|
|
||||||
assert summarization.get("enabled") is True
|
|
||||||
assert summarization.get("max_tokens") == 800
|
|
||||||
|
|
||||||
other_views = data.get("other_views", {})
|
|
||||||
assert other_views.get("default") == "(unset)"
|
|
||||||
assert other_views.get("execute") == "(default)"
|
|
||||||
assert other_views.get("apply") == "(default)"
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context set YAML output should match the spec envelope")
|
|
||||||
def step_ctx_set_yaml_spec(context: Any) -> None:
|
|
||||||
output = context.ctx_output.strip()
|
|
||||||
assert output, "No CLI output captured"
|
|
||||||
payload = yaml.safe_load(output)
|
|
||||||
|
|
||||||
assert payload.get("command") == "project context set"
|
|
||||||
assert payload.get("status") == "ok"
|
|
||||||
assert payload.get("exit_code") == 0
|
|
||||||
|
|
||||||
timing = payload.get("timing", {})
|
|
||||||
assert isinstance(timing.get("duration_ms"), int)
|
|
||||||
|
|
||||||
messages = payload.get("messages", [])
|
|
||||||
assert messages and messages[0].get("text") == "Context policy updated"
|
|
||||||
|
|
||||||
data = payload.get("data", {})
|
|
||||||
context_policy = data.get("context_policy", {})
|
|
||||||
assert context_policy.get("project") == "local/ctx-app"
|
|
||||||
assert context_policy.get("view") == "strategize"
|
|
||||||
assert context_policy.get("include_resources") == ["repo"]
|
|
||||||
assert context_policy.get("exclude_paths") == ["**/node_modules/**"]
|
|
||||||
|
|
||||||
limits = data.get("limits", {})
|
|
||||||
assert limits.get("hot_max_tokens") == 12_000
|
|
||||||
assert limits.get("warm_max_decisions") == 50
|
|
||||||
assert limits.get("cold_max_decisions") == 200
|
|
||||||
assert limits.get("query_limit") == 20
|
|
||||||
assert limits.get("max_file_size") == "1 MB"
|
|
||||||
assert limits.get("max_total_size") == "50 MB"
|
|
||||||
|
|
||||||
summarization = data.get("summarization", {})
|
|
||||||
assert summarization.get("enabled") is True
|
|
||||||
assert summarization.get("max_tokens") == 800
|
|
||||||
|
|
||||||
other_views = data.get("other_views", {})
|
|
||||||
assert other_views.get("default") == "(unset)"
|
|
||||||
assert other_views.get("execute") == "(default)"
|
|
||||||
assert other_views.get("apply") == "(default)"
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context set rich output should include spec panels")
|
|
||||||
def step_ctx_set_rich_spec(context: Any) -> None:
|
|
||||||
output = context.ctx_output
|
|
||||||
assert "Context Policy" in output
|
|
||||||
assert "Limits" in output
|
|
||||||
assert "Summarization" in output
|
|
||||||
assert "Other Views" in output
|
|
||||||
assert "Project: local/ctx-app" in output
|
|
||||||
assert "View: strategize" in output
|
|
||||||
assert "Include: repo" in output
|
|
||||||
assert "Exclude: **/node_modules/**" in output
|
|
||||||
assert "Hot Tokens: 12000 (soft cap)" in output
|
|
||||||
assert "Warm Decisions: 50" in output
|
|
||||||
assert "Cold Decisions: 200" in output
|
|
||||||
assert "Query Limit: 20" in output
|
|
||||||
assert "Max File Size: 1 MB" in output
|
|
||||||
assert "Max Total Size: 50 MB" in output
|
|
||||||
assert "Enabled: yes" in output
|
|
||||||
assert "Max Tokens: 800" in output
|
|
||||||
assert "default: (unset)" in output
|
|
||||||
assert "execute: (default)" in output
|
|
||||||
assert "apply: (default)" in output
|
|
||||||
assert "✓ Context policy updated" in output
|
|
||||||
|
|
||||||
|
|
||||||
@then("the context show command should succeed")
|
@then("the context show command should succeed")
|
||||||
def step_ctx_show_ok(context: Any) -> None:
|
def step_ctx_show_ok(context: Any) -> None:
|
||||||
assert context.ctx_exit_code == 0, (
|
assert context.ctx_exit_code == 0, (
|
||||||
@@ -610,77 +265,8 @@ def step_ctx_show_fail(context: Any) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _read_stored_policy(context: Any) -> Any:
|
|
||||||
"""Read the stored policy from the test DB."""
|
|
||||||
from cleveragents.cli.commands.project_context import (
|
|
||||||
_read_policy,
|
|
||||||
)
|
|
||||||
|
|
||||||
return _read_policy(context.ctx_session_factory, "local/ctx-app")
|
|
||||||
|
|
||||||
|
|
||||||
@then('the stored policy default view should include resource "{res}"')
|
|
||||||
def step_policy_include_res(context: Any, res: str) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert res in policy.default_view.include_resources, (
|
|
||||||
f"{res} not in {policy.default_view.include_resources}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the stored policy default view should exclude resource "{res}"')
|
|
||||||
def step_policy_exclude_res(context: Any, res: str) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert res in policy.default_view.exclude_resources, (
|
|
||||||
f"{res} not in {policy.default_view.exclude_resources}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the stored policy default view should include path "{path}"')
|
|
||||||
def step_policy_include_path(context: Any, path: str) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert path in policy.default_view.include_paths, (
|
|
||||||
f"{path} not in {policy.default_view.include_paths}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the stored policy default view should exclude path "{path}"')
|
|
||||||
def step_policy_exclude_path(context: Any, path: str) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert path in policy.default_view.exclude_paths, (
|
|
||||||
f"{path} not in {policy.default_view.exclude_paths}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then("the stored policy default view max file size should be {size:d}")
|
|
||||||
def step_policy_max_file_size(context: Any, size: int) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert policy.default_view.max_file_size == size, (
|
|
||||||
f"Expected {size}, got {policy.default_view.max_file_size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then("the stored policy default view max total size should be {size:d}")
|
|
||||||
def step_policy_max_total_size(context: Any, size: int) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert policy.default_view.max_total_size == size, (
|
|
||||||
f"Expected {size}, got {policy.default_view.max_total_size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@then('the stored policy strategize view should include resource "{res}"')
|
|
||||||
def step_policy_strat_include_res(context: Any, res: str) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert policy.strategize_view is not None, "strategize_view is None"
|
|
||||||
assert res in policy.strategize_view.include_resources
|
|
||||||
|
|
||||||
|
|
||||||
@then("the stored policy strategize view should be None")
|
|
||||||
def step_policy_strat_none(context: Any) -> None:
|
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
assert policy.strategize_view is None, (
|
|
||||||
f"Expected None, got {policy.strategize_view}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Generic Then assertions shared across context commands
|
||||||
|
|
||||||
@then("the context inspect command should succeed")
|
@then("the context inspect command should succeed")
|
||||||
def step_ctx_inspect_ok(context: Any) -> None:
|
def step_ctx_inspect_ok(context: Any) -> None:
|
||||||
@@ -707,11 +293,10 @@ def step_not_implemented(context: Any, msg: str) -> None:
|
|||||||
|
|
||||||
@then('the resolved view should include resource "{res}"')
|
@then('the resolved view should include resource "{res}"')
|
||||||
def step_resolved_view_include_res(context: Any, res: str) -> None:
|
def step_resolved_view_include_res(context: Any, res: str) -> None:
|
||||||
# The show command output contains the resolved view
|
"""Verify that a resource appears in at least one resolved view."""
|
||||||
# We read the policy directly for verification
|
from cleveragents.cli.commands.project_context import _read_policy
|
||||||
policy = _read_stored_policy(context)
|
|
||||||
# Determine which phase was queried from the output
|
policy = _read_policy(context.ctx_session_factory, "local/ctx-app")
|
||||||
# For simplicity, check all possible resolved views
|
|
||||||
found = False
|
found = False
|
||||||
for phase in ["default", "strategize", "execute", "apply"]:
|
for phase in ["default", "strategize", "execute", "apply"]:
|
||||||
resolved = policy.resolve_view(phase)
|
resolved = policy.resolve_view(phase)
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
"""Step definitions for the ``agents project context set`` CLI command.
|
||||||
|
|
||||||
|
Extracted from ``project_context_cli_steps.py`` to keep that file under
|
||||||
|
the 500-line limit mandated by CONTRIBUTING.md. All shared fixtures and
|
||||||
|
runners are imported back into this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# third-party
|
||||||
|
import yaml
|
||||||
|
from behave import given, then, when # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
# Shared helpers from the parent CLI steps module
|
||||||
|
from features.steps.project_context_cli_steps import (
|
||||||
|
_run_with_container,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# When steps — context set operations
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@when(
|
||||||
|
'I run context set on "{project}" with view "{view}" and include-resource "{res}"'
|
||||||
|
)
|
||||||
|
def step_ctx_set_include_res(context: Any, project: str, view: str, res: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
include_resource=[res],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when(
|
||||||
|
'I run context set on "{project}" with view "{view}" and exclude-resource "{res}"'
|
||||||
|
)
|
||||||
|
def step_ctx_set_exclude_res(context: Any, project: str, view: str, res: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
exclude_resource=[res],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with view "{view}" and include-path "{path}"')
|
||||||
|
def step_ctx_set_include_path(context: Any, project: str, view: str, path: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
include_path=[path],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with view "{view}" and exclude-path "{path}"')
|
||||||
|
def step_ctx_set_exclude_path(context: Any, project: str, view: str, path: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
exclude_path=[path],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set spec example on "{project}" with format "{fmt}"')
|
||||||
|
def step_ctx_set_spec_example(context: Any, project: str, fmt: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view="strategize",
|
||||||
|
include_resource=["repo"],
|
||||||
|
exclude_path=["**/node_modules/**"],
|
||||||
|
hot_max_tokens=12_000,
|
||||||
|
warm_max_decisions=50,
|
||||||
|
cold_max_decisions=200,
|
||||||
|
query_limit=20,
|
||||||
|
max_file_size=1_048_576,
|
||||||
|
max_total_size=50 * 1_048_576,
|
||||||
|
summarize=True,
|
||||||
|
summary_max_tokens=800,
|
||||||
|
output_format=fmt,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with view "{view}" and max-file-size {size:d}')
|
||||||
|
def step_ctx_set_max_file_size(
|
||||||
|
context: Any, project: str, view: str, size: int
|
||||||
|
) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
max_file_size=size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with view "{view}" and max-total-size {size:d}')
|
||||||
|
def step_ctx_set_max_total_size(
|
||||||
|
context: Any, project: str, view: str, size: int
|
||||||
|
) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
max_total_size=size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with invalid view "{view}"')
|
||||||
|
def step_ctx_set_invalid_view(context: Any, project: str, view: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@when('I run context set on "{project}" with view "{view}" and clear flag')
|
||||||
|
def step_ctx_set_clear(context: Any, project: str, view: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view=view,
|
||||||
|
clear=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Given steps — context set pre-conditions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@given('I have set a strategize view on "{project}" with defaults')
|
||||||
|
def step_given_strategize_view(context: Any, project: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view="strategize",
|
||||||
|
include_resource=["strat-default"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given('I have set a default view on "{project}" with include-resource "{res}"')
|
||||||
|
def step_given_default_view(context: Any, project: str, res: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view="default",
|
||||||
|
include_resource=[res],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given('I have set a strategize view on "{project}" with include-resource "{res}"')
|
||||||
|
def step_given_strategize_view_res(context: Any, project: str, res: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view="strategize",
|
||||||
|
include_resource=[res],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@given('I have set an execute view on "{project}" with include-resource "{res}"')
|
||||||
|
def step_given_execute_view_res(context: Any, project: str, res: str) -> None:
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
context_set,
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_with_container(
|
||||||
|
context,
|
||||||
|
context_set,
|
||||||
|
project=project,
|
||||||
|
view="execute",
|
||||||
|
include_resource=[res],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Then assertions — context set output and policy verification
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@then("the context set command should succeed")
|
||||||
|
def step_ctx_set_ok(context: Any) -> None:
|
||||||
|
assert context.ctx_exit_code == 0, (
|
||||||
|
f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the context set command should fail")
|
||||||
|
def step_ctx_set_fail(context: Any) -> None:
|
||||||
|
assert context.ctx_exit_code != 0, (
|
||||||
|
f"Expected non-zero exit, got {context.ctx_exit_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the context set JSON output should match the spec envelope")
|
||||||
|
def step_ctx_set_json_spec(context: Any) -> None:
|
||||||
|
output = context.ctx_output.strip()
|
||||||
|
assert output, "No CLI output captured"
|
||||||
|
payload = json.loads(output)
|
||||||
|
|
||||||
|
assert payload.get("command") == "project context set"
|
||||||
|
assert payload.get("status") == "ok"
|
||||||
|
assert payload.get("exit_code") == 0
|
||||||
|
|
||||||
|
timing = payload.get("timing", {})
|
||||||
|
assert isinstance(timing.get("duration_ms"), int)
|
||||||
|
|
||||||
|
messages = payload.get("messages", [])
|
||||||
|
assert messages and messages[0].get("text") == "Context policy updated"
|
||||||
|
|
||||||
|
data = payload.get("data", {})
|
||||||
|
|
||||||
|
context_policy = data.get("context_policy", {})
|
||||||
|
assert context_policy.get("project") == "local/ctx-app"
|
||||||
|
assert context_policy.get("view") == "strategize"
|
||||||
|
assert context_policy.get("include_resources") == ["repo"]
|
||||||
|
assert context_policy.get("exclude_paths") == ["**/node_modules/**"]
|
||||||
|
|
||||||
|
limits = data.get("limits", {})
|
||||||
|
assert limits.get("hot_max_tokens") == 12_000
|
||||||
|
assert limits.get("warm_max_decisions") == 50
|
||||||
|
assert limits.get("cold_max_decisions") == 200
|
||||||
|
assert limits.get("query_limit") == 20
|
||||||
|
assert limits.get("max_file_size") == "1 MB"
|
||||||
|
assert limits.get("max_total_size") == "50 MB"
|
||||||
|
|
||||||
|
summarization = data.get("summarization", {})
|
||||||
|
assert summarization.get("enabled") is True
|
||||||
|
assert summarization.get("max_tokens") == 800
|
||||||
|
|
||||||
|
other_views = data.get("other_views", {})
|
||||||
|
assert other_views.get("default") == "(unset)"
|
||||||
|
assert other_views.get("execute") == "(default)"
|
||||||
|
assert other_views.get("apply") == "(default)"
|
||||||
|
|
||||||
|
|
||||||
|
@then("the context set YAML output should match the spec envelope")
|
||||||
|
def step_ctx_set_yaml_spec(context: Any) -> None:
|
||||||
|
output = context.ctx_output.strip()
|
||||||
|
assert output, "No CLI output captured"
|
||||||
|
payload = yaml.safe_load(output)
|
||||||
|
|
||||||
|
assert payload.get("command") == "project context set"
|
||||||
|
assert payload.get("status") == "ok"
|
||||||
|
assert payload.get("exit_code") == 0
|
||||||
|
|
||||||
|
timing = payload.get("timing", {})
|
||||||
|
assert isinstance(timing.get("duration_ms"), int)
|
||||||
|
|
||||||
|
messages = payload.get("messages", [])
|
||||||
|
assert messages and messages[0].get("text") == "Context policy updated"
|
||||||
|
|
||||||
|
data = payload.get("data", {})
|
||||||
|
context_policy = data.get("context_policy", {})
|
||||||
|
assert context_policy.get("project") == "local/ctx-app"
|
||||||
|
assert context_policy.get("view") == "strategize"
|
||||||
|
assert context_policy.get("include_resources") == ["repo"]
|
||||||
|
assert context_policy.get("exclude_paths") == ["**/node_modules/**"]
|
||||||
|
|
||||||
|
limits = data.get("limits", {})
|
||||||
|
assert limits.get("hot_max_tokens") == 12_000
|
||||||
|
assert limits.get("warm_max_decisions") == 50
|
||||||
|
assert limits.get("cold_max_decisions") == 200
|
||||||
|
assert limits.get("query_limit") == 20
|
||||||
|
assert limits.get("max_file_size") == "1 MB"
|
||||||
|
assert limits.get("max_total_size") == "50 MB"
|
||||||
|
|
||||||
|
summarization = data.get("summarization", {})
|
||||||
|
assert summarization.get("enabled") is True
|
||||||
|
assert summarization.get("max_tokens") == 800
|
||||||
|
|
||||||
|
other_views = data.get("other_views", {})
|
||||||
|
assert other_views.get("default") == "(unset)"
|
||||||
|
assert other_views.get("execute") == "(default)"
|
||||||
|
assert other_views.get("apply") == "(default)"
|
||||||
|
|
||||||
|
|
||||||
|
@then("the context set rich output should include spec panels")
|
||||||
|
def step_ctx_set_rich_spec(context: Any) -> None:
|
||||||
|
output = context.ctx_output
|
||||||
|
assert "Context Policy" in output
|
||||||
|
assert "Limits" in output
|
||||||
|
assert "Summarization" in output
|
||||||
|
assert "Other Views" in output
|
||||||
|
assert "Project: local/ctx-app" in output
|
||||||
|
assert "View: strategize" in output
|
||||||
|
assert "Include: repo" in output
|
||||||
|
assert "Exclude: **/node_modules/**" in output
|
||||||
|
assert "Hot Tokens: 12000 (soft cap)" in output
|
||||||
|
assert "Warm Decisions: 50" in output
|
||||||
|
assert "Cold Decisions: 200" in output
|
||||||
|
assert "Query Limit: 20" in output
|
||||||
|
assert "Max File Size: 1 MB" in output
|
||||||
|
assert "Max Total Size: 50 MB" in output
|
||||||
|
assert "Enabled: yes" in output
|
||||||
|
assert "Max Tokens: 800" in output
|
||||||
|
assert "default: (unset)" in output
|
||||||
|
assert "execute: (default)" in output
|
||||||
|
assert "apply: (default)" in output
|
||||||
|
assert "\u2713 Context policy updated" in output
|
||||||
|
|
||||||
|
|
||||||
|
def _read_stored_policy(context: Any) -> Any:
|
||||||
|
"""Read the stored policy from the test DB."""
|
||||||
|
from cleveragents.cli.commands.project_context import (
|
||||||
|
_read_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _read_policy(context.ctx_session_factory, "local/ctx-app")
|
||||||
|
|
||||||
|
|
||||||
|
@then('the stored policy default view should include resource "{res}"')
|
||||||
|
def step_policy_include_res(context: Any, res: str) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert res in policy.default_view.include_resources, (
|
||||||
|
f"{res} not in {policy.default_view.include_resources}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the stored policy default view should exclude resource "{res}"')
|
||||||
|
def step_policy_exclude_res(context: Any, res: str) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert res in policy.default_view.exclude_resources, (
|
||||||
|
f"{res} not in {policy.default_view.exclude_resources}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the stored policy default view should include path "{path}"')
|
||||||
|
def step_policy_include_path(context: Any, path: str) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert path in policy.default_view.include_paths, (
|
||||||
|
f"{path} not in {policy.default_view.include_paths}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the stored policy default view should exclude path "{path}"')
|
||||||
|
def step_policy_exclude_path(context: Any, path: str) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert path in policy.default_view.exclude_paths, (
|
||||||
|
f"{path} not in {policy.default_view.exclude_paths}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the stored policy default view max file size should be {size:d}")
|
||||||
|
def step_policy_max_file_size(context: Any, size: int) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert policy.default_view.max_file_size == size, (
|
||||||
|
f"Expected {size}, got {policy.default_view.max_file_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then("the stored policy default view max total size should be {size:d}")
|
||||||
|
def step_policy_max_total_size(context: Any, size: int) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert policy.default_view.max_total_size == size, (
|
||||||
|
f"Expected {size}, got {policy.default_view.max_total_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@then('the stored policy strategize view should include resource "{res}"')
|
||||||
|
def step_policy_strat_include_res(context: Any, res: str) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert policy.strategize_view is not None, "strategize_view is None"
|
||||||
|
assert res in policy.strategize_view.include_resources
|
||||||
|
|
||||||
|
|
||||||
|
@then("the stored policy strategize view should be None")
|
||||||
|
def step_policy_strat_none(context: Any) -> None:
|
||||||
|
policy = _read_stored_policy(context)
|
||||||
|
assert policy.strategize_view is None, (
|
||||||
|
f"Expected None, got {policy.strategize_view}"
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Rendering helpers for CLI commands."""
|
"""Rendering helpers for CLI commands."""
|
||||||
|
|
||||||
from .project_context_set import ( # noqa: F401
|
from .project_context_set import (
|
||||||
build_context_set_payload,
|
build_context_set_payload,
|
||||||
render_context_set_plain,
|
render_context_set_plain,
|
||||||
render_context_set_rich,
|
render_context_set_rich,
|
||||||
|
|||||||
Reference in New Issue
Block a user