chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user