mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): suppress auto-update migration notice on fresh installs (#4224)
Fresh `dcode` installs no longer show the "now updates automatically by default" migration notice, which only applies to users upgrading from a pre-auto-update version. --- The one-time "dcode now updates automatically by default. vX will be installed on the next launch" notice (`should_announce_auto_update_default`, added in #3994) is meant only for users who ran dcode *before* auto-update became the opt-out default. It was leaking into brand-new installs, which never experienced the old behavior. On a fresh install the version cache is cold on the first launch (so `_run_startup_auto_update` returns early), then the notice fires on a later launch — confusing, and it delays the actual update by an extra launch. The fix pre-acknowledges the notice on the first launch ever — the canonical fresh-install signal in `should_show_whats_new` (when no `seen_version` exists yet) — so new installs never see it, while existing users upgrading into the auto-update era still get it exactly once. Made by [Open SWE](https://openswe.vercel.app/agents/7d20f2db-f40d-f031-7651-118a24f284ad) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -2583,6 +2583,32 @@ def mark_auto_update_default_acknowledged() -> bool:
|
||||
return _write_update_state({"auto_update_default_acknowledged": True})
|
||||
|
||||
|
||||
def _note_install_baseline() -> None:
|
||||
"""Pre-acknowledge the auto-update default notice for a fresh install.
|
||||
|
||||
The migration notice (`should_announce_auto_update_default`) is intended to
|
||||
warn users who ran dcode *before* auto-update became the opt-out default; a
|
||||
brand-new install never experienced the old behavior, so the notice is
|
||||
meaningless to it. Call this on the first launch ever (see
|
||||
`should_show_whats_new`) so the notice never leaks into a new install — the
|
||||
notice itself fires pre-TUI in `_run_startup_auto_update`.
|
||||
|
||||
Writes nothing when the user already set an explicit preference.
|
||||
"""
|
||||
if is_auto_update_explicitly_set():
|
||||
return
|
||||
if not mark_auto_update_default_acknowledged():
|
||||
# Fail-soft: the same unwritable state dir also drops the adjacent
|
||||
# `seen_version` write, so the install stays "first run ever" and the
|
||||
# stamp is retried next launch. Log the operation for context — the
|
||||
# generic write warning in `_write_update_state` can't say which write
|
||||
# failed when both fire back-to-back.
|
||||
logger.debug(
|
||||
"Could not stamp install baseline; the auto-update default notice "
|
||||
"will be re-evaluated on the next launch",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# "What's new" tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2603,7 +2629,11 @@ def should_show_whats_new() -> bool:
|
||||
"""Return `True` if this is the first launch on a newer version."""
|
||||
seen = get_seen_version()
|
||||
if seen is None:
|
||||
# First run ever — mark current as seen, don't show banner.
|
||||
# First run ever — mark current as seen, don't show banner. This is the
|
||||
# canonical fresh-install signal, so also pre-acknowledge the
|
||||
# auto-update default migration notice (which only applies to users who
|
||||
# predate the opt-out default) before it can fire on a later launch.
|
||||
_note_install_baseline()
|
||||
mark_version_seen(__version__)
|
||||
return False
|
||||
try:
|
||||
|
||||
@@ -27,6 +27,7 @@ from deepagents_code.update_check import (
|
||||
_extract_release_times,
|
||||
_install_extra_uv_tool_command,
|
||||
_latest_from_releases,
|
||||
_note_install_baseline,
|
||||
_parse_version,
|
||||
_uv_tool_bin_dir,
|
||||
cleanup_update_logs,
|
||||
@@ -3687,6 +3688,55 @@ class TestAutoUpdateDefaultMigration:
|
||||
config_path.write_text("not [ valid toml", encoding="utf-8")
|
||||
assert is_auto_update_explicitly_set() is False
|
||||
|
||||
def test_baseline_acknowledges_implicit_default(
|
||||
self,
|
||||
config_path, # noqa: ARG002
|
||||
state_file, # noqa: ARG002
|
||||
) -> None:
|
||||
"""`_note_install_baseline` pre-acknowledges the implicit-default notice.
|
||||
|
||||
On a fresh install the migration notice has no meaning, so stamping the
|
||||
baseline suppresses it.
|
||||
"""
|
||||
import os
|
||||
|
||||
os.environ.pop("DEEPAGENTS_CODE_AUTO_UPDATE", None)
|
||||
assert should_announce_auto_update_default() is True
|
||||
_note_install_baseline()
|
||||
assert should_announce_auto_update_default() is False
|
||||
|
||||
def test_baseline_skips_when_explicitly_set(self, config_path, state_file) -> None: # noqa: ARG002
|
||||
"""An explicit preference means no migration scenario, so no stamp.
|
||||
|
||||
With an explicit choice the notice is already suppressed; the baseline
|
||||
must not record an acknowledgement. Asserting on state *content* (rather
|
||||
than file existence) keeps the test robust to unrelated state another
|
||||
code path might write to the same file.
|
||||
"""
|
||||
with patch.dict("os.environ", {"DEEPAGENTS_CODE_AUTO_UPDATE": "1"}):
|
||||
_note_install_baseline()
|
||||
if state_file.exists():
|
||||
state = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
assert "auto_update_default_acknowledged" not in state
|
||||
|
||||
def test_baseline_tolerates_write_failure(self, config_path, tmp_path) -> None: # noqa: ARG002
|
||||
"""The fresh-install baseline stays fail-soft when the state write fails.
|
||||
|
||||
An unwritable state dir must not crash startup; the stamp is simply
|
||||
retried on the next launch (see `should_show_whats_new`).
|
||||
"""
|
||||
import os
|
||||
|
||||
os.environ.pop("DEEPAGENTS_CODE_AUTO_UPDATE", None)
|
||||
# Point the state file beneath an existing *file* so the parent
|
||||
# `mkdir`/write raises `OSError`, simulating an unwritable state dir.
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("not a directory", encoding="utf-8")
|
||||
with patch(
|
||||
"deepagents_code.update_check.UPDATE_STATE_FILE", blocker / "state.json"
|
||||
):
|
||||
_note_install_baseline() # must not raise
|
||||
|
||||
def test_mark_tolerates_write_failure(self, config_path, tmp_path) -> None: # noqa: ARG002
|
||||
"""A failed acknowledgement write returns `False` without raising.
|
||||
|
||||
@@ -3915,8 +3965,16 @@ class TestShouldShowWhatsNew:
|
||||
with patch("deepagents_code.update_check.UPDATE_STATE_FILE", path):
|
||||
yield path
|
||||
|
||||
def test_first_run_returns_false_and_marks(self, state_file) -> None:
|
||||
"""First run: returns False and writes current version as seen."""
|
||||
def test_first_run_returns_false_and_marks(
|
||||
self,
|
||||
state_file,
|
||||
config_path, # noqa: ARG002
|
||||
) -> None:
|
||||
"""First run: returns False and writes current version as seen.
|
||||
|
||||
Injects `config_path` so the first-run baseline's explicit-preference
|
||||
check reads a clean temp config instead of the developer's real one.
|
||||
"""
|
||||
from deepagents_code.update_check import should_show_whats_new
|
||||
|
||||
with patch("deepagents_code.update_check.__version__", "1.0.0"):
|
||||
@@ -3953,3 +4011,54 @@ class TestShouldShowWhatsNew:
|
||||
assert should_notify_update("2.0.0") is False
|
||||
# Notification throttle still works
|
||||
assert should_notify_update("2.0.0") is False
|
||||
|
||||
def test_first_run_suppresses_auto_update_notice(
|
||||
self,
|
||||
state_file,
|
||||
config_path, # noqa: ARG002
|
||||
) -> None:
|
||||
"""A fresh install's first run pre-acknowledges the migration notice.
|
||||
|
||||
The auto-update default notice only applies to users who predate the
|
||||
opt-out default, so a brand-new install must never see it.
|
||||
"""
|
||||
import os
|
||||
|
||||
from deepagents_code.update_check import should_show_whats_new
|
||||
|
||||
os.environ.pop("DEEPAGENTS_CODE_AUTO_UPDATE", None)
|
||||
assert should_announce_auto_update_default() is True
|
||||
with patch("deepagents_code.update_check.__version__", "1.0.0"):
|
||||
assert should_show_whats_new() is False
|
||||
assert should_announce_auto_update_default() is False
|
||||
# The baseline write must not clobber the adjacent `seen_version` write;
|
||||
# both land in the same state file on first run.
|
||||
data = json.loads(state_file.read_text(encoding="utf-8"))
|
||||
assert data["seen_version"] == "1.0.0"
|
||||
|
||||
def test_existing_install_still_sees_notice(
|
||||
self,
|
||||
state_file, # noqa: ARG002
|
||||
config_path, # noqa: ARG002
|
||||
) -> None:
|
||||
"""An upgrade for an existing install does not pre-acknowledge.
|
||||
|
||||
When a prior `seen_version` already exists, the first-run baseline does
|
||||
not fire, so a returning user still gets the one-time migration notice.
|
||||
"""
|
||||
import os
|
||||
|
||||
from deepagents_code.update_check import should_show_whats_new
|
||||
|
||||
os.environ.pop("DEEPAGENTS_CODE_AUTO_UPDATE", None)
|
||||
mark_version_seen("1.0.0")
|
||||
with patch("deepagents_code.update_check.__version__", "2.0.0"):
|
||||
assert should_show_whats_new() is True
|
||||
assert should_announce_auto_update_default() is True
|
||||
|
||||
@pytest.fixture
|
||||
def config_path(self, tmp_path):
|
||||
"""Override DEFAULT_CONFIG_PATH so explicit-preference checks are clean."""
|
||||
path = tmp_path / "config.toml"
|
||||
with patch("deepagents_code.update_check.DEFAULT_CONFIG_PATH", path):
|
||||
yield path
|
||||
|
||||
Reference in New Issue
Block a user