test_smoke_model.py 13 KB

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