test_smoke_model.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. from __future__ import annotations
  2. import json
  3. import runpy
  4. import subprocess
  5. from pathlib import Path
  6. from types import SimpleNamespace
  7. import pytest
  8. from deepseek_harness import RunResult
  9. ROOT = Path(__file__).resolve().parents[3]
  10. SMOKE = runpy.run_path(ROOT / "scripts" / "smoke-python-runtime.py")
  11. def live_result(**overrides: object) -> RunResult:
  12. values = {
  13. "session_id": "installed-wheel-live-api",
  14. "final_response": SMOKE["LIVE_API_SENTINEL"],
  15. "finish_reason": "completed",
  16. "events": [{"type": "tool/call", "data": {"name": "unrelated_tool"}}],
  17. "notifications": [],
  18. }
  19. values.update(overrides)
  20. return RunResult(**values)
  21. @pytest.fixture
  22. def live_smoke(monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace:
  23. import deepseek_harness
  24. state = SimpleNamespace(
  25. prompts=[], session_ids=[], challenges=[], checked_logs=[], closed=False,
  26. create_bytes=SMOKE["LIVE_API_SENTINEL"].encode("utf-8"),
  27. create_result=live_result(), verify_result=live_result(), receipt_mode="copy",
  28. )
  29. globals_ = SMOKE["smoke_sdk_live"].__globals__
  30. token_hex = globals_["secrets"].token_hex
  31. def fresh_challenge(size: int) -> str:
  32. assert len(state.prompts) == 1
  33. value = token_hex(size)
  34. state.challenges.append(value)
  35. return value
  36. class ScriptedHarness:
  37. def __init__(self, **kwargs: object) -> None:
  38. state.root = Path(kwargs["cwd"])
  39. assert "toolChoice" not in kwargs
  40. def __enter__(self) -> ScriptedHarness:
  41. return self
  42. def __exit__(self, *args: object) -> None:
  43. state.closed = True
  44. def run(self, prompt: str, *, session_id: str) -> RunResult:
  45. state.prompts.append(prompt)
  46. state.session_ids.append(session_id)
  47. if len(state.prompts) == 1:
  48. state.marker = Path(prompt.splitlines()[-1])
  49. assert state.marker.parent == state.root
  50. if state.create_bytes is not None:
  51. state.marker.write_bytes(state.create_bytes)
  52. return state.create_result
  53. assert len(state.prompts) == 2
  54. assert len(state.challenges) == 1
  55. challenge = state.challenges[0]
  56. assert all(challenge not in sent for sent in state.prompts)
  57. assert str(state.marker) not in prompt
  58. assert "previous turn" in prompt and "changed externally" in prompt
  59. assert state.marker.read_bytes() == challenge.encode("ascii")
  60. receipt = Path(prompt.splitlines()[-1])
  61. assert receipt != state.marker and not receipt.exists()
  62. if state.receipt_mode == "copy":
  63. receipt.write_bytes(state.marker.read_bytes())
  64. elif state.receipt_mode == "stale":
  65. receipt.write_bytes(state.create_bytes)
  66. elif state.receipt_mode == "wrong":
  67. receipt.write_bytes(b"wrong")
  68. elif state.receipt_mode == "newline":
  69. receipt.write_bytes(state.marker.read_bytes() + b"\n")
  70. elif state.receipt_mode == "changed-source":
  71. receipt.write_bytes(state.marker.read_bytes())
  72. state.marker.write_bytes(b"changed")
  73. elif state.receipt_mode != "missing":
  74. raise AssertionError(state.receipt_mode)
  75. return state.verify_result
  76. monkeypatch.setenv("DEEPSEEK_API_KEY", "unit-test-key")
  77. monkeypatch.setenv("DEEPSEEK_BASE_URL", "https://api.invalid")
  78. monkeypatch.setattr(deepseek_harness, "DeepSeekHarness", ScriptedHarness)
  79. monkeypatch.setattr(globals_["secrets"], "token_hex", fresh_challenge)
  80. monkeypatch.setitem(globals_, "assert_zstd_session_log", state.checked_logs.append)
  81. return state
  82. def test_live_smoke_requires_fresh_external_content(live_smoke: SimpleNamespace) -> None:
  83. SMOKE["smoke_sdk_live"]()
  84. assert len(live_smoke.prompts) == 2
  85. assert live_smoke.session_ids == ["installed-wheel-live-api"] * 2
  86. assert len(live_smoke.checked_logs) == 1
  87. assert live_smoke.closed and not live_smoke.root.exists()
  88. @pytest.mark.parametrize("label", ["create", "verify"])
  89. @pytest.mark.parametrize(("overrides", "message"), [
  90. ({"finish_reason": "error"}, "turn ended with 'error'"),
  91. ({"finish_reason": "error", "events": [{
  92. "type": "turn/end", "data": {"turn": 1, "reason": {
  93. "kind": "error", "error": {"code": "AUTH", "status": 401},
  94. }},
  95. }]}, "turn ended with.*AUTH.*401"),
  96. ({"events": []}, "turn made no model-requested tool call"),
  97. ({"final_response": "PYTHON_SDK_LIVE_OK extra"}, "turn returned"),
  98. ])
  99. def test_live_smoke_rejects_invalid_turn_before_continuing(
  100. live_smoke: SimpleNamespace, label: str, overrides: dict[str, object], message: str,
  101. ) -> None:
  102. setattr(live_smoke, f"{label}_result", live_result(**overrides))
  103. with pytest.raises(AssertionError, match=f"{label} {message}"):
  104. SMOKE["smoke_sdk_live"]()
  105. assert len(live_smoke.prompts) == (1 if label == "create" else 2)
  106. assert not live_smoke.checked_logs
  107. assert live_smoke.closed
  108. if label == "create":
  109. assert not live_smoke.challenges
  110. @pytest.mark.parametrize("content", [None, b"wrong", b"PYTHON_SDK_LIVE_OK\n"])
  111. def test_live_smoke_rejects_bad_create_before_host_overwrite(
  112. live_smoke: SimpleNamespace, content: bytes | None,
  113. ) -> None:
  114. live_smoke.create_bytes = content
  115. with pytest.raises(AssertionError, match="create turn (did not create|wrote unexpected bytes)"):
  116. SMOKE["smoke_sdk_live"]()
  117. assert len(live_smoke.prompts) == 1
  118. assert not live_smoke.challenges and not live_smoke.checked_logs
  119. assert live_smoke.closed
  120. @pytest.mark.parametrize(("mode", "message"), [
  121. ("missing", "did not create receipt"),
  122. ("stale", "wrote unexpected bytes to receipt"),
  123. ("wrong", "wrote unexpected bytes to receipt"),
  124. ("newline", "wrote unexpected bytes to receipt"),
  125. ("changed-source", "changed source file"),
  126. ])
  127. def test_live_smoke_rejects_unrelated_tool_without_exact_receipt(
  128. live_smoke: SimpleNamespace, mode: str, message: str,
  129. ) -> None:
  130. live_smoke.receipt_mode = mode
  131. with pytest.raises(AssertionError, match=f"verify turn {message}"):
  132. SMOKE["smoke_sdk_live"]()
  133. assert len(live_smoke.prompts) == 2
  134. assert not live_smoke.checked_logs
  135. assert live_smoke.closed
  136. @pytest.mark.parametrize(
  137. ("prompt_name", "expected"),
  138. [
  139. ("SNAPSHOT_DIRECT_CHILD_PROMPT", "DIRECT_CHILD_OK"),
  140. ("SNAPSHOT_WORKFLOW_CHILD_PROMPT", "WORKFLOW_CHILD_OK"),
  141. ],
  142. )
  143. def test_child_prompt_precedes_runtime_context(prompt_name: str, expected: str) -> None:
  144. chunks = SMOKE["completion_chunks"]({
  145. "messages": [
  146. {"role": "user", "content": SMOKE[prompt_name]},
  147. {"role": "user", "content": "Current runtime context"},
  148. ],
  149. })
  150. assert any(
  151. choice.get("delta", {}).get("content") == expected
  152. for chunk in chunks
  153. for choice in chunk.get("choices", [])
  154. )
  155. def test_mcp_smoke_requests_the_discovered_tool() -> None:
  156. chunks = SMOKE["completion_chunks"]({
  157. "messages": [{"role": "user", "content": SMOKE["MCP_PROMPT"]}],
  158. "tools": [{"type": "function", "function": {"name": "mcp__fixture__add"}}],
  159. })
  160. calls = [
  161. call
  162. for chunk in chunks
  163. for choice in chunk.get("choices", [])
  164. for call in choice.get("delta", {}).get("tool_calls", [])
  165. ]
  166. assert calls[0]["function"] == {
  167. "name": "mcp__fixture__add",
  168. "arguments": '{"a": 19, "b": 23}',
  169. }
  170. def test_mcp_smoke_accepts_the_external_server_result() -> None:
  171. chunks = SMOKE["completion_chunks"]({
  172. "messages": [
  173. {"role": "user", "content": SMOKE["MCP_PROMPT"]},
  174. {
  175. "role": "assistant",
  176. "tool_calls": [{
  177. "id": "mcp-add",
  178. "type": "function",
  179. "function": {"name": "mcp__fixture__add", "arguments": '{}'},
  180. }],
  181. },
  182. {"role": "tool", "tool_call_id": "mcp-add", "content": "42"},
  183. ],
  184. })
  185. assert any(
  186. choice.get("delta", {}).get("content") == SMOKE["MCP_TEXT"]
  187. for chunk in chunks
  188. for choice in chunk.get("choices", [])
  189. )
  190. def test_snapshot_comparison_preserves_opaque_generation_provenance() -> None:
  191. normalize = SMOKE["normalize_session_format_comparison"]
  192. expected = {
  193. "header": {"type": "session", "version": 0, "otherVersion": 7},
  194. "accepted": {
  195. "type": "session-log-deepseek/delivery-accepted",
  196. "data": {"sessionId": "s", "throughSeq": 4},
  197. },
  198. "source": {
  199. "kind": "session-reference",
  200. "references": [{"sessionId": "other", "capturedThroughSeq": 8}],
  201. },
  202. }
  203. actual = {
  204. "header": {"type": "session", "version": 1, "otherVersion": 7},
  205. "accepted": {
  206. "type": "session-log-deepseek/delivery-accepted",
  207. "data": {"sessionId": "s", "sessionFormatVersion": 1, "throughSeq": 4},
  208. },
  209. "source": {
  210. "kind": "session-reference",
  211. "references": [{
  212. "sessionId": "other",
  213. "capturedFormatVersion": 1,
  214. "capturedThroughSeq": 8,
  215. }],
  216. },
  217. }
  218. assert normalize(expected) != normalize(actual)
  219. assert normalize(expected)["header"] == normalize(actual)["header"]
  220. assert normalize(expected)["header"]["otherVersion"] == 7
  221. assert normalize(actual)["accepted"] == actual["accepted"]
  222. assert normalize(actual)["source"] == actual["source"]
  223. def test_snapshot_value_scrubs_system_nodes_without_erasing_header_fields() -> None:
  224. normalize = SMOKE["normalize_snapshot_value"]
  225. system = {
  226. "type": "system/message",
  227. "data": {"message": {"role": "system", "content": [{"type": "text", "text": "prompt"}]}},
  228. }
  229. header = {"type": "request/header", "data": {"header": {"system": "unexpected"}}}
  230. assert normalize(system, [])["data"]["message"]["content"] == [{"type": "text", "text": "{{system}}"}]
  231. assert normalize(header, []) == header
  232. empty = {"type": "system/message", "data": {"message": {"role": "system", "content": []}}}
  233. assert normalize(empty, []) == empty
  234. def test_snapshot_value_normalizes_embedded_assistant_stream_timing() -> None:
  235. normalize = SMOKE["normalize_snapshot_value"]
  236. event = {
  237. "type": "assistant/message",
  238. "seq": 4,
  239. "time": 100,
  240. "data": {
  241. "stream": [
  242. {"type": "chunk", "time": 101, "chunk": {"type": "finish"}},
  243. {"type": "text-chunks", "time0": 102, "dt": [1, 2], "texts": ["a", "b", "c"]},
  244. ],
  245. },
  246. }
  247. normalized = normalize(event, [])
  248. assert normalized["time"] == 0
  249. assert normalized["data"]["stream"] == [
  250. {"type": "chunk", "time": 0, "chunk": {"type": "finish"}},
  251. {"type": "text-chunks", "time0": 0, "dt": [0, 0], "texts": ["a", "b", "c"]},
  252. ]
  253. def test_snapshot_comparison_expands_embedded_assistant_streams() -> None:
  254. normalize = SMOKE["normalize_session_format_comparison"]
  255. expected = [
  256. {
  257. "type": "assistant/chunk",
  258. "seq": 4,
  259. "time": 0,
  260. "data": {"turn": 1, "step": 1, "chunk": {
  261. "type": "text-delta", "index": 0, "text": "done",
  262. }},
  263. },
  264. {
  265. "type": "assistant/message",
  266. "seq": 5,
  267. "time": 0,
  268. "data": {"turn": 1, "step": 1, "message": {"role": "assistant"}},
  269. "sourceEventSeqs": [4],
  270. "surfaceOp": "append",
  271. },
  272. ]
  273. actual = [{
  274. "type": "assistant/message",
  275. "seq": 4,
  276. "time": 0,
  277. "data": {
  278. "turn": 1,
  279. "step": 1,
  280. "message": {"role": "assistant"},
  281. "stream": [{
  282. "type": "text-chunks", "time0": 0, "index": 0, "dt": [], "texts": ["done"],
  283. }],
  284. },
  285. "surfaceOp": "append",
  286. }]
  287. assert normalize(actual, 2) == normalize(expected, 1)
  288. tool_result = {
  289. "type": "tool/result",
  290. "data": {"turn": 1, "step": 1},
  291. "sourceEventSeqs": [4],
  292. }
  293. assert normalize(tool_result, 1)["sourceEventSeqs"] == [4]
  294. assert normalize(tool_result, 2)["sourceEventSeqs"] == [4]
  295. def test_snapshot_stream_expands_reasoning_and_tool_call_records() -> None:
  296. expand = SMOKE["expand_snapshot_stream_member"]
  297. assert expand({
  298. "type": "reasoning-chunks", "time0": 0, "index": 1,
  299. "dt": [], "texts": ["think"],
  300. }) == [{"type": "reasoning-delta", "index": 1, "text": "think"}]
  301. assert expand({
  302. "type": "tool-call-chunks", "time0": 0, "index": 2,
  303. "id": "call-1", "name": "read", "dt": [1], "args": ["{", "}"],
  304. }) == [
  305. {"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "{"},
  306. {"type": "tool-call-delta", "index": 2, "id": "call-1", "name": "read", "argumentsDelta": "}"},
  307. ]
  308. def test_snapshot_file_builder_order_is_checked_outside_update_mode(tmp_path: Path) -> None:
  309. compare = SMOKE["compare_snapshot_files"]
  310. with pytest.raises(AssertionError, match="snapshot builder produced"):
  311. compare({}, False, tmp_path, ("result.json",))
  312. def test_snapshot_comparison_expands_sdk_wrapped_attempts() -> None:
  313. normalize = SMOKE["normalize_session_format_comparison"]
  314. actual = [{
  315. "method": "session.event",
  316. "payload": {
  317. "sessionId": "s",
  318. "event": {
  319. "type": "assistant/attempt",
  320. "seq": 7,
  321. "time": 0,
  322. "data": {
  323. "turn": 1,
  324. "step": 1,
  325. "stream": [{"type": "chunk", "time": 0, "chunk": {"type": "finish"}}],
  326. },
  327. },
  328. },
  329. }]
  330. assert normalize(actual) == [{
  331. "method": "session.event",
  332. "payload": {
  333. "sessionId": "s",
  334. "event": {
  335. "type": "assistant/chunk",
  336. "data": {"turn": 1, "step": 1, "chunk": {"type": "finish"}},
  337. },
  338. },
  339. }]
  340. def test_snapshot_generation_names_select_highest_role_without_double_counting(
  341. tmp_path: Path,
  342. ) -> None:
  343. render = SMOKE["snapshot_session_filename"]
  344. select = SMOKE["selected_snapshot_session_files"]
  345. assert render(0, 0) == "session.jsonl"
  346. assert render(0, 2) == "session.v2.jsonl"
  347. assert render(3, 0) == "session.3.jsonl"
  348. assert render(3, 2) == "session.3.v2.jsonl"
  349. (tmp_path / "session.jsonl").write_text(
  350. '{"type":"session","version":0}\n', encoding="utf-8",
  351. )
  352. (tmp_path / "session.v1.jsonl").write_text(
  353. '{"type":"session","version":1}\n', encoding="utf-8",
  354. )
  355. (tmp_path / "session.1.jsonl").write_text(
  356. '{"type":"session","version":0}\n', encoding="utf-8",
  357. )
  358. assert {index: path.name for index, path in select(tmp_path).items()} == {
  359. 0: "session.v1.jsonl",
  360. 1: "session.1.jsonl",
  361. }
  362. def test_snapshot_comparison_accepts_v3_output_against_v2_without_rewriting(tmp_path: Path) -> None:
  363. predecessor = '{"type":"session","version":2}\n'
  364. successor = '{"type":"session","version":3}\n'
  365. old_path = tmp_path / "session.v2.jsonl"
  366. old_path.write_text(predecessor, encoding="utf-8")
  367. files = {"session.v3.jsonl": successor}
  368. SMOKE["compare_snapshot_files"](files, False, tmp_path, ("session.v2.jsonl",))
  369. assert old_path.read_text(encoding="utf-8") == predecessor
  370. assert not (tmp_path / "session.v3.jsonl").exists()
  371. SMOKE["compare_snapshot_files"](files, True, tmp_path, ("session.v2.jsonl",))
  372. assert old_path.read_text(encoding="utf-8") == predecessor
  373. assert (tmp_path / "session.v3.jsonl").read_text(encoding="utf-8") == successor
  374. assert SMOKE["selected_snapshot_session_files"](tmp_path) == {0: tmp_path / "session.v3.jsonl"}
  375. @pytest.mark.parametrize("filenames", [
  376. ("session.1.v2.jsonl", "session.v2.jsonl"),
  377. ("session.v2.jsonl",),
  378. ("session.v2.jsonl", "session.2.v2.jsonl"),
  379. ])
  380. def test_snapshot_builder_checks_role_order_and_count_across_generations(
  381. tmp_path: Path, filenames: tuple[str, ...],
  382. ) -> None:
  383. files = {"session.v3.jsonl": "", "session.1.v3.jsonl": ""}
  384. with pytest.raises(AssertionError, match="snapshot builder produced"):
  385. SMOKE["compare_snapshot_files"](files, False, tmp_path, filenames)
  386. def test_snapshot_generation_comparison_rejects_changed_payload(tmp_path: Path) -> None:
  387. (tmp_path / "session.v2.jsonl").write_text(
  388. '{"type":"session","version":2,"id":"expected"}\n', encoding="utf-8",
  389. )
  390. with pytest.raises(AssertionError, match="executable snapshot mismatch"):
  391. SMOKE["compare_snapshot_files"](
  392. {"session.v3.jsonl": '{"type":"session","version":3,"id":"changed"}\n'},
  393. False, tmp_path, ("session.v2.jsonl",),
  394. )
  395. @pytest.mark.parametrize("version", [2, 4])
  396. @pytest.mark.parametrize("update", [False, True])
  397. def test_snapshot_comparison_rejects_noncurrent_writer(
  398. tmp_path: Path, version: int, update: bool,
  399. ) -> None:
  400. golden = '{"type":"session","version":2}\n'
  401. (tmp_path / "session.v2.jsonl").write_text(golden, encoding="utf-8")
  402. content = json.dumps({"type": "session", "version": version}) + "\n"
  403. with pytest.raises(AssertionError, match="expected current Session format v3"):
  404. SMOKE["compare_snapshot_files"](
  405. {f"session.v{version}.jsonl": content}, update, tmp_path, ("session.v2.jsonl",),
  406. )
  407. assert (tmp_path / "session.v2.jsonl").read_text(encoding="utf-8") == golden
  408. assert not (tmp_path / "session.v4.jsonl").exists()
  409. @pytest.mark.parametrize("version", [2, 3, 4])
  410. def test_persisted_session_requires_current_writer(version: int) -> None:
  411. content = json.dumps({"type": "session", "version": version}) + "\n"
  412. path = Path(f"session.v{version}.jsonl")
  413. if version == 3:
  414. assert SMOKE["assert_persisted_session_version"](path, content) == version
  415. else:
  416. with pytest.raises(AssertionError, match="expected current Session format v3"):
  417. SMOKE["assert_persisted_session_version"](path, content)
  418. def test_snapshot_generation_filename_must_match_header(tmp_path: Path) -> None:
  419. (tmp_path / "session.v1.jsonl").write_text(
  420. '{"type":"session","version":0}\n', encoding="utf-8",
  421. )
  422. with pytest.raises(AssertionError, match="filename declares Session format v1"):
  423. SMOKE["selected_snapshot_session_files"](tmp_path)
  424. @pytest.mark.parametrize("returncode", [1, -1073741819, 3221225477])
  425. def test_profile_plugin_failure_reports_native_exit_status(monkeypatch: pytest.MonkeyPatch, returncode: int) -> None:
  426. def failed_install(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
  427. return subprocess.CompletedProcess(args=[], returncode=returncode, stdout="", stderr="")
  428. monkeypatch.setattr(subprocess, "run", failed_install)
  429. with pytest.raises(AssertionError) as error:
  430. SMOKE["smoke_sdk_profile_plugin"]("http://127.0.0.1:1")
  431. message = str(error.value)
  432. assert f"returncode={returncode}" in message
  433. assert f"0x{returncode & 0xffffffff:08x}" in message
  434. assert "stdout='' stderr=''" in message