|
|
|
@@ -0,0 +1,295 @@
|
|
|
|
|
*** Settings ***
|
|
|
|
|
Documentation E2E acceptance test for M4 (v3.3.0): Corrections, Subplans, and Checkpoints.
|
|
|
|
|
...
|
|
|
|
|
... Exercises subplan spawning during plan execution, correction flows
|
|
|
|
|
... (revert and append modes), checkpoint creation and rollback through
|
|
|
|
|
... real CLI commands with real LLM API keys.
|
|
|
|
|
...
|
|
|
|
|
... Zero mocking — all CLI invocations use real providers.
|
|
|
|
|
Resource common_e2e.resource
|
|
|
|
|
Suite Setup E2E Suite Setup
|
|
|
|
|
Suite Teardown E2E Suite Teardown
|
|
|
|
|
|
|
|
|
|
*** Variables ***
|
|
|
|
|
${ACTION_NAME} local/m4-e2e-action
|
|
|
|
|
${RESOURCE_NAME} local/m4-e2e-repo
|
|
|
|
|
${PROJECT_NAME} local/m4-e2e-project
|
|
|
|
|
|
|
|
|
|
*** Test Cases ***
|
|
|
|
|
M4 Corrections Subplans And Checkpoints
|
|
|
|
|
[Documentation] End-to-end acceptance test for the M4 milestone.
|
|
|
|
|
...
|
|
|
|
|
... Tests subplan spawning, correction modes (revert/append),
|
|
|
|
|
... checkpoint creation, and rollback with real LLM API keys.
|
|
|
|
|
[Tags] E2E tdd_issue tdd_issue_1253 tdd_expected_fail
|
|
|
|
|
[Timeout] 20 minutes
|
|
|
|
|
|
|
|
|
|
# ---- Step 0: Skip gracefully if no LLM API keys are available ----
|
|
|
|
|
Skip If No LLM Keys
|
|
|
|
|
|
|
|
|
|
# ---- Step 0a: Initialise workspace (creates database) ----
|
|
|
|
|
${init}= Run CleverAgents Command init --force --yes
|
|
|
|
|
Should Be Equal As Integers ${init.rc} 0 msg=agents init failed
|
|
|
|
|
|
|
|
|
|
# ---- Step 0b: Select actor dynamically based on available API keys ----
|
|
|
|
|
${has_openai}= Evaluate bool(__import__('os').environ.get('OPENAI_API_KEY', ''))
|
|
|
|
|
IF ${has_openai}
|
|
|
|
|
${actor}= Set Variable openai/gpt-4o-mini
|
|
|
|
|
ELSE
|
|
|
|
|
${actor}= Set Variable anthropic/claude-sonnet-4-20250514
|
|
|
|
|
END
|
|
|
|
|
|
|
|
|
|
# ---- Step 1: Create temp git repo with sample project files ----
|
|
|
|
|
${repo_dir}= Create Temp Git Repo m4-acceptance-repo
|
|
|
|
|
Create Directory ${repo_dir}${/}src
|
|
|
|
|
Create File ${repo_dir}${/}src${/}main.py print("hello")\n
|
|
|
|
|
Create File ${repo_dir}${/}src${/}utils.py def helper(): return True\n
|
|
|
|
|
${git_add}= Run Process git add . cwd=${repo_dir} timeout=60s on_timeout=kill
|
|
|
|
|
Should Be Equal As Integers ${git_add.rc} 0 msg=git add failed
|
|
|
|
|
${git_commit}= Run Process git commit -m Add initial source files cwd=${repo_dir} timeout=60s on_timeout=kill
|
|
|
|
|
Should Be Equal As Integers ${git_commit.rc} 0 msg=git commit failed
|
|
|
|
|
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${repo_dir} 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}
|
|
|
|
|
|
|
|
|
|
# ---- Step 2: Register resource and project ----
|
|
|
|
|
${r_resource}= Run CleverAgents Command
|
|
|
|
|
... resource add git-checkout ${RESOURCE_NAME}
|
|
|
|
|
... --path ${repo_dir} --branch ${branch} --format plain
|
|
|
|
|
... expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_resource} resource add
|
|
|
|
|
|
|
|
|
|
${r_project}= Run CleverAgents Command
|
|
|
|
|
... project create ${PROJECT_NAME}
|
|
|
|
|
... --resource ${RESOURCE_NAME} --format plain
|
|
|
|
|
... expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_project} project create
|
|
|
|
|
Output Should Contain ${r_project} ${PROJECT_NAME}
|
|
|
|
|
|
|
|
|
|
# ---- Step 3: Create action that encourages decomposition ----
|
|
|
|
|
${action_yaml}= Catenate SEPARATOR=\n
|
|
|
|
|
... name: ${ACTION_NAME}
|
|
|
|
|
... description: |
|
|
|
|
|
... ${SPACE}${SPACE}Multi-phase project: Phase 1 - refactor main.py into modular classes.
|
|
|
|
|
... ${SPACE}${SPACE}Phase 2 - add comprehensive error handling to all modules.
|
|
|
|
|
... ${SPACE}${SPACE}Phase 3 - add unit tests for each module.
|
|
|
|
|
... ${SPACE}${SPACE}Each phase should be a separate subplan.
|
|
|
|
|
... definition_of_done: |
|
|
|
|
|
... ${SPACE}${SPACE}All three phases completed as separate subplans.
|
|
|
|
|
... ${SPACE}${SPACE}Each subplan produces file modifications.
|
|
|
|
|
... ${SPACE}${SPACE}Results merged using three-way merge strategy.
|
|
|
|
|
... strategy_actor: ${actor}
|
|
|
|
|
... execution_actor: ${actor}
|
|
|
|
|
${action_path}= Set Variable ${SUITE_HOME}${/}m4_action.yaml
|
|
|
|
|
Create File ${action_path} ${action_yaml}
|
|
|
|
|
${r_action}= Run CleverAgents Command
|
|
|
|
|
... action create --config ${action_path} --format plain
|
|
|
|
|
... expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_action} action create
|
|
|
|
|
|
|
|
|
|
# ---- Step 4: Plan use ----
|
|
|
|
|
${r_use}= Run CleverAgents Command
|
|
|
|
|
... plan use ${ACTION_NAME} ${PROJECT_NAME} --format plain
|
|
|
|
|
... expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_use} plan use
|
|
|
|
|
${plan_ids}= Get Regexp Matches ${r_use.stdout} [0-9A-HJ-NP-Z]{26}
|
|
|
|
|
Should Not Be Empty ${plan_ids} msg=Expected a ULID plan ID in plan use output
|
|
|
|
|
${plan_id}= Set Variable ${plan_ids}[0]
|
|
|
|
|
Log Plan ID: ${plan_id}
|
|
|
|
|
|
|
|
|
|
# ---- Step 5: Plan execute — strategize phase ----
|
|
|
|
|
${r_strat}= Run CleverAgents Command
|
|
|
|
|
... plan execute ${plan_id} --format plain
|
|
|
|
|
... timeout=180s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_strat} plan execute (strategize)
|
|
|
|
|
|
|
|
|
|
# ---- Step 6: Inspect decision tree — assert subplans and merge ----
|
|
|
|
|
${r_tree}= Run CleverAgents Command
|
|
|
|
|
... plan tree ${plan_id} --format json
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_tree} plan tree
|
|
|
|
|
Log Decision tree: ${r_tree.stdout}
|
|
|
|
|
# Extract decision IDs from "decision_id" JSON keys
|
|
|
|
|
${decision_ids}= Get Regexp Matches ${r_tree.stdout}
|
|
|
|
|
... "decision_id"\\s*:\\s*"([0-9A-HJ-NP-Z]{26})" 1
|
|
|
|
|
${decision_count}= Get Length ${decision_ids}
|
|
|
|
|
Should Be True ${decision_count} >= 2
|
|
|
|
|
... msg=Expected at least 2 decisions in tree (root + child), found ${decision_count}
|
|
|
|
|
# Assert subplan decisions: look for decision type fields containing subplan
|
|
|
|
|
${subplan_type_matches}= Get Regexp Matches ${r_tree.stdout}
|
|
|
|
|
... "type"\\s*:\\s*"[^"]*subplan[^"]*"
|
|
|
|
|
${subplan_type_count}= Get Length ${subplan_type_matches}
|
|
|
|
|
# Also check for non-empty children arrays (structural proof of hierarchy)
|
|
|
|
|
${children_matches}= Get Regexp Matches ${r_tree.stdout}
|
|
|
|
|
... "children"\\s*:\\s*\\[\\s*\\{
|
|
|
|
|
${children_count}= Get Length ${children_matches}
|
|
|
|
|
${has_subplan_evidence}= Evaluate ${subplan_type_count} > 0 or ${children_count} > 0
|
|
|
|
|
Should Be True ${has_subplan_evidence}
|
|
|
|
|
... msg=No subplan evidence: need subplan decision type or non-empty children array (types=${subplan_type_count}, children=${children_count})
|
|
|
|
|
Log Subplan type decisions: ${subplan_type_count}, Non-empty children: ${children_count}
|
|
|
|
|
# Assert merge strategy: look for merge-specific JSON fields/values
|
|
|
|
|
${merge_type_matches}= Get Regexp Matches ${r_tree.stdout}
|
|
|
|
|
... "type"\\s*:\\s*"[^"]*merge[^"]*"
|
|
|
|
|
${merge_chosen_matches}= Get Regexp Matches ${r_tree.stdout}
|
|
|
|
|
... "chosen"\\s*:\\s*"[^"]*[Mm]erge[^"]*"
|
|
|
|
|
${merge_type_count}= Get Length ${merge_type_matches}
|
|
|
|
|
${merge_chosen_count}= Get Length ${merge_chosen_matches}
|
|
|
|
|
${has_merge_evidence}= Evaluate ${merge_type_count} > 0 or ${merge_chosen_count} > 0
|
|
|
|
|
Should Be True ${has_merge_evidence}
|
|
|
|
|
... msg=No merge evidence: need merge decision type or merge in chosen text (types=${merge_type_count}, chosen=${merge_chosen_count})
|
|
|
|
|
Log Merge type decisions: ${merge_type_count}, Merge in chosen: ${merge_chosen_count}
|
|
|
|
|
|
|
|
|
|
# ---- Step 7: Plan execute — execute phase ----
|
|
|
|
|
${r_exec}= Run CleverAgents Command
|
|
|
|
|
... plan execute ${plan_id} --format plain
|
|
|
|
|
... timeout=300s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_exec} plan execute (execute)
|
|
|
|
|
Should Not Be Empty ${r_exec.stdout} msg=Execute phase produced no output
|
|
|
|
|
Log Execute output: ${r_exec.stdout}
|
|
|
|
|
|
|
|
|
|
# ---- Step 8: Assert checkpoint exists after execution ----
|
|
|
|
|
${r_status_json}= Run CleverAgents Command
|
|
|
|
|
... plan status ${plan_id} --format json
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_status_json} plan status (json)
|
|
|
|
|
Log Plan status JSON: ${r_status_json.stdout}
|
|
|
|
|
${checkpoint_matches}= Get Regexp Matches ${r_status_json.stdout}
|
|
|
|
|
... "last_checkpoint_id"\\s*:\\s*"([^"]+)" 1
|
|
|
|
|
${has_checkpoint}= Get Length ${checkpoint_matches}
|
|
|
|
|
Should Be True ${has_checkpoint} > 0
|
|
|
|
|
... msg=No checkpoint ID found — M4 AC requires checkpoint creation after execution (bug #1253)
|
|
|
|
|
|
|
|
|
|
# ---- Step 9: Exercise correction — revert mode ----
|
|
|
|
|
${decision_id_revert}= Set Variable ${decision_ids}[0]
|
|
|
|
|
${r_correct_revert}= Run CleverAgents Command
|
|
|
|
|
... plan correct ${decision_id_revert}
|
|
|
|
|
... --mode revert
|
|
|
|
|
... --guidance Improve implementation approach
|
|
|
|
|
... --yes --format plain
|
|
|
|
|
... timeout=180s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_correct_revert} plan correct (revert)
|
|
|
|
|
# Assert correction-specific output
|
|
|
|
|
${revert_lower}= Evaluate ($r_correct_revert.stdout).lower()
|
|
|
|
|
${has_revert_kw}= Evaluate 'revert' in $revert_lower or 'supersed' in $revert_lower
|
|
|
|
|
Should Be True ${has_revert_kw}
|
|
|
|
|
... msg=Revert correction output should contain correction-specific keywords
|
|
|
|
|
|
|
|
|
|
# ---- Step 10: Re-fetch tree after revert, select active decision for append ----
|
|
|
|
|
${r_tree_post}= Run CleverAgents Command
|
|
|
|
|
... plan tree ${plan_id} --format json
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_tree_post} plan tree (post-revert)
|
|
|
|
|
Log Post-revert tree: ${r_tree_post.stdout}
|
|
|
|
|
# Extract active (non-superseded) decision IDs
|
|
|
|
|
${post_decision_ids}= Get Regexp Matches ${r_tree_post.stdout}
|
|
|
|
|
... "decision_id"\\s*:\\s*"([0-9A-HJ-NP-Z]{26})" 1
|
|
|
|
|
${post_count}= Get Length ${post_decision_ids}
|
|
|
|
|
Should Be True ${post_count} > 0
|
|
|
|
|
... msg=No decisions found in post-revert tree
|
|
|
|
|
# Use the last decision ID in JSON output order. In the current
|
|
|
|
|
# plan tree serialization, non-superseded decisions appear after
|
|
|
|
|
# superseded ones because revert appends new decisions at the end.
|
|
|
|
|
# If this assumption breaks, filter by "superseded": false instead.
|
|
|
|
|
${last_idx}= Evaluate len($post_decision_ids) - 1
|
|
|
|
|
${decision_id_append}= Set Variable ${post_decision_ids}[${last_idx}]
|
|
|
|
|
# Exercise append correction on the active decision
|
|
|
|
|
${r_correct_append}= Run CleverAgents Command
|
|
|
|
|
... plan correct ${decision_id_append}
|
|
|
|
|
... --mode append
|
|
|
|
|
... --guidance Add error handling to implementation
|
|
|
|
|
... --yes --format plain
|
|
|
|
|
... timeout=180s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_correct_append} plan correct (append)
|
|
|
|
|
${append_lower}= Evaluate ($r_correct_append.stdout).lower()
|
|
|
|
|
${has_append_kw}= Evaluate 'append' in $append_lower
|
|
|
|
|
Should Be True ${has_append_kw}
|
|
|
|
|
... msg=Append correction output should contain correction-specific keywords
|
|
|
|
|
|
|
|
|
|
# ---- Step 11: Exercise checkpoint rollback ----
|
|
|
|
|
Exercise Checkpoint Rollback ${plan_id} ${checkpoint_matches}[0]
|
|
|
|
|
|
|
|
|
|
# ---- Step 11b: Verify plan state is compatible with lifecycle-apply after rollback ----
|
|
|
|
|
${r_post_rollback}= Run CleverAgents Command
|
|
|
|
|
... plan status ${plan_id} --format json
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_post_rollback} plan status (post-rollback)
|
|
|
|
|
Log Post-rollback status: ${r_post_rollback.stdout}
|
|
|
|
|
# Plan should not be in a terminal/errored state after rollback
|
|
|
|
|
Should Not Match Regexp ${r_post_rollback.stdout}
|
|
|
|
|
... "processing_state"\\s*:\\s*"(errored|cancelled)"
|
|
|
|
|
... msg=Plan is in errored/cancelled state after rollback — lifecycle-apply will fail
|
|
|
|
|
|
|
|
|
|
# ---- Step 12: Plan status — verify plan ID ----
|
|
|
|
|
${r_status}= Run CleverAgents Command
|
|
|
|
|
... plan status ${plan_id} --format plain
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_status} plan status (plain)
|
|
|
|
|
Output Should Contain ${r_status} ${plan_id}
|
|
|
|
|
|
|
|
|
|
# ---- Step 13: Plan diff ----
|
|
|
|
|
${r_diff}= Run CleverAgents Command
|
|
|
|
|
... plan diff ${plan_id} --format plain
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_diff} plan diff
|
|
|
|
|
Should Not Be Empty ${r_diff.stdout} msg=Plan diff produced no output
|
|
|
|
|
|
|
|
|
|
# ---- Step 14: Plan lifecycle-apply ----
|
|
|
|
|
${r_apply}= Run CleverAgents Command
|
|
|
|
|
... plan lifecycle-apply ${plan_id} --format plain
|
|
|
|
|
... timeout=120s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_apply} plan lifecycle-apply
|
|
|
|
|
|
|
|
|
|
# ---- Step 15: Poll until terminal state and assert 'applied' ----
|
|
|
|
|
Wait Until Plan Terminal ${plan_id}
|
|
|
|
|
${r_final}= Run CleverAgents Command
|
|
|
|
|
... plan status ${plan_id} --format json
|
|
|
|
|
... timeout=60s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_final} final plan status
|
|
|
|
|
Log Final plan status JSON: ${r_final.stdout}
|
|
|
|
|
Should Match Regexp ${r_final.stdout}
|
|
|
|
|
... "phase"\\s*:\\s*"apply"
|
|
|
|
|
... msg=Plan did not reach Apply phase after lifecycle-apply
|
|
|
|
|
Should Match Regexp ${r_final.stdout}
|
|
|
|
|
... "processing_state"\\s*:\\s*"applied"
|
|
|
|
|
... msg=Plan did not reach 'applied' terminal state — got a non-success state
|
|
|
|
|
|
|
|
|
|
*** Keywords ***
|
|
|
|
|
Fail If Command Failed
|
|
|
|
|
[Documentation] Fail with truncated diagnostic output if command returned non-zero rc.
|
|
|
|
|
... Keeps failure messages CI-safe by truncating stdout/stderr to 500 chars.
|
|
|
|
|
[Arguments] ${result} ${label}
|
|
|
|
|
Should Not Contain ${result.stdout}${result.stderr} Traceback
|
|
|
|
|
Should Not Contain ${result.stdout}${result.stderr} INTERNAL
|
|
|
|
|
IF ${result.rc} != 0
|
|
|
|
|
${out_trunc}= Evaluate ($result.stdout)[:500]
|
|
|
|
|
${err_trunc}= Evaluate ($result.stderr)[:500]
|
|
|
|
|
Fail ${label} failed (rc=${result.rc}) stdout=${out_trunc} stderr=${err_trunc}
|
|
|
|
|
END
|
|
|
|
|
|
|
|
|
|
Exercise Checkpoint Rollback
|
|
|
|
|
[Documentation] Rollback to a checkpoint and require success (rc=0).
|
|
|
|
|
[Arguments] ${plan_id} ${checkpoint_id}
|
|
|
|
|
Log Exercising rollback to checkpoint: ${checkpoint_id}
|
|
|
|
|
${r_rollback}= Run CleverAgents Command
|
|
|
|
|
... plan rollback ${plan_id} ${checkpoint_id}
|
|
|
|
|
... --yes --format plain
|
|
|
|
|
... timeout=120s expected_rc=None
|
|
|
|
|
Fail If Command Failed ${r_rollback} plan rollback
|
|
|
|
|
Should Not Be Empty ${r_rollback.stdout} msg=Rollback produced no output
|
|
|
|
|
Log Rollback output: ${r_rollback.stdout}
|
|
|
|
|
|
|
|
|
|
Wait Until Plan Terminal
|
|
|
|
|
[Documentation] Poll plan status until it reaches a terminal state.
|
|
|
|
|
... Polls every 10 seconds for up to 3 minutes.
|
|
|
|
|
[Arguments] ${plan_id}
|
|
|
|
|
FOR ${i} IN RANGE 18
|
|
|
|
|
${poll}= Run CleverAgents Command
|
|
|
|
|
... plan status ${plan_id} --format json
|
|
|
|
|
... timeout=30s expected_rc=None
|
|
|
|
|
${is_terminal}= Get Regexp Matches ${poll.stdout}
|
|
|
|
|
... "is_terminal"\\s*:\\s*true
|
|
|
|
|
${terminal_count}= Get Length ${is_terminal}
|
|
|
|
|
IF ${terminal_count} > 0 RETURN
|
|
|
|
|
Sleep 10s Waiting for plan to reach terminal state...
|
|
|
|
|
END
|
|
|
|
|
Fail Plan did not reach terminal state within 3 minutes (polled 18 times)
|