test_smoke_model.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. from __future__ import annotations
  2. import runpy
  3. import subprocess
  4. from pathlib import Path
  5. from types import SimpleNamespace
  6. import pytest
  7. ROOT = Path(__file__).resolve().parents[3]
  8. SMOKE = runpy.run_path(ROOT / "scripts" / "smoke-python-runtime.py")
  9. @pytest.mark.parametrize(
  10. ("behavior", "error"),
  11. [
  12. ("read-current", None),
  13. ("no-verify-tool", "verify turn made no model-requested tool call"),
  14. ("no-create-tool", "create turn made no model-requested tool call"),
  15. ("create-error", "create turn ended with.*AUTH.*401"),
  16. ("stale-answer", "verify turn returned"),
  17. ("missing-create", "real-model tool turn did not create"),
  18. ("wrong-create", "real-model tool turn wrote unexpected text"),
  19. ("modify-verify", "real-model tool turn wrote unexpected text"),
  20. ],
  21. )
  22. def test_live_smoke_requires_fresh_file_observation(
  23. monkeypatch: pytest.MonkeyPatch, behavior: str, error: str | None,
  24. ) -> None:
  25. import deepseek_harness
  26. smoke_live = SMOKE["smoke_sdk_live"]
  27. sentinel = SMOKE["LIVE_API_SENTINEL"]
  28. prompts: list[str] = []
  29. session_ids: list[str] = []
  30. log_checks: list[Path] = []
  31. class ScriptedHarness:
  32. def __init__(self, *, cwd: str, **_kwargs: object) -> None:
  33. self.marker = Path(cwd) / "live-api-marker.txt"
  34. def __enter__(self) -> ScriptedHarness:
  35. return self
  36. def __exit__(self, *_args: object) -> None:
  37. pass
  38. def run(self, prompt: str, *, session_id: str) -> SimpleNamespace:
  39. prompts.append(prompt)
  40. session_ids.append(session_id)
  41. events = [{"type": "tool/call"}]
  42. if len(prompts) == 1:
  43. assert not self.marker.exists()
  44. if behavior == "create-error":
  45. return SimpleNamespace(finish_reason="error", final_response="", events=[{
  46. "type": "turn/end",
  47. "data": {"turn": 1, "reason": {
  48. "kind": "error", "error": {"code": "AUTH", "status": 401},
  49. }},
  50. }])
  51. if behavior == "no-create-tool":
  52. events = []
  53. if behavior != "missing-create":
  54. self.marker.write_text(
  55. ("wrong" if behavior == "wrong-create" else sentinel) + "\n",
  56. encoding="utf-8",
  57. )
  58. response = sentinel
  59. else:
  60. current = self.marker.read_text(encoding="utf-8").strip()
  61. assert current != sentinel, "verification must require new world state"
  62. assert all(current not in text for text in prompts), "prompts must not reveal the answer"
  63. response = sentinel if behavior == "stale-answer" else current
  64. if behavior == "no-verify-tool":
  65. events = []
  66. if behavior == "modify-verify":
  67. self.marker.write_text("changed\n", encoding="utf-8")
  68. return SimpleNamespace(finish_reason="completed", final_response=response, events=events)
  69. monkeypatch.setenv("DEEPSEEK_API_KEY", "fixture-key")
  70. monkeypatch.setenv("DEEPSEEK_BASE_URL", "https://fixture.invalid")
  71. monkeypatch.setattr(deepseek_harness, "DeepSeekHarness", ScriptedHarness)
  72. monkeypatch.setitem(smoke_live.__globals__, "assert_zstd_session_log", log_checks.append)
  73. if error is None:
  74. smoke_live()
  75. assert len(prompts) == 2
  76. assert session_ids[0] == session_ids[1]
  77. assert len(log_checks) == 1
  78. else:
  79. with pytest.raises(AssertionError, match=error):
  80. smoke_live()
  81. assert not log_checks
  82. @pytest.mark.parametrize(
  83. ("prompt_name", "expected"),
  84. [
  85. ("SNAPSHOT_DIRECT_CHILD_PROMPT", "DIRECT_CHILD_OK"),
  86. ("SNAPSHOT_WORKFLOW_CHILD_PROMPT", "WORKFLOW_CHILD_OK"),
  87. ],
  88. )
  89. def test_child_prompt_precedes_runtime_context(prompt_name: str, expected: str) -> None:
  90. chunks = SMOKE["completion_chunks"]({
  91. "messages": [
  92. {"role": "user", "content": SMOKE[prompt_name]},
  93. {"role": "user", "content": "Current runtime context"},
  94. ],
  95. })
  96. assert any(
  97. choice.get("delta", {}).get("content") == expected
  98. for chunk in chunks
  99. for choice in chunk.get("choices", [])
  100. )
  101. def test_mcp_smoke_requests_the_discovered_tool() -> None:
  102. chunks = SMOKE["completion_chunks"]({
  103. "messages": [{"role": "user", "content": SMOKE["MCP_PROMPT"]}],
  104. "tools": [{"type": "function", "function": {"name": "mcp__fixture__add"}}],
  105. })
  106. calls = [
  107. call
  108. for chunk in chunks
  109. for choice in chunk.get("choices", [])
  110. for call in choice.get("delta", {}).get("tool_calls", [])
  111. ]
  112. assert calls[0]["function"] == {
  113. "name": "mcp__fixture__add",
  114. "arguments": '{"a": 19, "b": 23}',
  115. }
  116. def test_mcp_smoke_accepts_the_external_server_result() -> None:
  117. chunks = SMOKE["completion_chunks"]({
  118. "messages": [
  119. {"role": "user", "content": SMOKE["MCP_PROMPT"]},
  120. {
  121. "role": "assistant",
  122. "tool_calls": [{
  123. "id": "mcp-add",
  124. "type": "function",
  125. "function": {"name": "mcp__fixture__add", "arguments": '{}'},
  126. }],
  127. },
  128. {"role": "tool", "tool_call_id": "mcp-add", "content": "42"},
  129. ],
  130. })
  131. assert any(
  132. choice.get("delta", {}).get("content") == SMOKE["MCP_TEXT"]
  133. for chunk in chunks
  134. for choice in chunk.get("choices", [])
  135. )
  136. def test_snapshot_comparison_normalizes_only_session_generation_provenance() -> None:
  137. normalize = SMOKE["normalize_session_format_comparison"]
  138. expected = {
  139. "header": {"type": "session", "version": 0, "otherVersion": 7},
  140. "accepted": {
  141. "type": "session-log-deepseek/delivery-accepted",
  142. "data": {"sessionId": "s", "throughSeq": 4},
  143. },
  144. "source": {
  145. "kind": "session-reference",
  146. "references": [{"sessionId": "other", "capturedThroughSeq": 8}],
  147. },
  148. }
  149. actual = {
  150. "header": {"type": "session", "version": 1, "otherVersion": 7},
  151. "accepted": {
  152. "type": "session-log-deepseek/delivery-accepted",
  153. "data": {"sessionId": "s", "sessionFormatVersion": 1, "throughSeq": 4},
  154. },
  155. "source": {
  156. "kind": "session-reference",
  157. "references": [{
  158. "sessionId": "other",
  159. "capturedFormatVersion": 1,
  160. "capturedThroughSeq": 8,
  161. }],
  162. },
  163. }
  164. assert normalize(expected) == normalize(actual)
  165. assert normalize(expected)["header"]["otherVersion"] == 7
  166. def test_snapshot_value_normalizes_embedded_assistant_stream_timing() -> None:
  167. normalize = SMOKE["normalize_snapshot_value"]
  168. event = {
  169. "type": "assistant/message",
  170. "seq": 4,
  171. "time": 100,
  172. "data": {
  173. "stream": [
  174. {"type": "chunk", "time": 101, "chunk": {"type": "finish"}},
  175. {"type": "text-chunks", "time0": 102, "dt": [1, 2], "texts": ["a", "b", "c"]},
  176. ],
  177. },
  178. }
  179. normalized = normalize(event, [])
  180. assert normalized["time"] == 0
  181. assert normalized["data"]["stream"] == [
  182. {"type": "chunk", "time": 0, "chunk": {"type": "finish"}},
  183. {"type": "text-chunks", "time0": 0, "dt": [0, 0], "texts": ["a", "b", "c"]},
  184. ]
  185. def test_snapshot_comparison_expands_embedded_assistant_streams() -> None:
  186. normalize = SMOKE["normalize_session_format_comparison"]
  187. expected = [
  188. {
  189. "type": "assistant/chunk",
  190. "seq": 4,
  191. "time": 0,
  192. "data": {"turn": 1, "step": 1, "chunk": {
  193. "type": "text-delta", "index": 0, "text": "done",
  194. }},
  195. },
  196. {
  197. "type": "assistant/message",
  198. "seq": 5,
  199. "time": 0,
  200. "data": {"turn": 1, "step": 1, "message": {"role": "assistant"}},
  201. "sourceEventSeqs": [4],
  202. "surfaceOp": "append",
  203. },
  204. ]
  205. actual = [{
  206. "type": "assistant/message",
  207. "seq": 4,
  208. "time": 0,
  209. "data": {
  210. "turn": 1,
  211. "step": 1,
  212. "message": {"role": "assistant"},
  213. "stream": [{
  214. "type": "text-chunks", "time0": 0, "index": 0, "dt": [], "texts": ["done"],
  215. }],
  216. },
  217. "surfaceOp": "append",
  218. }]
  219. assert normalize(actual, 2) == normalize(expected, 1)
  220. tool_result = {
  221. "type": "tool/result",
  222. "data": {"turn": 1, "step": 1},
  223. "sourceEventSeqs": [4],
  224. }
  225. assert normalize(tool_result, 1)["sourceEventSeqs"] == [4]
  226. assert normalize(tool_result, 2)["sourceEventSeqs"] == [4]
  227. def test_snapshot_stream_expands_reasoning_and_tool_call_records() -> None:
  228. expand = SMOKE["expand_snapshot_stream_member"]
  229. assert expand({
  230. "type": "reasoning-chunks", "time0": 0, "index": 1,
  231. "dt": [], "texts": ["think"],
  232. }) == [{"type": "reasoning-delta", "index": 1, "text": "think"}]
  233. assert expand({
  234. "type": "tool-call-chunks", "time0": 0, "index": 2,
  235. "id": "call-1", "name": "read", "dt": [1], "args": ["{", "}"],
  236. }) == [
  237. {"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "{"},
  238. {"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "}"},
  239. ]
  240. def test_snapshot_file_builder_order_is_checked_outside_update_mode(tmp_path: Path) -> None:
  241. compare = SMOKE["compare_snapshot_files"]
  242. with pytest.raises(AssertionError, match="snapshot builder produced"):
  243. compare({}, False, tmp_path, ("result.json",))
  244. def test_snapshot_comparison_expands_sdk_wrapped_attempts() -> None:
  245. normalize = SMOKE["normalize_session_format_comparison"]
  246. actual = [{
  247. "method": "session.event",
  248. "payload": {
  249. "sessionId": "s",
  250. "event": {
  251. "type": "assistant/attempt",
  252. "seq": 7,
  253. "time": 0,
  254. "data": {
  255. "turn": 1,
  256. "step": 1,
  257. "stream": [{"type": "chunk", "time": 0, "chunk": {"type": "finish"}}],
  258. },
  259. },
  260. },
  261. }]
  262. assert normalize(actual) == [{
  263. "method": "session.event",
  264. "payload": {
  265. "sessionId": "s",
  266. "event": {
  267. "type": "assistant/chunk",
  268. "data": {"turn": 1, "step": 1, "chunk": {"type": "finish"}},
  269. },
  270. },
  271. }]
  272. def test_snapshot_generation_names_select_highest_role_without_double_counting(
  273. tmp_path: Path,
  274. ) -> None:
  275. render = SMOKE["snapshot_session_filename"]
  276. select = SMOKE["selected_snapshot_session_files"]
  277. assert render(0, 0) == "session.jsonl"
  278. assert render(0, 2) == "session.v2.jsonl"
  279. assert render(3, 0) == "session.3.jsonl"
  280. assert render(3, 2) == "session.3.v2.jsonl"
  281. (tmp_path / "session.jsonl").write_text(
  282. '{"type":"session","version":0}\n', encoding="utf-8",
  283. )
  284. (tmp_path / "session.v1.jsonl").write_text(
  285. '{"type":"session","version":1}\n', encoding="utf-8",
  286. )
  287. (tmp_path / "session.1.jsonl").write_text(
  288. '{"type":"session","version":0}\n', encoding="utf-8",
  289. )
  290. assert {index: path.name for index, path in select(tmp_path).items()} == {
  291. 0: "session.v1.jsonl",
  292. 1: "session.1.jsonl",
  293. }
  294. def test_snapshot_generation_filename_must_match_header(tmp_path: Path) -> None:
  295. (tmp_path / "session.v1.jsonl").write_text(
  296. '{"type":"session","version":0}\n', encoding="utf-8",
  297. )
  298. with pytest.raises(AssertionError, match="filename declares Session format v1"):
  299. SMOKE["selected_snapshot_session_files"](tmp_path)
  300. @pytest.mark.parametrize("returncode", [1, -1073741819, 3221225477])
  301. def test_profile_plugin_failure_reports_native_exit_status(monkeypatch: pytest.MonkeyPatch, returncode: int) -> None:
  302. def failed_install(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
  303. return subprocess.CompletedProcess(args=[], returncode=returncode, stdout="", stderr="")
  304. monkeypatch.setattr(subprocess, "run", failed_install)
  305. with pytest.raises(AssertionError) as error:
  306. SMOKE["smoke_sdk_profile_plugin"]("http://127.0.0.1:1")
  307. message = str(error.value)
  308. assert f"returncode={returncode}" in message
  309. assert f"0x{returncode & 0xffffffff:08x}" in message
  310. assert "stdout='' stderr=''" in message