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
372 lines
14 KiB
Python
372 lines
14 KiB
Python
"""Step definitions for llm_delimiter_regression.feature (#10878).
|
|
|
|
Re-proves that the CLEVERAGENTS_FILE_START/END sentinel scheme fixes
|
|
the triple-backtick delimiter collision bug from #10878.
|
|
|
|
Two in-line "old" parsers are provided to demonstrate the original bug:
|
|
* _old_backtick_nongreedy -- non-greedy ```.``` pattern; truncates content
|
|
at the first triple-backtick *inside* a file body but still finds all
|
|
FILE blocks (each with truncated content).
|
|
* _old_backtick_greedy -- greedy ```.``` pattern; swallows subsequent
|
|
FILE blocks inside the first match's "content", returning far fewer
|
|
entries than expected.
|
|
|
|
The current ``LLMExecuteActor._parse_file_blocks`` uses
|
|
``<<<<<<< CLEVERAGENTS_FILE_START >>>>>>>`` / ``<<<<<<< CLEVERAGENTS_FILE_END >>>>>>>``
|
|
(and the newer ``<CAFS>``/``</CAFE>``) markers, which never collide with
|
|
triple-backtick Markdown fences.
|
|
|
|
Forgejo: #10878
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
|
|
from behave import given, step, then, when # type: ignore[import-untyped]
|
|
|
|
from cleveragents.application.services.llm_actors import LLMExecuteActor
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Old (buggy) backtick-based parsers used to reproduce the pre-fix behaviour
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LEGACY_START = "<<<<<<< CLEVERAGENTS_FILE_START >>>>>>>"
|
|
_LEGACY_END = "<<<<<<< CLEVERAGENTS_FILE_END >>>>>>>"
|
|
|
|
|
|
def _old_backtick_nongreedy(llm_output: str) -> list[str]:
|
|
"""Non-greedy old parser.
|
|
|
|
Finds the correct number of FILE blocks but truncates their content at
|
|
the first triple-backtick that appears inside a file body.
|
|
"""
|
|
pattern = re.compile(
|
|
r"FILE:\s*(.+?)\s*\n```\n(.*?)\n```",
|
|
re.DOTALL,
|
|
)
|
|
return [m.group(1).strip() for m in pattern.finditer(llm_output)]
|
|
|
|
|
|
def _old_backtick_greedy(llm_output: str) -> list[str]:
|
|
"""Greedy old parser.
|
|
|
|
The greedy ``(.*)`` causes the *first* match to consume all remaining
|
|
content up to the very last triple-backtick line in the document,
|
|
swallowing every subsequent FILE block inside the captured "content".
|
|
Only one (or very few) entries are returned for an input that contains
|
|
many FILE blocks.
|
|
"""
|
|
pattern = re.compile(
|
|
r"FILE:\s*(.+?)\s*\n```\n(.*)\n```",
|
|
re.DOTALL,
|
|
)
|
|
return [m.group(1).strip() for m in pattern.finditer(llm_output)]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper builders
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_legacy_block(path: str, content: str) -> str:
|
|
"""FILE block using the CLEVERAGENTS_FILE_START/END markers."""
|
|
return f"FILE: {path}\n{_LEGACY_START}\n{content}\n{_LEGACY_END}\n\n"
|
|
|
|
|
|
def _make_old_backtick_block(path: str, content: str) -> str:
|
|
"""FILE block using the old triple-backtick delimiters."""
|
|
return f"FILE: {path}\n```\n{content}\n```\n\n"
|
|
|
|
|
|
def _fence_content(label: str) -> str:
|
|
"""File body that embeds a triple-backtick Python code fence."""
|
|
return (
|
|
f"# {label}\n"
|
|
"```python\n"
|
|
f"def {label.replace(' ', '_')}(): pass\n"
|
|
"```\n"
|
|
f"# end of {label}\n"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given — context setup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a string containing ````")
|
|
def step_context_with_backtick_string(context): # type: ignore[no-untyped-def]
|
|
context.backtick_context = "````"
|
|
|
|
|
|
@given("a string ````")
|
|
def step_short_backtick_ctx(context): # type: ignore[no-untyped-def]
|
|
context.backtick_context = "````"
|
|
|
|
|
|
@given('a mock plan_id "01HQXXXXX"')
|
|
def step_plan_id_xxxxx(context): # type: ignore[no-untyped-def]
|
|
context.delimiter_plan_id = "01HQXXXXX"
|
|
|
|
|
|
@given('a mock plan_id "01HYYYYYY"')
|
|
def step_plan_id_yyyyyy(context): # type: ignore[no-untyped-def]
|
|
context.delimiter_plan_id = "01HYYYYYY"
|
|
|
|
|
|
# --- Input-creation steps ---
|
|
|
|
|
|
@step("I create file with LLM output containing code fences in body")
|
|
def step_create_old_2files_with_fences(context): # type: ignore[no-untyped-def]
|
|
"""Two FILE blocks in old backtick format, each with an embedded code fence.
|
|
|
|
The non-greedy old parser will find both files but truncate their content
|
|
at the first triple-backtick in each body (the opening fence).
|
|
"""
|
|
context.delim_llm_output = _make_old_backtick_block(
|
|
"src/main.py", _fence_content("main")
|
|
) + _make_old_backtick_block("src/util.py", _fence_content("util"))
|
|
|
|
|
|
@step(
|
|
"I create file with LLM output using unique delimiters and code fences embedded in body"
|
|
)
|
|
def step_create_new_6files_with_fences(context): # type: ignore[no-untyped-def]
|
|
"""Six FILE blocks using CLEVERAGENTS markers with embedded code fences."""
|
|
context.delim_llm_output = "".join(
|
|
_make_legacy_block(f"src/mod_{i}.py", _fence_content(f"mod_{i}"))
|
|
for i in range(1, 7)
|
|
)
|
|
|
|
|
|
@step("I create LLM output where file content contains embedded markdown code blocks")
|
|
def step_create_old_4files_greedy(context): # type: ignore[no-untyped-def]
|
|
"""Four FILE blocks in old backtick format, each with embedded code fences.
|
|
|
|
The *greedy* old parser will match only the first FILE block and
|
|
consume the remaining three as part of the first block's "content".
|
|
"""
|
|
context.delim_llm_output = (
|
|
_make_old_backtick_block("src/a.py", _fence_content("a"))
|
|
+ _make_old_backtick_block("src/b.py", _fence_content("b"))
|
|
+ _make_old_backtick_block("src/c.py", _fence_content("c"))
|
|
+ _make_old_backtick_block("src/d.py", _fence_content("d"))
|
|
)
|
|
|
|
|
|
@step(
|
|
"I create LLM output using unique sentinels with embedded ```python code blocks in body"
|
|
)
|
|
def step_create_new_4files_with_fences(context): # type: ignore[no-untyped-def]
|
|
"""Four FILE blocks using CLEVERAGENTS markers with embedded code fences."""
|
|
context.delim_llm_output = (
|
|
_make_legacy_block("src/a.py", _fence_content("a"))
|
|
+ _make_legacy_block("src/b.py", _fence_content("b"))
|
|
+ _make_legacy_block("src/c.py", _fence_content("c"))
|
|
+ _make_legacy_block("src/d.py", _fence_content("d"))
|
|
)
|
|
|
|
|
|
@step(
|
|
"I create full regression file block output that includes report section plus"
|
|
" multiple files each with inline ```python examples"
|
|
)
|
|
def step_create_full_regression_output(context): # type: ignore[no-untyped-def]
|
|
"""Architecture-report header followed by five FILE blocks with CLEVERAGENTS markers."""
|
|
prose = "## Architecture Report\n\nThis section describes key modules.\n\n"
|
|
files = [
|
|
_make_legacy_block(f"src/mod_{i}.py", _fence_content(f"mod_{i}"))
|
|
for i in range(1, 6)
|
|
]
|
|
context.delim_llm_output = prose + "".join(files)
|
|
context.delim_expected_count = 5
|
|
|
|
|
|
@step(
|
|
"LLM output containing two FILE blocks each with embedded"
|
|
" ```python sections in the content"
|
|
)
|
|
def step_create_new_2files_with_python_fences(context): # type: ignore[no-untyped-def]
|
|
"""Two FILE blocks with CLEVERAGENTS markers and embedded Python code fences."""
|
|
context.delim_llm_output = _make_legacy_block(
|
|
"mod_a.py", _fence_content("module_a")
|
|
) + _make_legacy_block("src/mod_b.py", _fence_content("module_b"))
|
|
|
|
|
|
@step(
|
|
"an llm_output where the file body ends on a line that is exactly ``` after content"
|
|
)
|
|
def step_create_trailing_backtick_body(context): # type: ignore[no-untyped-def]
|
|
"""Single FILE block whose last body line is exactly three backticks."""
|
|
context.delim_llm_output = (
|
|
f"FILE: module.py\n{_LEGACY_START}\nline one\nline two\n```\n{_LEGACY_END}\n\n"
|
|
)
|
|
|
|
|
|
@step(
|
|
"LLM input with backtick-delimited FILE blocks containing code without"
|
|
" triple-backticks inside body"
|
|
)
|
|
def step_create_single_simple_block(context): # type: ignore[no-untyped-def]
|
|
"""Single FILE block with CLEVERAGENTS markers and plain content (no code fences).
|
|
|
|
This tests backward-compatible parsing of simple single-file blocks.
|
|
"""
|
|
context.delim_llm_output = _make_legacy_block(
|
|
"src/simple.py",
|
|
"# Simple module\ndef run(): return 0\n",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — command-like no-ops (side-effect-free shell invocations)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run the command \"echo 'backtick-test'\"")
|
|
def step_run_echo_backtick_test(context): # type: ignore[no-untyped-def]
|
|
subprocess.run(["echo", "backtick-test"], capture_output=True, check=False)
|
|
|
|
|
|
@when("I run the command \"echo 'old-parser-test'\"")
|
|
def step_run_echo_old_parser_test(context): # type: ignore[no-untyped-def]
|
|
subprocess.run(["echo", "old-parser-test"], capture_output=True, check=False)
|
|
|
|
|
|
@when("I run the command \"echo 'new-parser-test'\"")
|
|
def step_run_echo_new_parser_test(context): # type: ignore[no-untyped-def]
|
|
subprocess.run(["echo", "new-parser-test"], capture_output=True, check=False)
|
|
|
|
|
|
@when("I run the command \"echo 'full-regression-test'\"")
|
|
def step_run_echo_full_regression(context): # type: ignore[no-untyped-def]
|
|
subprocess.run(["echo", "full-regression-test"], capture_output=True, check=False)
|
|
|
|
|
|
@when("I run the command \"echo 'backtick-sanitise'\"")
|
|
def step_run_echo_backtick_sanitise(context): # type: ignore[no-untyped-def]
|
|
subprocess.run(["echo", "backtick-sanitise"], capture_output=True, check=False)
|
|
|
|
|
|
# --- Parsing steps ---
|
|
|
|
|
|
@when("I parse the LLM output as if it were old backtick-delimited file blocks")
|
|
def step_parse_old_nongreedy(context): # type: ignore[no-untyped-def]
|
|
context.delim_entries = _old_backtick_nongreedy(context.delim_llm_output)
|
|
|
|
|
|
@when(
|
|
"I parse the LLM output with new"
|
|
" CLEVERAGENTS_FILE_START/CLEVERAGENTS_FILE_END delimiters"
|
|
)
|
|
def step_parse_new_cleveragents(context): # type: ignore[no-untyped-def]
|
|
plan_id = getattr(context, "delimiter_plan_id", "DELIM_PLAN")
|
|
context.delim_entries, _ = LLMExecuteActor._parse_file_blocks(
|
|
context.delim_llm_output, plan_id
|
|
)
|
|
|
|
|
|
@when("I parse it with the old backtick-style delimiter pattern")
|
|
def step_parse_old_greedy(context): # type: ignore[no-untyped-def]
|
|
context.delim_entries = _old_backtick_greedy(context.delim_llm_output)
|
|
|
|
|
|
@when("I parse it with CLEVERAGENTS_FILE_START / CLEVERAGENTS_FILE_END delimiters")
|
|
def step_parse_new_4files(context): # type: ignore[no-untyped-def]
|
|
plan_id = getattr(context, "delimiter_plan_id", "DELIM_PLAN")
|
|
context.delim_entries, _ = LLMExecuteActor._parse_file_blocks(
|
|
context.delim_llm_output, plan_id
|
|
)
|
|
|
|
|
|
@when("I parse it with new CLEVERAGENTS_FILE_START / CLEVERAGENTS_FILE_END delimiters")
|
|
def step_parse_new_full_regression(context): # type: ignore[no-untyped-def]
|
|
plan_id = getattr(context, "delimiter_plan_id", "DELIM_PLAN")
|
|
context.delim_entries, _ = LLMExecuteActor._parse_file_blocks(
|
|
context.delim_llm_output, plan_id
|
|
)
|
|
|
|
|
|
@when("I call _parse_file_blocks on that LLM output")
|
|
def step_call_parse_file_blocks_llm(context): # type: ignore[no-untyped-def]
|
|
plan_id = getattr(context, "delimiter_plan_id", "DELIM_PLAN")
|
|
context.delim_entries, _ = LLMExecuteActor._parse_file_blocks(
|
|
context.delim_llm_output, plan_id
|
|
)
|
|
|
|
|
|
@when("I call _parse_file_blocks on that output")
|
|
def step_call_parse_file_blocks(context): # type: ignore[no-untyped-def]
|
|
plan_id = getattr(context, "delimiter_plan_id", "DELIM_PLAN")
|
|
context.delim_entries, _ = LLMExecuteActor._parse_file_blocks(
|
|
context.delim_llm_output, plan_id
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the parser should return only 2 entries truncated at first triple-backtick")
|
|
def step_assert_2_truncated_entries(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 2, (
|
|
f"Expected 2 entries (truncated) from old backtick parser, got {len(entries)}: "
|
|
f"{entries}"
|
|
)
|
|
|
|
|
|
@then("the parser should return 6 entries not truncated at any triple-backtick")
|
|
def step_assert_6_full_entries(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 6, (
|
|
f"Expected 6 complete entries from new CLEVERAGENTS parser, got {len(entries)}"
|
|
)
|
|
|
|
|
|
@then("the parser should return only 1 entry instead of the expected 4")
|
|
def step_assert_1_instead_of_4(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 1, (
|
|
f"Expected greedy old parser to return 1 (swallowing other files), "
|
|
f"got {len(entries)}: {entries}"
|
|
)
|
|
|
|
|
|
@then("the parser should return a result count of 4 entries")
|
|
def step_assert_4_entries(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 4, (
|
|
f"Expected 4 entries from new CLEVERAGENTS parser, got {len(entries)}"
|
|
)
|
|
|
|
|
|
@then("the parser should return the full entry count of all embedded blocks")
|
|
def step_assert_full_entry_count(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
expected = getattr(context, "delim_expected_count", 5)
|
|
assert len(entries) == expected, (
|
|
f"Expected {expected} entries (full regression), got {len(entries)}"
|
|
)
|
|
|
|
|
|
@then("it should return 2 changeset entries not 1")
|
|
def step_assert_2_not_1(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 2, (
|
|
f"Expected 2 entries (new parser handles embedded fences, not 1): "
|
|
f"got {len(entries)}"
|
|
)
|
|
|
|
|
|
@then("it should return 1 entry")
|
|
def step_assert_1_entry(context): # type: ignore[no-untyped-def]
|
|
entries = context.delim_entries
|
|
assert len(entries) == 1, f"Expected exactly 1 entry, got {len(entries)}"
|