Files
Artemis-Eapil/tests/unit/graph/test_checkpoints.py
T
somew1nd 67248c887e feat: improve task execution, verification and packaging
Add execution incidents, action bursts and shared Flash history.

Expose verification and Explorer settings with streamed check results.

Support graceful cancellation, recording recovery and bundled UI resources.

Add CI quality checks and clean up comments and documentation.
2026-09-02 21:45:51 -07:00

705 lines
26 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Checkpoint scheduling and harvest tests: queue/spawn separation, supersede
without verdict loss, applicability gating, repair quota, halt policy,
timeouts, unconditional record_step, and fail-open axis separation."""
import asyncio
import base64
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from artemis.agents.checker.checker import CheckReport, CheckVerdict
from artemis.context import ArtemisContext, ExecutionSetup
from artemis.graph.checkpoints import (
CheckpointRun,
PendingCheckpoint,
checker_note_key,
harvest_finished_checkpoints,
harvest_run,
is_checker_note_key,
queue_checkpoints,
read_ledger,
spawn_pending_checkpoints,
)
from artemis.graph.graph import execution_check_node, wrap_note_tool, wrap_update_note_tool
from artemis.graph.state import State
from artemis.utils.plan_grammar import CheckItem, parse_plan, subgoal_hash
GOAL_TEXT = "Create the alarm"
GOAL_KEY = subgoal_hash(GOAL_TEXT)
PLAN_WITH_CHECKS = (
f"- [x] {GOAL_TEXT}\n"
" - verify: the alarm list shows 7:30 AM\n"
" - assert: a toast appeared\n"
"- [ ] Next milestone\n"
)
def _make_ctx(tmp_path, **setup_kwargs):
setup_kwargs.setdefault("disable_checker", False)
# These tests exercise the midway checkpoint machinery, which is off in
# the factory layering — pin it on unless a test overrides it.
setup_kwargs.setdefault("disable_midway_checks", False)
ctx = MagicMock(spec=ArtemisContext)
ctx.execution_setup = ExecutionSetup(**setup_kwargs)
ctx.data_engine = MagicMock()
ctx.data_engine.base_dir = tmp_path
ctx.pending_checkpoints = []
ctx.checkpoint_tasks = {}
ctx.checkpoint_attempt_seq = {}
ctx.checkpoint_repairs = {}
ctx.assert_halt = False
ctx.final_check_attempts = 0
ctx.planner_task = None
ctx.last_validated_plan = None
ctx.pending_validated_plan = None
return ctx
def _make_state(**overrides):
state = MagicMock(spec=State)
state.initial_goal = "the user goal"
state.user_stop_requested = False
state.operator_feedback = None
state.operator_raw_data = None
state.structured_decisions = None
defaults = {
"operator_raw_thinking": None,
"operator_native_thinking": None,
}
for k, v in {**defaults, **overrides}.items():
setattr(state, k, v)
return state
def _write_plan(tmp_path, content=PLAN_WITH_CHECKS):
notes = tmp_path / "notes"
notes.mkdir(parents=True, exist_ok=True)
path = notes / "task_plan.md"
path.write_text(content, encoding="utf-8")
return path
def _pending(plan=PLAN_WITH_CHECKS, kinds=("verify", "assert")):
snapshot = parse_plan(plan)
item = next(i for i in snapshot.top_level if i.key == GOAL_KEY)
items = tuple(
ci for ci in snapshot.check_items_of(item) if ci.when == "on_complete" and ci.kind in kinds
)
return PendingCheckpoint(
checkpoint_id=GOAL_KEY,
subgoal_text=GOAL_TEXT,
check_items=items,
plan_text=plan,
trigger_ts=1.0,
)
def _done_run(ctx, report_or_exc, attempt_id=f"{GOAL_KEY}#1", pending=None):
loop = asyncio.get_event_loop()
fut = loop.create_future()
if isinstance(report_or_exc, Exception):
fut.set_exception(report_or_exc)
else:
fut.set_result(report_or_exc)
return CheckpointRun(attempt_id=attempt_id, task=fut, checkpoint=pending or _pending())
def _verdict(kind="verify", status="failed", text=None, evidence="concrete evidence"):
default_text = "the alarm list shows 7:30 AM" if kind == "verify" else "a toast appeared"
return CheckVerdict(
item_text=text or default_text,
kind=kind,
status=status,
evidence=evidence,
suggestion="fix it" if (kind == "verify" and status == "failed") else "",
)
# --- §8.1 / §8.2: queueing ------------------------------------------------------------
def test_queue_checkpoints_enqueues_every_completion_with_items(tmp_path):
ctx = _make_ctx(tmp_path)
state = _make_state()
plan = (
"- [x] A\n - verify: VA\n"
"- [x] B\n - verify: VB\n"
"- [x] C\n" # no check items -> never queued
)
after = parse_plan(plan)
queue_checkpoints(ctx, state, after, ["A", "B", "C"], plan)
assert [p.checkpoint_id for p in ctx.pending_checkpoints] == [
subgoal_hash("A"),
subgoal_hash("B"),
]
# Only queued, never spawned here
assert ctx.checkpoint_tasks == {}
def test_queue_checkpoints_ignores_at_end_items(tmp_path):
ctx = _make_ctx(tmp_path)
plan = "- [x] A\n - assert@end: final only\n"
queue_checkpoints(ctx, _make_state(), parse_plan(plan), ["A"], plan)
assert ctx.pending_checkpoints == []
def test_queue_checkpoints_respects_midway_gate_and_user_stop(tmp_path):
plan = "- [x] A\n - verify: VA\n"
after = parse_plan(plan)
ctx = _make_ctx(tmp_path, disable_midway_checks=True)
queue_checkpoints(ctx, _make_state(), after, ["A"], plan)
assert ctx.pending_checkpoints == []
# Legacy master alias also gates
ctx2 = _make_ctx(tmp_path, disable_checker=True)
queue_checkpoints(ctx2, _make_state(), after, ["A"], plan)
assert ctx2.pending_checkpoints == []
ctx3 = _make_ctx(tmp_path)
queue_checkpoints(ctx3, _make_state(user_stop_requested=True), after, ["A"], plan)
assert ctx3.pending_checkpoints == []
@pytest.mark.asyncio
async def test_process_plan_write_only_queues(tmp_path):
"""The plan-write pipeline enqueues but never spawns: spawning waits for the
turn's step to be recorded."""
plan_before = PLAN_WITH_CHECKS.replace(f"- [x] {GOAL_TEXT}", f"- [/] {GOAL_TEXT}")
task_plan_path = _write_plan(tmp_path, plan_before)
ctx = _make_ctx(tmp_path)
state = _make_state()
async def fake_invoke(tool, args, tool_call_id, state, record_trace=None):
task_plan_path.write_text(args["content"], encoding="utf-8")
return "Success"
mock_tool = MagicMock()
mock_tool.name = "save_note"
mock_tool.description = "save"
with patch("artemis.graph.graph.invoke_tool_with_injection", side_effect=fake_invoke):
wrapped = wrap_note_tool(ctx, mock_tool)
await wrapped.ainvoke(
{"key": "task_plan", "content": PLAN_WITH_CHECKS, "tool_call_id": "t1"}
)
assert len(ctx.pending_checkpoints) == 1
pc = ctx.pending_checkpoints[0]
assert pc.checkpoint_id == GOAL_KEY
assert [ci.kind for ci in pc.check_items] == ["verify", "assert"]
assert pc.plan_text == PLAN_WITH_CHECKS
assert ctx.checkpoint_tasks == {}
# --- §8.1 / §8.10: spawn after record_step; record_step unconditional ----------------
def _raw_data():
return {
"screenshot_b64": base64.b64encode(b"img").decode(),
"xml_hierarchy": [],
"ocr_results": [],
"width": 1080,
"height": 2400,
}
@pytest.mark.asyncio
async def test_execution_check_spawns_after_record_step_with_correct_anchor(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
ctx.data_engine.record_step.return_value = "step-uuid-42"
ctx.pending_checkpoints.append(_pending())
state = _make_state(operator_raw_data=_raw_data())
captured = {}
async def fake_check(ctx_arg, check_items, anchor, goal, subgoal_text, attempt_id=None):
captured["anchor"] = anchor
captured["goal"] = goal
return CheckReport(verdicts=[])
with (
patch(
"artemis.agents.checker.checker.run_checkpoint_check",
side_effect=fake_check,
),
patch("artemis.graph.graph._get_active_subgoal_hashes", return_value=("h", None)),
):
update = await execution_check_node(state, ctx)
assert ctx.data_engine.record_step.called
assert update["current_step_id"] == "step-uuid-42"
assert GOAL_KEY in ctx.checkpoint_tasks
# Let the spawned task run
await ctx.checkpoint_tasks[GOAL_KEY].task
assert captured["anchor"].anchor_step_id == "step-uuid-42"
assert captured["anchor"].plan_text == PLAN_WITH_CHECKS
assert captured["goal"] == "the user goal"
@pytest.mark.asyncio
async def test_execution_check_no_spawn_after_user_stop(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
ctx.data_engine.record_step.return_value = "sid"
ctx.pending_checkpoints.append(_pending())
state = _make_state(operator_raw_data=_raw_data(), user_stop_requested=True)
with patch("artemis.graph.graph._get_active_subgoal_hashes", return_value=("h", None)):
await execution_check_node(state, ctx)
assert ctx.checkpoint_tasks == {}
# Step is still recorded regardless
assert ctx.data_engine.record_step.called
@pytest.mark.asyncio
async def test_planner_flag_is_advisory_hint_only(tmp_path):
"""A flagged plan change is advisory: no rollback, the turn's actions
still run, the Operator gets the concern + reason as feedback, the turn
is recorded (tagged planner_flagged) and the ratchet baseline advances so
the same change is not re-flagged on every later write."""
plan_path = _write_plan(tmp_path)
plan_on_disk = plan_path.read_text(encoding="utf-8")
ctx = _make_ctx(tmp_path)
ctx.data_engine.record_step.return_value = "flagged-step"
ctx.last_validated_plan = "- [ ] old baseline\n"
ctx.pending_validated_plan = plan_on_disk
flagged = asyncio.get_event_loop().create_future()
flagged.set_result({"status": "failed", "feedback": "milestone 2 drifts from the goal"})
ctx.planner_task = flagged
state = _make_state(operator_raw_data=_raw_data())
with patch("artemis.graph.graph._get_active_subgoal_hashes", return_value=("h", None)):
update = await execution_check_node(state, ctx)
# Actions are not suppressed and the plan file is untouched.
assert update["checker_success"] is True
assert "structured_decisions" not in update
assert plan_path.read_text(encoding="utf-8") == plan_on_disk
assert update["current_step_id"] == "flagged-step"
extra = ctx.data_engine.record_step.call_args.kwargs["extra_metadata"]
assert extra.get("planner_flagged") is True
# Hint + reason reach the Operator through operator_feedback.
feedback = update["operator_feedback"]
assert feedback and feedback[0].startswith("[planner]")
assert "milestone 2 drifts from the goal" in feedback[0]
assert "NOT rolled back" in feedback[0]
# Baseline advanced to the reviewed content.
assert ctx.last_validated_plan == plan_on_disk
assert ctx.pending_validated_plan is None
assert ctx.planner_task is None
@pytest.mark.asyncio
async def test_concurrency_cap_leaves_excess_pending(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path, max_concurrent_checkpoints=1)
plans = []
for name in ("A", "B"):
plan = f"- [x] {name}\n - verify: V{name}\n"
plans.append(plan)
snapshot = parse_plan(plan)
item = snapshot.top_level[0]
ctx.pending_checkpoints.append(
PendingCheckpoint(
checkpoint_id=item.key,
subgoal_text=name,
check_items=snapshot.check_items_of(item),
plan_text=plan,
trigger_ts=0.0,
)
)
async def fake_check(*a, **k):
return CheckReport(verdicts=[])
with patch("artemis.agents.checker.checker.run_checkpoint_check", side_effect=fake_check):
await spawn_pending_checkpoints(ctx, _make_state(), "sid")
assert len(ctx.checkpoint_tasks) == 1
assert len(ctx.pending_checkpoints) == 1
for run in ctx.checkpoint_tasks.values():
await run.task
# --- §8.3: supersede never loses verdicts --------------------------------------------
@pytest.mark.asyncio
async def test_supersede_books_finished_unharvested_attempt_first(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
failed_report = CheckReport(verdicts=[_verdict("verify", "failed")])
ctx.checkpoint_tasks[GOAL_KEY] = _done_run(ctx, failed_report)
ctx.checkpoint_attempt_seq[GOAL_KEY] = 1
ctx.pending_checkpoints.append(_pending())
async def fake_check(*a, **k):
return CheckReport(verdicts=[])
with patch("artemis.agents.checker.checker.run_checkpoint_check", side_effect=fake_check):
await spawn_pending_checkpoints(ctx, _make_state(), "sid")
new_run = ctx.checkpoint_tasks[GOAL_KEY]
await new_run.task
# The failed verdict was booked before replacement — never dropped
records = read_ledger(tmp_path)
failed = [r for r in records if r["status"] == "failed"]
assert len(failed) == 1
assert failed[0]["attempt_id"] == f"{GOAL_KEY}#1"
# And a fresh attempt id was allocated
assert new_run.attempt_id == f"{GOAL_KEY}#2"
# Superseded harvest is ledger-only: no plan revert happened
assert "- [x]" in (tmp_path / "notes" / "task_plan.md").read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_supersede_cancels_running_attempt_and_books_superseded(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
async def hang():
await asyncio.sleep(3600)
running = asyncio.create_task(hang())
await asyncio.sleep(0) # let it start
ctx.checkpoint_tasks[GOAL_KEY] = CheckpointRun(
attempt_id=f"{GOAL_KEY}#1", task=running, checkpoint=_pending()
)
ctx.checkpoint_attempt_seq[GOAL_KEY] = 1
ctx.pending_checkpoints.append(_pending())
async def fake_check(*a, **k):
return CheckReport(verdicts=[])
with patch("artemis.agents.checker.checker.run_checkpoint_check", side_effect=fake_check):
await spawn_pending_checkpoints(ctx, _make_state(), "sid")
await ctx.checkpoint_tasks[GOAL_KEY].task
assert running.cancelled()
superseded = [r for r in read_ledger(tmp_path) if r["status"] == "superseded"]
assert superseded and superseded[0]["attempt_id"] == f"{GOAL_KEY}#1"
# --- §8.4: harvest only takes done(); applicability gates side effects ---------------
@pytest.mark.asyncio
async def test_harvest_only_takes_done_tasks(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
async def hang():
await asyncio.sleep(3600)
running = asyncio.create_task(hang())
await asyncio.sleep(0)
ctx.checkpoint_tasks["running"] = CheckpointRun(
attempt_id="running#1", task=running, checkpoint=_pending()
)
ctx.checkpoint_tasks[GOAL_KEY] = _done_run(
ctx, CheckReport(verdicts=[_verdict("verify", "passed")])
)
findings = harvest_finished_checkpoints(ctx, _make_state())
assert findings == []
# The running attempt is untouched (never awaited, never removed)
assert "running" in ctx.checkpoint_tasks
assert GOAL_KEY not in ctx.checkpoint_tasks
assert not running.done()
running.cancel()
with pytest.raises(asyncio.CancelledError):
await running
@pytest.mark.asyncio
async def test_stale_verdict_is_ledger_only_when_subgoal_text_changed(tmp_path):
# Plan no longer contains the anchored subgoal text
_write_plan(tmp_path, "- [x] Create the alarm (rephrased)\n")
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert findings == [] # no repair injection
assert ctx.checkpoint_repairs == {}
records = read_ledger(tmp_path)
assert records and records[0]["status"] == "failed" # but still booked
# --- §8.5: verify FAIL repairs, bounded by quota -------------------------------------
@pytest.mark.asyncio
async def test_verify_fail_reverts_subgoal_and_injects_finding(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
content = plan_path.read_text(encoding="utf-8")
assert f"- [/] {GOAL_TEXT}" in content # [x] -> [/], forward-looking
assert any("verify failed" in f for f in findings)
assert any("fix it" in f for f in findings)
assert ctx.checkpoint_repairs[GOAL_KEY] == 1
@pytest.mark.asyncio
async def test_verify_fail_beyond_repair_quota_keeps_failed_without_revert(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path, checkpoint_max_repairs=2)
ctx.checkpoint_repairs[GOAL_KEY] = 2
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert findings == []
assert f"- [x] {GOAL_TEXT}" in plan_path.read_text(encoding="utf-8")
# The verdict is NOT rewritten: it stays failed in the ledger
assert read_ledger(tmp_path)[0]["status"] == "failed"
@pytest.mark.asyncio
async def test_verify_fail_after_user_stop_never_reverts(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
harvest_run(ctx, _make_state(user_stop_requested=True), run, allow_side_effects=True)
assert f"- [x] {GOAL_TEXT}" in plan_path.read_text(encoding="utf-8")
# --- §8.6: assert FAIL is a result, never a repair -----------------------------------
@pytest.mark.asyncio
async def test_assert_fail_is_ledger_only(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("assert", "failed")]))
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert findings == []
assert f"- [x] {GOAL_TEXT}" in plan_path.read_text(encoding="utf-8")
assert ctx.assert_halt is False
assert read_ledger(tmp_path)[0]["status"] == "failed"
@pytest.mark.asyncio
async def test_assert_fail_halt_policy_latches_halt(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path, assert_failure_policy="halt")
run = _done_run(ctx, CheckReport(verdicts=[_verdict("assert", "failed")]))
harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert ctx.assert_halt is True
@pytest.mark.asyncio
async def test_ledger_is_append_only_fail_then_pass_both_present(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run1 = _done_run(ctx, CheckReport(verdicts=[_verdict("assert", "failed")]))
harvest_run(ctx, _make_state(), run1, allow_side_effects=True)
run2 = _done_run(
ctx,
CheckReport(verdicts=[_verdict("assert", "passed")]),
attempt_id=f"{GOAL_KEY}#2",
)
harvest_run(ctx, _make_state(), run2, allow_side_effects=True)
statuses = [r["status"] for r in read_ledger(tmp_path)]
assert statuses == ["failed", "passed"] # first failure permanently retained
# --- §8.7 / §8.13: timeouts and fail-open axis separation ----------------------------
@pytest.mark.asyncio
async def test_timeout_records_inconclusive_not_passed(tmp_path):
_write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, TimeoutError())
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert findings == [] # released (fail-open): no repair loop
records = read_ledger(tmp_path)
assert {r["status"] for r in records} == {"inconclusive"}
assert all(r["status"] != "passed" for r in records)
# --- Verify-finding four-layer persistence -------------------------------------------
def _checker_note_path(tmp_path):
return tmp_path / "notes" / f"{checker_note_key(GOAL_KEY)}.md"
@pytest.mark.asyncio
async def test_verify_fail_writes_finding_line_and_checker_note(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
harvest_run(ctx, _make_state(), run, allow_side_effects=True)
content = plan_path.read_text(encoding="utf-8")
lines = content.splitlines()
# Standing headline pinned directly under the (now reverted) subgoal,
# naming the failed criterion and pointing at the detail note.
idx = lines.index(f"- [/] {GOAL_TEXT}")
assert lines[idx + 1].startswith(" - finding: verify failed")
assert checker_note_key(GOAL_KEY) in lines[idx + 1]
# Layer 3: the system note carries the full criterion/evidence/suggestion.
note = _checker_note_path(tmp_path).read_text(encoding="utf-8")
assert "the alarm list shows 7:30 AM" in note
assert "concrete evidence" in note
assert "fix it" in note
@pytest.mark.asyncio
async def test_finding_line_regrows_on_model_plan_write(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
harvest_run(ctx, _make_state(), run, allow_side_effects=True)
assert "- finding:" in plan_path.read_text(encoding="utf-8")
# The model rewrites the plan WITHOUT the finding line...
clean_rewrite = PLAN_WITH_CHECKS.replace(f"- [x] {GOAL_TEXT}", f"- [/] {GOAL_TEXT}")
async def fake_invoke(tool, args, tool_call_id, state, record_trace=None):
plan_path.write_text(args["content"], encoding="utf-8")
return "Success"
mock_tool = MagicMock()
mock_tool.name = "save_note"
mock_tool.description = "save"
with patch("artemis.graph.graph.invoke_tool_with_injection", side_effect=fake_invoke):
wrapped = wrap_note_tool(ctx, mock_tool)
await wrapped.ainvoke({"key": "task_plan", "content": clean_rewrite, "tool_call_id": "t1"})
# ...and the deterministic projection grows it back on the write path.
assert "- finding: verify failed" in plan_path.read_text(encoding="utf-8")
@pytest.mark.asyncio
async def test_finding_line_removed_after_verify_passes_note_kept(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run1 = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
harvest_run(ctx, _make_state(), run1, allow_side_effects=True)
assert "- finding:" in plan_path.read_text(encoding="utf-8")
run2 = _done_run(
ctx,
CheckReport(verdicts=[_verdict("verify", "passed")]),
attempt_id=f"{GOAL_KEY}#2",
)
harvest_run(ctx, _make_state(), run2, allow_side_effects=True)
# Layer 2 headline retired; layer 3 note stays on disk with the full
# per-attempt repair log (fail then pass).
assert "- finding:" not in plan_path.read_text(encoding="utf-8")
note = _checker_note_path(tmp_path).read_text(encoding="utf-8")
assert f"attempt {GOAL_KEY}#1 — verify failed" in note
assert f"attempt {GOAL_KEY}#2 — verify passed" in note
@pytest.mark.asyncio
async def test_finding_line_removed_on_repair_quota_exhaustion(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path, checkpoint_max_repairs=1)
run1 = _done_run(ctx, CheckReport(verdicts=[_verdict("verify", "failed")]))
harvest_run(ctx, _make_state(), run1, allow_side_effects=True)
assert "- finding:" in plan_path.read_text(encoding="utf-8")
# Re-complete, fail again: quota (1) is exhausted -> finding settled.
plan_path.write_text(
plan_path.read_text(encoding="utf-8").replace(f"- [/] {GOAL_TEXT}", f"- [x] {GOAL_TEXT}"),
encoding="utf-8",
)
run2 = _done_run(
ctx,
CheckReport(verdicts=[_verdict("verify", "failed")]),
attempt_id=f"{GOAL_KEY}#2",
)
harvest_run(ctx, _make_state(), run2, allow_side_effects=True)
content = plan_path.read_text(encoding="utf-8")
assert "- finding:" not in content
assert f"- [x] {GOAL_TEXT}" in content # no further revert either
@pytest.mark.asyncio
async def test_checker_note_prefix_writes_rejected_by_wrappers(tmp_path):
ctx = _make_ctx(tmp_path)
save_tool = MagicMock()
save_tool.name = "save_note"
save_tool.description = "save"
update_tool = MagicMock()
update_tool.name = "update_note"
update_tool.description = "update"
with patch("artemis.graph.graph.invoke_tool_with_injection", new_callable=AsyncMock) as inv:
wrapped_save = wrap_note_tool(ctx, save_tool)
result = await wrapped_save.ainvoke(
{"key": checker_note_key(GOAL_KEY), "content": "spoof", "tool_call_id": "t1"}
)
assert "reserved" in str(result.content)
wrapped_update = wrap_update_note_tool(ctx, update_tool)
result2 = await wrapped_update.ainvoke(
{
"key": "checker:abc",
"target": "a",
"replacement": "b",
"tool_call_id": "t2",
}
)
assert "reserved" in str(result2.content)
inv.assert_not_awaited()
assert is_checker_note_key("checker-123") and is_checker_note_key("checker:123")
assert not is_checker_note_key("checkerboard_notes") and not is_checker_note_key("notes")
@pytest.mark.asyncio
async def test_exception_fail_open_releases_but_books_inconclusive(tmp_path):
plan_path = _write_plan(tmp_path)
ctx = _make_ctx(tmp_path)
run = _done_run(ctx, RuntimeError("checker blew up"))
findings = harvest_run(ctx, _make_state(), run, allow_side_effects=True)
# Release: no revert, no finding
assert findings == []
assert f"- [x] {GOAL_TEXT}" in plan_path.read_text(encoding="utf-8")
# Verdict axis: inconclusive, never rewritten to passed
for r in read_ledger(tmp_path):
assert r["status"] == "inconclusive"
assert "checker blew up" in r["evidence"]