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
## 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
496 lines
18 KiB
Python
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
|