This commit is contained in:
@@ -57,7 +57,12 @@ def _context() -> TaskContext:
|
||||
|
||||
def test_ai_planner_returns_single_planned_step_for_action_decision() -> None:
|
||||
client = FakeToolCallingClient(
|
||||
ToolCallDecision(tool_name="tap", arguments={"x": 1, "y": 2})
|
||||
ToolCallDecision(
|
||||
tool_name="tap",
|
||||
arguments={"x": 1, "y": 2},
|
||||
purpose="Open the send control.",
|
||||
expected_outcome="The message composer is focused.",
|
||||
)
|
||||
)
|
||||
planner = AIPlanner(client=client)
|
||||
|
||||
@@ -66,8 +71,11 @@ def test_ai_planner_returns_single_planned_step_for_action_decision() -> None:
|
||||
assert len(steps) == 1
|
||||
step = steps[0]
|
||||
assert step.action == "tap"
|
||||
assert step.description == "AI planner: tap({'x': 1, 'y': 2})"
|
||||
assert step.description == "AI planner: Open the send control."
|
||||
assert step.args == {"x": 1, "y": 2}
|
||||
assert step.purpose == "Open the send control."
|
||||
assert step.expected_outcome == "The message composer is focused."
|
||||
assert step.expected_text == "The message composer is focused."
|
||||
|
||||
|
||||
def test_ai_planner_finish_task_success_returns_empty_plan() -> None:
|
||||
@@ -204,6 +212,8 @@ def test_ai_planner_propagates_rationale_and_thinking_to_planned_step() -> None:
|
||||
arguments={"x": 1, "y": 2},
|
||||
text_output="Previous step opened settings. Now tapping account.",
|
||||
thinking="I need to navigate deeper.",
|
||||
purpose="Open account settings.",
|
||||
expected_outcome="The account settings page is visible.",
|
||||
)
|
||||
)
|
||||
planner = AIPlanner(client=client)
|
||||
@@ -212,6 +222,8 @@ def test_ai_planner_propagates_rationale_and_thinking_to_planned_step() -> None:
|
||||
|
||||
assert steps[0].rationale == "Previous step opened settings. Now tapping account."
|
||||
assert steps[0].thinking == "I need to navigate deeper."
|
||||
assert steps[0].purpose == "Open account settings."
|
||||
assert steps[0].expected_outcome == "The account settings page is visible."
|
||||
|
||||
|
||||
def test_history_summary_returns_compact_format() -> None:
|
||||
@@ -226,12 +238,16 @@ def test_history_summary_returns_compact_format() -> None:
|
||||
action="tap",
|
||||
success=True,
|
||||
rationale="Opened settings.",
|
||||
arguments={"x": 1, "y": 2},
|
||||
purpose="Open settings.",
|
||||
expected_outcome="Settings is visible.",
|
||||
page="Home",
|
||||
),
|
||||
WorldEvent(
|
||||
action="swipe",
|
||||
success=False,
|
||||
rationale=None,
|
||||
arguments={"start_y": 700, "end_y": 200},
|
||||
page="Settings",
|
||||
),
|
||||
]
|
||||
@@ -241,8 +257,24 @@ def test_history_summary_returns_compact_format() -> None:
|
||||
summary = _history_summary(state)
|
||||
|
||||
assert summary == [
|
||||
{"page": "Home", "action": "tap", "rationale": "Opened settings.", "success": True},
|
||||
{"page": "Settings", "action": "swipe", "rationale": None, "success": False},
|
||||
{
|
||||
"page": "Home",
|
||||
"action": "tap",
|
||||
"arguments": {"x": 1, "y": 2},
|
||||
"rationale": "Opened settings.",
|
||||
"purpose": "Open settings.",
|
||||
"expected_outcome": "Settings is visible.",
|
||||
"success": True,
|
||||
},
|
||||
{
|
||||
"page": "Settings",
|
||||
"action": "swipe",
|
||||
"arguments": {"start_y": 700, "end_y": 200},
|
||||
"rationale": None,
|
||||
"purpose": None,
|
||||
"expected_outcome": None,
|
||||
"success": False,
|
||||
},
|
||||
]
|
||||
# Must not contain scene element data
|
||||
for entry in summary:
|
||||
|
||||
@@ -193,7 +193,12 @@ def test_multi_step_timeline_records_actual_per_step_prompts(tmp_path) -> None:
|
||||
"""When AIPlanner is used, each timeline step's prompt is the real
|
||||
per-step user prompt (containing scene JSON), not the bare task goal."""
|
||||
decisions = [
|
||||
ToolCallDecision(tool_name="tap", arguments={"x": 1, "y": 2}),
|
||||
ToolCallDecision(
|
||||
tool_name="tap",
|
||||
arguments={"x": 1, "y": 2},
|
||||
purpose="Open the send control.",
|
||||
expected_outcome="The message composer is focused.",
|
||||
),
|
||||
ToolCallDecision(tool_name="finish_task", arguments={"success": True}),
|
||||
]
|
||||
client = ScriptedToolCallingClient(decisions)
|
||||
@@ -228,6 +233,12 @@ def test_multi_step_timeline_records_actual_per_step_prompts(tmp_path) -> None:
|
||||
assert "Current Scene (JSON)" in prompt
|
||||
assert "Call exactly one tool" in prompt
|
||||
assert "tap the button" in prompt
|
||||
assert records[0]["tool_call"]["args"] == {"x": 1, "y": 2}
|
||||
assert records[0]["tool_call"]["purpose"] == "Open the send control."
|
||||
assert (
|
||||
records[0]["tool_call"]["expected_outcome"]
|
||||
== "The message composer is focused."
|
||||
)
|
||||
|
||||
|
||||
def test_non_ai_planner_falls_back_to_task_goal_for_prompt(tmp_path) -> None:
|
||||
|
||||
@@ -96,7 +96,14 @@ def _decision_payload(**overrides: object) -> dict[str, object]:
|
||||
|
||||
def test_authenticated_host_resolves_planner_decision(tmp_path) -> None:
|
||||
fake_client = _FakeToolCallingClient(
|
||||
decision=ToolCallDecision(tool_name="tap", arguments={"x": 1, "y": 2})
|
||||
decision=ToolCallDecision(
|
||||
tool_name="tap",
|
||||
arguments={"x": 1, "y": 2},
|
||||
text_output="The login button is visible. Opening it.",
|
||||
thinking="The next screen should be the account page.",
|
||||
purpose="Open the login screen.",
|
||||
expected_outcome="The login form is visible.",
|
||||
)
|
||||
)
|
||||
client, fake_client, _pool = _build_client(tmp_path, fake_client=fake_client)
|
||||
|
||||
@@ -107,7 +114,14 @@ def test_authenticated_host_resolves_planner_decision(tmp_path) -> None:
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"tool_name": "tap", "arguments": {"x": 1, "y": 2}}
|
||||
assert response.json() == {
|
||||
"tool_name": "tap",
|
||||
"arguments": {"x": 1, "y": 2},
|
||||
"rationale": "The login button is visible. Opening it.",
|
||||
"thinking": "The next screen should be the account page.",
|
||||
"purpose": "Open the login screen.",
|
||||
"expected_outcome": "The login form is visible.",
|
||||
}
|
||||
assert len(fake_client.calls) == 1
|
||||
assert fake_client.calls[0]["system_prompt"] == "you are a planner"
|
||||
assert fake_client.calls[0]["timeout"] == 30.0
|
||||
@@ -232,7 +246,14 @@ def _seed_attempt(pool: DevicePool, task_id: str, host_id: str = "host-a") -> No
|
||||
|
||||
def test_successful_decision_is_persisted_with_correct_fields(tmp_path) -> None:
|
||||
fake_client = _FakeToolCallingClient(
|
||||
decision=ToolCallDecision(tool_name="tap", arguments={"x": 10, "y": 20})
|
||||
decision=ToolCallDecision(
|
||||
tool_name="tap",
|
||||
arguments={"x": 10, "y": 20},
|
||||
text_output="The settings tab is visible. Opening it.",
|
||||
thinking="A tap should navigate to settings.",
|
||||
purpose="Open settings.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
)
|
||||
client, fake_client, pool = _build_client(tmp_path, fake_client=fake_client)
|
||||
_seed_attempt(pool, "task-log-1")
|
||||
@@ -259,6 +280,10 @@ def test_successful_decision_is_persisted_with_correct_fields(tmp_path) -> None:
|
||||
assert row.tool_name == "tap"
|
||||
assert '"x": 10' in row.arguments_json
|
||||
assert '"y": 20' in row.arguments_json
|
||||
assert row.rationale == "The settings tab is visible. Opening it."
|
||||
assert row.thinking == "A tap should navigate to settings."
|
||||
assert row.purpose == "Open settings."
|
||||
assert row.expected_outcome == "The settings page is visible."
|
||||
|
||||
|
||||
def test_failed_decision_persists_nothing(tmp_path) -> None:
|
||||
|
||||
@@ -1773,12 +1773,21 @@ def test_record_planner_decision_stores_rationale_and_thinking(
|
||||
now=now,
|
||||
rationale="Previous step opened settings. Now tapping account.",
|
||||
thinking="I need to navigate to account settings.",
|
||||
purpose="Open account settings.",
|
||||
expected_outcome="The account settings page is visible.",
|
||||
)
|
||||
|
||||
decisions = database.repository.list_planner_decisions(task_id=task_id, attempt=1)
|
||||
decisions = database.repository.list_planner_decisions(
|
||||
task_id=task_id, attempt=1
|
||||
)
|
||||
assert len(decisions) == 1
|
||||
assert decisions[0].rationale == "Previous step opened settings. Now tapping account."
|
||||
assert (
|
||||
decisions[0].rationale
|
||||
== "Previous step opened settings. Now tapping account."
|
||||
)
|
||||
assert decisions[0].thinking == "I need to navigate to account settings."
|
||||
assert decisions[0].purpose == "Open account settings."
|
||||
assert decisions[0].expected_outcome == "The account settings page is visible."
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
@@ -1804,9 +1813,13 @@ def test_record_planner_decision_stores_null_rationale_and_thinking(
|
||||
# rationale and thinking omitted (default None)
|
||||
)
|
||||
|
||||
decisions = database.repository.list_planner_decisions(task_id=task_id, attempt=1)
|
||||
decisions = database.repository.list_planner_decisions(
|
||||
task_id=task_id, attempt=1
|
||||
)
|
||||
assert len(decisions) == 1
|
||||
assert decisions[0].rationale is None
|
||||
assert decisions[0].thinking is None
|
||||
assert decisions[0].purpose is None
|
||||
assert decisions[0].expected_outcome is None
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
@@ -94,6 +94,44 @@ def test_unknown_task_id_returns_404(tmp_path) -> None:
|
||||
assert resp.status_code == 404, resp.text
|
||||
|
||||
|
||||
def test_planner_decision_history_returns_reusable_action_metadata(tmp_path) -> None:
|
||||
app, pool, scheduler, _ = _build_app(tmp_path)
|
||||
task_id = scheduler.submit(goal="open settings")
|
||||
pool.store.record_planner_decision(
|
||||
host_id="host-a",
|
||||
task_id=task_id,
|
||||
attempt=0,
|
||||
system_prompt="system",
|
||||
user_prompt="open settings",
|
||||
tool_name="tap",
|
||||
arguments_json='{"x": 12, "y": 34}',
|
||||
now=datetime.now(UTC),
|
||||
rationale="The settings tab is visible. Opening it.",
|
||||
thinking="A tap should navigate to settings.",
|
||||
purpose="Open settings.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
|
||||
response = _client_for(app).get(f"/v1/tasks/{task_id}/planner-decisions?attempt=0")
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["items"] == [
|
||||
{
|
||||
"step_index": 1,
|
||||
"attempt": 0,
|
||||
"system_prompt": "system",
|
||||
"user_prompt": "open settings",
|
||||
"tool_name": "tap",
|
||||
"arguments": {"x": 12, "y": 34},
|
||||
"rationale": "The settings tab is visible. Opening it.",
|
||||
"thinking": "A tap should navigate to settings.",
|
||||
"purpose": "Open settings.",
|
||||
"expected_outcome": "The settings page is visible.",
|
||||
"created_at": response.json()["items"][0]["created_at"],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_task_status_exposes_distributed_metadata_without_lease_secret(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
@@ -299,7 +337,10 @@ def test_submit_with_explicit_target_is_listed_and_not_rerouted(tmp_path) -> Non
|
||||
assert task["assigned_host_id"] == "host-b"
|
||||
assert task["assigned_device_id"] == "device-b"
|
||||
listed = client.get("/v1/tasks").json()["items"]
|
||||
assert next(item for item in listed if item["id"] == task_id)["target_host_id"] == "host-b"
|
||||
assert (
|
||||
next(item for item in listed if item["id"] == task_id)["target_host_id"]
|
||||
== "host-b"
|
||||
)
|
||||
|
||||
|
||||
def test_submit_rejects_incomplete_or_foreign_target(tmp_path) -> None:
|
||||
@@ -429,9 +470,7 @@ def test_list_tasks_returns_summary_with_pagination_and_status_filter(
|
||||
assert [item["id"] for item in queued_only["items"]] == [second_id]
|
||||
assert all(item["status"] == "queued" for item in queued_only["items"])
|
||||
|
||||
assigned_only = client.get(
|
||||
"/v1/tasks", params={"status": "assigned"}
|
||||
).json()
|
||||
assigned_only = client.get("/v1/tasks", params={"status": "assigned"}).json()
|
||||
assert assigned_only["total"] == 1
|
||||
assert [item["id"] for item in assigned_only["items"]] == [assigned_id]
|
||||
|
||||
|
||||
@@ -24,3 +24,31 @@ def test_executor_retries_until_transient_tool_succeeds() -> None:
|
||||
assert result.attempts == 3
|
||||
assert result.result == {"ok": True}
|
||||
|
||||
|
||||
def test_executor_keeps_action_metadata_out_of_device_tool_arguments() -> None:
|
||||
calls: list[tuple[int, int]] = []
|
||||
|
||||
def tap(*, x: int, y: int) -> dict[str, bool]:
|
||||
calls.append((x, y))
|
||||
return {"ok": True}
|
||||
|
||||
executor = Executor(
|
||||
tools={"tap": tap},
|
||||
config=ExecutorConfig(max_retries=1, backoff_seconds=0),
|
||||
)
|
||||
step = PlannedStep(
|
||||
action="tap",
|
||||
description="Open settings.",
|
||||
args={"x": 12, "y": 34},
|
||||
purpose="Open settings.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
|
||||
result = executor.execute(step)
|
||||
|
||||
assert result.success is True
|
||||
assert calls == [(12, 34)]
|
||||
assert result.to_dict()["step"]["purpose"] == "Open settings."
|
||||
assert (
|
||||
result.to_dict()["step"]["expected_outcome"] == "The settings page is visible."
|
||||
)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from skills_learning.models import FlowStep, FlowTemplateSkill, SkillMetadata
|
||||
from skills_learning.models import (
|
||||
FlowStep,
|
||||
FlowTemplateSkill,
|
||||
SkillMetadata,
|
||||
skill_embedding_text,
|
||||
)
|
||||
from skills_learning.store import SkillStore
|
||||
from skills_learning.synthesis import extract_tool_calls, synthesize_flow_skill
|
||||
|
||||
@@ -10,12 +15,19 @@ def _record(
|
||||
args: dict[str, object] | None = None,
|
||||
*,
|
||||
result: dict[str, object] | None = None,
|
||||
purpose: str | None = None,
|
||||
expected_outcome: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
tool_call: dict[str, object] = {
|
||||
"action": action,
|
||||
"args": args or {},
|
||||
}
|
||||
if purpose is not None:
|
||||
tool_call["purpose"] = purpose
|
||||
if expected_outcome is not None:
|
||||
tool_call["expected_outcome"] = expected_outcome
|
||||
return {
|
||||
"tool_call": {
|
||||
"action": action,
|
||||
"args": args or {},
|
||||
},
|
||||
"tool_call": tool_call,
|
||||
"result": result or {},
|
||||
}
|
||||
|
||||
@@ -50,6 +62,33 @@ def test_first_time_synthesis_has_literal_steps_and_no_parameters() -> None:
|
||||
assert skill.parameters == {}
|
||||
|
||||
|
||||
def test_synthesis_preserves_action_metadata_for_reuse_and_embedding() -> None:
|
||||
skill = synthesize_flow_skill(
|
||||
"open settings",
|
||||
[
|
||||
_record(
|
||||
"tap",
|
||||
{"x": 12, "y": 34},
|
||||
purpose="Open the settings tab.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert skill.steps == [
|
||||
FlowStep(
|
||||
"tap",
|
||||
{"x": 12, "y": 34},
|
||||
purpose="Open the settings tab.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
]
|
||||
assert FlowStep.from_dict(skill.steps[0].to_dict()) == skill.steps[0]
|
||||
embedding_text = skill_embedding_text(skill)
|
||||
assert "Open the settings tab." in embedding_text
|
||||
assert "The settings page is visible." in embedding_text
|
||||
|
||||
|
||||
def test_second_execution_promotes_differing_argument_to_parameter() -> None:
|
||||
store = SkillStore()
|
||||
store.create_version(
|
||||
|
||||
@@ -632,6 +632,36 @@ def test_anthropic_client_tool_only_response_has_none_thinking_and_text_output()
|
||||
assert decision.text_output is None
|
||||
|
||||
|
||||
def test_anthropic_client_separates_required_action_metadata_from_arguments() -> None:
|
||||
messages = FakeMessages(
|
||||
response={
|
||||
"content": [
|
||||
{
|
||||
"type": "tool_use",
|
||||
"name": "tap",
|
||||
"input": {
|
||||
"x": 10,
|
||||
"y": 20,
|
||||
"purpose": "Open the account screen.",
|
||||
"expected_outcome": "The account screen is visible.",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
client = AnthropicToolCallingClient(
|
||||
model="test-model", transport=FakeTransport(messages)
|
||||
)
|
||||
|
||||
decision = client.decide(
|
||||
system_prompt="s", user_prompt="u", screenshot=None, tools=[TAP_SPEC], timeout=1
|
||||
)
|
||||
|
||||
assert decision.arguments == {"x": 10, "y": 20}
|
||||
assert decision.purpose == "Open the account screen."
|
||||
assert decision.expected_outcome == "The account screen is visible."
|
||||
|
||||
|
||||
def test_openai_client_captures_reasoning_content() -> None:
|
||||
completions = FakeCompletions(
|
||||
response={
|
||||
@@ -691,6 +721,42 @@ def test_openai_client_captures_message_content_as_text_output() -> None:
|
||||
assert decision.text_output == "Previous step succeeded. Now tapping login."
|
||||
|
||||
|
||||
def test_openai_client_separates_required_action_metadata_from_arguments() -> None:
|
||||
completions = FakeCompletions(
|
||||
response={
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"name": "tap",
|
||||
"arguments": (
|
||||
'{"x": 5, "y": 6, '
|
||||
'"purpose": "Open settings.", '
|
||||
'"expected_outcome": "Settings is visible."}'
|
||||
),
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
client = OpenAIToolCallingClient(
|
||||
model="test-model", transport=FakeOpenAITransport(completions)
|
||||
)
|
||||
|
||||
decision = client.decide(
|
||||
system_prompt="s", user_prompt="u", screenshot=None, tools=[TAP_SPEC], timeout=1
|
||||
)
|
||||
|
||||
assert decision.arguments == {"x": 5, "y": 6}
|
||||
assert decision.purpose == "Open settings."
|
||||
assert decision.expected_outcome == "Settings is visible."
|
||||
|
||||
|
||||
def test_openai_client_no_reasoning_content_gives_none_thinking() -> None:
|
||||
completions = FakeCompletions(
|
||||
response={
|
||||
|
||||
@@ -13,7 +13,9 @@ from runtime.tool_specs import (
|
||||
)
|
||||
|
||||
|
||||
def test_action_tool_specs_has_five_entries_and_all_tool_specs_adds_finish_task() -> None:
|
||||
def test_action_tool_specs_has_five_entries_and_all_tool_specs_adds_finish_task() -> (
|
||||
None
|
||||
):
|
||||
assert len(ACTION_TOOL_SPECS) == 5
|
||||
assert len(ALL_TOOL_SPECS) == 6
|
||||
assert ALL_TOOL_SPECS == [*ACTION_TOOL_SPECS, FINISH_TASK_SPEC]
|
||||
@@ -33,32 +35,70 @@ def test_every_tool_spec_schema_forbids_additional_properties() -> None:
|
||||
|
||||
|
||||
def test_tap_spec_requires_x_and_y() -> None:
|
||||
assert TAP_SPEC.parameters["required"] == ["x", "y"]
|
||||
assert set(TAP_SPEC.parameters["properties"]) == {"x", "y"}
|
||||
assert TAP_SPEC.parameters["required"] == ["x", "y", "purpose", "expected_outcome"]
|
||||
assert set(TAP_SPEC.parameters["properties"]) == {
|
||||
"x",
|
||||
"y",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
}
|
||||
|
||||
|
||||
def test_swipe_spec_requires_coordinates_and_makes_duration_optional() -> None:
|
||||
assert SWIPE_SPEC.parameters["required"] == ["start_x", "start_y", "end_x", "end_y"]
|
||||
assert SWIPE_SPEC.parameters["required"] == [
|
||||
"start_x",
|
||||
"start_y",
|
||||
"end_x",
|
||||
"end_y",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
]
|
||||
assert set(SWIPE_SPEC.parameters["properties"]) == {
|
||||
"start_x",
|
||||
"start_y",
|
||||
"end_x",
|
||||
"end_y",
|
||||
"duration_ms",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
}
|
||||
assert "duration_ms" not in SWIPE_SPEC.parameters["required"]
|
||||
assert SWIPE_SPEC.parameters["properties"]["duration_ms"]["default"] == 500
|
||||
|
||||
|
||||
def test_input_text_spec_requires_text() -> None:
|
||||
assert INPUT_TEXT_SPEC.parameters["required"] == ["text"]
|
||||
assert set(INPUT_TEXT_SPEC.parameters["properties"]) == {"text"}
|
||||
assert INPUT_TEXT_SPEC.parameters["required"] == [
|
||||
"text",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
]
|
||||
assert set(INPUT_TEXT_SPEC.parameters["properties"]) == {
|
||||
"text",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
}
|
||||
|
||||
|
||||
def test_launch_and_terminate_app_specs_require_app_id() -> None:
|
||||
for spec in (LAUNCH_APP_SPEC, TERMINATE_APP_SPEC):
|
||||
assert spec.parameters["required"] == ["app_id"]
|
||||
assert set(spec.parameters["properties"]) == {"app_id"}
|
||||
assert spec.parameters["required"] == [
|
||||
"app_id",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
]
|
||||
assert set(spec.parameters["properties"]) == {
|
||||
"app_id",
|
||||
"purpose",
|
||||
"expected_outcome",
|
||||
}
|
||||
|
||||
|
||||
def test_action_metadata_is_required_nonempty_text() -> None:
|
||||
for spec in ACTION_TOOL_SPECS:
|
||||
for field_name in ("purpose", "expected_outcome"):
|
||||
field = spec.parameters["properties"][field_name]
|
||||
assert field["type"] == "string"
|
||||
assert field["minLength"] == 1
|
||||
|
||||
|
||||
def test_finish_task_spec_requires_success_and_reason() -> None:
|
||||
|
||||
@@ -61,7 +61,9 @@ def test_observe_leaves_current_page_unchanged_without_semantic_scene() -> None:
|
||||
assert model.state.current_page == "Chat"
|
||||
|
||||
|
||||
def test_observe_updates_current_app_for_successful_launch_and_clear_for_terminate() -> None:
|
||||
def test_observe_updates_current_app_for_successful_launch_and_clear_for_terminate() -> (
|
||||
None
|
||||
):
|
||||
model = WorldModel(config=WorldConfig(history_size=2))
|
||||
launch = PlannedStep(
|
||||
action="launch_app",
|
||||
@@ -188,3 +190,22 @@ def test_observe_appends_semantic_or_raw_scene_history_with_eviction() -> None:
|
||||
assert [event.action for event in model.state.history] == ["second", "third"]
|
||||
assert model.state.history[0].scene_summary.to_dict() == _scene().to_dict()
|
||||
assert model.state.history.maxlen == 2
|
||||
|
||||
|
||||
def test_observe_records_executed_action_arguments_and_metadata() -> None:
|
||||
model = WorldModel(config=WorldConfig(history_size=2))
|
||||
step = PlannedStep(
|
||||
action="tap",
|
||||
description="Open settings.",
|
||||
args={"x": 12, "y": 34},
|
||||
purpose="Open settings.",
|
||||
expected_outcome="The settings page is visible.",
|
||||
)
|
||||
|
||||
model.observe(_scene(), _semantic_scene("Settings"), step, _result(step))
|
||||
|
||||
event = model.state.history[0]
|
||||
assert event.action == "tap"
|
||||
assert event.arguments == {"x": 12, "y": 34}
|
||||
assert event.purpose == "Open settings."
|
||||
assert event.expected_outcome == "The settings page is visible."
|
||||
|
||||
@@ -66,12 +66,18 @@ def test_world_event_with_rationale_thinking_page() -> None:
|
||||
success=True,
|
||||
rationale="Previous step opened settings. Now tapping account.",
|
||||
thinking="I should navigate to account settings.",
|
||||
arguments={"x": 12, "y": 34},
|
||||
purpose="Open account settings.",
|
||||
expected_outcome="The account settings page is visible.",
|
||||
page="Settings",
|
||||
)
|
||||
data = event.to_dict()
|
||||
|
||||
assert data["rationale"] == "Previous step opened settings. Now tapping account."
|
||||
assert data["thinking"] == "I should navigate to account settings."
|
||||
assert data["arguments"] == {"x": 12, "y": 34}
|
||||
assert data["purpose"] == "Open account settings."
|
||||
assert data["expected_outcome"] == "The account settings page is visible."
|
||||
assert data["page"] == "Settings"
|
||||
assert data["scene_summary"] is None
|
||||
|
||||
@@ -82,6 +88,9 @@ def test_world_event_all_optional_fields_none() -> None:
|
||||
|
||||
assert data["rationale"] is None
|
||||
assert data["thinking"] is None
|
||||
assert data["arguments"] == {}
|
||||
assert data["purpose"] is None
|
||||
assert data["expected_outcome"] is None
|
||||
assert data["page"] is None
|
||||
assert data["scene_summary"] is None
|
||||
assert data["action"] == "swipe"
|
||||
|
||||
Reference in New Issue
Block a user