@asm-agent/coding-agent 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -7
  3. package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
  4. package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
  5. package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
  6. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
  7. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
  8. package/dist/asm-agent-runtime/src/rlm/__pycache__/_winjob.cpython-313.pyc +0 -0
  9. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
  10. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
  11. package/dist/asm-agent-runtime/src/rlm/__pycache__/harness.cpython-313.pyc +0 -0
  12. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
  13. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-313.pyc +0 -0
  14. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
  15. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-313.pyc +0 -0
  16. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
  17. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
  18. package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
  19. package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
  20. package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
  21. package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
  22. package/dist/asm-agent-runtime/test/__pycache__/test_agent_message_skill.cpython-313.pyc +0 -0
  23. package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
  24. package/dist/asm-agent-runtime/test/__pycache__/test_harness.cpython-313.pyc +0 -0
  25. package/dist/asm-agent-runtime/test/__pycache__/test_mcp.cpython-313.pyc +0 -0
  26. package/dist/asm-agent-runtime/test/__pycache__/test_mcp_base.cpython-313.pyc +0 -0
  27. package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
  28. package/dist/asm-agent-runtime/test/__pycache__/test_subagent_registry.cpython-313.pyc +0 -0
  29. package/dist/asm-agent-runtime/test/__pycache__/test_winjob.cpython-313.pyc +0 -0
  30. package/dist/asm-agent-runtime/test/test_bash.py +74 -19
  31. package/dist/asm-agent-runtime/test/test_repl.py +67 -3
  32. package/dist/asm-agent-runtime/uv.lock +446 -155
  33. package/dist/bundle/{anthropic-XSMQHGYB.js → anthropic-ZOZNPFXS.js} +2 -2
  34. package/dist/bundle/{azure-openai-responses-XLQAIKKH.js → azure-openai-responses-TDHCU647.js} +2 -2
  35. package/dist/bundle/{bundled-modules-5OCN73BM.js → bundled-modules-Q2YODC4B.js} +3 -3
  36. package/dist/bundle/{chunk-IAQLPSMN.js → chunk-AMKLNEAK.js} +47 -47
  37. package/dist/bundle/{chunk-2LPSALBF.js → chunk-FPY7FFJS.js} +978 -1821
  38. package/dist/bundle/{chunk-E3YE2Q7A.js → chunk-G6SC4YYW.js} +4973 -1926
  39. package/dist/bundle/{chunk-YPNK4MRU.js → chunk-QLHAP3FT.js} +1 -1
  40. package/dist/bundle/{cli-main-QYX5R3LF.js → cli-main-277AG2BR.js} +3 -3
  41. package/dist/bundle/cli.js +1 -1
  42. package/dist/bundle/{google-ZXJEPWJA.js → google-Q3AJWUOY.js} +1 -1
  43. package/dist/bundle/{google-vertex-WAF3RIJ6.js → google-vertex-2CSNOUBS.js} +1 -1
  44. package/dist/bundle/{main-EOUAQCJH.js → main-TNXWJXIU.js} +3 -3
  45. package/dist/bundle/{mistral-GDZXSXRS.js → mistral-XRYJGLLG.js} +1 -1
  46. package/dist/bundle/{openai-codex-responses-7XMPW4LG.js → openai-codex-responses-C6BRO2BN.js} +2 -2
  47. package/dist/bundle/{openai-completions-LNSVGDMF.js → openai-completions-DNXCK2JW.js} +4 -4
  48. package/dist/bundle/{openai-responses-MFZOBMIT.js → openai-responses-BEMWVIXE.js} +2 -2
  49. package/dist/config.d.ts +0 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -3
  52. package/dist/config.js.map +1 -1
  53. package/dist/core/agent-session-config.d.ts +1 -2
  54. package/dist/core/agent-session-config.d.ts.map +1 -1
  55. package/dist/core/agent-session-config.js +0 -3
  56. package/dist/core/agent-session-config.js.map +1 -1
  57. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  58. package/dist/core/agent-session-runtime.js +0 -8
  59. package/dist/core/agent-session-runtime.js.map +1 -1
  60. package/dist/core/agent-session-services.d.ts +0 -2
  61. package/dist/core/agent-session-services.d.ts.map +1 -1
  62. package/dist/core/agent-session-services.js +0 -22
  63. package/dist/core/agent-session-services.js.map +1 -1
  64. package/dist/core/agent-session.d.ts +8 -1
  65. package/dist/core/agent-session.d.ts.map +1 -1
  66. package/dist/core/agent-session.js +100 -22
  67. package/dist/core/agent-session.js.map +1 -1
  68. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  69. package/dist/core/kernel/bootstrap.js +1 -1
  70. package/dist/core/kernel/bootstrap.js.map +1 -1
  71. package/dist/core/kernel/repl-manager.d.ts +32 -8
  72. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  73. package/dist/core/kernel/repl-manager.js +401 -102
  74. package/dist/core/kernel/repl-manager.js.map +1 -1
  75. package/dist/core/kernel/shared.d.ts +11 -7
  76. package/dist/core/kernel/shared.d.ts.map +1 -1
  77. package/dist/core/kernel/shared.js +2 -5
  78. package/dist/core/kernel/shared.js.map +1 -1
  79. package/dist/core/mcp/mcp-manager.d.ts +4 -2
  80. package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
  81. package/dist/core/mcp/mcp-manager.js +9 -7
  82. package/dist/core/mcp/mcp-manager.js.map +1 -1
  83. package/dist/core/model-registry.d.ts.map +1 -1
  84. package/dist/core/model-registry.js +1 -1
  85. package/dist/core/model-registry.js.map +1 -1
  86. package/dist/core/ollama-models.d.ts +3 -1
  87. package/dist/core/ollama-models.d.ts.map +1 -1
  88. package/dist/core/ollama-models.js +31 -10
  89. package/dist/core/ollama-models.js.map +1 -1
  90. package/dist/core/prime-inference-auth.d.ts +0 -9
  91. package/dist/core/prime-inference-auth.d.ts.map +1 -1
  92. package/dist/core/prime-inference-auth.js +0 -50
  93. package/dist/core/prime-inference-auth.js.map +1 -1
  94. package/dist/core/provider-display-names.d.ts.map +1 -1
  95. package/dist/core/provider-display-names.js +0 -1
  96. package/dist/core/provider-display-names.js.map +1 -1
  97. package/dist/core/session-manager.d.ts +0 -4
  98. package/dist/core/session-manager.d.ts.map +1 -1
  99. package/dist/core/session-manager.js +0 -22
  100. package/dist/core/session-manager.js.map +1 -1
  101. package/dist/core/settings-manager.d.ts +4 -16
  102. package/dist/core/settings-manager.d.ts.map +1 -1
  103. package/dist/core/settings-manager.js +13 -43
  104. package/dist/core/settings-manager.js.map +1 -1
  105. package/dist/core/slash-commands.d.ts.map +1 -1
  106. package/dist/core/slash-commands.js +0 -5
  107. package/dist/core/slash-commands.js.map +1 -1
  108. package/dist/core/tools/acp-mcp.d.ts +6 -0
  109. package/dist/core/tools/acp-mcp.d.ts.map +1 -0
  110. package/dist/core/tools/acp-mcp.js +86 -0
  111. package/dist/core/tools/acp-mcp.js.map +1 -0
  112. package/dist/core/tools/index.d.ts +1 -0
  113. package/dist/core/tools/index.d.ts.map +1 -1
  114. package/dist/core/tools/index.js +1 -0
  115. package/dist/core/tools/index.js.map +1 -1
  116. package/dist/core/tools/ipython.d.ts.map +1 -1
  117. package/dist/core/tools/ipython.js +9 -4
  118. package/dist/core/tools/ipython.js.map +1 -1
  119. package/dist/main.d.ts.map +1 -1
  120. package/dist/main.js +2 -11
  121. package/dist/main.js.map +1 -1
  122. package/dist/modes/acp/acp-mode.d.ts.map +1 -1
  123. package/dist/modes/acp/acp-mode.js +1 -1
  124. package/dist/modes/acp/acp-mode.js.map +1 -1
  125. package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
  126. package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
  127. package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
  128. package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
  129. package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
  130. package/dist/modes/agents-view/agents-view-mode.js +0 -19
  131. package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
  132. package/dist/modes/daemon/daemon-mode.d.ts +1 -10
  133. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  134. package/dist/modes/daemon/daemon-mode.js +134 -274
  135. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  136. package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
  137. package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
  138. package/dist/modes/daemon/daemon-protocol.js +6 -8
  139. package/dist/modes/daemon/daemon-protocol.js.map +1 -1
  140. package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
  141. package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
  142. package/dist/modes/daemon/daemon-supervisor.js +18 -52
  143. package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
  144. package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
  145. package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
  146. package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
  147. package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
  148. package/dist/modes/interactive/auth-flows.d.ts +0 -2
  149. package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
  150. package/dist/modes/interactive/auth-flows.js +1 -101
  151. package/dist/modes/interactive/auth-flows.js.map +1 -1
  152. package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
  153. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.js +13 -3
  155. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  156. package/dist/modes/interactive/components/footer.d.ts +1 -1
  157. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/footer.js +2 -2
  159. package/dist/modes/interactive/components/footer.js.map +1 -1
  160. package/dist/modes/interactive/components/mermaid.d.ts +12 -0
  161. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  162. package/dist/modes/interactive/components/mermaid.js +63 -0
  163. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  164. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  165. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/settings-selector.js +10 -0
  167. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  168. package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
  169. package/dist/modes/interactive/feature-hints.js +0 -4
  170. package/dist/modes/interactive/feature-hints.js.map +1 -1
  171. package/dist/modes/interactive/interactive-mode.d.ts +1 -7
  172. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  173. package/dist/modes/interactive/interactive-mode.js +23 -279
  174. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  175. package/dist/utils/version-check.d.ts.map +1 -1
  176. package/dist/utils/version-check.js +1 -1
  177. package/dist/utils/version-check.js.map +1 -1
  178. package/docs/settings.md +1 -31
  179. package/docs/usage.md +1 -4
  180. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  181. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  182. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  183. package/examples/extensions/sandbox/package-lock.json +2 -2
  184. package/examples/extensions/sandbox/package.json +1 -1
  185. package/examples/extensions/with-deps/package-lock.json +2 -2
  186. package/examples/extensions/with-deps/package.json +1 -1
  187. package/package.json +6 -5
  188. package/dist/core/agent-traces.d.ts +0 -130
  189. package/dist/core/agent-traces.d.ts.map +0 -1
  190. package/dist/core/agent-traces.js +0 -746
  191. package/dist/core/agent-traces.js.map +0 -1
  192. package/dist/core/telemetry.d.ts +0 -92
  193. package/dist/core/telemetry.d.ts.map +0 -1
  194. package/dist/core/telemetry.js +0 -633
  195. package/dist/core/telemetry.js.map +0 -1
@@ -176,20 +176,20 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
176
176
  self.assertFalse(records[-1]["active"])
177
177
  await _poll_group_dead(handle.pid)
178
178
 
179
- async def test_delivered_status_wins_when_shell_dies_during_drain(self):
179
+ async def test_delivered_status_wins_when_shell_dies_during_completion(self):
180
180
  entered = threading.Event()
181
181
  release = threading.Event()
182
- original = bash_module.BashHandle._drain_grace
182
+ original = bash_module.BashHandle._wait_for_completion
183
183
 
184
- def held_drain(handle_self):
185
- # Hold only the first caller (the reporter, which drains right after
186
- # reading status 0); any later caller must proceed normally.
187
- if not entered.is_set():
188
- entered.set()
189
- release.wait(10)
190
- original(handle_self)
184
+ def held_completion(handle_self):
185
+ output = original(handle_self)
186
+ entered.set()
187
+ release.wait(10)
188
+ return output
191
189
 
192
- with mock.patch.object(bash_module.BashHandle, "_drain_grace", held_drain):
190
+ with mock.patch.object(
191
+ bash_module.BashHandle, "_wait_for_completion", held_completion
192
+ ):
193
193
  # Background job keeps the shell alive in `wait` after status 0 is delivered.
194
194
  handle = bash("sleep 30 & true")
195
195
  try:
@@ -337,7 +337,7 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
337
337
  self.skipTest("POSIX-only gate")
338
338
  with tempfile.TemporaryDirectory() as tmp:
339
339
  marker = os.path.join(tmp, "ran")
340
- script = bash_module._status_script(f"touch {marker}")
340
+ script = bash_module._status_script(f"touch {marker}", "a" * 32, "b" * 32)
341
341
  parent, child = socket.socketpair()
342
342
  proc = subprocess.Popen(
343
343
  [bash_module._shell(), "-c", script],
@@ -539,26 +539,29 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
539
539
  bash_module._shell()
540
540
  which.assert_not_called()
541
541
 
542
- async def test_pump_paused_between_read_and_commit_does_not_lose_output(self):
543
- # Reviewer repro: the chunk is out of the pipe (FIONREAD 0) but not yet
544
- # in the buffer; the drain fence must wait for the commit.
542
+ async def test_pump_delayed_past_old_quiescence_bound_captures_all_output(self):
543
+ # The ordered sentinel must wait through a pump delay beyond the old 500 ms bound.
545
544
  original_write = bash_module._BoundedBuffer.write
546
545
  delayed_once = threading.Event()
547
546
 
548
547
  def delayed_write(buffer_self, chunk):
549
548
  if not delayed_once.is_set():
550
549
  delayed_once.set()
551
- time.sleep(0.3)
550
+ time.sleep(0.7)
552
551
  original_write(buffer_self, chunk)
553
552
 
554
553
  with mock.patch.object(bash_module._BoundedBuffer, "write", delayed_write):
555
- result = await asyncio.wait_for(bash("printf between-read-and-write"), timeout=5)
554
+ result = await asyncio.wait_for(bash("printf delayed-output-complete"), timeout=5)
556
555
  self.assertEqual(result.exit_code, 0)
557
- self.assertIn("between-read-and-write", result.output)
556
+ self.assertEqual(result.output, "delayed-output-complete")
557
+
558
+ async def test_completion_marker_split_across_reads_is_removed(self):
559
+ with mock.patch.object(bash_module, "_READ_CHUNK", 7):
560
+ result = await asyncio.wait_for(bash("printf exact-pre-fence-output"), timeout=5)
561
+ self.assertEqual(result.output, "exact-pre-fence-output")
558
562
 
559
563
  async def test_slow_pump_does_not_lose_foreground_output(self):
560
- # A descheduled pump must not let _drain_grace conclude quiescence
561
- # while the shell's output still sits unread in the pipe.
564
+ # Finalization waits for the pump to parse the ordered sentinel.
562
565
  original_pump = bash_module.BashHandle._pump
563
566
 
564
567
  def slow_pump(handle_self):
@@ -570,6 +573,58 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
570
573
  self.assertEqual(result.exit_code, 0)
571
574
  self.assertIn("slow-pump-x", result.output)
572
575
 
576
+ async def test_sentinel_like_output_and_echoed_wrapper_do_not_truncate(self):
577
+ token = "0123456789abcdef" * 4
578
+ raw_lookalike = "\x1easm-agent-complete:not-this-invocation\x1f"
579
+ command = (
580
+ "set -x\n"
581
+ "printf '\\036asm-agent-complete:not-this-invocation\\037'\n"
582
+ "if [ -r /proc/$$/cmdline ]; then cat /proc/$$/cmdline; fi\n"
583
+ "printf '\\nafter-sentinel-lookalike\\n'"
584
+ )
585
+ with mock.patch.object(bash_module.secrets, "token_hex", return_value=token):
586
+ result = await asyncio.wait_for(bash(command), timeout=5)
587
+ actual_marker = (
588
+ bash_module._COMPLETION_PREFIX + token.encode() + bash_module._COMPLETION_SUFFIX
589
+ )
590
+ self.assertIn(raw_lookalike, result.output)
591
+ self.assertIn("after-sentinel-lookalike", result.output)
592
+ self.assertNotIn(actual_marker.decode(), result.output)
593
+
594
+ async def test_user_alias_cannot_replace_completion_emitter(self):
595
+ if os.path.basename(bash_module._shell()) != "bash":
596
+ self.skipTest("bash alias expansion semantics")
597
+ handle = bash(
598
+ "shopt -s expand_aliases; alias command='printf alias-expanded'; sleep 30 &"
599
+ )
600
+ try:
601
+ result = await asyncio.wait_for(handle, timeout=5)
602
+ self.assertEqual(result.exit_code, 0)
603
+ self.assertNotIn("alias-expanded", result.output)
604
+ finally:
605
+ handle.kill(signal.SIGKILL)
606
+ await _poll_group_dead(handle.pid)
607
+
608
+ async def test_user_function_cannot_replace_completion_emitter(self):
609
+ # The backslash in `\command` defeats alias expansion only: a shell
610
+ # function named `command` would otherwise swallow both fence frames
611
+ # and wedge the await behind the background job until shell death.
612
+ handle = bash("command() { printf function-expanded; }; sleep 30 &")
613
+ try:
614
+ result = await asyncio.wait_for(handle, timeout=5)
615
+ self.assertEqual(result.exit_code, 0)
616
+ self.assertNotIn("function-expanded", result.output)
617
+ finally:
618
+ handle.kill(signal.SIGKILL)
619
+ await _poll_group_dead(handle.pid)
620
+
621
+ async def test_shell_killed_before_sentinel_finalizes_from_exit(self):
622
+ result = await asyncio.wait_for(
623
+ bash("printf output-before-shell-kill; kill -KILL $$"), timeout=5
624
+ )
625
+ self.assertEqual(result.exit_code, -signal.SIGKILL)
626
+ self.assertIn("output-before-shell-kill", result.output)
627
+
573
628
  async def test_relative_bash_shell_override_rejected(self):
574
629
  with mock.patch.dict(os.environ, {"ASM_AGENT_BASH_SHELL": "bash"}):
575
630
  with self.assertRaises(ValueError):
@@ -117,7 +117,7 @@ class ReplTest(unittest.TestCase):
117
117
 
118
118
  def test_ready_handshake_and_startup_time(self):
119
119
  self.assertEqual(self.ready_event["event"], "ready")
120
- self.assertEqual(self.ready_event["protocol"], 2)
120
+ self.assertEqual(self.ready_event["protocol"], 3)
121
121
  major, minor = sys.version_info[:2]
122
122
  self.assertTrue(self.ready_event["python"].startswith(f"{major}.{minor}."))
123
123
  # Loose bound for loaded CI machines; still catches an order-of-magnitude regression.
@@ -779,11 +779,75 @@ class ReplTest(unittest.TestCase):
779
779
  while request.get("event") != "host_request":
780
780
  request = self.repl.read_event()
781
781
  self.assertEqual(request["data"], {"type": "demo", "value": 7})
782
- self.repl.send({"type": "host_reply", "id": request["id"], "data": {"status": "ok", "answer": 42}})
782
+ envelope = {"status": "ok", "result": {"status": "weird", "answer": 42}}
783
+ self.repl.send({"type": "host_reply", "id": request["id"], "data": envelope})
783
784
  events = self.repl.until_done("hr")
784
- self.assertEqual(one(events, "result")["text"], "{'status': 'ok', 'answer': 42}")
785
+ self.assertEqual(one(events, "result")["text"], repr(envelope))
785
786
  self.assertEqual(one(events, "done")["status"], "ok")
786
787
 
788
+ def test_typed_host_request_unwraps_exact_handler_result(self):
789
+ code = "\n".join(
790
+ [
791
+ "from rlm import host_request",
792
+ "reply = await host_request('demo')",
793
+ "reply",
794
+ ]
795
+ )
796
+ self.repl.send({"type": "execute", "id": "typed-hr", "code": code})
797
+ request = self.repl.read_event()
798
+ while request.get("event") != "host_request":
799
+ request = self.repl.read_event()
800
+ handler_result = {"status": "weird", "answer": 42}
801
+ self.repl.send(
802
+ {
803
+ "type": "host_reply",
804
+ "id": request["id"],
805
+ "data": {"status": "ok", "result": handler_result},
806
+ }
807
+ )
808
+ events = self.repl.until_done("typed-hr")
809
+ self.assertEqual(one(events, "result")["text"], repr(handler_result))
810
+ self.assertEqual(one(events, "done")["status"], "ok")
811
+
812
+ def test_typed_host_request_raises_host_error(self):
813
+ code = "from rlm import host_request\nawait host_request('demo')"
814
+ self.repl.send({"type": "execute", "id": "typed-error", "code": code})
815
+ request = self.repl.read_event()
816
+ while request.get("event") != "host_request":
817
+ request = self.repl.read_event()
818
+ self.repl.send(
819
+ {
820
+ "type": "host_reply",
821
+ "id": request["id"],
822
+ "data": {"status": "error", "error": "handler exploded"},
823
+ }
824
+ )
825
+ events = self.repl.until_done("typed-error")
826
+ self.assertEqual(one(events, "error")["ename"], "RuntimeError")
827
+ self.assertEqual(one(events, "error")["evalue"], "handler exploded")
828
+ self.assertEqual(one(events, "done")["status"], "error")
829
+
830
+ def test_typed_host_request_rejects_unexpected_envelope_status(self):
831
+ code = "from rlm import host_request\nawait host_request('demo')"
832
+ self.repl.send({"type": "execute", "id": "typed-unexpected", "code": code})
833
+ request = self.repl.read_event()
834
+ while request.get("event") != "host_request":
835
+ request = self.repl.read_event()
836
+ self.repl.send(
837
+ {
838
+ "type": "host_reply",
839
+ "id": request["id"],
840
+ "data": {"status": "partial", "result": {}},
841
+ }
842
+ )
843
+ events = self.repl.until_done("typed-unexpected")
844
+ self.assertEqual(one(events, "error")["ename"], "RuntimeError")
845
+ self.assertEqual(
846
+ one(events, "error")["evalue"],
847
+ "host request demo returned unexpected status: 'partial'",
848
+ )
849
+ self.assertEqual(one(events, "done")["status"], "error")
850
+
787
851
  def test_host_reply_for_unknown_id_dropped(self):
788
852
  self.repl.send({"type": "host_reply", "id": "no-such-request", "data": {"status": "ok"}})
789
853
  events = self.repl.execute("ok", "'alive'")