test(e2e): E2E acceptance criteria for M5 (v3.4.0) — ACMS v1 and context scaling #811

Merged
hurui200320 merged 2 commits from test/e2e-m5-acceptance into master 2026-03-19 06:53:06 +00:00
14 changed files with 1119 additions and 26 deletions
+13
View File
@@ -35,6 +35,19 @@
passes its lifecycle service instance to `_get_plan_executor()`,
preventing stale-cache regressions.
(`features/plan_lifecycle_cli_coverage.feature`)
- Added M5 (v3.4.0) E2E acceptance test suite `robot/e2e/m5_acceptance.robot`
with 21 zero-mock test cases covering context assembly, context policy
configuration, budget enforcement, context analysis, 10,000+ file scaling,
and plan execution with real LLM calls (`openai/gpt-4o-mini`). (#745)
- Fixed `project context set` writing policy changes via `session.flush()`
instead of `session.commit()`, causing silently lost data on
`session.close()`. (#745)
- Added `session_factory` DI provider to `Container` for CLI project-context
commands. The four `project context` subcommands (`set`, `show`, `inspect`,
`simulate`) previously called `container.session_factory()` which did not
exist, causing `AttributeError` at runtime. (#745)
- Added Google/Gemini API key pattern (`AIzaSy...`) to secret redaction in
`redaction.py`. (#745)
- Added `--execution-env-priority` flag to `agents plan use` command, accepting
`fallback` (default) or `override` to control execution environment routing
precedence per ADR-043. Includes `ExecutionEnvPriority` StrEnum on the domain
@@ -1,7 +1,7 @@
Feature: Application Container coverage boost for _build_checkpoint_service and _build_trace_service
Feature: Application Container coverage boost for _build_checkpoint_service, _build_trace_service, and _build_session_factory
As a developer
I want to exercise the _build_checkpoint_service and _build_trace_service helper functions
So that lines 187-195 and 204-211 of container.py are covered by tests
I want to exercise the _build_checkpoint_service, _build_trace_service, and _build_session_factory helper functions
So that the corresponding builder functions in container.py are covered by tests
Background:
Given a clean container state for coverage boost tests
@@ -37,3 +37,21 @@ Feature: Application Container coverage boost for _build_checkpoint_service and
When I build a trace service with the explicit settings
Then the result should be a TraceService instance
And the trace service should use the explicitly provided settings
# -----------------------------------------------------------------
# _build_session_factory
# -----------------------------------------------------------------
Scenario: Build session factory with in-memory database returns callable sessionmaker
When I build a session factory with an in-memory database URL
Then the result should be a callable sessionmaker
And calling the session factory should produce a Session
Scenario: Build session factory with invalid URL raises an error
When I build a session factory with an invalid database URL
Then a database error should be raised when the factory is called
Scenario: Container.session_factory resolves a usable session factory
When I resolve session_factory from the container with an in-memory database URL
Then the resolved session factory should be callable
And calling the resolved session factory should produce a Session
+15
View File
@@ -672,6 +672,21 @@ Feature: Consolidated Security
Then the redacted value should be "***REDACTED***"
Scenario: Redact Google/Gemini API key pattern
Given a string containing "AIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q"
When I redact the string
Then the redacted value should be "***REDACTED***"
Scenario: Mixed text with Gemini API key is partially redacted
Given a string containing "Using key AIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q for Gemini"
When I redact the string
Then the redacted value should contain "***REDACTED***"
And the redacted value should contain "Using key"
And the redacted value should contain "for Gemini"
And the redacted value should not contain "AIzaSy"
Scenario: Redact generic bearer token
Given a string containing "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz"
When I redact the string
@@ -135,8 +135,30 @@ Feature: Project context CLI coverage boost
When I run coverage-boost context simulate on "local/cov-app" with strategy hints "bfs,dfs"
Then the coverage-boost command should succeed
# --- _save_policy_json commit() regression test (line 133) ---
# Exercises _save_policy_json then reads back via a *real* separate
# session (no _SafeSession) to prove data persists across session
# boundaries. If commit() were reverted to flush(), this test fails.
Scenario: Policy saved with commit persists across separate sessions
When I save a policy for "local/cov-app" via a real session factory and read it back via a separate session
Then the policy read from the separate session should contain the saved data
# --- context_set ACMS overrides for remaining fields (lines 589-622) ---
Scenario: Context set with all ACMS pipeline overrides
When I run coverage-boost context set on "local/cov-app" with all ACMS overrides
Then the coverage-boost command should succeed
And the stored ACMS config should reflect all overrides
# --- _save_policy_json rollback path (contextlib.suppress + re-raise) ---
# Verifies that when commit() fails, the exception is re-raised and the
# session is rolled back, releasing DB locks.
Scenario: Save policy rollback re-raises after commit failure
When I attempt to save a policy with a commit that will fail
Then the original commit exception should be re-raised
And the session should not be in a dirty state after the failure
# --- _save_policy_json on nonexistent project (0 rows affected) ---
Scenario: Save policy on nonexistent project row updates zero rows
When I save a policy for a nonexistent project "local/no-such-project"
Then the save should complete without error
And the policy should not be retrievable for "local/no-such-project"
@@ -9,12 +9,14 @@ Targets the remaining uncovered lines in
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from behave import given, then, when
from cleveragents.application.container import (
_build_checkpoint_service,
_build_session_factory,
_build_trace_service,
reset_container,
)
@@ -163,3 +165,104 @@ def step_verify_trace_explicit_settings(context):
assert context.boost_trace_svc._settings is context.boost_explicit_settings, (
"Expected the explicitly provided settings instance"
)
# -------------------------------------------------------------------
# _build_session_factory scenarios
# -------------------------------------------------------------------
@when("I build a session factory with an in-memory database URL")
def step_build_session_factory(context: Any) -> None:
"""Call _build_session_factory with an in-memory database URL."""
context.boost_session_factory = _build_session_factory(_IN_MEMORY_URL)
@then("the result should be a callable sessionmaker")
def step_verify_sessionmaker_callable(context: Any) -> None:
"""Assert the returned object is a callable sessionmaker."""
from sqlalchemy.orm import sessionmaker
assert isinstance(context.boost_session_factory, sessionmaker), (
f"Expected sessionmaker, got {type(context.boost_session_factory).__name__}"
)
assert callable(context.boost_session_factory)
@then("calling the session factory should produce a Session")
def step_verify_session_factory_produces_session(context: Any) -> None:
"""Assert that calling the factory returns a Session instance."""
from sqlalchemy.orm import Session
session = context.boost_session_factory()
try:
assert isinstance(session, Session), (
f"Expected Session, got {type(session).__name__}"
)
finally:
session.close()
@when("I build a session factory with an invalid database URL")
def step_build_session_factory_invalid(context: Any) -> None:
"""Call _build_session_factory with an invalid URL — error deferred to usage."""
# SQLAlchemy lazily connects; the factory itself succeeds, but
# calling it to create a session and executing SQL raises.
context.boost_session_factory = _build_session_factory(
"sqlite:///nonexistent/path/to/db.sqlite"
)
@then("a database error should be raised when the factory is called")
def step_verify_session_factory_error(context: Any) -> None:
"""Assert that using the factory with an invalid URL raises an error."""
from sqlalchemy import text as sa_text
from sqlalchemy.exc import OperationalError
session = context.boost_session_factory()
try:
# Force a connection attempt by executing a trivial query.
session.execute(sa_text("SELECT 1"))
raise AssertionError("Expected OperationalError, but no error was raised")
except OperationalError:
pass # Expected
finally:
session.close()
@when("I resolve session_factory from the container with an in-memory database URL")
def step_resolve_container_session_factory(context: Any) -> None:
"""Resolve session_factory from the DI container using an in-memory URL."""
from unittest.mock import patch
with patch.dict("os.environ", {"CLEVERAGENTS_DATABASE_URL": _IN_MEMORY_URL}):
from cleveragents.application.container import get_container
reset_container()
try:
container = get_container()
context.boost_resolved_factory = container.session_factory()
finally:
reset_container()
@then("the resolved session factory should be callable")
def step_verify_resolved_factory_callable(context: Any) -> None:
"""Assert the container-resolved factory is callable."""
assert callable(context.boost_resolved_factory), (
"Expected a callable session factory from the container"
)
@then("calling the resolved session factory should produce a Session")
def step_verify_resolved_factory_produces_session(context: Any) -> None:
"""Assert that calling the resolved factory returns a Session instance."""
from sqlalchemy.orm import Session
session = context.boost_resolved_factory()
try:
assert isinstance(session, Session), (
f"Expected Session, got {type(session).__name__}"
)
finally:
session.close()
@@ -22,13 +22,22 @@ from sqlalchemy.orm import Session, sessionmaker
class _SafeSession:
"""Thin wrapper so ``close()`` is a no-op."""
"""Thin wrapper that prevents ``close()`` from destroying the shared session.
Instead of a pure no-op, ``close()`` issues a ``rollback()`` on the
underlying session to reset any dirty/rolled-back state accumulated
during the previous call. This prevents phantom failures when the
production error path (e.g. ``_save_policy_json`` rollback) leaves
the session in an invalidated state.
"""
def __init__(self, real: Session) -> None:
object.__setattr__(self, "_real", real)
def close(self) -> None:
"""No-op."""
"""Reset session state without closing the underlying connection."""
real: Session = object.__getattribute__(self, "_real")
real.rollback()
def __getattr__(self, name: str) -> Any:
return getattr(object.__getattribute__(self, "_real"), name)
@@ -176,7 +185,7 @@ def _seed_policy_blob(context: Any, ns: str, blob: dict) -> None:
),
{"blob": _json.dumps(blob), "ns": ns},
)
session.flush()
session.commit()
def _load_raw_blob(context: Any, ns: str) -> dict | None:
@@ -590,6 +599,102 @@ def step_cb_fewer_fragments(context: Any) -> None:
)
# ------------------------------------------------------------------
# flush() → commit() regression test: real separate sessions
# ------------------------------------------------------------------
Outdated
Review

M4 — Missing try/finally. If _save_policy_json or _load_policy_json raises, engine.dispose() and os.unlink(db_path) on lines 638-639 are skipped, leaking the temp file and engine.

engine = create_engine(db_url, echo=False)
try:
    Base.metadata.create_all(engine)
    ...
    context.cb_regression_loaded = loaded
finally:
    engine.dispose()
    if os.path.exists(db_path):
        os.unlink(db_path)
**M4 — Missing `try/finally`.** If `_save_policy_json` or `_load_policy_json` raises, `engine.dispose()` and `os.unlink(db_path)` on lines 638-639 are skipped, leaking the temp file and engine. ```python engine = create_engine(db_url, echo=False) try: Base.metadata.create_all(engine) ... context.cb_regression_loaded = loaded finally: engine.dispose() if os.path.exists(db_path): os.unlink(db_path) ```
@when(
'I save a policy for "{project}" via a real session factory and read it back via a separate session'
)
def step_save_and_read_real_sessions(context: Any, project: str) -> None:
"""Exercise _save_policy_json + _load_policy_json via real sessions.
Uses a file-based SQLite database so that separate sessions truly
see each other's committed data. The _SafeSession wrapper is NOT
used here if production code uses flush() instead of commit(),
the second session will not see the written data.
"""
import os
import tempfile
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker as real_sessionmaker
from cleveragents.cli.commands.project_context import (
_load_policy_json,
_save_policy_json,
)
from cleveragents.infrastructure.database.models import Base
# Create a file-based SQLite DB so separate sessions are independent.
fd, db_path = tempfile.mkstemp(suffix=".db")
os.close(fd)
db_url = f"sqlite:///{db_path}"
# M4: Use separate engines so that flush() vs commit() visibility
# is truly tested — a shared engine/pool could serve the same
# underlying connection, masking the difference.
engine_seed = create_engine(db_url, echo=False)
engine_write = create_engine(db_url, echo=False)
engine_read = create_engine(db_url, echo=False)
try:
Base.metadata.create_all(engine_seed)
factory_write = real_sessionmaker(bind=engine_write, expire_on_commit=False)
factory_read = real_sessionmaker(bind=engine_read, expire_on_commit=False)
# Seed the project row so UPDATE has a target.
from datetime import UTC, datetime
now = datetime.now(tz=UTC).isoformat()
session = real_sessionmaker(bind=engine_seed, expire_on_commit=False)()
try:
session.execute(
text(
"INSERT INTO ns_projects (namespaced_name, namespace, tags_json, created_at, updated_at) "
"VALUES (:ns, :nsp, :tags, :ca, :ua)"
),
{"ns": project, "nsp": "local", "tags": "[]", "ca": now, "ua": now},
)
session.commit()
finally:
session.close()
# Write policy via production code (uses commit) with write-only factory.
policy_dict = {
"regression_marker": "commit_test",
"acms_config": {"hot_max_tokens": 42},
}
_save_policy_json(factory_write, project, policy_dict)
# Read back via a SEPARATE engine/factory to prove commit visibility.
loaded = _load_policy_json(factory_read, project)
context.cb_regression_loaded = loaded
finally:
engine_seed.dispose()
engine_write.dispose()
engine_read.dispose()
# Clean up the DB file and any WAL/SHM journal files left by
# SQLite WAL mode to avoid leaking temp files.
for suffix in ("", "-wal", "-shm"):
path = db_path + suffix
if os.path.exists(path):
os.unlink(path)
@then("the policy read from the separate session should contain the saved data")
def step_assert_regression_data(context: Any) -> None:
loaded = context.cb_regression_loaded
assert loaded is not None, "Policy was None — data did not persist across sessions"
assert loaded.get("regression_marker") == "commit_test", (
f"Expected 'commit_test', got {loaded.get('regression_marker')}"
)
assert loaded.get("acms_config", {}).get("hot_max_tokens") == 42, (
f"Expected hot_max_tokens=42, got {loaded.get('acms_config', {}).get('hot_max_tokens')}"
)
@then("the stored ACMS config should reflect all overrides")
def step_assert_all_acms(context: Any) -> None:
blob = _load_raw_blob(context, "local/cov-app")
@@ -607,3 +712,99 @@ def step_assert_all_acms(context: Any) -> None:
assert acms["skeleton_ratio"] == 0.5
assert acms["temporal_scope"] == "recent"
assert acms["auto_refresh"] is False
# ------------------------------------------------------------------
# Rollback path test for _save_policy_json (P3-14)
# ------------------------------------------------------------------
@when("I attempt to save a policy with a commit that will fail")
def step_save_policy_commit_fail(context: Any) -> None:
"""Force a commit failure and verify the rollback path.
Uses the existing ``context.cb_session_factory`` (set up by the
Background step which already seeded the ``local/cov-app`` project
row) to ensure the UPDATE actually modifies data. Monkey-patches
``commit()`` on the underlying real session to simulate a commit
failure.
"""
from cleveragents.cli.commands.project_context import _save_policy_json
factory = context.cb_session_factory
# Get the _SafeSession wrapper and the real session underneath.
safe_session = factory()
real: Session = object.__getattribute__(safe_session, "_real")
original_commit = real.commit
def failing_commit() -> None:
raise RuntimeError("Simulated commit failure")
real.commit = failing_commit # type: ignore[assignment]
context.cb_rollback_exception = None
try:
_save_policy_json(factory, "local/cov-app", {"marker": "rollback_test"})
except RuntimeError as exc:
context.cb_rollback_exception = exc
finally:
# Restore original commit so subsequent scenarios can use it.
real.commit = original_commit # type: ignore[assignment]
# Store the session for dirty-state check.
context.cb_rollback_session = real
@then("the original commit exception should be re-raised")
def step_assert_commit_exception_reraised(context: Any) -> None:
exc = context.cb_rollback_exception
assert exc is not None, "_save_policy_json did not re-raise the commit exception"
assert "Simulated commit failure" in str(exc), (
f"Expected 'Simulated commit failure', got: {exc}"
)
@then("the session should not be in a dirty state after the failure")
def step_assert_session_clean_after_rollback(context: Any) -> None:
session: Session = context.cb_rollback_session
# After rollback, the session should have no pending changes.
assert not session.dirty, "Session has dirty objects after rollback"
assert not session.new, "Session has new objects after rollback"
# ------------------------------------------------------------------
# Nonexistent project test for _save_policy_json (P3-15)
# ------------------------------------------------------------------
@when('I save a policy for a nonexistent project "{project}"')
def step_save_policy_nonexistent(context: Any, project: str) -> None:
"""Call _save_policy_json for a project that has no ns_projects row."""
from cleveragents.cli.commands.project_context import _save_policy_json
context.cb_nonexistent_exception = None
try:
_save_policy_json(
context.cb_session_factory,
project,
{"marker": "nonexistent_test"},
)
except Exception as exc:
context.cb_nonexistent_exception = exc
@then("the save should complete without error")
def step_assert_save_no_error(context: Any) -> None:
exc = context.cb_nonexistent_exception
# _save_policy_json does not currently check rowcount, so it
# completes without error even when 0 rows are affected.
assert exc is None, f"_save_policy_json raised an unexpected error: {exc}"
@then('the policy should not be retrievable for "{project}"')
def step_assert_policy_not_retrievable(context: Any, project: str) -> None:
from cleveragents.cli.commands.project_context import _load_policy_json
loaded = _load_policy_json(context.cb_session_factory, project)
assert loaded is None, f"Expected None for nonexistent project, got: {loaded}"
+1
View File
@@ -697,6 +697,7 @@ def e2e_tests(session: nox.Session):
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GOOGLE_API_KEY",
"GEMINI_API_KEY",
):
value = os.environ.get(key)
if value:
+22 -15
View File
@@ -62,19 +62,23 @@ Run CleverAgents Command
[Documentation] Run a CleverAgents CLI command and return the result.
...
... Executes ``python -m cleveragents <args>`` using the
... venv Python. Returns the Process result object.
... venv Python. Runs in ``SUITE_HOME`` so the CLI creates
... its ``.cleveragents`` workspace inside the per-suite
... temp directory, avoiding cross-run database pollution.
... Returns the Process result object.
[Arguments] @{args} ${expected_rc}=${0} ${timeout}=120s ${cwd}=${SUITE_HOME}
${result}= Run Process ${PYTHON} -m cleveragents @{args}
... cwd=${cwd}
... timeout=${timeout}
... on_timeout=kill
... env:CLEVERAGENTS_HOME=${SUITE_HOME}
... env:CLEVERAGENTS_AUTO_APPLY_MIGRATIONS=true
... env:NO_COLOR=1
Log STDOUT: ${result.stdout}
Log STDERR: ${result.stderr}
Log STDOUT: ${result.stdout} level=DEBUG
Log STDERR: ${result.stderr} level=DEBUG
IF '${expected_rc}' != 'None'
Should Be Equal As Integers ${result.rc} ${expected_rc}
... CleverAgents command failed with rc=${result.rc}.\nSTDOUT: ${result.stdout}\nSTDERR: ${result.stderr}
... CleverAgents command failed with rc=${result.rc}. Check DEBUG-level log entries above.
END
RETURN ${result}
@@ -119,6 +123,8 @@ Safe Parse Json Field
END
# Strategy 2: last-line fallback — scan lines in reverse for a parseable
# JSON object that contains the requested field.
${strategy1_err}= Set Variable ${value}
${strategy2_err}= Set Variable no JSON lines found
${lines}= Evaluate list(reversed([l.strip() for l in $stdout.splitlines() if l.strip().startswith('{') and l.strip().endswith('}')]))
FOR ${line} IN @{lines}
${ls} ${lv}= Run Keyword And Ignore Error
@@ -126,8 +132,9 @@ Safe Parse Json Field
IF '${ls}' == 'PASS' and '${lv}' != ''
RETURN ${lv}
END
${strategy2_err}= Set Variable ${lv}
END
Log Safe Parse Json Field: JSON parse failed for field '${field_name}': ${value} WARN
Log Safe Parse Json Field: JSON parse failed for field '${field_name}'. Strategy 1: ${strategy1_err}; Strategy 2: ${strategy2_err} WARN
RETURN ${EMPTY}
Create Temp Git Repo
@@ -137,16 +144,16 @@ Create Temp Git Repo
[Arguments] ${name}=test-repo
${repo_dir}= Set Variable ${SUITE_HOME}${/}${name}
Create Directory ${repo_dir}
${git_init}= Run Process git init cwd=${repo_dir}
Should Be Equal As Integers ${git_init.rc} 0 git init failed: ${git_init.stderr}
${git_cfg_name}= Run Process git config user.name E2E Test cwd=${repo_dir}
Should Be Equal As Integers ${git_cfg_name.rc} 0 git config user.name failed: ${git_cfg_name.stderr}
${git_cfg_email}= Run Process git config user.email e2e@test.local cwd=${repo_dir}
Should Be Equal As Integers ${git_cfg_email.rc} 0 git config user.email failed: ${git_cfg_email.stderr}
${r1}= Run Process git init cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r1.rc} 0 msg=git init failed (rc=${r1.rc}). Check DEBUG logs above.
${r2}= Run Process git config user.name E2E Test cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r2.rc} 0 msg=git config user.name failed
${r3}= Run Process git config user.email e2e@test.local cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r3.rc} 0 msg=git config user.email failed
# Create an initial commit so the repo has a HEAD
Create File ${repo_dir}${/}README.md # Test Repository\n
${git_add}= Run Process git add . cwd=${repo_dir}
Should Be Equal As Integers ${git_add.rc} 0 git add failed: ${git_add.stderr}
${git_commit}= Run Process git commit -m Initial commit cwd=${repo_dir}
Should Be Equal As Integers ${git_commit.rc} 0 git commit failed: ${git_commit.stderr}
${r4}= Run Process git add . cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r4.rc} 0 msg=git add failed (rc=${r4.rc}). Check DEBUG logs above.
${r5}= Run Process git commit -m Initial commit cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r5.rc} 0 msg=git commit failed (rc=${r5.rc}). Check DEBUG logs above.
RETURN ${repo_dir}
+1 -1
View File
@@ -103,7 +103,7 @@ M1 Full Plan Lifecycle
# ── 11. Verify post-apply commit in target repo ──────────────
${git_log}= Run Process git log -1 --oneline
... cwd=${repo_path}
... cwd=${repo_path} timeout=60s on_timeout=kill
Log Git log after apply: ${git_log.stdout}
# The repo should have at least the initial commit; if apply worked
# there will be a second commit from CleverAgents
+5 -3
View File
@@ -28,10 +28,12 @@ M2 Full Actor Compiler And LLM Integration
${repo_dir}= Create Temp Git Repo m2-e2e-repo
Create Directory ${repo_dir}${/}src
Create File ${repo_dir}${/}src${/}main.py print("hello world")\n
Run Process git add . cwd=${repo_dir}
Run Process git commit -m Add source files cwd=${repo_dir}
${r_add}= Run Process git add . cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r_add.rc} 0 msg=git add failed (rc=${r_add.rc}). Check DEBUG logs above.
${r_commit}= Run Process git commit -m Add source files cwd=${repo_dir} timeout=60s on_timeout=kill
Should Be Equal As Integers ${r_commit.rc} 0 msg=git commit failed (rc=${r_commit.rc}). Check DEBUG logs above.
# Detect the default branch name created by git init
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${repo_dir}
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${repo_dir} timeout=60s on_timeout=kill
${branch}= Strip String ${branch_result.stdout}
Log Detected branch: ${branch}
+671
View File
@@ -0,0 +1,671 @@
*** Settings ***
Documentation M5 (v3.4.0) E2E acceptance test — ACMS v1 and context scaling.
...
... Exercises context policy configuration, context assembly,
... context analysis, and plan execution through real CLI
... commands with real LLM API keys. This suite uses **zero
... mocking** — every LLM call hits a real provider endpoint.
...
... **Structural vs. behavioural scope:** Tests in sections
... 1b4 that use ``project context simulate`` or ``inspect``
... are *structural / plumbing* validations — they verify CLI
... execution, JSON serialization, and stored configuration but
... do **not** exercise actual ACMS indexing or budget enforcement
... because the ``ContextTierService`` is an in-memory singleton
... that starts empty per CLI process. Each affected test has
... a ``[Documentation]`` note explaining this limitation.
... Behavioural ACMS validation is deferred until the full
... indexing pipeline is wired.
Resource common_e2e.resource
Library OperatingSystem
Library String
Library Collections
Library Process
Suite Setup M5 Acceptance Suite Setup
Suite Teardown M5 Acceptance Suite Teardown
*** Variables ***
${PROJECT_POLICY} local/m5-policy
${PROJECT_BUDGET} local/m5-budget
${PROJECT_ANALYSIS} local/m5-analysis
${PROJECT_PLAN} local/m5-plan
*** Keywords ***
M5 Acceptance Suite Setup
[Documentation] Create an isolated workspace with ``agents init``, build a
... synthetic codebase, initialise a git repo, register it as a
... resource, and disable mock AI.
E2E Suite Setup
# Create workspace directory inside the suite home
${ws}= Set Variable ${SUITE_HOME}${/}workspace
Create Directory ${ws}
Outdated
Review

H4 — Git return codes not checked. All 5 git commands discard their return values. If git fails (not installed, permission error), subsequent tests fail with unrelated errors. Capture results and assert rc == 0.

**H4 — Git return codes not checked.** All 5 git commands discard their return values. If git fails (not installed, permission error), subsequent tests fail with unrelated errors. Capture results and assert `rc == 0`.
Set Suite Variable ${WS} ${ws}
# Initialize the CleverAgents workspace
${result}= Run CLI init m5-workspace
Log Init stdout: ${result.stdout} level=DEBUG
Log Init stderr: ${result.stderr} level=DEBUG
Should Be Equal As Integers ${result.rc} 0
... Workspace init failed (rc=${result.rc}). Check DEBUG-level log entries above.
# Create synthetic source files for context testing
Create Synthetic Codebase ${ws}
# Initialize a git repository — check every return code
${git_init}= Run Process git init cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${git_init.rc} 0 msg=git init failed (rc=${git_init.rc}). Check DEBUG logs above.
${git_cfg_name}= Run Process git config user.name E2E Test cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${git_cfg_name.rc} 0 msg=git config user.name failed
${git_cfg_email}= Run Process git config user.email e2e@test.local cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${git_cfg_email.rc} 0 msg=git config user.email failed
${git_add}= Run Process git add . cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${git_add.rc} 0 msg=git add failed (rc=${git_add.rc}). Check DEBUG logs above.
${git_commit}= Run Process git commit -m Initial commit cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${git_commit.rc} 0 msg=git commit failed (rc=${git_commit.rc}). Check DEBUG logs above.
# Detect the default branch created by git init
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${ws} timeout=60s on_timeout=kill
Should Be Equal As Integers ${branch_result.rc} 0 msg=git rev-parse failed
${branch}= Strip String ${branch_result.stdout}
Set Suite Variable ${WS_BRANCH} ${branch}
# Register the workspace as a git-checkout resource and make it
# available for linking to individual projects.
${res_name}= Set Variable local/m5-ws-resource
Set Suite Variable ${WS_RESOURCE} ${res_name}
${r_add}= Run CLI resource add git-checkout ${res_name} --path ${ws} --branch ${branch}
Should Be Equal As Integers ${r_add.rc} 0 msg=resource add failed (rc=${r_add.rc}). Check DEBUG logs above.
# Mark suite setup as complete so Section 1 tests can skip cleanly
# if setup failed partway through.
Set Suite Variable ${SUITE_SETUP_COMPLETE} ${TRUE}
M5 Acceptance Suite Teardown
[Documentation] Delegate to the common E2E teardown.
E2E Suite Teardown
Create Synthetic Codebase
[Documentation] Populate the workspace with small Python files and one
... deliberately large file for budget-enforcement testing.
Outdated
Review

H3 — Missing on_timeout=kill. Per commit 3eecb79, all Run Process calls should use on_timeout=kill. This keyword and all 5 git commands in Suite Setup (lines 41-45) lack it. Git commands also have no timeout at all — a hung process could block CI indefinitely.

**H3 — Missing `on_timeout=kill`.** Per commit 3eecb79, all `Run Process` calls should use `on_timeout=kill`. This keyword and all 5 git commands in Suite Setup (lines 41-45) lack it. Git commands also have no timeout at all — a hung process could block CI indefinitely.
[Arguments] ${base_dir}
${main_py}= Catenate SEPARATOR=\n
... """Main entry point for M5 E2E test project."""
... ${EMPTY}
... def main() -> None:
... ${SPACE}${SPACE}${SPACE}${SPACE}"""Run the application."""
... ${SPACE}${SPACE}${SPACE}${SPACE}print("Hello from M5 test")
... ${EMPTY}
... if __name__ == "__main__":
... ${SPACE}${SPACE}${SPACE}${SPACE}main()
Create File ${base_dir}${/}main.py ${main_py}
${utils_py}= Catenate SEPARATOR=\n
... """Utility helpers."""
... ${EMPTY}
... def add(a: int, b: int) -> int:
... ${SPACE}${SPACE}${SPACE}${SPACE}return a + b
... ${EMPTY}
... def multiply(a: int, b: int) -> int:
... ${SPACE}${SPACE}${SPACE}${SPACE}return a * b
Create File ${base_dir}${/}utils.py ${utils_py}
Create File ${base_dir}${/}config.py TIMEOUT = 30\nMAX_RETRIES = 3\nDEBUG = False
# Large file (>1 KiB) for budget testing
${large_content}= Evaluate "# auto-generated large file\\n" + ("x = 1\\n" * 250)
Create File ${base_dir}${/}large_file.py ${large_content}
Run CLI
[Documentation] Run ``python -m cleveragents`` inside the workspace directory
... with the correct environment variables.
[Arguments] @{args} ${expected_rc}=${0} ${timeout}=120s
${result}= Run Process ${PYTHON} -m cleveragents @{args}
... cwd=${WS}
... env:CLEVERAGENTS_HOME=${SUITE_HOME}
... env:CLEVERAGENTS_AUTO_APPLY_MIGRATIONS=true
... env:NO_COLOR=1
... timeout=${timeout}
... on_timeout=kill
Log STDOUT: ${result.stdout} level=DEBUG
Log STDERR: ${result.stderr} level=DEBUG
# Do not embed raw stdout/stderr in assertion failure messages —
# they may contain API key material. DEBUG-level logs above have the details.
Should Be Equal As Integers ${result.rc} ${expected_rc}
... CLI failed (rc=${result.rc}). Check DEBUG-level log entries above for stdout/stderr.
RETURN ${result}
Combined Output
[Documentation] Return the concatenation of stdout and stderr (lowercased).
[Arguments] ${result}
${raw}= Set Variable ${result.stdout}\n${result.stderr}
${combined}= Convert To Lower Case ${raw}
RETURN ${combined}
Skip If No OpenAI Key
[Documentation] Skip the current test if ``OPENAI_API_KEY`` is not set.
... Uses ``Evaluate`` with ``os.environ.get`` to avoid
... storing the key value in any Robot variable.
${is_missing}= Evaluate len(os.environ.get('OPENAI_API_KEY', '')) == 0 modules=os
Skip If ${is_missing} OPENAI_API_KEY not set — required for openai/gpt-4o-mini tests.
Link Resource To Project
[Documentation] Link the suite-level workspace resource to a project.
[Arguments] ${project_name}
${result}= Run CLI project link-resource ${project_name} ${WS_RESOURCE}
Should Be Equal As Integers ${result.rc} 0
... msg=project link-resource failed (rc=${result.rc}). Check DEBUG logs above.
Extract JSON From Stdout
[Documentation] Extract the first JSON object from stdout that may contain
... leading non-JSON text (e.g. structlog debug messages).
... Uses ``raw_decode`` with ``strict=False`` to tolerate
... literal control characters that Rich's ``console.print``
... may inject when it wraps long lines inside JSON string
... values. Wrapped in ``TRY/EXCEPT`` for clear failure messages.
[Arguments] ${text}
TRY
${start}= Evaluate $text.index('{')
${json_obj}= Evaluate json.JSONDecoder(strict=False).raw_decode($text, $start)[0] modules=json
EXCEPT AS ${err}
Fail Failed to extract JSON object from stdout: ${err}
END
RETURN ${json_obj}
Plan Test Setup
[Documentation] Setup for plan execution tests — skip if no API key
... and verify prerequisite variables exist.
Outdated
Review

H2 — No resource linked. The 10K file test generates files and sets --include-path, but no resource is linked to the local/m5-scale project via resource add + project link-resource. The simulate command may produce generic output without actually indexing the 10,000 files.

Same issue affects the budget enforcement project (local/m5-budget) and the plan execution project (local/m5-plan).

**H2 — No resource linked.** The 10K file test generates files and sets `--include-path`, but no resource is linked to the `local/m5-scale` project via `resource add` + `project link-resource`. The `simulate` command may produce generic output without actually indexing the 10,000 files. Same issue affects the budget enforcement project (`local/m5-budget`) and the plan execution project (`local/m5-plan`).
[Arguments] @{required_vars}
Skip If No OpenAI Key
FOR ${var} IN @{required_vars}
Variable Should Exist \${${var}}
... msg=Prerequisite not met: ${var} not set — earlier test may have failed
END
*** Test Cases ***
# -----------------------------------------------------------------------
# 1. Context Assembly — context add / list / show / clear
# -----------------------------------------------------------------------
Context Assembly — Add Files To Context
[Documentation] ``context-load`` adds files; ``context list`` reflects them.
[Tags] E2E
[Setup] Variable Should Exist ${SUITE_SETUP_COMPLETE}
... msg=Prerequisite not met: suite setup did not complete
${result}= Run CLI context-load main.py utils.py
Should Be Equal As Integers ${result.rc} 0
${list}= Run CLI context list
Should Contain ${list.stdout} main.py
Should Contain ${list.stdout} utils.py
Context Assembly — Show File Content
[Documentation] ``context show <file>`` displays file content.
[Tags] E2E
[Setup] Variable Should Exist ${SUITE_SETUP_COMPLETE}
... msg=Prerequisite not met: suite setup did not complete
# Ensure files are loaded (idempotent)
Run CLI context-load main.py
${result}= Run CLI context show main.py
Should Contain ${result.stdout} Hello from M5 test
Context Assembly — Show Context Summary
[Documentation] ``context show`` (no args) displays a summary.
... Verifies the output is non-empty, contains structural
... keywords, and that the CLI exit code is 0. Also
... checks the output is not just an error message by
... verifying the absence of common error indicators.
[Tags] E2E
[Setup] Variable Should Exist ${SUITE_SETUP_COMPLETE}
... msg=Prerequisite not met: suite setup did not complete
Run CLI context-load main.py utils.py
${result}= Run CLI context show
Should Not Be Empty ${result.stdout}
# The summary lists aggregate stats (file count, total size) — not
# individual file names. Verify the structural summary content.
${combined}= Combined Output ${result}
Should Contain ${combined} context summary msg=Expected summary header in context show output
Should Contain ${combined} total files msg=Expected file count in context show output
# Guard against false positives: ensure the output is not an error.
Should Not Contain ${combined} traceback msg=Output should not contain a traceback
Should Not Contain ${combined} error: msg=Output should not contain an error message
Context Assembly — Clear Context
[Documentation] ``context clear --yes`` removes all loaded files.
... Asserts files are present before clearing so the
... ``Should Not Contain`` checks are not vacuously true.
[Tags] E2E
[Setup] Variable Should Exist ${SUITE_SETUP_COMPLETE}
... msg=Prerequisite not met: suite setup did not complete
Run CLI context-load config.py
# Verify precondition: config.py IS loaded before we clear
${list_before}= Run CLI context list
Should Contain ${list_before.stdout} config.py
... msg=Precondition failed: config.py should be loaded before clear
${clear}= Run CLI context clear --yes
Should Be Equal As Integers ${clear.rc} 0
${list}= Run CLI context list
Should Not Contain ${list.stdout} config.py msg=config.py should be removed after clear
Should Not Contain ${list.stdout} main.py msg=main.py should be removed after clear
Should Not Contain ${list.stdout} utils.py msg=utils.py should be removed after clear
# -----------------------------------------------------------------------
# 1b. Context Scaling — 10,000+ files
# -----------------------------------------------------------------------
Context Scaling — Structural Plumbing for 10K File Projects
[Documentation] **Structural / plumbing test.** Generates 10,000+ small
... Python files, creates a project with ``--include-path``,
... and verifies that ``project context simulate`` completes
... without timeout and returns well-formed JSON.
...
... *Limitation:* ``project context simulate`` does **not**
... perform filesystem scanning — the ``ContextTierService``
... is an in-memory singleton that starts empty per CLI
... process. Therefore ``fragment_count`` is 0 and
... ``total_tokens`` is 0 regardless of the generated files.
... This test validates the CLI plumbing (policy storage,
... simulate command execution, JSON serialization) but
... **not** actual 10K-file indexing. Behavioral validation
... of M5 criterion *"Projects with 10,000+ files index
... without timeout"* is deferred until the full ACMS
... indexing pipeline is wired.
[Tags] E2E
[Setup] Variable Should Exist ${SUITE_SETUP_COMPLETE}
... msg=Prerequisite not met: suite setup did not complete
# Generate 10,000 tiny .py files in a subdirectory
${scale_dir}= Set Variable ${WS}${/}scale_src
Create Directory ${scale_dir}
${script}= Catenate SEPARATOR=\n
... import os, sys
... d = sys.argv[1]
... for i in range(10000):
... ${SPACE}${SPACE}${SPACE}${SPACE}with open(os.path.join(d, f"mod_{i:05d}.py"), "w") as f:
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}f.write(f"# module {i}\\ndef fn_{i}(): return {i}\\n")
${gen_script}= Set Variable ${WS}${/}_gen_10k.py
Create File ${gen_script} ${script}
${gen}= Run Process ${PYTHON} ${gen_script} ${scale_dir}
... timeout=120s on_timeout=kill
Should Be Equal As Integers ${gen.rc} 0 msg=10K file generation failed
# Create a project with the scale directory and link resource
${scale_proj}= Set Variable local/m5-scale
${result}= Run CLI project create ${scale_proj}
Should Be Equal As Integers ${result.rc} 0
Link Resource To Project ${scale_proj}
${result}= Run CLI
... project context set ${scale_proj}
... --view default
... --include-path scale_src/**/*.py
Should Be Equal As Integers ${result.rc} 0
# Simulate must complete within 600s (10 minutes)
${result}= Run CLI
... project context simulate ${scale_proj}
... --format json timeout=600s
Should Be Equal As Integers ${result.rc} 0
# Parse JSON and verify simulation output is structurally valid.
# NOTE: All simulate assertions below are structural only — they verify
# the CLI returns well-formed JSON with the expected schema. Because the
# ContextTierService is empty (no indexing pipeline), total_tokens and
# fragment_count will be 0. This is a known limitation documented above.
${sim_json}= Extract JSON From Stdout ${result.stdout}
Should Be True 'total_tokens' in $sim_json
... msg=Simulate JSON missing 'total_tokens' field
Should Be True 'fragment_count' in $sim_json
... msg=Simulate JSON missing 'fragment_count' field
Should Be True type($sim_json.get('total_tokens')) in (int, float)
... msg=total_tokens should be numeric
Should Be True type($sim_json.get('fragment_count')) in (int, float)
... msg=fragment_count should be numeric
# -----------------------------------------------------------------------
# 2. Context Policy Configuration — view-specific settings
# -----------------------------------------------------------------------
Context Policy — Create Project For Policy Tests
[Documentation] Pre-requisite: create a project and link the workspace resource.
[Tags] E2E
${result}= Run CLI project create ${PROJECT_POLICY}
Should Be Equal As Integers ${result.rc} 0
Link Resource To Project ${PROJECT_POLICY}
Set Suite Variable ${POLICY_PROJECT_CREATED} ${TRUE}
Context Policy — Set Default View
[Documentation] Configure the default context view with include/exclude
... paths and file-size limits.
[Tags] E2E
[Setup] Variable Should Exist ${POLICY_PROJECT_CREATED}
... msg=Prerequisite not met: policy project not created
${result}= Run CLI
... project context set ${PROJECT_POLICY}
... --view default
... --include-path src/**/*.py
... --exclude-path **/__pycache__/**
... --max-file-size 262144
... --max-total-size 1048576
Outdated
Review

H1/M7 — Vacuous assertion (confirmed bug). Should Contain 500 always matches cold_max_decisions: 5000. Additionally, all three ACMS values (8000, 500, 5000) match their defaults in project_context.py:59-61, so this entire section passes even if context set fails silently.

Fix: Use non-default values (e.g., --warm-max-decisions 750, --hot-max-tokens 12000) and anchored assertions like Should Contain ${result.stdout} "warm_max_decisions": 750.

**H1/M7 — Vacuous assertion (confirmed bug).** `Should Contain 500` always matches `cold_max_decisions: 5000`. Additionally, all three ACMS values (8000, 500, 5000) match their defaults in `project_context.py:59-61`, so this entire section passes even if `context set` fails silently. **Fix:** Use non-default values (e.g., `--warm-max-decisions 750`, `--hot-max-tokens 12000`) and anchored assertions like `Should Contain ${result.stdout} "warm_max_decisions": 750`.
Should Be Equal As Integers ${result.rc} 0
Context Policy — Set Strategize View Override
[Documentation] The strategize view overrides the default with tighter
... limits and additional include paths.
Outdated
Review

M5 — No skip guard for missing OPENAI_API_KEY. The Plan Execution section requires openai/gpt-4o-mini but has no graceful skip mechanism. Consider adding a setup keyword that checks the key using Evaluate len($key)==0 to avoid string interpolation of the actual key value.

**M5 — No skip guard for missing OPENAI_API_KEY.** The Plan Execution section requires `openai/gpt-4o-mini` but has no graceful skip mechanism. Consider adding a setup keyword that checks the key using `Evaluate len($key)==0` to avoid string interpolation of the actual key value.
[Tags] E2E
[Setup] Variable Should Exist ${POLICY_PROJECT_CREATED}
... msg=Prerequisite not met: policy project not created
${result}= Run CLI
... project context set ${PROJECT_POLICY}
... --view strategize
... --include-path src/**/*.py
... --include-path docs/**/*.md
... --max-file-size 131072
Should Be Equal As Integers ${result.rc} 0
Context Policy — Verify Default View
[Documentation] ``project context show --view default`` returns the
... configured values. Uses parsed JSON assertions to
... avoid substring collisions (e.g. ``1024`` matching
... inside ``10240``).
[Tags] E2E
[Setup] Variable Should Exist ${POLICY_PROJECT_CREATED}
... msg=Prerequisite not met: policy project not created
${result}= Run CLI
... project context show ${PROJECT_POLICY}
... --view default --format json
Should Be Equal As Integers ${result.rc} 0
${policy_json}= Extract JSON From Stdout ${result.stdout}
# When --view is specified, the JSON uses 'resolved_view' (not 'default_view').
${rv}= Evaluate $policy_json.get('resolved_view', {})
Should Be True $rv.get('max_file_size') == 262144
... msg=resolved default view max_file_size should be 262144
Should Be True $rv.get('max_total_size') == 1048576
... msg=resolved default view max_total_size should be 1048576
Should Be True 'src/**/*.py' in $rv.get('include_paths', [])
... msg=resolved default view should include src/**/*.py
Should Be True '**/__pycache__/**' in $rv.get('exclude_paths', [])
... msg=resolved default view should exclude **/__pycache__/**
Context Policy — Verify Strategize View
[Documentation] ``project context show --view strategize`` returns the
... overridden values. Uses parsed JSON assertions.
[Tags] E2E
[Setup] Variable Should Exist ${POLICY_PROJECT_CREATED}
... msg=Prerequisite not met: policy project not created
${result}= Run CLI
... project context show ${PROJECT_POLICY}
... --view strategize --format json
Should Be Equal As Integers ${result.rc} 0
${policy_json}= Extract JSON From Stdout ${result.stdout}
# When --view is specified, the JSON uses 'resolved_view'.
${rv}= Evaluate $policy_json.get('resolved_view', {})
Should Be True $rv.get('max_file_size') == 131072
... msg=resolved strategize view max_file_size should be 131072
Should Be True 'docs/**/*.md' in $rv.get('include_paths', [])
... msg=resolved strategize view should include docs/**/*.md
# -----------------------------------------------------------------------
# 3. Budget Enforcement — max_file_size / max_total_size
# -----------------------------------------------------------------------
Budget Enforcement — Create Project With Tight Budget
[Documentation] Create a project, link resource, and set tight budget constraints.
[Tags] E2E
${result}= Run CLI project create ${PROJECT_BUDGET}
Should Be Equal As Integers ${result.rc} 0
Link Resource To Project ${PROJECT_BUDGET}
${result}= Run CLI
... project context set ${PROJECT_BUDGET}
... --view default
... --include-path **/*.py
... --max-file-size 1024
... --max-total-size 4096
Should Be Equal As Integers ${result.rc} 0
Set Suite Variable ${BUDGET_PROJECT_CREATED} ${TRUE}
Budget Enforcement — Verify Constraints Stored
[Documentation] The JSON policy output must contain the budget values.
... Uses parsed JSON assertions to avoid substring
... collisions (e.g. ``1024`` matching inside ``10240``).
[Tags] E2E
[Setup] Variable Should Exist ${BUDGET_PROJECT_CREATED}
... msg=Prerequisite not met: budget project not created
${result}= Run CLI
... project context show ${PROJECT_BUDGET}
... --view default --format json
Should Be Equal As Integers ${result.rc} 0
${policy_json}= Extract JSON From Stdout ${result.stdout}
# When --view is specified, the JSON uses 'resolved_view'.
${rv}= Evaluate $policy_json.get('resolved_view', {})
Should Be True $rv.get('max_file_size') == 1024
... msg=Budget resolved view max_file_size should be 1024
Should Be True $rv.get('max_total_size') == 4096
... msg=Budget resolved view max_total_size should be 4096
Budget Enforcement — Simulate Context Assembly (Structural)
[Documentation] **Structural / plumbing test.** Verifies that
... ``project context simulate`` returns well-formed JSON
... reflecting the configured budget constraints in the
... ``acms_config`` section.
...
... *Limitation:* ``max_file_size`` filtering is only enforced
... in the repo indexing path (``repo_indexing_utils``), not
... in the simulate command. The ``ContextTierService`` is
... empty per process, so simulate operates on zero fragments.
... Behavioral budget enforcement (verifying ``large_file.py``
... exclusion) requires the full ACMS indexing pipeline and
... is deferred to a follow-up issue.
[Tags] E2E
[Setup] Variable Should Exist ${BUDGET_PROJECT_CREATED}
... msg=Prerequisite not met: budget project not created
${result}= Run CLI
... project context simulate ${PROJECT_BUDGET}
... --format json
Should Be Equal As Integers ${result.rc} 0
# Parse JSON and verify structural fields present.
# NOTE: Assertions are structural only — they verify the CLI returns
# well-formed JSON with the expected schema. The ContextTierService is
# empty, so no actual file inclusion/exclusion is tested here.
# Budget constraints (max_file_size, max_total_size) are stored in the
# ContextView policy, not in the simulate output's acms_config.
${sim_json}= Extract JSON From Stdout ${result.stdout}
Should Be True 'acms_config' in $sim_json
... msg=Simulate JSON missing 'acms_config' field
Should Be True 'total_tokens' in $sim_json
... msg=Simulate JSON missing 'total_tokens' field
Should Be True 'fragment_count' in $sim_json
... msg=Simulate JSON missing 'fragment_count' field
Should Be True type($sim_json['total_tokens']) in (int, float)
... msg=total_tokens should be numeric
# -----------------------------------------------------------------------
# 4. Context Analysis — meaningful summaries
# -----------------------------------------------------------------------
Context Analysis — Create Project With ACMS Config
[Documentation] Create a project and configure ACMS pipeline parameters.
... Uses **non-default** values to avoid vacuous assertions —
... defaults are 8000 / 500 / 5000.
[Tags] E2E
${result}= Run CLI project create ${PROJECT_ANALYSIS}
Should Be Equal As Integers ${result.rc} 0
Link Resource To Project ${PROJECT_ANALYSIS}
${result}= Run CLI
... project context set ${PROJECT_ANALYSIS}
... --view default
... --include-path **/*.py
... --hot-max-tokens 12000
... --warm-max-decisions 750
... --cold-max-decisions 3500
... --temporal-scope current
Should Be Equal As Integers ${result.rc} 0
Set Suite Variable ${ANALYSIS_PROJECT_CREATED} ${TRUE}
Context Analysis — Inspect Context Tiers (Structural)
[Documentation] **Structural / plumbing test.** Verifies that
... ``project context inspect`` returns well-formed JSON with
... the expected ``tier_metrics`` and ``tier_budget`` sections,
... and that each section contains the documented field names.
...
... *Limitation:* ``tier_metrics`` always has 7 keys with
... zero-value counters and ``tier_budget`` always has 3 keys
... with the configured values, regardless of whether any data
... was indexed — the ``ContextTierService`` is empty per
... process. These assertions verify JSON schema correctness,
... not ACMS behavioral state.
[Tags] E2E
[Setup] Variable Should Exist ${ANALYSIS_PROJECT_CREATED}
... msg=Prerequisite not met: analysis project not created
${result}= Run CLI
... project context inspect ${PROJECT_ANALYSIS}
... --format json
Should Be Equal As Integers ${result.rc} 0
# Parse inspect output and verify tier data schema.
${inspect_json}= Extract JSON From Stdout ${result.stdout}
Should Be True 'tier_metrics' in $inspect_json
... msg=Inspect JSON missing 'tier_metrics' field
Should Be True 'tier_budget' in $inspect_json
... msg=Inspect JSON missing 'tier_budget' field
# Verify tier_metrics contains the expected field names (schema check).
${metrics}= Evaluate $inspect_json.get('tier_metrics', {})
Should Be True 'hot_count' in $metrics
... msg=tier_metrics missing 'hot_count' field
Should Be True 'warm_count' in $metrics
... msg=tier_metrics missing 'warm_count' field
Should Be True 'cold_count' in $metrics
... msg=tier_metrics missing 'cold_count' field
# Verify tier_budget has the expected field names (schema check).
# NOTE: tier_budget comes from the ContextTierService default TierBudget,
# not the ACMS config. Values are always the TierBudget defaults
# (8000/500/5000) because the tier service is a fresh singleton per
# CLI process with no indexed data.
${budget}= Evaluate $inspect_json.get('tier_budget', {})
Should Be True 'max_tokens_hot' in $budget
... msg=tier_budget missing 'max_tokens_hot' field
Should Be True 'max_decisions_warm' in $budget
... msg=tier_budget missing 'max_decisions_warm' field
Should Be True 'max_decisions_cold' in $budget
... msg=tier_budget missing 'max_decisions_cold' field
Context Analysis — Simulate Produces Structured Output
[Documentation] **Structural / plumbing test.** Verifies that
... ``project context simulate`` returns well-formed JSON
... containing the required schema fields (``total_tokens``,
... ``budget_used``, ``strategies_used``, ``context_hash``).
...
... *Limitation:* The ``ContextTierService`` is empty per
... process, so ``total_tokens`` is 0 and ``budget_used``
... is 0.0. These assertions verify JSON schema
... correctness, not ACMS behavioral output.
[Tags] E2E
[Setup] Variable Should Exist ${ANALYSIS_PROJECT_CREATED}
... msg=Prerequisite not met: analysis project not created
${result}= Run CLI
... project context simulate ${PROJECT_ANALYSIS}
... --format json
Should Be Equal As Integers ${result.rc} 0
# Parse JSON and verify all expected schema fields are present.
${sim_json}= Extract JSON From Stdout ${result.stdout}
Should Be True 'total_tokens' in $sim_json
... msg=Simulate JSON missing 'total_tokens' field
Should Be True 'budget_used' in $sim_json
... msg=Simulate JSON missing 'budget_used' field
Should Be True 'strategies_used' in $sim_json
... msg=Simulate JSON missing 'strategies_used' field
Should Be True 'context_hash' in $sim_json
... msg=Simulate JSON missing 'context_hash' field
# Verify types are correct (structural schema check).
Should Be True type($sim_json['total_tokens']) in (int, float)
... msg=total_tokens should be numeric
Should Be True type($sim_json['budget_used']) in (int, float)
... msg=budget_used should be numeric
Context Analysis — Show Full Policy With ACMS Config
[Documentation] ``project context show`` includes the ACMS pipeline config.
... Uses parsed JSON assertions to avoid substring collisions
... (e.g. 500 matching inside 3500).
[Tags] E2E
[Setup] Variable Should Exist ${ANALYSIS_PROJECT_CREATED}
... msg=Prerequisite not met: analysis project not created
${result}= Run CLI
... project context show ${PROJECT_ANALYSIS}
... --format json
Should Be Equal As Integers ${result.rc} 0
# Parse JSON and verify ACMS config values via dict access, avoiding
# fragile substring matches.
${policy_json}= Extract JSON From Stdout ${result.stdout}
${acms}= Evaluate $policy_json.get('acms_config', {})
Should Be True $acms.get('hot_max_tokens') == 12000
... msg=ACMS hot_max_tokens should be 12000 (configured value)
Should Be True $acms.get('warm_max_decisions') == 750
... msg=ACMS warm_max_decisions should be 750 (configured value)
Should Be True $acms.get('cold_max_decisions') == 3500
... msg=ACMS cold_max_decisions should be 3500 (configured value)
# -----------------------------------------------------------------------
# 5. Plan Execution — ACMS context with real LLM calls
# -----------------------------------------------------------------------
Plan Execution — Create Project And Configure ACMS
[Documentation] Set up a project with ACMS context for plan tests.
[Tags] E2E
[Setup] Plan Test Setup
${result}= Run CLI project create ${PROJECT_PLAN}
Should Be Equal As Integers ${result.rc} 0
Link Resource To Project ${PROJECT_PLAN}
${result}= Run CLI
... project context set ${PROJECT_PLAN}
... --view default
... --include-path **/*.py
... --hot-max-tokens 4000
Should Be Equal As Integers ${result.rc} 0
# Track prerequisite completion for downstream tests
Set Suite Variable ${PLAN_PROJECT_CREATED} ${TRUE}
Plan Execution — Create Action
[Documentation] Create an action definition from YAML config.
[Tags] E2E
[Setup] Plan Test Setup PLAN_PROJECT_CREATED
${action_yaml}= Catenate SEPARATOR=\n
... name: local/m5-e2e-action
... description: M5 E2E acceptance test action
... strategy_actor: openai/gpt-4o-mini
... execution_actor: openai/gpt-4o-mini
... definition_of_done: |
... ${SPACE}${SPACE}The plan produces a valid strategy.
... reusable: true
... automation_profile: trusted
Create File ${WS}${/}m5_action.yaml ${action_yaml}
${result}= Run CLI action create --config m5_action.yaml
Should Be Equal As Integers ${result.rc} 0
# Track prerequisite completion for downstream tests
Set Suite Variable ${PLAN_ACTION_CREATED} ${TRUE}
Plan Execution — Create Plan With Plan Use
[Documentation] ``plan use`` creates a plan in the Strategize phase
... referencing the ACMS-configured project. Uses the
... ``Extract JSON From Stdout`` keyword consistently with
... the rest of the suite (instead of fragile ``rindex``).
[Tags] E2E
[Setup] Plan Test Setup PLAN_ACTION_CREATED
${result}= Run CLI
... plan use local/m5-e2e-action ${PROJECT_PLAN}
... --format json timeout=300s
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} plan_id msg=Plan use did not return JSON with plan_id
# Use Extract JSON From Stdout consistently with plan resume and other
# tests — avoids fragile rindex-based extraction that can pick the wrong
# '{' if structlog lines also contain plan_id.
${plan_data}= Extract JSON From Stdout ${result.stdout}
Should Be True 'plan_id' in $plan_data
... msg=Plan use JSON missing 'plan_id' field
Set Suite Variable ${PLAN_ID} ${plan_data}[plan_id]
Plan Execution — Resume Plan For LLM Processing
[Documentation] ``plan resume`` transitions the plan from queued to
... processing, triggering real LLM strategy generation
... using the ACMS context pipeline. Assertions are
... outside the TRY block so that field-level failures
... report the actual missing field, not a misleading
... "did not return valid JSON" message.
[Tags] E2E
[Setup] Plan Test Setup PLAN_ID
${result}= Run CLI
... plan resume ${PLAN_ID} --format json
... timeout=300s
Should Be Equal As Integers ${result.rc} 0
# Parse JSON — TRY block only guards the extraction itself.
TRY
${resume_json}= Extract JSON From Stdout ${result.stdout}
EXCEPT AS ${err}
Fail Plan resume did not return valid JSON: ${err}
END
# Assertions outside TRY so failures report the actual issue.
Should Be True 'plan_id' in $resume_json
... msg=Plan resume JSON missing 'plan_id' field
Should Be True 'phase' in $resume_json
... msg=Plan resume JSON missing 'phase' field
# Verify the phase transitioned to an expected state — a plan stuck
# in 'queued' would indicate resume had no effect.
${phase}= Evaluate $resume_json.get('phase', '')
Should Not Be Equal As Strings ${phase} queued
... msg=Plan phase should have transitioned from 'queued' after resume
+30
View File
@@ -4,8 +4,14 @@ Based on ADR-003 (Dependency Injection Framework).
Uses dependency-injector for managing service instances.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sqlalchemy.orm import Session, sessionmaker
import structlog
from dependency_injector import containers, providers
@@ -206,6 +212,19 @@ def _build_repo_indexing_service(
return RepoIndexingService(session_factory=factory)
def _build_session_factory(database_url: str) -> sessionmaker[Session]:
"""Build a SQLAlchemy session factory from a database URL.
Returns a ``sessionmaker`` instance (callable) that the CLI
``project context`` commands use to obtain short-lived sessions.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(database_url, echo=False)
return sessionmaker(bind=engine, expire_on_commit=False)
def _build_resource_registry_service(
database_url: str,
) -> ResourceRegistryService:
@@ -379,6 +398,17 @@ class Container(containers.DeclarativeContainer):
# Database URL - callable that returns proper path
database_url = providers.Callable(get_database_url)
# Session factory - builds a sessionmaker bound to the database URL.
# Used by CLI project-context commands for short-lived sessions.
# Singleton chosen intentionally — avoids creating duplicate engines
# per call. The DB URL is stable for the process lifetime; if
# dynamic URL refresh is ever needed, switch to Factory and add
# engine disposal logic.
session_factory = providers.Singleton(
_build_session_factory,
database_url=database_url,
)
# Unit of Work - Factory (new instance per request)
unit_of_work = providers.Factory(
UnitOfWork,
@@ -20,6 +20,7 @@ Based on ADR-014 (Context Management / ACMS) and the
from __future__ import annotations
import contextlib
import hashlib
import json as _json
from typing import Annotated, Any
@@ -130,7 +131,14 @@ def _save_policy_json(
"ns": namespaced_name,
},
)
session.flush()
session.commit()
except Exception:
# Explicit rollback on commit failure to release DB locks.
# Wrapped in contextlib.suppress so a rollback failure does not
# mask the original commit exception.
with contextlib.suppress(Exception):
session.rollback()
raise
finally:
session.close()
+2
View File
@@ -63,6 +63,8 @@ _SECRET_PATTERNS: list[re.Pattern[str]] = [
re.compile(r"sk-(?:proj-)?[A-Za-z0-9_-]{10,}"),
# Anthropic keys: sk-ant-api03-...
re.compile(r"sk-ant-[A-Za-z0-9_-]{10,}"),
# Google / Gemini API keys: AIzaSy...
re.compile(r"AIzaSy[A-Za-z0-9_-]{30,}"),
# Token IDs: tok_...
re.compile(r"tok_[A-Za-z0-9]{10,}"),
# Bearer tokens