Files
cleveragents-core/features/steps/plan_apply_render_steps.py
brent.edwards eb46f0ff54
CI / push-validation (push) Successful in 40s
CI / helm (push) Successful in 44s
CI / build (push) Successful in 1m14s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m36s
CI / e2e_tests (push) Successful in 1m19s
CI / typecheck (push) Successful in 2m22s
CI / security (push) Successful in 2m23s
CI / benchmark-regression (push) Failing after 41s
CI / integration_tests (push) Successful in 4m46s
CI / unit_tests (push) Failing after 20m13s
CI / benchmark-publish (push) Failing after 28m28s
CI / coverage (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
fix(plan): add tier hydration and improve architecture review output (#10938)
## Summary

This PR fixes issue #10878 where architecture reviews were truncated because the regex pattern for parsing file output would stop at the first ``` encountered in the Markdown report.

## Changes

- Change file delimiters from ``` to >>>>>>>/<<<<<<< to avoid Markdown conflicts
- Add tier hydration before strategize phase in plan_executor.py
- Increase max_tokens to 16384 in llm_actors.py for longer outputs
- Increase context_max_tokens_hot from 16000 to 32000 in settings.py
- Fix get_hot_view → get_hot_fragments in strategy_actor.py and plan_executor.py
- Add opencode to skip directories in context_tier_hydrator.py
- Change sandbox output location to plan-output/ directory in plan.py
- Add get_context_summary stub method to acms_service.py

## Testing

Run architecture review action and verify the output report is complete with all sections.

Reviewed-on: #10938
2026-05-19 12:43:34 +00:00

496 lines
18 KiB
Python

"""Step definitions for PlanApplyService rendering coverage tests."""
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Any
from behave import given, then, when
from behave.runner import Context
if TYPE_CHECKING:
pass
# ---------------------------------------------------------------------------
# Helpers — build plain data dicts for the render functions
# ---------------------------------------------------------------------------
def _make_diff_data(
mode: str = "revert",
original_decision_id: str = "dec-0",
new_decision_id: str | None = "dec-1",
guidance: str = "",
completed_at: bool = False,
patched_hint: str | None = None,
artifacts_path: str | None = None,
) -> dict[str, Any]:
"""Build a minimal correction-diff data dict for render function tests."""
now_iso = datetime.datetime.now(datetime.UTC).isoformat()
summary: dict[str, Any] = {
"correction": "corr-test-001",
"original_decision": original_decision_id,
"mode": mode,
"state": "complete" if completed_at else "executing",
"guidance": guidance or "",
"created_at": now_iso,
"completed_at": now_iso if completed_at else None,
}
reverted_decisions: list[dict[str, Any]] = []
added_decisions: list[dict[str, Any]] = []
if original_decision_id:
reverted_decisions.append(
{"decision_id": original_decision_id, "status": "reverted"}
)
if new_decision_id:
added_decisions.append({"decision_id": new_decision_id, "status": "added"})
comparison = {
"reverted_decisions": reverted_decisions,
"added_decisions": added_decisions,
"decisions_changed": len(reverted_decisions) + len(added_decisions),
"decisions_added": len(added_decisions),
"decisions_reverted": len(reverted_decisions),
}
if patched_hint is None and artifacts_path is None:
patch_preview = [
{
"note": (
"Patch preview is available after correction execution completes. "
"Current state: executing"
),
}
]
elif artifacts_path:
patch_preview = [
{
"note": "Corrected execution artifacts available",
"artifacts_path": artifacts_path,
}
]
else:
patch_preview = [{"note": patched_hint}]
return {
"correction_diff": summary,
"comparison": comparison,
"patch_preview": patch_preview,
}
def _import_render_functions() -> tuple:
"""Lazy-import the render functions."""
from cleveragents.application.services import plan_apply_service as mod
return (
mod._render_correction_diff_plain,
mod._render_correction_diff_rich,
mod._build_correction_diff_dict,
)
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given(
'I have a CorrectionAttemptRecord with mode="{mode}", '
'original_decision_id="{orig}", new_decision_id="{new}", '
'guidance="{guidance}", and archived_artifacts_path="{artifacts}"'
)
def step_corr_record_with_artifact(
context: Context,
mode: str,
orig: str,
new: str,
guidance: str,
artifacts: str,
) -> None:
"""Construct a minimal CorrectionAttemptRecord for _build_correction_diff_dict."""
from cleveragents.domain.models.core.correction import (
CorrectionAttemptRecord,
CorrectionAttemptState,
CorrectionMode,
)
record = CorrectionAttemptRecord(
correction_attempt_id="test-corr-001",
plan_id="plan-001",
original_decision_id=orig,
new_decision_id=new if new != "none" else None,
mode=CorrectionMode.REVERT if mode == "revert" else CorrectionMode.APPEND,
guidance=guidance or "",
archived_artifacts_path=artifacts if artifacts not in ("", "none") else None,
state=CorrectionAttemptState.COMPLETE,
)
context.corr_record: CORRECTIONATTEMPTRECORD = record # noqa: F821
@given(
'I have a CorrectionAttemptRecord with mode="{mode}", '
'original_decision_id="{orig}", no new_decision, '
'guidance="{guidance}", and no archived artifacts'
)
def step_corr_record_no_artifact(
context: Context, mode: str, orig: str, guidance: str
) -> None:
"""Construct a CorrectionAttemptRecord without new_decision or archived artifacts."""
from cleveragents.domain.models.core.correction import (
CorrectionAttemptRecord,
CorrectionAttemptState,
CorrectionMode,
)
record = CorrectionAttemptRecord(
correction_attempt_id="test-corr-002",
plan_id="plan-002",
original_decision_id=orig,
new_decision_id=None,
mode=CorrectionMode.REVERT if mode == "revert" else CorrectionMode.APPEND,
guidance=guidance or "",
archived_artifacts_path=None,
state=CorrectionAttemptState.PENDING,
)
context.corr_record: CORRECTIONATTEMPTRECORD = record # noqa: F821
@given(
'I have a correction diff dict with mode="{mode}", '
'original_decision_id="{orig}", guidance="{guidance}", '
'completed_at present, and one added decision "{added}"'
)
def step_diff_dict_with_artifacts(
context: Context, mode: str, orig: str, guidance: str, added: str
) -> None:
"""Build a plain data dict for render function tests."""
# Use the added value as new_decision_id if provided
nid = added if added != "none" and added else None
data = _make_diff_data(
mode=mode,
original_decision_id=orig,
new_decision_id=nid,
guidance=guidance,
completed_at=True,
)
context.diff_data = data
@given(
'I have a correction diff dict with mode="{mode}", '
'original_decision_id="{orig}", no guidance text, '
'and patch preview note about "{hint}"'
)
def step_diff_dict_no_guidance(
context: Context, mode: str, orig: str, hint: str
) -> None:
"""Build a plain data dict with missing fields."""
data = _make_diff_data(
mode=mode,
original_decision_id=orig,
new_decision_id=None,
guidance="",
completed_at=False,
patched_hint=f"Patch preview is available after correction execution completes. "
f"Current state: {hint}",
)
context.diff_data = data
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I call _build_correction_diff_dict on the record")
def step_build_corr_dict(context: Context) -> None:
"""Call the production _build_correction_diff_dict function & save dict output."""
_, _, build_func = _import_render_functions()
record = context.corr_record # type: ignore[attr-defined]
data = build_func(record)
context.diff_data = data
@when("I call _render_correction_diff_plain on the dict")
def step_render_plain(context: Context) -> None:
"""Call plain-text renderer and save output."""
render_plain, _, _ = _import_render_functions()
context.render_output = render_plain(context.diff_data) # type: ignore[attr-defined]
@when("I call _render_correction_diff_rich on the dict")
def step_render_rich(context: Context) -> None:
"""Call rich-text renderer and save output."""
_, render_rich, _ = _import_render_functions()
context.render_output = render_rich(context.diff_data) # type: ignore[attr-defined]
# ---------------------------------------------------------------------------
# Then steps — verify structure & content
# ---------------------------------------------------------------------------
@then('the result should contain "{key1}", "{key2}", and "{key3}" keys')
def step_result_contains_keys(
context: Context, key1: str, key2: str, key3: str
) -> None:
"""Verify the data dict has the expected top-level keys."""
for k in (key1, key2, key3):
assert k in context.diff_data, f"Missing key '{k}' in result"
@then(
"the comparison should have {n_reverted:d} reverted decision and "
"{n_added:d} added decisions"
)
def step_comparison_count(context: Context, n_reverted: int, n_added: int) -> None:
"""Verify the number of reverted/added decisions in comparison."""
cmp = context.diff_data["comparison"]
actual_reverted = len(cmp["reverted_decisions"])
actual_added = len(cmp["added_decisions"])
assert actual_reverted == n_reverted, (
f"Expected {n_reverted} reverted, got {actual_reverted}"
)
assert actual_added == n_added, f"Expected {n_added} added, got {actual_added}"
@then('the patch preview should include the artifacts path "{path}"')
def step_patch_has_artifacts(context: Context, path: str) -> None:
"""Verify patch-preview contains the specified artifacts path."""
patch = context.diff_data["patch_preview"][0]
assert patch.get("artifacts_path") == path, (
f"Expected artifacts_path='{path}', got {patch}"
)
@then("the patch preview should indicate correction execution is pending")
def step_patch_pending(context: Context) -> None:
"""Verify that the patch-preview note signals a pending/complete state."""
patch = context.diff_data["patch_preview"][0]
note_lower = patch.get("note", "").lower()
assert "pending" in note_lower or "complete" in note_lower, (
f"Expected pending/complete hint in: {patch['note']}"
)
@then('the output should contain "{text}" heading')
def step_output_contains_heading(context: Context, text: str) -> None:
"""Verify the rendered output contains a given heading string."""
assert text in context.render_output, ( # type: ignore[attr-defined]
f"Expected '{text}' in render output:\n{context.render_output}"
)
@then('the output should contain "{value}" mode value')
def step_output_contains_mode(context: Context, value: str) -> None:
"""Verify the rendered output contains a given mode string."""
assert value in context.render_output, ( # type: ignore[attr-defined]
f"Expected '{value}' in render output:\n{context.render_output}"
)
@then('the output should contain "{text}" trailer')
def step_output_contains_trailer(context: Context, text: str) -> None:
"""Verify the rendered output contains an end marker string."""
assert text in context.render_output, ( # type: ignore[attr-defined]
f"Expected '{text}' in render output:\n{context.render_output}"
)
@then('the output should contain the added decision line starting with "{marker} {id}"')
def step_output_contains_added_decision(context: Context, marker: str, id: str) -> None:
"""Verify rendered output contains an added-decision line."""
for line in context.render_output.splitlines(): # type: ignore[attr-defined]
stripped = line.strip()
if f"+ {id}" in stripped or f"{marker} {id}" in stripped:
break
else:
raise AssertionError(
f"Expected added decision '{marker} {id}' in output:\n{context.render_output}"
)
@then('the output should NOT contain "{text}" line')
def step_output_not_contains(context: Context, text: str) -> None:
"""Verify the rendered output does NOT contain a given text string."""
for line in context.render_output.splitlines(): # type: ignore[attr-defined]
assert text not in line.rstrip(), f"Unexpected '{text}' found in line: {line}"
@then("the output should contain the pending-execution note text")
def step_output_contains_pending_note(context: Context) -> None:
"""Verify render output includes the patch-preview hint."""
assert "Patch preview is available after" in context.render_output, ( # type: ignore[attr-defined]
f"Expected pending-note: {context.render_output}"
)
@then("the output should contain a red-colored reverted decision ID")
def step_output_contains_red_revert(context: Context) -> None:
"""Verify rich-rendered output has the [red] markup for reverted decisions."""
assert "[red]" in context.render_output, ( # type: ignore[attr-defined]
f"Expected '[red]' markup: {context.render_output}"
)
@then("the output should contain a green-colored added decision ID")
def step_output_contains_green_added(context: Context) -> None:
"""Verify rich-rendered output has the [green] markup for added decisions."""
assert "[green]" in context.render_output, ( # type: ignore[attr-defined]
f"Expected '[green]' markup: {context.render_output}"
)
@then('the output should contain "{marker} OK {marker}" checkmark marker at end')
def step_output_contains_marker(context: Context, marker: str) -> None:
"""Verify the rich-rendered output includes the [green]✔ OK footer."""
assert "OK" in context.render_output, ( # type: ignore[attr-defined]
f"Expected 'OK' footer: {context.render_output}"
)
@then("the output should contain the pending-execution note in dim markup")
def step_pending_in_dim(context: Context) -> None:
"""Verify rendered text includes '[dim]' color wrapper."""
assert "[dim]" in context.render_output, ( # type: ignore[attr-defined]
f"Expected '[dim]' markup: {context.render_output}"
)
# ---------------------------------------------------------------------------
# Comparison-count shortcut — avoids ambiguity with the generic matcher below.
# ---------------------------------------------------------------------------
@then('the comparison counts are "{n_rev}" reverted decisions and "{n_add}" new ones')
def step_comparison_counts_shorthand(context: Context, n_rev: str, n_add: str) -> None:
"""Verify the comparison dict has the expected revert/add counts.
This step uses literal string values from the Gherkin to disambiguate
from the parameterised "should have {n:d} ..." matcher above.
"""
cmp = context.diff_data["comparison"]
actual_rev = len(cmp["reverted_decisions"])
actual_add = len(cmp["added_decisions"])
assert str(actual_rev) == n_rev, f"Expected {n_rev} reverted, got {actual_rev}"
assert str(actual_add) == n_add, f"Expected {n_add} added, got {actual_add}"
# ---------------------------------------------------------------------------
# Singular / variant steps that the feature file uses
# ---------------------------------------------------------------------------
@then(
"the comparison should have {n_reverted:d} reverted decision"
" and {n_added:d} added decision"
)
def step_comparison_count_singular(
context: Context, n_reverted: int, n_added: int
) -> None:
"""Singular variant of the comparison count step (feature uses singular)."""
cmp = context.diff_data["comparison"]
actual_reverted = len(cmp["reverted_decisions"])
actual_added = len(cmp["added_decisions"])
assert actual_reverted == n_reverted, (
f"Expected {n_reverted} reverted, got {actual_reverted}"
)
assert actual_added == n_added, f"Expected {n_added} added, got {actual_added}"
@then("the patch preview should include the artifacts path")
def step_patch_has_any_artifacts(context: Context) -> None:
"""Verify patch-preview contains an artifacts_path (any value)."""
patch = context.diff_data["patch_preview"][0]
assert patch.get("artifacts_path"), (
f"Expected artifacts_path to be set, got: {patch}"
)
@then('the output should contain the mode value "{value}"')
def step_output_contains_mode_value(context: Context, value: str) -> None:
"""Verify rendered output contains the given mode value string."""
assert value in context.render_output, ( # type: ignore[attr-defined]
f"Expected mode value '{value}' in render output:\n{context.render_output}"
)
@then('the output should contain "{text}" timestamp')
def step_output_contains_timestamp(context: Context, text: str) -> None:
"""Verify rendered output contains a given timestamp label string."""
assert text in context.render_output, ( # type: ignore[attr-defined]
f"Expected '{text}' in render output:\n{context.render_output}"
)
@then('the output should NOT contain a "{text}" line')
def step_output_not_contains_a_line(context: Context, text: str) -> None:
"""Verify rendered output does NOT contain a given text (with 'a' article)."""
for line in context.render_output.splitlines(): # type: ignore[attr-defined]
assert text not in line.rstrip(), f"Unexpected '{text}' found in line: {line}"
@then('the output should contain "{text}" mode color markup')
def step_output_contains_mode_markup(context: Context, text: str) -> None:
"""Verify rendered output contains the given rich-markup mode string."""
assert text in context.render_output, ( # type: ignore[attr-defined]
f"Expected mode markup '{text}' in render output:\n{context.render_output}"
)
@then("the output should contain the rich green checkmark marker at end")
def step_output_contains_checkmark(context: Context) -> None:
"""Verify rich-rendered output includes the green checkmark footer."""
output = context.render_output # type: ignore[attr-defined]
assert "[green]" in output or "" in output or "OK" in output, (
f"Expected green checkmark / OK footer: {output}"
)
@given(
'I have a correction diff dict with mode="{mode}", '
'original_decision_id="{orig}", no guidance text, '
"and patch preview note about pending execution"
)
def step_diff_dict_no_guidance_pending(context: Context, mode: str, orig: str) -> None:
"""Build a plain data dict with no guidance and a pending-execution hint."""
data = _make_diff_data(
mode=mode,
original_decision_id=orig,
new_decision_id=None,
guidance="",
completed_at=False,
patched_hint=(
"Patch preview is available after correction execution completes. "
"Current state: executing"
),
)
context.diff_data = data
@given(
'I have a correction diff dict with mode="{mode}", '
'original_decision_id="{orig}", no guidance, '
"and patch preview note about pending execution"
)
def step_diff_dict_no_guidance_short_pending(
context: Context, mode: str, orig: str
) -> None:
"""Same as the 'no guidance text' variant - 'no guidance' short form."""
data = _make_diff_data(
mode=mode,
original_decision_id=orig,
new_decision_id=None,
guidance="",
completed_at=False,
patched_hint=(
"Patch preview is available after correction execution completes. "
"Current state: executing"
),
)
context.diff_data = data