Skip to content

Commit f148313

Browse files
committed
refactor: convert REPL loop to async with prompt_async
1 parent 2010ae9 commit f148313

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

iclaw/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
import asyncio
23
import json
34
import os
45
import sys
@@ -80,6 +81,10 @@ def _chat(provider, token, messages, model, tools=None, stream=False):
8081

8182

8283
def main():
84+
asyncio.run(_main())
85+
86+
87+
async def _main():
8388
github_token = load_github_token()
8489
provider_token = None
8590
token_expiry = 0
@@ -135,7 +140,7 @@ def main():
135140

136141
while True:
137142
try:
138-
user_input = session.prompt("> ").strip()
143+
user_input = (await session.prompt_async("> ")).strip()
139144
except (EOFError, KeyboardInterrupt):
140145
print("\nGoodbye!")
141146
break

tests/test_main.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,19 @@
66

77

88
def _mock_session(*inputs):
9-
"""Return a PromptSession mock whose .prompt() yields inputs in order."""
9+
"""Return a PromptSession mock whose .prompt_async() yields inputs in order."""
10+
1011
session = MagicMock()
12+
13+
async def _async_side_effect(*args, **kwargs):
14+
val = inputs_side_effect.pop(0)
15+
if isinstance(val, BaseException):
16+
raise val
17+
return val
18+
19+
inputs_side_effect = list(inputs)
20+
session.prompt_async = MagicMock(side_effect=_async_side_effect)
21+
# Keep .prompt() too in case anything still references it
1122
session.prompt.side_effect = list(inputs)
1223
return session
1324

@@ -280,9 +291,31 @@ def test_main_token_refresh(
280291
self, mock_ps, mock_chat, mock_cp, mock_load, mock_http
281292
):
282293
mock_ps.return_value = _mock_session("hello", ".exit")
294+
# asyncio.run() calls time.monotonic() internally (shutdown_asyncgens
295+
# etc.), and since patch("iclaw.main.time.monotonic") patches the real
296+
# time.monotonic globally, those internal calls consume side_effect
297+
# entries too. Use a callable that tracks call count so we can
298+
# control exactly which value the application code sees.
299+
#
300+
# We need: startup monotonic() returns a low value T so that
301+
# token_expiry = T + TOKEN_REFRESH_INTERVAL
302+
# and later monotonic() returns a value >= token_expiry to trigger
303+
# refresh. asyncio internals may consume 0-2 calls before the app
304+
# code runs.
305+
call_count = {"n": 0}
306+
307+
def _monotonic():
308+
call_count["n"] += 1
309+
# First few calls may be asyncio internals; return 0 for them
310+
# and for the startup call. Once we've had at least 2 calls
311+
# (startup done), switch to 99999 so the expiry check triggers.
312+
if call_count["n"] <= 2:
313+
return 0
314+
return 99999
315+
283316
with (
284317
patch("sys.stdout"),
285-
patch("iclaw.main.time.monotonic", side_effect=[0, 99999, 99999, 99999]),
318+
patch("iclaw.main.time.monotonic", side_effect=_monotonic),
286319
):
287320
main.main()
288321
# get_copilot_token called twice: once at startup, once on refresh

0 commit comments

Comments
 (0)