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.
189 lines
6.8 KiB
Python
189 lines
6.8 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.
|
|
|
|
"""Prompt assembly tests: full-set output is byte-stable, absent tools leave no trace."""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
|
|
from jinja2 import Template
|
|
import pytest
|
|
|
|
from artemis.agents.operator.prompt_builder import load_operator_prompts
|
|
from artemis.agents.operator.prompts import (
|
|
OPERATOR_PROMPT_TOOLSET,
|
|
apply_operator_prompt_contract,
|
|
)
|
|
from artemis.agents.prompt_assembly import gate_segment, render_tool_enum
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
OPERATOR_DEVICE_TOOLS = (
|
|
"click",
|
|
"input_text",
|
|
"swipe",
|
|
"press_key",
|
|
"manage_app",
|
|
"wait_for_delay",
|
|
"long_press",
|
|
)
|
|
|
|
|
|
def _tool_refs(text: str, tool: str) -> list[str]:
|
|
"""Finds real tool references (backticked or call form), ignoring prose."""
|
|
return re.findall(rf"(`{tool}`|\b{tool}\()", text)
|
|
|
|
|
|
# --- render_tool_enum ----------------------------------------------------------------
|
|
|
|
|
|
def test_enum_plain_list():
|
|
out = render_tool_enum(("a", "b", "c"), {"a", "b", "c"})
|
|
assert out == "`a`, `b`, `c`"
|
|
|
|
|
|
def test_enum_oxford_or_and():
|
|
tools = ("a", "b", "c")
|
|
assert render_tool_enum(tools, set(tools), final_sep="or") == "`a`, `b`, or `c`"
|
|
assert render_tool_enum(tools, set(tools), final_sep="and") == "`a`, `b`, and `c`"
|
|
|
|
|
|
def test_enum_two_items_no_oxford_comma():
|
|
assert render_tool_enum(("a", "b"), {"a", "b"}, final_sep="or") == "`a` or `b`"
|
|
|
|
|
|
def test_enum_single_and_empty():
|
|
assert render_tool_enum(("a", "b"), {"a"}) == "`a`"
|
|
assert render_tool_enum(("a", "b"), set()) == ""
|
|
|
|
|
|
def test_gate_segment():
|
|
assert gate_segment("teach", {"swipe"}, "swipe") == "teach"
|
|
assert gate_segment("teach", set(), "swipe") == ""
|
|
assert gate_segment("teach", {"a"}, "a", "b") == ""
|
|
|
|
|
|
# --- Operator prompt assembly --------------------------------------------------------
|
|
|
|
|
|
def test_full_toolset_is_the_default():
|
|
"""``available_tools=None`` must equal the explicit full set, byte for byte."""
|
|
for template in load_operator_prompts().values():
|
|
assert apply_operator_prompt_contract(template) == apply_operator_prompt_contract(
|
|
template, available_tools=OPERATOR_PROMPT_TOOLSET
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("tool", OPERATOR_DEVICE_TOOLS + ("video_analyzer", "recall_history"))
|
|
def test_removed_tool_leaves_no_reference(tool):
|
|
"""The executable definition of 'an absent tool costs the model nothing'."""
|
|
for name, template in load_operator_prompts().items():
|
|
out = apply_operator_prompt_contract(
|
|
template, available_tools=OPERATOR_PROMPT_TOOLSET - {tool}
|
|
)
|
|
assert not _tool_refs(out, tool), f"'{tool}' still referenced in {name}"
|
|
|
|
|
|
def test_removed_swipe_takes_its_teaching_along():
|
|
"""Gating removes the drag/slider teaching, not just the backticked name."""
|
|
template = load_operator_prompts()["main_template"]
|
|
out = apply_operator_prompt_contract(
|
|
template, available_tools=OPERATOR_PROMPT_TOOLSET - {"swipe"}
|
|
)
|
|
assert "swipe/drag" not in out
|
|
assert "sliders/SeekBars" not in out
|
|
# The generic gesture-scope sentences survive.
|
|
assert "Action Scope & Interaction Precision" in out
|
|
|
|
|
|
def test_removed_wait_for_delay_keeps_interval_line_coherent():
|
|
"""Only the wait_for_delay parenthetical goes; the SOP pacing step stays."""
|
|
template = load_operator_prompts()["main_template"]
|
|
out = apply_operator_prompt_contract(
|
|
template, available_tools=OPERATOR_PROMPT_TOOLSET - {"wait_for_delay"}
|
|
)
|
|
assert (
|
|
"Adhere strictly to the exact duration declared in `<Interval>`, then execute"
|
|
" a refresh action" in out
|
|
)
|
|
assert "Loading & Transitions" not in out
|
|
|
|
|
|
def test_reduced_enums_stay_well_formed():
|
|
template = load_operator_prompts()["main_template"]
|
|
out = apply_operator_prompt_contract(
|
|
template,
|
|
available_tools=OPERATOR_PROMPT_TOOLSET - {"manage_app", "wait_for_delay"},
|
|
)
|
|
assert (
|
|
"Physical device actions (`click`, `input_text`, `swipe`, `press_key`, `long_press`)" in out
|
|
)
|
|
assert (
|
|
"Turn-Ending Action (`click`, `swipe`, `input_text`, `long_press`, or `press_key`)" in out
|
|
)
|
|
|
|
|
|
def test_full_set_enum_slots_render_verbatim():
|
|
"""With every tool present the historical enumeration wording is reproduced."""
|
|
template = load_operator_prompts()["main_template"]
|
|
out = apply_operator_prompt_contract(template)
|
|
assert (
|
|
"Physical device actions (`click`, `input_text`, `swipe`, `press_key`,"
|
|
" `manage_app`, `wait_for_delay`, `long_press`)" in out
|
|
)
|
|
assert (
|
|
"Turn-Ending Action (`click`, `swipe`, `input_text`, `long_press`,"
|
|
" `press_key`, `manage_app`, or `wait_for_delay`)" in out
|
|
)
|
|
assert "Helper/Subagent tools (`ask_explorer`, `ask_diagnoser`, `video_analyzer`)" in out
|
|
assert "ADB/task tools (`run_adb_command`, `manage_task`)" in out
|
|
assert "(`read_note`, `list_notes`, `recall_history`)" in out
|
|
# save_note is a write-through tool, never a result-dependent one; the on-demand
|
|
# analyze_task_output is not advertised.
|
|
assert "`save_note`, and" not in out
|
|
assert "analyze_task_output" not in out
|
|
|
|
|
|
# --- Flash prompt assembly -----------------------------------------------------------
|
|
|
|
|
|
def _render_flash(available_tools: frozenset[str]) -> str:
|
|
template_text = (REPO_ROOT / "artemis/agents/flash/flash_runner.md").read_text(encoding="utf-8")
|
|
return Template(template_text).render(goal="test goal", available_tools=available_tools)
|
|
|
|
|
|
FLASH_FULL = frozenset({"manage_app", "wait_for_delay", "click_sequence", "click", "swipe"})
|
|
|
|
|
|
def test_flash_full_set_keeps_both_segments():
|
|
out = _render_flash(FLASH_FULL)
|
|
assert "**App Launching**" in out
|
|
assert "**Timed Waiting & Transitions**" in out
|
|
assert "{%" not in out
|
|
|
|
|
|
def test_flash_without_manage_app_drops_app_launching():
|
|
out = _render_flash(FLASH_FULL - {"manage_app"})
|
|
assert "**App Launching**" not in out
|
|
assert not _tool_refs(out, "manage_app")
|
|
# The neighbouring numbered rule survives.
|
|
assert "**Do not determine element coordinates based on guesswork.**" in out
|
|
|
|
|
|
def test_flash_without_wait_for_delay_drops_timed_waiting():
|
|
out = _render_flash(FLASH_FULL - {"wait_for_delay"})
|
|
assert "**Timed Waiting & Transitions**" not in out
|
|
assert not _tool_refs(out, "wait_for_delay")
|
|
assert "**State Drift & Progression Tolerance**" in out
|