M2b:导入改造产出可审阅灵胚(启镜物化世谱)与组装管线(符文展开/火候进入 IR),Kernel 打通采样参数链路
This commit is contained in:
1 parent
e6a3b7121f
commit
5f36e3785c
86 files changed
+424
-14455
No files matched your search
@@ -1,59 +0,0 @@
|
||||
"""道体注入与完整性校验测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.chat.send_message import _contract_system_messages
|
||||
from app.modules.runtime.contract import RuntimeContract
|
||||
from app.modules.runtime.integrity import validate_daoti_integrity
|
||||
|
||||
|
||||
def _contract(**kw: object) -> RuntimeContract:
|
||||
base: dict[str, object] = {
|
||||
"character_id": "c",
|
||||
"session_id": "s",
|
||||
"run_id": "r",
|
||||
"model_hint": "m",
|
||||
"trace_id": "t",
|
||||
}
|
||||
base.update(kw)
|
||||
return RuntimeContract(**base)
|
||||
|
||||
|
||||
def test_system_prompt_injected() -> None:
|
||||
msgs = _contract_system_messages(_contract(system_prompt="特殊系统指令"))
|
||||
assert "特殊系统指令" in msgs
|
||||
|
||||
|
||||
def test_mission_injected() -> None:
|
||||
msgs = _contract_system_messages(_contract(mission="守护用户"))
|
||||
assert any("守护用户" in m for m in msgs)
|
||||
|
||||
|
||||
def test_safety_guidelines_precede_safety_constraints() -> None:
|
||||
c = _contract(safety_guidelines="道体边界", safety_constraints=["司命约束"])
|
||||
msgs = _contract_system_messages(c)
|
||||
assert msgs.index("行为边界:\n道体边界") < msgs.index("安全约束:\n- 司命约束")
|
||||
|
||||
|
||||
def test_integrity_pass_when_consistent() -> None:
|
||||
c = _contract(mission="M", safety_guidelines="S")
|
||||
validate_daoti_integrity(c, mission="M", safety_guidelines="S") # 不抛即通过
|
||||
|
||||
|
||||
def test_integrity_raises_on_mission_tamper() -> None:
|
||||
c = _contract(mission="M")
|
||||
with pytest.raises(ValueError):
|
||||
validate_daoti_integrity(c, mission="被篡改")
|
||||
|
||||
|
||||
def test_integrity_raises_on_safety_tamper() -> None:
|
||||
c = _contract(safety_guidelines="S")
|
||||
with pytest.raises(ValueError):
|
||||
validate_daoti_integrity(c, safety_guidelines="被篡改")
|
||||
|
||||
|
||||
def test_integrity_skips_none_fields() -> None:
|
||||
# 道体字段未设(None)时跳过校验
|
||||
validate_daoti_integrity(_contract(), mission=None, safety_guidelines=None)
|
||||
@@ -1,48 +0,0 @@
|
||||
"""灵核情绪状态 system prompt 注入测试(设计 §11.5)。
|
||||
|
||||
验证 ``_contract_system_messages`` 把 ``emotion_state`` 注入到正确位置
|
||||
(personality_summary 后、memory_context 前),且 emotion_state=None 时不注入。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.application.chat.send_message import _contract_system_messages
|
||||
from app.modules.runtime.contract import RuntimeContract
|
||||
|
||||
|
||||
def _contract(**overrides) -> RuntimeContract:
|
||||
base = {
|
||||
"character_id": "c1",
|
||||
"session_id": "s1",
|
||||
"run_id": "r1",
|
||||
"model_hint": "m",
|
||||
"trace_id": "t",
|
||||
}
|
||||
base.update(overrides)
|
||||
return RuntimeContract(**base)
|
||||
|
||||
|
||||
def test_emotion_state_injected_between_personality_and_memory() -> None:
|
||||
contract = _contract(
|
||||
personality_summary="人格X",
|
||||
emotion_state="[当前情绪] 感动(强度 70%)",
|
||||
memory_context="记忆Y",
|
||||
)
|
||||
messages = _contract_system_messages(contract)
|
||||
personality_idx = next(i for i, m in enumerate(messages) if "人格X" in m)
|
||||
emotion_idx = next(i for i, m in enumerate(messages) if "感动" in m)
|
||||
memory_idx = next(i for i, m in enumerate(messages) if "记忆Y" in m)
|
||||
assert personality_idx < emotion_idx < memory_idx
|
||||
|
||||
|
||||
def test_emotion_state_omitted_when_none() -> None:
|
||||
contract = _contract(personality_summary="人格X", memory_context="记忆Y")
|
||||
messages = _contract_system_messages(contract)
|
||||
assert all("当前情绪" not in m for m in messages)
|
||||
|
||||
|
||||
def test_emotion_state_included_without_memory() -> None:
|
||||
"""emotion_state 独立于 memory_context 存在。"""
|
||||
contract = _contract(emotion_state="[当前情绪] 关切(强度 50%)")
|
||||
messages = _contract_system_messages(contract)
|
||||
assert any("关切" in m for m in messages)
|
||||
@@ -1,41 +0,0 @@
|
||||
"""世谱注入位置测试:before/after 相对 personality_summary。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.application.chat.send_message import _contract_system_messages
|
||||
from app.modules.runtime.contract import RuntimeContract
|
||||
|
||||
|
||||
def _contract(**kw: object) -> RuntimeContract:
|
||||
base: dict[str, object] = {
|
||||
"character_id": "c",
|
||||
"session_id": "s",
|
||||
"run_id": "r",
|
||||
"model_hint": "m",
|
||||
"trace_id": "t",
|
||||
}
|
||||
base.update(kw)
|
||||
return RuntimeContract(**base)
|
||||
|
||||
|
||||
def test_lorebook_before_precedes_personality() -> None:
|
||||
c = _contract(personality_summary="人格X", lorebook_before="世界设定前")
|
||||
msgs = _contract_system_messages(c)
|
||||
assert msgs.index("世界设定前") < msgs.index("当前人格:\n人格X")
|
||||
|
||||
|
||||
def test_lorebook_after_follows_personality() -> None:
|
||||
c = _contract(personality_summary="人格X", lorebook_after="世界设定后")
|
||||
msgs = _contract_system_messages(c)
|
||||
assert msgs.index("当前人格:\n人格X") < msgs.index("世界设定后")
|
||||
|
||||
|
||||
def test_no_lorebook_when_none() -> None:
|
||||
msgs = _contract_system_messages(_contract())
|
||||
assert not any("世界设定" in m for m in msgs)
|
||||
|
||||
|
||||
def test_lorebook_without_personality_still_injects() -> None:
|
||||
c = _contract(lorebook_before="前", lorebook_after="后")
|
||||
msgs = _contract_system_messages(c)
|
||||
assert "前" in msgs and "后" in msgs
|
||||
@@ -1,8 +0,0 @@
|
||||
"""测试 send_message 中的目标自动注入(集成验证)。"""
|
||||
|
||||
|
||||
def test_auto_persist_session_goal_is_callable():
|
||||
"""验证 auto_persist_session_goal 函数可导入且可调用。"""
|
||||
from app.services.goal_persist import auto_persist_session_goal
|
||||
|
||||
assert callable(auto_persist_session_goal)
|
||||
@@ -1,179 +0,0 @@
|
||||
"""对话 temporal_context 注入测试(辰正 §8.1)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.application.agent.runtime import AgentTurnResult
|
||||
from app.application.chat.send_message import _contract_system_messages
|
||||
from app.main import app
|
||||
from app.modules.runtime.contract import RuntimeContract
|
||||
|
||||
|
||||
def _make_contract(temporal: str | None) -> RuntimeContract:
|
||||
return RuntimeContract(
|
||||
character_id="char_x",
|
||||
session_id="sess_x",
|
||||
run_id="run_x",
|
||||
model_hint="default",
|
||||
system_instructions=["base"],
|
||||
personality_summary="人格X",
|
||||
memory_context="记忆Y",
|
||||
temporal_context=temporal,
|
||||
trace_id="trace_x",
|
||||
)
|
||||
|
||||
|
||||
def test_contract_system_messages_includes_temporal_context():
|
||||
messages = _contract_system_messages(_make_contract("现在是子时·深夜(01:42)。"))
|
||||
assert any("子时·深夜" in m for m in messages)
|
||||
|
||||
|
||||
def test_contract_system_messages_no_temporal_context_when_none():
|
||||
messages = _contract_system_messages(_make_contract(None))
|
||||
assert all("子时" not in m for m in messages)
|
||||
|
||||
|
||||
def test_contract_system_messages_temporal_after_memory_before_safety():
|
||||
"""注入位置:记忆之后、安全约束之前(设计 §8.1)。"""
|
||||
contract = RuntimeContract(
|
||||
character_id="c",
|
||||
session_id="s",
|
||||
run_id="r",
|
||||
model_hint="m",
|
||||
system_instructions=["base"],
|
||||
memory_context="MEMORY_MARKER",
|
||||
temporal_context="TEMPORAL_MARKER",
|
||||
safety_constraints=["SAFETY_MARKER"],
|
||||
trace_id="t",
|
||||
)
|
||||
messages = _contract_system_messages(contract)
|
||||
joined = "\n".join(messages)
|
||||
assert joined.index("MEMORY_MARKER") < joined.index("TEMPORAL_MARKER")
|
||||
assert joined.index("TEMPORAL_MARKER") < joined.index("SAFETY_MARKER")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §12 降级路径守护(设计 §12:辰正永不阻塞对话)
|
||||
# 1. event_store=None → 跳过辰正调用,temporal_context 保持 None,对话照常完成
|
||||
# 2. compute_live_snapshot raise → except 捕获,temporal_context 保持 None,对话不阻塞
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _create_session(client: AsyncClient) -> str:
|
||||
resp = await client.post("/api/v1/chat/sessions", json={"title": "辰正降级"})
|
||||
assert resp.status_code == 200
|
||||
return str(resp.json()["session_id"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def temporal_client(
|
||||
auth_headers: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
||||
) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""HTTP client:mock run_agent_turn,并通过 messages 捕获最终 system 消息用于断言。
|
||||
|
||||
复用 test_send_message_agent.py 的 mock 边界(只 mock run_agent_turn,跑真实 send_message
|
||||
全链路),让降级路径在生产代码路径中被真实驱动。
|
||||
"""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
async def capture_agent_turn(*, messages: Any, **_kwargs: object) -> AgentTurnResult:
|
||||
captured["messages"] = messages
|
||||
return AgentTurnResult(content="好的。", reasoning="")
|
||||
|
||||
monkeypatch.setattr("app.application.chat.send_message.run_agent_turn", capture_agent_turn)
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(
|
||||
transport=transport, base_url="http://test", headers=auth_headers
|
||||
) as http_client:
|
||||
http_client._temporal_captured = captured # type: ignore[attr-defined]
|
||||
yield http_client
|
||||
|
||||
|
||||
def _system_text(messages: list[Any]) -> str:
|
||||
return "\n".join(str(m.content) for m in messages if getattr(m, "role", "") == "system")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_context_skipped_when_event_store_is_none(
|
||||
temporal_client: AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""§12 降径 1:event_store=None 时跳过辰正调用,对话照常完成且不注入 temporal_context。"""
|
||||
from app.application.chat.send_message import drain_pending_streams
|
||||
|
||||
# 降径 1:明确将 event_store 置 None(route 经 getattr 默认也是 None,此处显式断言降径)
|
||||
# 测试 app.state 默认无 event_store 属性,monkeypatch.setattr 对对象属性可在不存在时
|
||||
# 创建并在 teardown 时删除(恢复"无属性"原状)。
|
||||
monkeypatch.setattr(app.state, "event_store", None, raising=False)
|
||||
|
||||
# spy:若被调用即标记(不应发生)
|
||||
called: dict[str, bool] = {"hit": False}
|
||||
|
||||
import app.services.time_sense as time_sense_mod
|
||||
|
||||
real_compute = time_sense_mod.compute_live_snapshot
|
||||
|
||||
async def spy_compute(*args: Any, **kwargs: Any) -> Any:
|
||||
called["hit"] = True
|
||||
return await real_compute(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(time_sense_mod, "compute_live_snapshot", spy_compute)
|
||||
|
||||
session_id = await _create_session(temporal_client)
|
||||
sent = await temporal_client.post(
|
||||
f"/api/v1/chat/sessions/{session_id}/messages",
|
||||
json={"content": "你好", "modality": "text"},
|
||||
)
|
||||
assert sent.status_code == 200
|
||||
await drain_pending_streams()
|
||||
|
||||
# 真实行为断言:辰正未被调用
|
||||
assert called["hit"] is False
|
||||
# 对话完成:run_agent_turn 被正常调用且 system 消息中无辰正上下文标记
|
||||
captured = temporal_client._temporal_captured # type: ignore[attr-defined]
|
||||
sys_text = _system_text(captured["messages"])
|
||||
assert "今日命中" not in sys_text # render_temporal_context 的稳定首行尾块标记
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_context_swallowed_when_compute_raises(
|
||||
temporal_client: AsyncClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""§12 降径 2:compute_live_snapshot raise 时 except 捕获,对话不阻塞且 temporal_context=None。"""
|
||||
|
||||
async def boom(*_args: Any, **_kwargs: Any) -> Any:
|
||||
raise RuntimeError("辰正服务故障(模拟)")
|
||||
|
||||
monkeypatch.setattr("app.services.time_sense.compute_live_snapshot", boom)
|
||||
|
||||
from app.application.chat.send_message import drain_pending_streams
|
||||
|
||||
session_id = await _create_session(temporal_client)
|
||||
sent = await temporal_client.post(
|
||||
f"/api/v1/chat/sessions/{session_id}/messages",
|
||||
json={"content": "你好", "modality": "text"},
|
||||
)
|
||||
# 真实行为断言:异常被 except 吞掉,HTTP 仍 200,对话照常完成
|
||||
assert sent.status_code == 200
|
||||
run_id = str(sent.json()["run_id"])
|
||||
await drain_pending_streams()
|
||||
|
||||
# 对话成功落库:run 进入终态 completed(而非 failed)
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.runtime import Run
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
run = (await db.execute(select(Run).where(Run.id == run_id))).scalar_one()
|
||||
assert run.status == "completed"
|
||||
|
||||
# temporal_context 未注入:system 消息中无辰正上下文标记
|
||||
captured = temporal_client._temporal_captured # type: ignore[attr-defined]
|
||||
sys_text = _system_text(captured["messages"])
|
||||
assert "子时" not in sys_text and "时态" not in sys_text
|
||||
Reference in new issue
Block a user