Files
cleveragents-core/robot/helper_plan_diff_artifacts.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

291 lines
9.3 KiB
Python

"""Helper utilities for plan diff and artifacts Robot integration tests.
Covers:
- Diff output in multiple formats (rich, plain, JSON, YAML)
- Artifacts summary rendering
- Empty changeset guard
- Apply summary persistence
- Merge failure handling
"""
from __future__ import annotations
import json
import sys
from cleveragents.application.services.plan_apply_service import PlanApplyService
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core.change import (
ChangeEntry,
ChangeOperation,
InMemoryChangeSetStore,
SpecChangeSet,
)
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
)
def _create_services() -> tuple[
PlanLifecycleService, InMemoryChangeSetStore, PlanApplyService
]:
"""Create service instances for testing."""
settings = Settings()
lifecycle = PlanLifecycleService(settings=settings)
store = InMemoryChangeSetStore()
apply_svc = PlanApplyService(lifecycle_service=lifecycle, changeset_store=store)
return lifecycle, store, apply_svc
def _create_plan_with_changeset(
lifecycle: PlanLifecycleService,
store: InMemoryChangeSetStore,
) -> str:
"""Create a test action, plan, and changeset. Return plan_id."""
lifecycle.create_action(
name="local/robot-diff-test",
description="Robot diff test",
definition_of_done="Tests pass",
strategy_actor="local/planner",
execution_actor="local/executor",
)
plan = lifecycle.use_action(action_name="local/robot-diff-test")
plan_id = plan.identity.plan_id
# Advance through strategize and execute
lifecycle.start_strategize(plan_id)
lifecycle.complete_strategize(plan_id)
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.STRATEGIZE:
lifecycle.execute_plan(plan_id)
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.EXECUTE:
lifecycle.start_execute(plan_id)
lifecycle.complete_execute(plan_id)
# Create changeset
cs = SpecChangeSet(
changeset_id="robot-cs-001",
plan_id=plan_id,
entries=[
ChangeEntry(
plan_id=plan_id,
resource_id="res-001",
tool_name="file-write",
operation=ChangeOperation.CREATE,
path="src/robot_new.py",
after_hash="abc123",
),
ChangeEntry(
plan_id=plan_id,
resource_id="res-002",
tool_name="file-edit",
operation=ChangeOperation.MODIFY,
path="src/robot_existing.py",
before_hash="old111",
after_hash="new222",
),
],
)
store._store[cs.changeset_id] = cs
plan = lifecycle.get_plan(plan_id)
plan.changeset_id = cs.changeset_id
plan.sandbox_refs = ["sandbox-robot-001"]
lifecycle.commit_plan(plan)
return plan_id
def _create_empty_changeset_plan(
lifecycle: PlanLifecycleService,
store: InMemoryChangeSetStore,
) -> str:
"""Create a plan with empty changeset. Return plan_id."""
lifecycle.create_action(
name="local/robot-empty-test",
description="Robot empty test",
definition_of_done="Tests pass",
strategy_actor="local/planner",
execution_actor="local/executor",
)
plan = lifecycle.use_action(action_name="local/robot-empty-test")
plan_id = plan.identity.plan_id
lifecycle.start_strategize(plan_id)
lifecycle.complete_strategize(plan_id)
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.STRATEGIZE:
lifecycle.execute_plan(plan_id)
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.EXECUTE:
lifecycle.start_execute(plan_id)
lifecycle.complete_execute(plan_id)
cs = SpecChangeSet(changeset_id="robot-cs-empty", plan_id=plan_id, entries=[])
store._store[cs.changeset_id] = cs
plan = lifecycle.get_plan(plan_id)
plan.changeset_id = cs.changeset_id
lifecycle.commit_plan(plan)
return plan_id
def _advance_to_apply_processing(lifecycle: PlanLifecycleService, plan_id: str) -> None:
"""Advance plan to apply/processing."""
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.EXECUTE and plan.state == ProcessingState.COMPLETE:
lifecycle.apply_plan(plan_id)
plan = lifecycle.get_plan(plan_id)
if plan.phase == PlanPhase.APPLY and plan.state == ProcessingState.QUEUED:
lifecycle.start_apply(plan_id)
def _diff_output() -> None:
"""Test diff output in rich format."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
output = apply_svc.diff(plan_id, fmt="rich")
assert "robot-cs-001" in output, f"Changeset ID missing: {output[:200]}"
assert "src/robot_new.py" in output, f"Path missing: {output[:200]}"
assert "src/robot_existing.py" in output, f"Path missing: {output[:200]}"
print("diff-output-ok")
def _artifacts_output() -> None:
"""Test artifacts output.
``format_output`` for machine-readable formats writes directly to
``sys.stdout`` and returns ``""``. We redirect stdout so the JSON
payload is captured for parsing.
"""
from io import StringIO
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
buf = StringIO()
old_stdout = sys.stdout
sys.stdout = buf
try:
output = apply_svc.artifacts(plan_id, fmt="json")
finally:
sys.stdout = old_stdout
# Combine: return value or captured stdout
effective = output or buf.getvalue().strip()
parsed = json.loads(effective)
assert parsed["changeset_id"] == "robot-cs-001"
assert "sandbox-robot-001" in parsed["sandbox_refs"]
assert len(parsed["files_changed"]) == 2
print("artifacts-output-ok")
def _empty_guard() -> None:
"""Test empty changeset guard blocks apply."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_empty_changeset_plan(lifecycle, store)
try:
apply_svc.guard_empty_changeset(plan_id)
raise AssertionError("Should have raised PlanError")
except PlanError as e:
assert "empty" in e.message.lower()
print("empty-guard-ok")
def _empty_guard_allow() -> None:
"""Test empty changeset guard allows with flag."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_empty_changeset_plan(lifecycle, store)
result = apply_svc.guard_empty_changeset(plan_id, allow_empty=True)
assert result is True
print("empty-guard-allow-ok")
def _apply_summary() -> None:
"""Test apply summary persistence."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
_advance_to_apply_processing(lifecycle, plan_id)
apply_svc.persist_apply_summary(plan_id, files_changed=7, validations_run=2)
plan = lifecycle.get_plan(plan_id)
assert plan.error_details is not None
assert plan.error_details["apply_files_changed"] == "7"
assert plan.error_details["apply_validations_run"] == "2"
assert "apply_completed_at" in plan.error_details
print("apply-summary-ok")
def _merge_failure() -> None:
"""Test merge failure handling."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
_advance_to_apply_processing(lifecycle, plan_id)
apply_svc.handle_merge_failure(plan_id, conflict_details="Conflict in src/main.py")
plan = lifecycle.get_plan(plan_id)
assert plan.processing_state == ProcessingState.ERRORED
assert "Merge failed" in (plan.error_message or "")
assert plan.error_details is not None
assert "merge_conflict" in plan.error_details
print("merge-failure-ok")
def _diff_json() -> None:
"""Test diff JSON format."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
output = apply_svc.diff(plan_id, fmt="json")
parsed = json.loads(output)
assert "entries" in parsed
assert "summary" in parsed
assert len(parsed["entries"]) == 2
print("diff-json-ok")
def _diff_yaml() -> None:
"""Test diff YAML format."""
lifecycle, store, apply_svc = _create_services()
plan_id = _create_plan_with_changeset(lifecycle, store)
output = apply_svc.diff(plan_id, fmt="yaml")
assert "changeset_id" in output
assert "robot-cs-001" in output
print("diff-yaml-ok")
COMMANDS: dict[str, object] = {
"diff-output": _diff_output,
"artifacts-output": _artifacts_output,
"empty-guard": _empty_guard,
"empty-guard-allow": _empty_guard_allow,
"apply-summary": _apply_summary,
"merge-failure": _merge_failure,
"diff-json": _diff_json,
"diff-yaml": _diff_yaml,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
print(f"Available: {', '.join(COMMANDS)}", file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1]
func = COMMANDS.get(cmd)
if func is None:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
func() # type: ignore[operator]