mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-04 00:22:43 +02:00
feat(cli): tell people at a terminal about a newer release and Tauric's notices
- once a day a background thread records the latest GitHub release and the notices for each version; the welcome screen shows them from that cache, so no run waits on the network - the release notice names the upgrade command for how TradingAgents was installed - TRADINGAGENTS_NO_UPDATE_CHECK=1 turns the check off; unattended and CI runs never make it - notices are read from a new `notices` key, so earlier versions keep reading `announcements`
This commit is contained in:
@@ -54,8 +54,7 @@ class TestCliSkipsPromptsFromEnv(unittest.TestCase):
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False), \
|
||||
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
|
||||
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
|
||||
mock.patch.object(cli_selections, "display_announcements"), \
|
||||
mock.patch.object(cli_selections.updates, "enabled", return_value=False), \
|
||||
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
|
||||
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
|
||||
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
|
||||
@@ -96,8 +95,7 @@ class TestResearchDepthSkippedFromEnv(unittest.TestCase):
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False), \
|
||||
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
|
||||
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
|
||||
mock.patch.object(cli_selections, "display_announcements"), \
|
||||
mock.patch.object(cli_selections.updates, "enabled", return_value=False), \
|
||||
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
|
||||
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
|
||||
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
|
||||
@@ -125,8 +123,7 @@ class TestReasoningEffortSkippedFromEnv(unittest.TestCase):
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False), \
|
||||
mock.patch.object(cli_selections, "DEFAULT_CONFIG", fake_cfg), \
|
||||
mock.patch.object(cli_selections, "fetch_announcements", return_value=None), \
|
||||
mock.patch.object(cli_selections, "display_announcements"), \
|
||||
mock.patch.object(cli_selections.updates, "enabled", return_value=False), \
|
||||
mock.patch.object(cli_selections, "get_ticker", return_value="AAPL"), \
|
||||
mock.patch.object(cli_selections, "get_analysis_date", return_value="2026-05-29"), \
|
||||
mock.patch.object(cli_selections, "select_analysts", return_value=[]), \
|
||||
|
||||
@@ -56,8 +56,7 @@ def test_flags_skip_exactly_their_own_steps(monkeypatch):
|
||||
|
||||
for step in ("get_ticker", "get_analysis_date", "select_analysts"):
|
||||
monkeypatch.setattr(selections, step, no_prompt)
|
||||
monkeypatch.setattr(selections, "fetch_announcements", lambda: None)
|
||||
monkeypatch.setattr(selections, "display_announcements", lambda *a: None)
|
||||
monkeypatch.setattr(selections.updates, "enabled", lambda: False)
|
||||
|
||||
chosen = selections._prompt_selections({}, FLAGS)
|
||||
|
||||
@@ -119,19 +118,6 @@ def test_save_and_show_answer_the_questions_after_the_run(monkeypatch, tmp_path)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_an_announcement_does_not_wait_for_enter_without_a_terminal(monkeypatch):
|
||||
from cli import announcements
|
||||
from cli.display import console
|
||||
|
||||
def no_wait(*a):
|
||||
raise AssertionError("waited for Enter with no terminal")
|
||||
|
||||
monkeypatch.setattr(announcements.getpass, "getpass", no_wait)
|
||||
monkeypatch.setattr(announcements.sys.stdin, "isatty", lambda: False)
|
||||
announcements.display_announcements(console, {"content": "Maintenance tonight", "require_attention": True})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_missing_key_without_a_terminal_names_the_variable(monkeypatch, capsys):
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
@@ -200,8 +186,7 @@ def test_an_unknown_analyst_error_names_the_analysts_as_users_know_them():
|
||||
def _selections_with_env(monkeypatch, keys=None):
|
||||
for name, value in UNATTENDED_ENV.items():
|
||||
monkeypatch.setenv(name, value)
|
||||
monkeypatch.setattr(selections, "fetch_announcements", lambda: None)
|
||||
monkeypatch.setattr(selections, "display_announcements", lambda *a: None)
|
||||
monkeypatch.setattr(selections.updates, "enabled", lambda: False)
|
||||
checked = keys if keys is not None else []
|
||||
monkeypatch.setattr(selections, "ensure_api_key", lambda provider: checked.append(provider))
|
||||
return checked
|
||||
|
||||
@@ -95,8 +95,7 @@ def _answer_every_prompt(monkeypatch):
|
||||
"""Drive the real selection flow, answering each prompt with a fixed value."""
|
||||
import cli.main as m
|
||||
|
||||
monkeypatch.setattr(cli_selections, "fetch_announcements", lambda: [])
|
||||
monkeypatch.setattr(cli_selections, "display_announcements", lambda *a: None)
|
||||
monkeypatch.setattr(cli_selections.updates, "enabled", lambda: False)
|
||||
monkeypatch.setattr(cli_selections, "get_ticker", lambda: "NVDA")
|
||||
monkeypatch.setattr(cli_selections, "get_analysis_date", lambda: "2026-09-01")
|
||||
monkeypatch.setattr(cli_selections, "ask_output_language", lambda default=None: "English")
|
||||
@@ -148,8 +147,7 @@ def test_a_remembered_endpoint_is_offered_back(monkeypatch):
|
||||
monkeypatch.setattr(cli_selections, "select_llm_provider", lambda default=None: ("openai_compatible", None))
|
||||
monkeypatch.setattr(cli_selections, "prompt_openai_compatible_url",
|
||||
lambda default=None: offered.setdefault("default", default) or "http://x/v1")
|
||||
monkeypatch.setattr(cli_selections, "fetch_announcements", lambda: [])
|
||||
monkeypatch.setattr(cli_selections, "display_announcements", lambda *a: None)
|
||||
monkeypatch.setattr(cli_selections.updates, "enabled", lambda: False)
|
||||
monkeypatch.setattr(cli_selections, "get_ticker", lambda: "NVDA")
|
||||
monkeypatch.setattr(cli_selections, "get_analysis_date", lambda: "2026-09-01")
|
||||
monkeypatch.setattr(cli_selections, "ask_output_language", lambda default=None: "English")
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
"""What's new reaches the person at a terminal: a newer release and Tauric's notices.
|
||||
|
||||
The check runs at most daily in a background thread that only writes a cache; the
|
||||
notice shown is read from that cache, so a run never waits on the network, and
|
||||
unattended runs contact no server at all.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from packaging.version import Version
|
||||
from rich.console import Console
|
||||
|
||||
import cli.selections as cli_selections
|
||||
import cli.updates as updates
|
||||
|
||||
|
||||
class _Tty(io.StringIO):
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
|
||||
# --- who sees it -----------------------------------------------------------------
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("stdin_tty, stdout_tty, ci, opt_out, expected", [
|
||||
(True, True, None, None, True),
|
||||
(False, True, None, None, False),
|
||||
(True, False, None, None, False),
|
||||
(True, True, "true", None, False),
|
||||
(True, True, None, "1", False),
|
||||
])
|
||||
def test_only_a_person_at_a_terminal_is_told(monkeypatch, stdin_tty, stdout_tty, ci, opt_out, expected):
|
||||
monkeypatch.setattr("sys.stdin", _Tty() if stdin_tty else io.StringIO())
|
||||
monkeypatch.setattr("sys.stdout", _Tty() if stdout_tty else io.StringIO())
|
||||
for name, value in (("CI", ci), ("TRADINGAGENTS_NO_UPDATE_CHECK", opt_out)):
|
||||
if value is None:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
else:
|
||||
monkeypatch.setenv(name, value)
|
||||
assert updates.enabled() is expected
|
||||
|
||||
|
||||
ENV = {
|
||||
"TRADINGAGENTS_LLM_PROVIDER": "openai",
|
||||
"TRADINGAGENTS_DEEP_THINK_LLM": "gpt-6-sol",
|
||||
"TRADINGAGENTS_QUICK_THINK_LLM": "gpt-6-luna",
|
||||
"TRADINGAGENTS_OUTPUT_LANGUAGE": "English",
|
||||
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "1",
|
||||
"TRADINGAGENTS_MAX_RISK_ROUNDS": "1",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_an_unattended_run_contacts_no_server(monkeypatch):
|
||||
monkeypatch.setattr("sys.stdin", io.StringIO()) # no terminal
|
||||
calls = []
|
||||
monkeypatch.setattr(updates.requests, "get", lambda *a, **k: calls.append(a))
|
||||
started = []
|
||||
monkeypatch.setattr(updates.threading, "Thread", lambda *a, **k: started.append(1))
|
||||
with mock.patch.dict(os.environ, ENV, clear=False), \
|
||||
mock.patch.object(cli_selections, "ensure_api_key"):
|
||||
cli_selections.get_user_selections({"ticker": "AAPL", "date": "2026-09-01", "analysts": "market"})
|
||||
assert calls == [] and started == []
|
||||
|
||||
|
||||
# --- what it says -----------------------------------------------------------------
|
||||
|
||||
LATEST = {"version": "0.7.0", "url": "https://github.com/TauricResearch/TradingAgents/releases/tag/v0.7.0",
|
||||
"summary": "Update notices and a ChatGPT plan provider."}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("running, shown", [
|
||||
("0.6.0", True), ("0.7.0.dev0", True), # a development build of the release is older than it
|
||||
("0.7.0", False), ("0.8.0.dev0", False), ("not-a-version", False),
|
||||
])
|
||||
def test_a_newer_release_is_announced_once_in_the_notice(running, shown):
|
||||
lines = updates.messages({"latest": LATEST}, running=running, upgrade=lambda: None)
|
||||
assert any("0.7.0 is available" in line for line in lines) is shown
|
||||
if shown:
|
||||
text = "\n".join(lines)
|
||||
assert f"you have {running}" in text and LATEST["url"] in text and LATEST["summary"] in text
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_notice_names_the_upgrade_command():
|
||||
lines = updates.messages({"latest": LATEST}, running="0.6.0", upgrade=lambda: "git -C /src pull && pip install /src")
|
||||
assert "Upgrade: git -C /src pull && pip install /src" in "\n".join(lines)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remote_text_cannot_style_or_break_the_terminal():
|
||||
body = "[bold red]Grab it[/bold red] now\x1b[2J with `--html`, a very long line " + "x" * 400 + "\n\nSecond paragraph."
|
||||
summary = updates.summary_of(body)
|
||||
assert "\n" not in summary and "Second paragraph" not in summary and len(summary) <= 160
|
||||
lines = updates.messages({"latest": {**LATEST, "summary": summary}}, running="0.6.0", upgrade=lambda: None)
|
||||
shown = "\n".join(lines)
|
||||
assert "[bold red]Grab it" in shown and "\x1b" not in shown # shown as plain text, controls stripped
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_notices_reach_only_the_versions_they_name():
|
||||
cache = {"notices": [
|
||||
{"id": "a", "message": "For everyone", "url": None},
|
||||
{"id": "b", "message": "Old installs: news is fixed", "max_version": "0.6.0"},
|
||||
{"id": "c", "message": "New installs only", "min_version": "0.7.0"},
|
||||
{"id": "d", "message": "Expired", "expires": "2020-01-01"},
|
||||
{"id": "e", "message": "[link=x]markup[/link]"},
|
||||
"a bare string is not a notice",
|
||||
]}
|
||||
text = "\n".join(updates.messages(cache, running="0.6.0", upgrade=lambda: None))
|
||||
assert "For everyone" in text and "Old installs" in text
|
||||
assert "New installs only" not in text and "Expired" not in text
|
||||
assert "[link=x]markup[/link]" in text and "bare string" not in text
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_notice_shows_through_its_bounds_and_date_and_hides_when_they_cannot_be_read():
|
||||
today = date(2026, 10, 16)
|
||||
applies = lambda **notice: updates._applies(notice, Version("0.7.0"), today) # noqa: E731
|
||||
assert applies(min_version="0.7.0", max_version="0.7.0", expires="2026-10-16")
|
||||
assert applies(expires=None) and applies(min_version=None, max_version=None)
|
||||
assert not applies(expires="2026-10-15") and not applies(min_version="0.7.1") and not applies(max_version="0.6.9")
|
||||
for unreadable in ("", "seven", [], False):
|
||||
assert not applies(min_version=unreadable) and not applies(max_version=unreadable)
|
||||
for unreadable in ("", "2026-10-16junk", "20261020", "2026-W42-5", "2026-10-16T23:00-08:00", 20261016):
|
||||
assert not applies(expires=unreadable)
|
||||
assert not updates._applies({"min_version": "0.7.0"}, Version("0.7.0.dev0"), today)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_notice_is_shown_as_plain_text_with_only_a_web_link():
|
||||
cache = {"notices": [
|
||||
{"id": "a", "message": ":warning: [b]x[/b] \u202eev\u200bil", "url": "https://tauric.ai/a"},
|
||||
{"id": "b", "message": "Local link", "url": "file:///etc/passwd"},
|
||||
{"id": "e", "message": "Malformed link", "url": "https://[docs"},
|
||||
{"id": "c", "message": {"not": "text"}},
|
||||
{"id": "d", "message": "\x1b\x07"},
|
||||
]}
|
||||
assert updates.messages(cache, running="0.7.0") == [":warning: [b]x[/b] evil https://tauric.ai/a", "Local link",
|
||||
"Malformed link https://[docs"]
|
||||
console = Console(record=True, width=120, color_system=None)
|
||||
with mock.patch.object(updates, "read_cache", lambda: cache), mock.patch("tradingagents.__version__", "0.7.0"):
|
||||
updates.show(console)
|
||||
assert ":warning: [b]x[/b] evil" in console.export_text()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_notices_are_read_from_their_own_key():
|
||||
# Notices have their own key, so ``announcements`` stays a list of strings.
|
||||
payload = {"announcements": ["Plain text for older clients"],
|
||||
"notices": [{"id": "x", "message": "Targeted", "max_version": "0.6.0"}]}
|
||||
assert updates.parse_notices(payload) == payload["notices"]
|
||||
assert updates.parse_notices({"announcements": ["old shape only"]}) is None
|
||||
assert updates.parse_notices({"notices": []}) == []
|
||||
|
||||
|
||||
# --- how it is checked ---------------------------------------------------------------
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_check_is_due_once_a_day():
|
||||
now = 1_000_000.0
|
||||
assert updates.due({}, now)
|
||||
assert not updates.due({"checked_at": now - 3600}, now)
|
||||
assert updates.due({"checked_at": now - 25 * 3600}, now)
|
||||
|
||||
|
||||
def _response(payload, status=200):
|
||||
return SimpleNamespace(status_code=status, ok=status < 400, json=lambda: payload)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_check_records_the_latest_release_and_notices(tmp_path, monkeypatch):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
|
||||
def get(url, timeout):
|
||||
assert timeout <= 3
|
||||
if "api.github.com" in url:
|
||||
return _response({"tag_name": "v0.7.0", "html_url": LATEST["url"],
|
||||
"body": "Update notices and a ChatGPT plan provider.\n\n## Added"})
|
||||
return _response({"announcements": ["old"], "notices": [{"id": "n", "message": "hi"}]})
|
||||
|
||||
monkeypatch.setattr(updates.requests, "get", get)
|
||||
updates.refresh(now=5000.0)
|
||||
cache = json.loads(cache_file.read_text())
|
||||
assert cache["checked_at"] == 5000.0
|
||||
assert cache["latest"] == {"version": "0.7.0", "url": LATEST["url"], "summary": LATEST["summary"]}
|
||||
assert cache["notices"] == [{"id": "n", "message": "hi"}]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_failed_check_keeps_what_was_known_and_waits_a_day(tmp_path, monkeypatch, capsys):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
cache_file.write_text(json.dumps({"checked_at": 1.0, "latest": LATEST, "notices": []}))
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
|
||||
def get(url, timeout):
|
||||
raise updates.requests.ConnectionError("offline")
|
||||
|
||||
monkeypatch.setattr(updates.requests, "get", get)
|
||||
updates.refresh(now=9000.0) # never raises
|
||||
cache = json.loads(cache_file.read_text())
|
||||
assert cache["checked_at"] == 9000.0 and cache["latest"] == LATEST
|
||||
assert capsys.readouterr() == ("", "") # the check never prints
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_only_a_notices_list_replaces_the_known_notices(tmp_path, monkeypatch):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
known = [{"id": "n", "message": "hi"}]
|
||||
cache_file.write_text(json.dumps({"checked_at": 1.0, "notices": known}))
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
for answer in (_response({"detail": "Not Found"}, 404), _response({"error": "portal"}), _response(["x"])):
|
||||
monkeypatch.setattr(updates.requests, "get", lambda url, timeout, answer=answer: answer)
|
||||
updates.refresh(now=9000.0)
|
||||
assert json.loads(cache_file.read_text())["notices"] == known
|
||||
monkeypatch.setattr(updates.requests, "get", lambda url, timeout: _response({"announcements": [], "notices": []}))
|
||||
updates.refresh(now=99000.0)
|
||||
assert json.loads(cache_file.read_text())["notices"] == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_an_unreadable_cache_is_no_news(tmp_path, monkeypatch):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
cache_file.write_text("{not json")
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
assert updates.read_cache() == {}
|
||||
|
||||
|
||||
# --- the upgrade command for how it was installed ----------------------------------
|
||||
|
||||
def _dist(direct=None, installer="pip"):
|
||||
files = {"direct_url.json": json.dumps(direct) if direct else None, "INSTALLER": installer}
|
||||
return SimpleNamespace(read_text=lambda name: files.get(name))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_upgrade_command_follows_the_install(tmp_path, monkeypatch):
|
||||
clone = tmp_path / "TradingAgents"
|
||||
(clone / ".git").mkdir(parents=True)
|
||||
monkeypatch.delenv("TRADINGAGENTS_IN_DOCKER", raising=False)
|
||||
monkeypatch.setattr(updates.sys, "executable", "/venv/bin/python")
|
||||
url = clone.as_uri()
|
||||
|
||||
editable = updates.upgrade_command(_dist({"url": url, "dir_info": {"editable": True}}))
|
||||
assert editable == f"git -C {clone} pull && /venv/bin/python -m pip install -e {clone}"
|
||||
local = updates.upgrade_command(_dist({"url": url, "dir_info": {}}, installer="uv"))
|
||||
assert local == f"git -C {clone} pull && uv pip install --python /venv/bin/python {clone}"
|
||||
vcs = updates.upgrade_command(_dist({"url": "https://github.com/TauricResearch/TradingAgents.git",
|
||||
"vcs_info": {"vcs": "git", "commit_id": "abc"}}))
|
||||
assert vcs == "/venv/bin/python -m pip install -U git+https://github.com/TauricResearch/TradingAgents.git"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_command_is_guessed_without_a_clone(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("TRADINGAGENTS_IN_DOCKER", raising=False)
|
||||
gone = (tmp_path / "deleted-clone").as_uri()
|
||||
assert updates.upgrade_command(_dist({"url": gone, "dir_info": {}})) is None
|
||||
assert updates.upgrade_command(_dist(None)) is None
|
||||
monkeypatch.setenv("TRADINGAGENTS_IN_DOCKER", "1")
|
||||
assert updates.upgrade_command(_dist(None)) == "git pull && docker compose build (on the host)"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_whats_new_never_waits_for_the_person(tmp_path, monkeypatch):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
cache_file.write_text(json.dumps({"latest": LATEST, "notices": [{"id": "x", "message": "Notice"}]}))
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
monkeypatch.setattr("builtins.input", lambda *a: (_ for _ in ()).throw(AssertionError("asked for input")))
|
||||
from rich.console import Console
|
||||
out = Console(file=io.StringIO(), width=200)
|
||||
updates.show(out)
|
||||
assert "Notice" in out.file.getvalue()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("stored", [
|
||||
{"latest": "x"}, {"latest": ["0.9.0"]}, {"notices": 5}, {"notices": {"id": "x"}}, {"checked_at": "today"}, [],
|
||||
])
|
||||
def test_a_cache_of_the_wrong_shape_is_no_news(tmp_path, monkeypatch, stored):
|
||||
cache_file = tmp_path / "update_check.json"
|
||||
cache_file.write_text(json.dumps(stored))
|
||||
monkeypatch.setattr(updates, "CACHE", cache_file)
|
||||
assert updates.messages(updates.read_cache(), running="0.6.0", upgrade=lambda: None) == []
|
||||
assert updates.due(updates.read_cache(), 1_000_000.0)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_check_dated_in_the_future_is_due():
|
||||
assert updates.due({"checked_at": 2_000_000.0}, 1_000_000.0)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_notice_with_an_unreadable_expiry_is_not_shown():
|
||||
cache = {"notices": [{"message": "Bad date", "expires": "next week"},
|
||||
{"message": "Number", "expires": 20991231},
|
||||
{"message": "Timestamp", "expires": "2999-01-01T00:00:00Z"},
|
||||
{"message": "A date", "expires": "2999-01-01"}]}
|
||||
text = "\n".join(updates.messages(cache, running="0.6.0", upgrade=lambda: None))
|
||||
assert "Bad date" not in text and "Number" not in text and "Timestamp" not in text and "A date" in text
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remote_text_cannot_carry_c1_or_direction_controls():
|
||||
lines = updates.messages({"notices": [{"message": "A\x9b2Jbevil"}]}, running="0.6.0", upgrade=lambda: None)
|
||||
assert lines == ["A 2Jbevil"] # the C1 control reads as a space, the override is dropped
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_uv_git_install_is_upgraded_with_uv(monkeypatch):
|
||||
monkeypatch.delenv("TRADINGAGENTS_IN_DOCKER", raising=False)
|
||||
monkeypatch.setattr(updates.sys, "executable", "/venv/bin/python")
|
||||
vcs = updates.upgrade_command(_dist({"url": "https://github.com/TauricResearch/TradingAgents.git",
|
||||
"vcs_info": {"vcs": "git"}}, installer="uv"))
|
||||
assert vcs == "uv pip install --python /venv/bin/python -U git+https://github.com/TauricResearch/TradingAgents.git"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_command_is_offered_on_windows(monkeypatch):
|
||||
monkeypatch.delenv("TRADINGAGENTS_IN_DOCKER", raising=False)
|
||||
monkeypatch.setattr(updates.os, "name", "nt")
|
||||
assert updates.upgrade_command(_dist({"url": "https://github.com/TauricResearch/TradingAgents.git",
|
||||
"vcs_info": {"vcs": "git"}})) is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_upgrade_command_is_worked_out_only_for_a_newer_release():
|
||||
asked = []
|
||||
updates.messages({"latest": LATEST}, running=LATEST["version"], upgrade=lambda: asked.append(1))
|
||||
assert asked == []
|
||||
Reference in New Issue
Block a user