177 lines
5.4 KiB
Python
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
|