Files
Artemis-Eapil/tests/unit/agents/test_operator_plan_ledger.py
somew1nd a45daf23f8 feat: add agentic video analysis and clarify action results
Report device actions as dispatched and preserve full failure details. Route
Flash through McpActionExecutor and use ToolFailure across helper tools and
agent loops instead of inferring failure from response text.

Require model-provided descriptions for coordinate targets. Keep observed
element metadata separate from those descriptions in safety checks, visual
summaries, history, and frontend labels.

Add Gemini Interactions API video analysis with configurable processing mode,
chunk size, and timeouts. Centralize Google provider capabilities and token
usage normalization, and update google-genai to 2.22.0.

Preserve system instructions on follow-up interactions, replan pending chunks
when falling back to static processing, and reject unanswered native video
and audio segments instead of caching placeholder results. Keep helper-tool
failure status intact through history tools and committee calls.

Validation: 1,762 Python unit tests passed (3 skipped), 40 frontend formatter
tests passed, and the production UI build completed. Ruff lint, formatting,
the quality ratchet, and git diff checks passed.
2026-09-08 17:05:54 -07:00

283 lines
10 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.
"""Tests for operator plan-ledger handling."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from artemis.agents.operator.operator import OperatorNode
from artemis.agents.operator.prompts import (
apply_operator_prompt_contract,
load_operator_prompts,
render_plan_ledger_bounce,
unwritten_action_streak,
)
from artemis.config.agent import MemoryTranscriptConfig
from artemis.context import ArtemisContext
from artemis.utils.plan_grammar import parse_plan, render_plan_grammar_spec
LEGACY_TRANSCRIPT = MemoryTranscriptConfig(enabled=False)
PLAN_NO_LEAF = """- [x] Open Google Maps and search for SFO
- [/] Read the commute duration and record it into note `commute_eta_info`
- verify: duration recorded
- [ ] Draft the ETA message
"""
PLAN_WITH_LEAF = """- [x] Open Google Maps and search for SFO
- [/] Read the commute duration and record it into note `commute_eta_info`
- [x] Open route options for SFO
- [/] Read the driving duration
- [/] Tap the "Driving" tab so the fastest route is highlighted
- verify: duration recorded
- [ ] Draft the ETA message
"""
def test_grammar_spec_teaches_deeper_nesting():
spec = render_plan_grammar_spec(include_checks=False)
assert "4 spaces for a sub-sub-goal" in spec
assert "Only zero-indent lines count as milestones" in spec
def test_active_milestone_and_leaf():
snap = parse_plan(PLAN_WITH_LEAF)
milestone = snap.active_milestone()
assert milestone is not None and milestone.text.startswith("Read the commute duration")
children = snap.children_of(milestone)
assert [c.text for c in children] == [
"Open route options for SFO",
"Read the driving duration",
'Tap the "Driving" tab so the fastest route is highlighted',
]
leaf = snap.active_leaf(milestone)
assert leaf is not None and leaf.text.startswith('Tap the "Driving" tab')
assert leaf.indent == 4
snap = parse_plan(PLAN_NO_LEAF)
milestone = snap.active_milestone()
assert milestone is not None
assert snap.active_leaf(milestone) is None
assert snap.children_of(milestone) == ()
def test_active_milestone_fallbacks():
all_pending = parse_plan("- [ ] First\n- [ ] Second\n")
assert all_pending.active_milestone().text == "First"
nested_only = parse_plan("- [ ] First\n - [/] Doing it\n- [ ] Second\n")
assert nested_only.active_milestone().text == "First"
assert parse_plan("- [x] First\n- [x] Second\n").active_milestone() is None
def _action_step(n, wrote_plan=False, action=True):
tool_calls = []
if wrote_plan:
tool_calls.append(
{"name": "update_note", "args": {"key": "task_plan", "target": "a", "replacement": "b"}}
)
step = {"step_id": f"s{n}", "step_number": n, "tool_calls": tool_calls}
if action:
step["action_taken"] = [{"action": "tap"}]
return step
def test_streak_counts_trailing_action_turns_without_plan_write():
assert unwritten_action_streak([]) == 0
assert unwritten_action_streak([_action_step(1, wrote_plan=True)]) == 0
assert unwritten_action_streak([_action_step(1)]) == 1
steps = [
_action_step(1),
_action_step(2, wrote_plan=True),
_action_step(3),
_action_step(4, action=False),
_action_step(5),
]
assert unwritten_action_streak(steps) == 2
def test_bounce_only_when_ledger_is_broken_or_stale():
assert render_plan_ledger_bounce(PLAN_WITH_LEAF, streak=0, stale_turns=4) is None
assert render_plan_ledger_bounce(PLAN_WITH_LEAF, streak=2, stale_turns=4) is None
stale = render_plan_ledger_bounce(PLAN_WITH_LEAF, streak=3, stale_turns=4)
assert stale and "stayed in progress for 4 action turns" in stale
assert 'Tap the "Driving" tab' in stale
assert render_plan_ledger_bounce(PLAN_WITH_LEAF, streak=9, stale_turns=0) is None
broken = render_plan_ledger_bounce(PLAN_NO_LEAF, streak=0, stale_turns=4)
assert broken and "has no in-progress `[/]` sub-goal beneath it" in broken
assert "Read the commute duration" in broken
assert render_plan_ledger_bounce("No task plan yet.", 0, 4) is None
assert render_plan_ledger_bounce("- [x] Done\n- [x] Also done\n", 0, 4) is None
def test_prompt_teaches_the_ledger_once_without_per_turn_reminder():
prompt = apply_operator_prompt_contract(load_operator_prompts()["main_template"])
assert prompt.count("**Live Sub-goal Ledger**") == 1
assert "mandatory every turn" not in prompt
assert "need no plan write" in prompt
assert "sub-sub-goal (4 spaces)" in prompt
assert "structurally broken" in prompt
def _make_node(tmp_path, plan_text, steps=None):
notes = tmp_path / "notes"
notes.mkdir(parents=True)
(notes / "task_plan.md").write_text(plan_text, encoding="utf-8")
ctx = MagicMock(spec=ArtemisContext)
ctx.execution_setup = None
ctx.data_engine = MagicMock()
ctx.data_engine.base_dir = str(tmp_path)
ctx.data_engine.current_session_id = "s"
ctx.data_engine.get_agent_friendly_steps.return_value = steps or []
state = MagicMock()
state.subagent_calls = []
state.initial_goal = "goal"
state.open_incident = None
node = OperatorNode(ctx, transcript_config=LEGACY_TRANSCRIPT)
return node, state, notes / "task_plan.md"
def _click_response(n_actions=1):
response = MagicMock()
response.content = ""
response.tool_calls = [
{
"name": "click",
"args": {"target": [50, 50], "target_description": "button"},
"id": f"call_{i}",
}
for i in range(n_actions)
]
return response
def _llm(side_effect=None, return_value=None):
llm = MagicMock()
llm.ainvoke = AsyncMock(side_effect=side_effect, return_value=return_value)
llm.bind_tools.return_value = llm
return llm
@pytest.mark.asyncio
async def test_gate_bounces_broken_ledger_once_then_executes(tmp_path):
node, state, _ = _make_node(tmp_path, PLAN_NO_LEAF)
seen = []
async def ainvoke(messages, *args, **kwargs):
seen.append(list(messages))
return _click_response()
with patch("artemis.agents.operator.operator.get_llm", return_value=_llm(ainvoke)):
update = await node(state)
assert len(seen) == 2, "first submission bounced, second executed"
bounce = [
m
for m in seen[1]
if isinstance(getattr(m, "content", None), str)
and m.content.startswith("Not executed: the active milestone")
]
assert bounce and bounce[0].status == "error"
assert json.loads(update["structured_decisions"])[0]["action"] == "tap"
@pytest.mark.asyncio
async def test_gate_lets_healthy_ledger_through_without_a_write(tmp_path):
node, state, _ = _make_node(tmp_path, PLAN_WITH_LEAF, steps=[_action_step(1)])
llm = _llm(return_value=_click_response())
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
update = await node(state)
assert llm.ainvoke.await_count == 1
assert update["structured_decisions"] is not None
@pytest.mark.asyncio
async def test_gate_bounces_stale_ledger(tmp_path):
stale_history = [_action_step(1), _action_step(2), _action_step(3)]
node, state, _ = _make_node(tmp_path, PLAN_WITH_LEAF, steps=stale_history)
seen = []
async def ainvoke(messages, *args, **kwargs):
seen.append(list(messages))
return _click_response()
with (
patch("artemis.agents.operator.operator.get_llm", return_value=_llm(ainvoke)),
patch.object(OperatorNode, "_plan_ledger_stale_turns", return_value=4),
):
await node(state)
assert len(seen) == 2
assert any(
isinstance(getattr(m, "content", None), str)
and "stayed in progress for 4 action turns" in m.content
for m in seen[1]
)
@pytest.mark.asyncio
async def test_gate_passes_when_plan_changed_in_turn(tmp_path):
node, state, plan_path = _make_node(tmp_path, PLAN_NO_LEAF)
async def ainvoke(messages, *args, **kwargs):
plan_path.write_text(PLAN_WITH_LEAF, encoding="utf-8")
return _click_response()
llm = _llm(ainvoke)
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
update = await node(state)
assert llm.ainvoke.await_count == 1
assert update["structured_decisions"] is not None
@pytest.mark.asyncio
async def test_gate_exempts_bursts_and_open_incidents(tmp_path):
node, state, _ = _make_node(tmp_path / "burst", PLAN_NO_LEAF)
llm = _llm(return_value=_click_response(n_actions=2))
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
update = await node(state)
assert llm.ainvoke.await_count == 1
assert len(json.loads(update["structured_decisions"])) == 2
node, state, _ = _make_node(tmp_path / "incident", PLAN_NO_LEAF)
state.open_incident = {"category": "intercepted"}
llm = _llm(return_value=_click_response())
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
await node(state)
assert llm.ainvoke.await_count == 1
@pytest.mark.asyncio
async def test_gate_inactive_without_plan_or_when_disabled(tmp_path):
llm = _llm(return_value=_click_response())
node, state, _ = _make_node(tmp_path / "a", "No task plan yet.")
with patch("artemis.agents.operator.operator.get_llm", return_value=llm):
await node(state)
assert llm.ainvoke.await_count == 1
llm.ainvoke.reset_mock()
node, state, _ = _make_node(tmp_path / "b", PLAN_NO_LEAF)
with (
patch("artemis.agents.operator.operator.get_llm", return_value=llm),
patch.object(OperatorNode, "_plan_ledger_gate_enabled", return_value=False),
):
await node(state)
assert llm.ainvoke.await_count == 1