Files
2026-07-13 13:12:33 +08:00

177 lines
5.4 KiB
Python

"""Unit tests for Agent._read_clarify_outcome.
meta_resolution stages one of five outcomes on ctx.metadata after the
awaiting branch runs (errors / cancelled / expired / race_lost — plus
resume, which is handled separately by _run_meta_resume). These tests
pin the user-visible text dictated by spec §10.
"""
from __future__ import annotations
import json
import pytest
from opensquilla.engine.agent import Agent
from opensquilla.engine.types import AgentConfig
from opensquilla.skills.meta.plan_serde import clarify_config_to_jsonable
from opensquilla.skills.meta.types import ClarifyField, ClarifyStepConfig
class _FakeAwaiting:
"""Mirrors AwaitingPeek's shape narrowly enough for the handler."""
def __init__(self, schema: ClarifyStepConfig) -> None:
self.run_id = "01TESTRUN0000000000000"
self.step_id = "collect"
self.awaiting_schema_json = json.dumps(
clarify_config_to_jsonable(schema), ensure_ascii=False,
)
@pytest.fixture()
def agent() -> Agent:
return Agent(
provider=None, # type: ignore[arg-type]
config=AgentConfig(model_id="stub"),
tool_definitions=[],
tool_handler=None,
)
@pytest.fixture()
def schema() -> ClarifyStepConfig:
return ClarifyStepConfig(
mode="form",
fields=(
ClarifyField(name="destination", type="string", required=True,
prompt="目的地"),
ClarifyField(name="days", type="int", required=True,
min=1, max=14, prompt="天数"),
),
intro="确认 2 件事。",
)
def test_no_outcome_returns_none(agent: Agent) -> None:
assert agent._read_clarify_outcome({}) is None
def test_parse_errors_render_error_lines_plus_form(
agent: Agent, schema: ClarifyStepConfig,
) -> None:
awaiting = _FakeAwaiting(schema)
metadata = {
"meta_clarify_errors": [
"destination: 'foo' bad",
"days: 'abc' is not an integer",
],
"meta_clarify_reprompt": awaiting,
}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, terminates = out
assert "未能解析回复" in text
assert "destination: 'foo' bad" in text
assert "abc" in text
assert "请回复以下字段" in text # form re-rendered
assert "destination" in text
assert terminates is True
# Metadata keys consumed
assert "meta_clarify_errors" not in metadata
assert "meta_clarify_reprompt" not in metadata
def test_user_cancel(agent: Agent, schema: ClarifyStepConfig) -> None:
metadata = {
"meta_clarify_cancelled": _FakeAwaiting(schema),
"meta_clarify_cancel_reason": "user_cancel",
}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, _ = out
assert text == "好,已取消。"
def test_three_strike_cancel(agent: Agent, schema: ClarifyStepConfig) -> None:
metadata = {
"meta_clarify_cancelled": _FakeAwaiting(schema),
"meta_clarify_cancel_reason": "parse_failure_limit",
}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, _ = out
assert "无法解析" in text
assert "已取消上一轮收集" in text
def test_expired(agent: Agent, schema: ClarifyStepConfig) -> None:
metadata = {"meta_clarify_expired": _FakeAwaiting(schema)}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, _ = out
assert "超时" in text
def test_race_lost(agent: Agent) -> None:
metadata = {"meta_clarify_race_lost": "01OLDRUN0000000000000"}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, _ = out
assert "已被处理" in text
def test_soft_progress_surfaces_captured_and_missing_fields(agent: Agent) -> None:
metadata = {
"meta_clarify_soft_progress": {
"filled": {"destination": "Tokyo"},
"newly_filled": ["destination"],
"missing_required": ["days"],
"ambiguous_fields": [
{"name": "budget", "reason": "多个金额都可能是预算"}
],
}
}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, terminates = out
assert "已记录" in text
assert "destination=Tokyo" in text
assert "还需要" in text
assert "days" in text
assert "budget" in text
assert "多个金额" in text
assert terminates is True
assert "meta_clarify_soft_progress" not in metadata
def test_proceed_blocked_surfaces_required_next_steps(agent: Agent) -> None:
metadata = {
"meta_clarify_proceed_blocked": {
"filled": {"destination": "Tokyo"},
"missing_required": ["days", "budget"],
}
}
out = agent._read_clarify_outcome(metadata)
assert out is not None
text, terminates = out
assert "现在还不能开始" in text
assert "还需要" in text
assert "days" in text
assert "budget" in text
assert "destination=Tokyo" in text
assert terminates is True
assert "meta_clarify_proceed_blocked" not in metadata
def test_resume_outcome_is_not_consumed_here(agent: Agent) -> None:
"""meta_resume is handled by _run_meta_resume, not this helper."""
metadata = {"meta_resume": ("fake", "fake")}
out = agent._read_clarify_outcome(metadata)
assert out is None
assert "meta_resume" in metadata # still there for the resume handler