feat(tunnel): add Cloudflare tunnel support for EvoSci deploy and update documentation
This commit is contained in:
@@ -46,6 +46,12 @@ def deploy(
|
||||
"--port",
|
||||
help="Port for langgraph dev (default: config.langgraph_dev_port = 6174)",
|
||||
),
|
||||
tunnel: bool = typer.Option(
|
||||
False,
|
||||
"--tunnel",
|
||||
help="Expose the server over a public Cloudflare tunnel (no auth — "
|
||||
"anyone with the URL can drive the agent; trusted use only)",
|
||||
),
|
||||
debug: bool = typer.Option(
|
||||
False,
|
||||
"--debug",
|
||||
@@ -64,6 +70,7 @@ def deploy(
|
||||
RUNTIME,
|
||||
_is_port_occupied,
|
||||
is_langgraph_dev_running,
|
||||
read_tunnel_url,
|
||||
start_langgraph_dev,
|
||||
stop_langgraph_dev,
|
||||
)
|
||||
@@ -155,6 +162,13 @@ def deploy(
|
||||
f"[bold red]{DANGEROUS_BANNER_MESSAGE}[/bold red]"
|
||||
)
|
||||
|
||||
if tunnel:
|
||||
console.print(
|
||||
"[bold white on red] ⚠ PUBLIC TUNNEL [/bold white on red] "
|
||||
"[bold red]Public URL, no auth — share only with people you "
|
||||
"trust.[/bold red]"
|
||||
)
|
||||
|
||||
# 6. ccproxy lifecycle (only if any provider uses OAuth)
|
||||
_ccproxy_proc = None
|
||||
if config.anthropic_auth_mode == "oauth" or config.openai_auth_mode == "oauth":
|
||||
@@ -186,6 +200,7 @@ def deploy(
|
||||
file_persistence=file_persistence,
|
||||
jobs_per_worker=jobs_per_worker,
|
||||
deploy_mode=True,
|
||||
tunnel=tunnel,
|
||||
)
|
||||
atexit.register(stop_langgraph_dev, proc)
|
||||
except Exception as exc:
|
||||
@@ -196,13 +211,33 @@ def deploy(
|
||||
# here, the subprocess is up.
|
||||
console.print("[green]✓[/green] langgraph dev ready")
|
||||
|
||||
# 8b. Cloudflare tunnel URL — the local server is healthy, but cloudflared
|
||||
# establishes the public tunnel a few seconds later and prints the random
|
||||
# URL into the log. Poll for it so we can surface it in the ready banner.
|
||||
public_url: str | None = None
|
||||
if tunnel:
|
||||
with console.status(
|
||||
"[dim]Waiting for Cloudflare tunnel URL...[/dim]", spinner="dots"
|
||||
):
|
||||
public_url = read_tunnel_url()
|
||||
if public_url:
|
||||
console.print("[green]✓[/green] tunnel up")
|
||||
else:
|
||||
console.print(
|
||||
"[yellow]⚠ Tunnel URL not detected within the wait window. "
|
||||
f"Check the log ({_shorten(str(RUNTIME.log_file))}) for a "
|
||||
"trycloudflare.com URL.[/yellow]"
|
||||
)
|
||||
|
||||
# 9. Ready banner
|
||||
log_hint = _shorten(str(RUNTIME.log_file))
|
||||
public_line = f"[bold]Public URL:[/bold] {public_url}\n" if public_url else ""
|
||||
console.print(
|
||||
Panel(
|
||||
Text.from_markup(
|
||||
f"[bold]Endpoint:[/bold] "
|
||||
f"http://localhost:{effective_port}\n"
|
||||
f"{public_line}"
|
||||
f"[bold]Assistant ID:[/bold] EvoScientist\n"
|
||||
f"[bold]Connect via:[/bold] any LangChain SDK / "
|
||||
f"LangGraph-compatible UI\n"
|
||||
|
||||
@@ -15,6 +15,7 @@ import atexit
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -253,6 +254,16 @@ _PROCESS: subprocess.Popen | None = None
|
||||
# sub-agents' cwd / EVOSCIENTIST_WORKSPACE_DIR env match the new workspace.
|
||||
_PROCESS_WORKSPACE: Path | None = None
|
||||
|
||||
# Byte offset into ``RUNTIME.log_file`` captured the instant before the current
|
||||
# subprocess was spawned. ``read_tunnel_url`` scans only bytes written after
|
||||
# this point so a stale ``trycloudflare.com`` URL from a previous (appended,
|
||||
# not-yet-rotated) session can never be misreported as the live tunnel.
|
||||
_LOG_OFFSET_AT_START: int = 0
|
||||
|
||||
# Cloudflare quick-tunnel public URL, as printed by cloudflared into the
|
||||
# langgraph dev log. Mirrors langgraph_api/tunneling/cloudflare.py.
|
||||
_TUNNEL_URL_RE = re.compile(r"https://[A-Za-z0-9.-]+\.trycloudflare\.com")
|
||||
|
||||
# Whether async sub-agents are usable in this process.
|
||||
#
|
||||
# - CLI / serve parent process: starts False; flipped True after
|
||||
@@ -525,6 +536,7 @@ def start_langgraph_dev(
|
||||
file_persistence: bool = True,
|
||||
jobs_per_worker: int = 10,
|
||||
deploy_mode: bool = False,
|
||||
tunnel: bool = False,
|
||||
) -> subprocess.Popen:
|
||||
"""Start langgraph dev as a background subprocess.
|
||||
|
||||
@@ -538,6 +550,14 @@ def start_langgraph_dev(
|
||||
``.langgraph_api/`` cache so async-task / Store / scheduler state
|
||||
survives subprocess restarts. Set False to suppress periodic
|
||||
flushes (workspace stays cleaner; state is in-memory only).
|
||||
jobs_per_worker: Concurrent runs per worker (``--n-jobs-per-worker``).
|
||||
deploy_mode: When True, the subprocess loads full MCP + async
|
||||
sub-agents (``EVOSCIENTIST_DEPLOY_MODE=full``); otherwise stripped.
|
||||
tunnel: When True, pass ``--tunnel`` so langgraph dev exposes the
|
||||
server over a public Cloudflare quick-tunnel. The random
|
||||
``*.trycloudflare.com`` URL is written to the log; read it back
|
||||
with :func:`read_tunnel_url`. SECURITY: the tunnel has no auth and
|
||||
the deployed agent can run shell — only enable for trusted use.
|
||||
|
||||
Returns:
|
||||
The Popen handle for the langgraph dev process.
|
||||
@@ -626,6 +646,14 @@ def start_langgraph_dev(
|
||||
# one fd — a problem on heavy ``/resume`` cycling that could eventually
|
||||
# exhaust the process's open-file limit.
|
||||
log_handle = open(RUNTIME.log_file, "ab") # closed in finally below
|
||||
# Remember where this session's output begins so ``read_tunnel_url`` only
|
||||
# scans lines this subprocess writes — never a stale URL left in the
|
||||
# appended-to log by a previous tunnel session.
|
||||
global _LOG_OFFSET_AT_START
|
||||
try:
|
||||
_LOG_OFFSET_AT_START = RUNTIME.log_file.stat().st_size
|
||||
except OSError:
|
||||
_LOG_OFFSET_AT_START = 0
|
||||
|
||||
# Propagate workspace to the subprocess so deployed sub-agents resolve
|
||||
# paths.WORKSPACE_ROOT to the same dir as the CLI's main agent. cwd alone
|
||||
@@ -685,6 +713,7 @@ def start_langgraph_dev(
|
||||
str(jobs_per_worker),
|
||||
"--no-browser",
|
||||
"--no-reload",
|
||||
*(["--tunnel"] if tunnel else []),
|
||||
],
|
||||
cwd=str(workspace_dir),
|
||||
stdout=log_handle,
|
||||
@@ -741,6 +770,40 @@ def start_langgraph_dev(
|
||||
)
|
||||
|
||||
|
||||
def read_tunnel_url(timeout: float = 35.0, poll_interval: float = 0.5) -> str | None:
|
||||
"""Poll the langgraph dev log for the Cloudflare quick-tunnel public URL.
|
||||
|
||||
Started with ``tunnel=True``, langgraph dev shells out to cloudflared,
|
||||
which prints a random ``https://<words>.trycloudflare.com`` URL once the
|
||||
tunnel is established — typically a few seconds after the local server is
|
||||
already healthy. We scan only the bytes written since this subprocess
|
||||
started (``_LOG_OFFSET_AT_START``) so a stale URL from an earlier session
|
||||
in the same appended-to log is never returned.
|
||||
|
||||
Args:
|
||||
timeout: Max seconds to wait for the URL to appear. cloudflared may
|
||||
also need to download its binary on first use, so the default is
|
||||
generous (langgraph_api itself waits up to 30s internally).
|
||||
poll_interval: Seconds between log re-reads.
|
||||
|
||||
Returns:
|
||||
The public tunnel URL, or ``None`` if it never appeared in time.
|
||||
"""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with open(RUNTIME.log_file, "rb") as fh:
|
||||
fh.seek(_LOG_OFFSET_AT_START)
|
||||
chunk = fh.read().decode("utf-8", errors="replace")
|
||||
except OSError:
|
||||
chunk = ""
|
||||
match = _TUNNEL_URL_RE.search(chunk)
|
||||
if match:
|
||||
return match.group(0)
|
||||
time.sleep(poll_interval)
|
||||
return None
|
||||
|
||||
|
||||
def stop_langgraph_dev(proc: subprocess.Popen | None = None) -> None:
|
||||
"""Gracefully stop a langgraph dev process.
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
|
||||
<details>
|
||||
<summary>📦 Release Highlights — version changelog</summary>
|
||||
|
||||
- **[10 Jun 2026]** **[v0.1.5](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.5)** — Dangerous mode (real-filesystem access with safety checks), LangGraph streaming v3 pipeline, opt-in Anthropic prompt caching via OpenRouter, claude-fable-5, free-scrolling TUI, Windows CI support.
|
||||
- **[11 Jun 2026]** **[v0.1.5](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.5)** — Dangerous mode (real-filesystem access with safety checks), LangGraph streaming v3 pipeline, opt-in Anthropic prompt caching via OpenRouter, claude-fable-5, free-scrolling TUI, Windows CI support, public Cloudflare tunnel for `EvoSci deploy` (`--tunnel`).
|
||||
- **[07 Jun 2026]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — Auxiliary model for background tasks & tool selection, observation-memory lifecycle, Qwen3.7-Max/Plus (DashScope), UI-backend selection, plus an OpenRouter multi-turn reasoning fix.
|
||||
- **[03 Jun 2026]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — Multimodal handling (image + PDF/doc flatten/hoisting, text-only model fallback), runtime-context middleware, memory middleware → profile files with stream timeline narration, textual CJK-input fix.
|
||||
- **[02 Jun 2026]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — Browser WebUI mode, `EvoSci deploy` standalone LangGraph server, default model → claude-sonnet-4-6, MiniMax M3, plus sandbox-timeout and async-notifier channel-routing fixes.
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
|
||||
<details>
|
||||
<summary>📦 版本更新摘要(changelog)</summary>
|
||||
|
||||
- **[2026 年 6 月 10 日]** **[v0.1.5](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.5)** — Dangerous 模式(带安全检查的真实文件系统访问)、LangGraph streaming v3 流水线、OpenRouter Anthropic prompt caching(可选启用)、claude-fable-5、TUI 自由滚动,以及 Windows CI 支持。
|
||||
- **[2026 年 6 月 11 日]** **[v0.1.5](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.5)** — Dangerous 模式(带安全检查的真实文件系统访问)、LangGraph streaming v3 流水线、OpenRouter Anthropic prompt caching(可选启用)、claude-fable-5、TUI 自由滚动、Windows CI 支持,以及 `EvoSci deploy` 公网 Cloudflare 隧道(`--tunnel`)。
|
||||
- **[2026 年 6 月 7 日]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — 辅助模型(后台任务与工具选择)、observation 记忆生命周期、Qwen3.7-Max/Plus(DashScope)、UI 后端选择,以及 OpenRouter 多轮推理修复。
|
||||
- **[2026 年 6 月 3 日]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — 多模态处理(图片 + PDF/文档 flatten/hoisting、纯文本模型回退)、runtime-context 中间件、memory 中间件迁移至 profile 文件 + stream 时间线叙述、textual 中文输入修复。
|
||||
- **[2026 年 6 月 2 日]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — 浏览器 WebUI 模式(beta)、`EvoSci deploy` 独立 LangGraph 服务器、默认模型 → claude-sonnet-4-6、MiniMax M3,以及 sandbox 超时与 async-notifier 渠道路由修复。
|
||||
|
||||
@@ -74,6 +74,8 @@ def _run_deploy_once(
|
||||
cwd: str | None = None,
|
||||
port_occupied: bool = False,
|
||||
langgraph_dev_running: bool = True, # health-check passes after start
|
||||
tunnel: bool = False,
|
||||
tunnel_url: str | None = None,
|
||||
):
|
||||
"""Run ``deploy()`` end-to-end with all external dependencies mocked.
|
||||
Returns a ``captured`` dict with observation points."""
|
||||
@@ -124,6 +126,7 @@ def _run_deploy_once(
|
||||
file_persistence=True,
|
||||
jobs_per_worker=10,
|
||||
deploy_mode=False,
|
||||
tunnel=False,
|
||||
):
|
||||
captured["langgraph_dev_started"] = True
|
||||
captured["workspace_passed"] = str(workspace_dir) if workspace_dir else None
|
||||
@@ -131,6 +134,7 @@ def _run_deploy_once(
|
||||
captured["deploy_mode_passed"] = deploy_mode
|
||||
captured["jobs_per_worker_passed"] = jobs_per_worker
|
||||
captured["file_persistence_passed"] = file_persistence
|
||||
captured["tunnel_passed"] = tunnel
|
||||
return SimpleNamespace(pid=99999)
|
||||
|
||||
def _fake_stop_langgraph_dev(_proc=None):
|
||||
@@ -138,6 +142,10 @@ def _run_deploy_once(
|
||||
|
||||
monkeypatch.setattr(lgm, "start_langgraph_dev", _fake_start_langgraph_dev)
|
||||
monkeypatch.setattr(lgm, "stop_langgraph_dev", _fake_stop_langgraph_dev)
|
||||
# Never poll a real log for the tunnel URL in tests.
|
||||
monkeypatch.setattr(
|
||||
lgm, "read_tunnel_url", lambda *a, **k: tunnel_url, raising=False
|
||||
)
|
||||
|
||||
# ccproxy mocks
|
||||
from EvoScientist import ccproxy_manager as ccp
|
||||
@@ -196,7 +204,7 @@ def _run_deploy_once(
|
||||
if cwd is not None:
|
||||
monkeypatch.setattr(os, "getcwd", lambda: cwd)
|
||||
|
||||
deploy_server.deploy(workdir=workdir, port=port, debug=debug)
|
||||
deploy_server.deploy(workdir=workdir, port=port, debug=debug, tunnel=tunnel)
|
||||
return captured
|
||||
|
||||
|
||||
@@ -234,6 +242,27 @@ def test_deploy_starts_langgraph_dev_with_deploy_mode_true(monkeypatch, tmp_path
|
||||
)
|
||||
|
||||
|
||||
def test_deploy_tunnel_default_off(monkeypatch, tmp_path):
|
||||
"""Without ``--tunnel``, start_langgraph_dev is called with tunnel=False."""
|
||||
config = _make_config(default_workdir=str(tmp_path))
|
||||
captured = _run_deploy_once(monkeypatch, config)
|
||||
|
||||
assert captured["tunnel_passed"] is False
|
||||
|
||||
|
||||
def test_deploy_tunnel_flag_passed_through(monkeypatch, tmp_path):
|
||||
"""``--tunnel`` propagates to start_langgraph_dev(tunnel=True)."""
|
||||
config = _make_config(default_workdir=str(tmp_path))
|
||||
captured = _run_deploy_once(
|
||||
monkeypatch,
|
||||
config,
|
||||
tunnel=True,
|
||||
tunnel_url="https://demo-xyz.trycloudflare.com",
|
||||
)
|
||||
|
||||
assert captured["tunnel_passed"] is True
|
||||
|
||||
|
||||
def test_deploy_workdir_cli_arg_beats_config(monkeypatch, tmp_path):
|
||||
cli_ws = tmp_path / "cli_ws"
|
||||
cfg_ws = tmp_path / "cfg_ws"
|
||||
|
||||
@@ -267,3 +267,30 @@ def test_async_subagents_available_init_false_without_env(monkeypatch):
|
||||
|
||||
reloaded = importlib.reload(mgr)
|
||||
assert reloaded._ASYNC_SUBAGENTS_AVAILABLE is False
|
||||
|
||||
|
||||
def test_tunnel_true_appends_flag(monkeypatch, tmp_path, runtime_paths):
|
||||
"""``tunnel=True`` must add ``--tunnel`` to the langgraph dev argv."""
|
||||
captured = _patch_start_prereqs(monkeypatch, tmp_path, runtime_paths)
|
||||
|
||||
with pytest.raises(_PopenAbort):
|
||||
manager.start_langgraph_dev(
|
||||
workspace_dir=tmp_path,
|
||||
port=16190,
|
||||
tunnel=True,
|
||||
)
|
||||
|
||||
assert "--tunnel" in captured["args"]
|
||||
|
||||
|
||||
def test_tunnel_false_default_omits_flag(monkeypatch, tmp_path, runtime_paths):
|
||||
"""``tunnel`` defaults to False — no ``--tunnel`` in the argv."""
|
||||
captured = _patch_start_prereqs(monkeypatch, tmp_path, runtime_paths)
|
||||
|
||||
with pytest.raises(_PopenAbort):
|
||||
manager.start_langgraph_dev(
|
||||
workspace_dir=tmp_path,
|
||||
port=16191,
|
||||
)
|
||||
|
||||
assert "--tunnel" not in captured["args"]
|
||||
|
||||
@@ -24,10 +24,12 @@ def reset_module_state():
|
||||
manager._PROCESS = None
|
||||
manager._PROCESS_WORKSPACE = None
|
||||
manager._ASYNC_SUBAGENTS_AVAILABLE = False
|
||||
manager._LOG_OFFSET_AT_START = 0
|
||||
yield
|
||||
manager._PROCESS = None
|
||||
manager._PROCESS_WORKSPACE = None
|
||||
manager._ASYNC_SUBAGENTS_AVAILABLE = False
|
||||
manager._LOG_OFFSET_AT_START = 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -468,3 +470,141 @@ class TestStartLanggraphDevRotatesLog:
|
||||
# under ``tmp_path``). The ``pid_dir`` we redirected to must
|
||||
# exist, proving the function reached past the mkdir prelude.
|
||||
assert pid_dir.is_dir()
|
||||
|
||||
|
||||
class TestStartLanggraphDevCapturesLogOffset:
|
||||
"""``start_langgraph_dev`` must capture ``_LOG_OFFSET_AT_START`` at the
|
||||
right moment — after ``_rotate_log_if_needed`` + ``open('ab')`` but
|
||||
before ``subprocess.Popen`` — so ``read_tunnel_url`` scans only this
|
||||
session's bytes. The existing ``TestReadTunnelUrl`` tests monkeypatch
|
||||
the offset directly (consumer side); these guard the producer side, so
|
||||
a regression moving the capture line would actually be caught.
|
||||
"""
|
||||
|
||||
def _patch_prereqs(self, tmp_path, monkeypatch, log):
|
||||
"""Mock everything up to (but not including) Popen, redirecting all
|
||||
runtime paths under ``tmp_path``."""
|
||||
pid_dir = tmp_path / "pids"
|
||||
monkeypatch.setattr(
|
||||
manager,
|
||||
"RUNTIME",
|
||||
dataclasses.replace(
|
||||
manager.LanggraphRuntimePaths.for_directory(pid_dir),
|
||||
log_file=log,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(manager, "_can_bind_port", lambda port: True)
|
||||
fake_config = tmp_path / "langgraph.json"
|
||||
fake_config.write_text("{}")
|
||||
monkeypatch.setattr(manager, "_langgraph_exe", lambda: "/fake/langgraph")
|
||||
monkeypatch.setattr(manager, "_packaged_langgraph_config", lambda: fake_config)
|
||||
|
||||
def test_offset_equals_existing_log_size(self, tmp_path, monkeypatch):
|
||||
"""No rotation → offset is the pre-existing (appended-to) log size,
|
||||
so a stale URL above that offset is never re-read."""
|
||||
log = tmp_path / "langgraph_dev.log"
|
||||
log.write_bytes(b"x" * 512)
|
||||
# Keep the log well under the rotation threshold so it is NOT rotated.
|
||||
monkeypatch.setattr(manager, "_LOG_ROTATION_BYTES", 10**9)
|
||||
self._patch_prereqs(tmp_path, monkeypatch, log)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def _fake_popen(args, **kwargs):
|
||||
# Read the global at the instant Popen is invoked — this is
|
||||
# strictly after the capture line in start_langgraph_dev.
|
||||
captured["offset"] = manager._LOG_OFFSET_AT_START
|
||||
raise FileNotFoundError("stop before real spawn")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"EvoScientist.langgraph_dev.manager.subprocess.Popen", _fake_popen
|
||||
)
|
||||
try:
|
||||
manager.start_langgraph_dev(workspace_dir=tmp_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
assert captured["offset"] == 512
|
||||
|
||||
def test_offset_zero_after_forced_rotation(self, tmp_path, monkeypatch):
|
||||
"""Forced rotation moves the old log away; the fresh ``open('ab')``
|
||||
starts empty → offset 0 (scan the whole new file)."""
|
||||
log = tmp_path / "langgraph_dev.log"
|
||||
log.write_bytes(b"x" * 4096)
|
||||
monkeypatch.setattr(manager, "_LOG_ROTATION_BYTES", 1024)
|
||||
self._patch_prereqs(tmp_path, monkeypatch, log)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def _fake_popen(args, **kwargs):
|
||||
captured["offset"] = manager._LOG_OFFSET_AT_START
|
||||
raise FileNotFoundError("stop before real spawn")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"EvoScientist.langgraph_dev.manager.subprocess.Popen", _fake_popen
|
||||
)
|
||||
try:
|
||||
manager.start_langgraph_dev(workspace_dir=tmp_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
assert (tmp_path / "langgraph_dev.log.1").exists() # rotation happened
|
||||
assert captured["offset"] == 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# read_tunnel_url
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestReadTunnelUrl:
|
||||
"""``read_tunnel_url`` scrapes the Cloudflare tunnel URL from the log,
|
||||
scanning only bytes written after the current subprocess started."""
|
||||
|
||||
def test_returns_url_when_present(self, tmp_path, runtime_paths, monkeypatch):
|
||||
log = tmp_path / "langgraph_dev.log"
|
||||
log.write_text(
|
||||
"INFO server up\n"
|
||||
"[cloudflared] Your quick Tunnel has been created! Visit it at:\n"
|
||||
"[cloudflared] https://happy-tiger-demo.trycloudflare.com\n"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
manager, "RUNTIME", dataclasses.replace(runtime_paths, log_file=log)
|
||||
)
|
||||
monkeypatch.setattr(manager, "_LOG_OFFSET_AT_START", 0)
|
||||
|
||||
assert (
|
||||
manager.read_tunnel_url(timeout=1.0)
|
||||
== "https://happy-tiger-demo.trycloudflare.com"
|
||||
)
|
||||
|
||||
def test_returns_none_on_timeout(self, tmp_path, runtime_paths, monkeypatch):
|
||||
log = tmp_path / "langgraph_dev.log"
|
||||
log.write_text("INFO server up — but no tunnel line ever printed\n")
|
||||
monkeypatch.setattr(
|
||||
manager, "RUNTIME", dataclasses.replace(runtime_paths, log_file=log)
|
||||
)
|
||||
monkeypatch.setattr(manager, "_LOG_OFFSET_AT_START", 0)
|
||||
|
||||
assert manager.read_tunnel_url(timeout=0.2, poll_interval=0.05) is None
|
||||
|
||||
def test_ignores_stale_url_before_offset(
|
||||
self, tmp_path, runtime_paths, monkeypatch
|
||||
):
|
||||
"""A URL from a previous session (before the offset) must be skipped;
|
||||
only this session's bytes count."""
|
||||
stale = "[cloudflared] https://old-stale-url.trycloudflare.com\n"
|
||||
log = tmp_path / "langgraph_dev.log"
|
||||
log.write_text(stale)
|
||||
monkeypatch.setattr(
|
||||
manager, "RUNTIME", dataclasses.replace(runtime_paths, log_file=log)
|
||||
)
|
||||
# Offset points past the stale line — nothing fresh yet → None.
|
||||
monkeypatch.setattr(manager, "_LOG_OFFSET_AT_START", len(stale.encode()))
|
||||
assert manager.read_tunnel_url(timeout=0.2, poll_interval=0.05) is None
|
||||
|
||||
# Now this session appends its own fresh URL → returned.
|
||||
with open(log, "a") as fh:
|
||||
fh.write("[cloudflared] https://fresh-new-url.trycloudflare.com\n")
|
||||
assert (
|
||||
manager.read_tunnel_url(timeout=1.0)
|
||||
== "https://fresh-new-url.trycloudflare.com"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user