fix(code): preserve uv tool context when installing extras (#4201)

Split extra installation into a display path and an execution path so
user-facing recovery instructions match the public install script, while
the in-app uv flow still preserves the existing tool environment. This
avoids dropping the recorded Python interpreter or `--with` packages
when `/install <extra>` rewrites the uv-managed `dcode` tool.

## Changes
- Add `INSTALL_SCRIPT_COMMAND` and make `install_extras_command` /
`install_extra_command` display the promoted `curl -LsSf
https://langch.in/dcode | DEEPAGENTS_CODE_EXTRAS=... bash` reinstall
flow instead of raw `uv tool install`
- Add `_install_extra_uv_tool_command` for the actual uv-managed
execution path, merging the requested extra with installed extras
through `_uv_tool_install_command`
- Extend `_uv_tool_install_command` with `extras_to_add`, preserving the
uv receipt’s `--python` interpreter and existing `--with` packages while
adding the new extra
- Update `perform_install_extra` to use `_install_extra_uv_tool_command`
for uv-managed installs and report `ToolRequirementIntrospectionError`
when the receipt cannot be safely preserved
- Keep unsupported install guidance in `perform_install_extra`
receipt-free, so Homebrew/unknown installs get the install-script
recovery command without failing on uv receipt parsing
- Update `DeepAgentsApp._install_extra` and the `--install` CLI fallback
path to surface the install-script command in user-facing manual
recovery messages
This commit is contained in:
Mason Daugherty
2026-06-24 02:27:39 -04:00
committed by GitHub
parent 8e62957910
commit fcc616cf9b
7 changed files with 632 additions and 73 deletions
+45 -2
View File
@@ -4388,9 +4388,11 @@ class DeepAgentsApp(App):
ExtrasIntrospectionError,
)
from deepagents_code.update_check import (
ToolRequirementIntrospectionError,
create_update_log_path,
editable_extra_hint,
install_extra_command,
install_extra_recovery_command,
is_valid_extra_name,
perform_install_extra,
)
@@ -4425,7 +4427,11 @@ class DeepAgentsApp(App):
if extra not in KNOWN_EXTRAS and not force:
try:
manual_cmd = await asyncio.to_thread(install_extra_command, extra)
except (ExtrasIntrospectionError, ValueError) as exc:
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
) as exc:
logger.warning("/install command failed", exc_info=True)
await self._mount_message(
ErrorMessage(f"Install failed: {type(exc).__name__}: {exc}"),
@@ -4452,7 +4458,11 @@ class DeepAgentsApp(App):
)
try:
manual_cmd = await asyncio.to_thread(install_extra_command, extra)
except (ExtrasIntrospectionError, ValueError) as exc:
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
) as exc:
logger.warning("/install command failed", exc_info=True)
await self._mount_message(
ErrorMessage(
@@ -4464,6 +4474,24 @@ class DeepAgentsApp(App):
success, output = await perform_install_extra(extra, log_path=log_path)
except (OSError, asyncio.CancelledError) as exc:
logger.warning("/install command failed", exc_info=True)
# Best-effort upgrade of `manual_cmd` to the install-method-specific
# recovery command. On failure, keep the install-script command
# already bound above so the hint is never empty. `manual_cmd` is
# rendered into a Textual `Content` (literal, not Rich markup), so no
# bracket escaping is needed here.
try:
manual_cmd = await asyncio.to_thread(
install_extra_recovery_command, extra
)
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
):
logger.warning(
"/install recovery command failed (install raised)",
exc_info=True,
)
await self._mount_message(
ErrorMessage(
f"Install failed: {type(exc).__name__}: {exc}\n"
@@ -4477,6 +4505,21 @@ class DeepAgentsApp(App):
# Tail the last 200 chars — uv resolver prints the resolved
# error at the end, not the beginning.
detail = f": {output[-200:]}" if output else ""
# See the OSError branch above: best-effort recovery command, falling
# back to the already-bound install-script command on failure.
try:
manual_cmd = await asyncio.to_thread(
install_extra_recovery_command, extra
)
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
):
logger.warning(
"/install recovery command failed (install reported failure)",
exc_info=True,
)
await self._mount_message(
ErrorMessage(
f"Install failed{detail}\n"
+35 -5
View File
@@ -2722,11 +2722,17 @@ def cli_main() -> None:
from rich.markup import escape
from deepagents_code.config import _is_editable_install
from deepagents_code.extras_info import KNOWN_EXTRAS
from deepagents_code.extras_info import (
KNOWN_EXTRAS,
ExtrasIntrospectionError,
)
from deepagents_code.update_check import (
ToolRequirementIntrospectionError,
create_update_log_path,
editable_extra_hint,
install_extra_command,
install_extra_recovery_command,
install_extras_command,
is_valid_extra_name,
perform_install_extra,
)
@@ -2799,10 +2805,24 @@ def cli_main() -> None:
# Tail the last 200 chars — uv resolver prints the resolved
# error at the end, not the beginning.
detail = f": {output[-200:]}" if output else ""
try:
manual_cmd = install_extra_recovery_command(extra)
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
):
logger.warning(
"--install recovery command failed (install reported failure)",
exc_info=True,
)
# Keep the install-script command bound above; fall back to a
# bare extras command only if that was never set.
manual_cmd = manual_cmd or install_extras_command((extra,))
console.print(
f"[bold red]Install failed[/bold red]{escape(detail)}\n"
f"Log: {log_path}\n"
f"Run manually: [cyan]{manual_cmd}[/cyan]",
f"Run manually: [cyan]{escape(manual_cmd)}[/cyan]",
markup=True,
highlight=False,
)
@@ -2813,9 +2833,19 @@ def cli_main() -> None:
except Exception as exc:
logger.warning("--install failed", exc_info=True)
log_line = f"\nLog: {log_path}" if log_path else ""
fallback_cmd = (
manual_cmd or f"uv tool install -U 'deepagents-code[{extra}]'"
)
# This is the catch-all for any unexpected install failure, so
# the recovery-hint guard is intentionally broad too: it must
# never raise a second error over the original one. `manual_cmd`
# may be unset here (the failure could predate its assignment),
# so fall back to a bare extras command.
try:
fallback_cmd = install_extra_recovery_command(extra)
except Exception: # best-effort hint, never re-raise here
logger.warning(
"--install recovery command failed (unexpected error)",
exc_info=True,
)
fallback_cmd = manual_cmd or install_extras_command((extra,))
console.print(
f"[bold red]Error:[/bold red] "
f"{type(exc).__name__}: {escape(str(exc))}"
+107 -31
View File
@@ -129,6 +129,9 @@ since that one-liner is the path we promote.
_UPGRADE_TIMEOUT = 120 # seconds
"""Wall-clock cap for `perform_upgrade` and `perform_install_extra`."""
INSTALL_SCRIPT_COMMAND = "curl -LsSf https://langch.in/dcode | bash"
"""Promoted public install command for Deep Agents Code."""
UPDATE_LOG_DIR: Path = DEFAULT_STATE_DIR / "update_logs"
"""Directory for persisted update command logs."""
@@ -1872,19 +1875,32 @@ def _uv_tool_install_command(
version: str | None,
include_prereleases: bool | None,
distribution_name: str,
extras_to_add: Iterable[str] = (),
) -> str:
"""Return the receipt-preserving `uv tool install -U` command.
Args:
version: Optional exact `deepagents-code` version pin.
include_prereleases: Whether to include alpha/beta/rc releases. When
`None`, follows the installed version's channel.
distribution_name: Name of the installed distribution to inspect.
extras_to_add: Extra names to merge with already-installed extras.
Raises:
ExtrasIntrospectionError: If a metadata-sourced extra name fails PEP 508
validation.
Propagates `ToolRequirementIntrospectionError` if the uv tool receipt's
interpreter or `--with` packages cannot be determined safely from the tool
receipt.
"""
from deepagents_code.extras_info import (
ExtrasIntrospectionError,
installed_extra_names,
)
extras = installed_extra_names(distribution_name, strict=True)
extras = set(installed_extra_names(distribution_name, strict=True))
extras.update(extras_to_add)
try:
requirement = _dcode_extras_requirement(extras, version=version)
except ValueError as exc:
@@ -2077,7 +2093,7 @@ def install_package_command(
def install_extras_command(extras: Iterable[str]) -> str:
"""Return the uv command that installs the exact set of dcode extras.
"""Return the install-script command that installs dcode extras.
Args:
extras: Extra names to include in the tool reinstall. Validated by
@@ -2085,10 +2101,16 @@ def install_extras_command(extras: Iterable[str]) -> str:
that fails PEP 508 validation.
Returns:
Shell command string suitable for display in error messages and
execution via `perform_install_extra`.
Shell command string suitable for display in error messages.
"""
return f"uv tool install -U {_dcode_extras_requirement(extras)}"
names = sorted(extras)
_dcode_extras_requirement(names)
if not names:
return INSTALL_SCRIPT_COMMAND
extras_env = shlex.quote(",".join(names))
return (
f"curl -LsSf https://langch.in/dcode | DEEPAGENTS_CODE_EXTRAS={extras_env} bash"
)
def install_extra_command(
@@ -2096,11 +2118,13 @@ def install_extra_command(
*,
distribution_name: str = "deepagents-code",
) -> str:
"""Return the shell command that adds `extra` to the installed dcode tool.
"""Return the install-script command that adds `extra` to dcode.
The documented install path is `uv tool install` (see
`scripts/install.sh`), so extras must be preserved across reinstalls.
Single-quoting the bracket form keeps zsh from globbing it.
The promoted install path is the install script (see `scripts/install.sh`).
This helper is display-only and avoids uv receipt introspection so
unsupported installs can surface method-specific guidance before any uv
receipt is read. Already-detected extras from distribution metadata are
included when available, so following the command does not drop them.
Args:
extra: The extra name (e.g. `'quickjs'`, `'daytona'`, `'fireworks'`).
@@ -2110,35 +2134,83 @@ def install_extra_command(
already-installed extras.
Returns:
Shell command string suitable for display in error messages and
for execution via `perform_install_extra`.
Shell command string suitable for display in error messages.
Raises:
ExtrasIntrospectionError: If installed extras cannot be determined
safely from distribution metadata.
ValueError: If `extra` or any already-installed extra fails PEP 508
validation.
ValueError: If `extra` fails PEP 508 validation.
"""
from deepagents_code.extras_info import (
ExtrasIntrospectionError,
installed_extra_names,
)
if not is_valid_extra_name(extra):
msg = (
f"Invalid extra name {extra!r}: must match PEP 508 "
f"({_EXTRA_NAME_RE.pattern})"
)
raise ValueError(msg)
try:
extras = installed_extra_names(distribution_name, strict=True)
except ExtrasIntrospectionError as exc:
msg = str(exc)
raise ExtrasIntrospectionError(msg) from exc
from deepagents_code.extras_info import installed_extra_names
extras = installed_extra_names(distribution_name)
extras.add(extra)
return install_extras_command(extras)
def install_extra_recovery_command(extra: str) -> str:
"""Return a manual recovery command for the current install method.
uv-managed installs can preserve the uv receipt's Python interpreter and
`--with` requirements, so their recovery command uses the same uv path as
the automatic installer. Unsupported methods keep the install-script command
and deliberately avoid reading uv receipts.
Args:
extra: Extra name to add.
Returns:
Shell command string suitable for display in error messages.
Propagates `ValueError` if `extra` fails PEP 508 validation, and (on the uv
path) `ExtrasIntrospectionError` if installed extras cannot be determined
safely or `ToolRequirementIntrospectionError` if the uv receipt's
interpreter or `--with` packages cannot be preserved safely.
"""
if detect_install_method() == "uv":
return _install_extra_uv_tool_command(extra)
return install_extra_command(extra)
def _install_extra_uv_tool_command(
extra: str,
*,
distribution_name: str = "deepagents-code",
) -> str:
"""Return the receipt-preserving uv command that installs one dcode extra.
Args:
extra: The extra name to add. Validated against PEP 508 grammar before
interpolation into the shell command.
distribution_name: Name of the installed distribution to inspect for
already-installed extras and uv receipt requirements.
Raises:
ValueError: If `extra` fails PEP 508 validation.
Propagates `ExtrasIntrospectionError` if installed extras cannot be
determined safely from distribution metadata, and
`ToolRequirementIntrospectionError` if the uv tool receipt's interpreter or
`--with` packages cannot be preserved safely.
"""
if not is_valid_extra_name(extra):
msg = (
f"Invalid extra name {extra!r}: must match PEP 508 "
f"({_EXTRA_NAME_RE.pattern})"
)
raise ValueError(msg)
return _uv_tool_install_command(
version=None,
include_prereleases=None,
distribution_name=distribution_name,
extras_to_add=(extra,),
)
def editable_extra_hint(extra: str) -> str:
"""Return the canonical action hint for editable installs missing an extra.
@@ -2209,15 +2281,15 @@ async def perform_install_extra(
# right escape hatch but would conflict with the brew-managed binary.
return False, (
"Homebrew install detected — extras are not supported via brew. "
"Reinstall with `uv tool install -U 'deepagents-code["
f"{extra}]'` to switch to a uv-managed tool install with extras."
f"Reinstall with `{install_extra_command(extra)}` to switch to a "
"uv-managed tool install with extras."
)
if method == "other":
return False, (
"Unsupported install method detected — cannot add extras without "
"knowing which environment provides `dcode`. Reinstall with "
f"`uv tool install -U 'deepagents-code[{extra}]'` to switch to a "
"uv-managed tool install with extras."
f"`{install_extra_command(extra)}` to switch to a uv-managed tool "
"install with extras."
)
if not shutil.which("uv"):
@@ -2229,8 +2301,12 @@ async def perform_install_extra(
from deepagents_code.extras_info import ExtrasIntrospectionError
try:
cmd = install_extra_command(extra)
except (ExtrasIntrospectionError, ValueError) as exc:
cmd = _install_extra_uv_tool_command(extra)
except (
ExtrasIntrospectionError,
ToolRequirementIntrospectionError,
ValueError,
) as exc:
return False, f"{type(exc).__name__}: {exc}"
return await _run_install_subprocess(cmd, progress=progress, log_path=log_path)
+37
View File
@@ -7732,6 +7732,14 @@ class TestInstallExtraModelSwitch:
monkeypatch.setattr(
update_check, "install_extra_command", lambda extra: f"uv install {extra}"
)
# Inert stub: install succeeds below, so the recovery command (only
# built on failure) is never invoked. Patched to guard against an
# accidental real call introspecting the host's install state.
monkeypatch.setattr(
update_check,
"install_extra_recovery_command",
lambda extra: f"uv install {extra}",
)
monkeypatch.setattr(
update_check,
"perform_install_extra",
@@ -7771,6 +7779,14 @@ class TestInstallExtraModelSwitch:
monkeypatch.setattr(
update_check, "install_extra_command", lambda extra: f"uv install {extra}"
)
# Inert stub: install succeeds below, so the recovery command (only
# built on failure) is never invoked. Patched to guard against an
# accidental real call introspecting the host's install state.
monkeypatch.setattr(
update_check,
"install_extra_recovery_command",
lambda extra: f"uv install {extra}",
)
monkeypatch.setattr(
update_check,
"perform_install_extra",
@@ -7808,6 +7824,14 @@ class TestInstallExtraModelSwitch:
monkeypatch.setattr(
update_check, "install_extra_command", lambda extra: f"uv install {extra}"
)
# Inert stub: install succeeds below, so the recovery command (only
# built on failure) is never invoked. Patched to guard against an
# accidental real call introspecting the host's install state.
monkeypatch.setattr(
update_check,
"install_extra_recovery_command",
lambda extra: f"uv install {extra}",
)
monkeypatch.setattr(
update_check,
"perform_install_extra",
@@ -7843,6 +7867,14 @@ class TestInstallExtraModelSwitch:
monkeypatch.setattr(
update_check, "install_extra_command", lambda extra: f"uv install {extra}"
)
# Inert stub: install succeeds below, so the recovery command (only
# built on failure) is never invoked. Patched to guard against an
# accidental real call introspecting the host's install state.
monkeypatch.setattr(
update_check,
"install_extra_recovery_command",
lambda extra: f"uv install {extra}",
)
monkeypatch.setattr(
update_check,
"perform_install_extra",
@@ -7933,6 +7965,11 @@ class TestInstallExtraModelSwitch:
monkeypatch.setattr(
update_check, "install_extra_command", lambda extra: f"uv install {extra}"
)
monkeypatch.setattr(
update_check,
"install_extra_recovery_command",
lambda extra: f"uv install {extra}",
)
monkeypatch.setattr(
update_check,
"perform_install_extra",
@@ -14,6 +14,10 @@ import pytest
from deepagents_code.app import DeepAgentsApp
from deepagents_code.widgets.messages import AppMessage, ErrorMessage
MANUAL_EXTRA_COMMAND = (
"curl -LsSf https://langch.in/dcode | DEEPAGENTS_CODE_EXTRAS=quickjs bash"
)
async def test_install_slash_usage_when_no_extra() -> None:
"""`/install` with no argument prints a usage hint plus the valid extras."""
@@ -293,6 +297,14 @@ async def test_install_slash_failure_surfaces_log_path_and_manual_cmd() -> None:
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.install_extra_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
@@ -306,7 +318,8 @@ async def test_install_slash_failure_surfaces_log_path_and_manual_cmd() -> None:
assert "Install failed" in joined
assert "resolver: conflict" in joined
assert "/tmp/deepagents-install.log" in joined
assert "uv tool install -U 'deepagents-code" in joined
assert "curl -LsSf https://langch.in/dcode" in joined
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in joined
assert "quickjs" in joined
@@ -321,6 +334,14 @@ async def test_install_slash_exception_surfaces_log_path_and_manual_cmd() -> Non
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.install_extra_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
@@ -334,10 +355,120 @@ async def test_install_slash_exception_surfaces_log_path_and_manual_cmd() -> Non
assert "OSError" in joined
assert "disk full" in joined
assert "/tmp/deepagents-install.log" in joined
assert "uv tool install -U 'deepagents-code" in joined
assert "curl -LsSf https://langch.in/dcode" in joined
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in joined
assert "quickjs" in joined
async def test_install_slash_failure_renders_recovery_bracket_literally() -> None:
"""A uv recovery command's `[extra]` bracket renders literally in the TUI.
The TUI mounts recovery commands as Textual `Content`, so — unlike the
Rich-markup CLI path — the bracket must not be backslash-escaped.
"""
uv_cmd = "uv tool install -U 'deepagents-code[quickjs]'"
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.install_extra_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
return_value=uv_cmd,
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(False, "resolver: conflict"),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
joined = "\n".join(str(m._content) for m in app.query(ErrorMessage))
assert "deepagents-code[quickjs]" in joined
assert "deepagents-code\\[quickjs]" not in joined
async def test_install_slash_failure_recovery_error_keeps_prior_command() -> None:
"""A recovery-command error on a failed install keeps the prior command.
The TUI shows the command resolved before the failure rather than crashing
or showing nothing.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.install_extra_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
side_effect=ValueError("bad receipt"),
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
return_value=(False, "resolver: conflict"),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
joined = "\n".join(str(m._content) for m in app.query(ErrorMessage))
assert "Install failed" in joined
assert MANUAL_EXTRA_COMMAND in joined
async def test_install_slash_exception_recovery_error_keeps_prior_command() -> None:
"""A raised install plus a failed recovery command keeps the prior command.
When `perform_install_extra` raises and the recovery command also fails, the
TUI still surfaces the command resolved before the failure.
"""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.create_update_log_path",
return_value="/tmp/deepagents-install.log",
),
patch(
"deepagents_code.update_check.install_extra_command",
return_value=MANUAL_EXTRA_COMMAND,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
side_effect=ValueError("bad receipt"),
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
side_effect=OSError("disk full"),
),
):
await app._handle_command("/install quickjs")
await pilot.pause()
joined = "\n".join(str(m._content) for m in app.query(ErrorMessage))
assert "OSError" in joined
assert MANUAL_EXTRA_COMMAND in joined
async def test_install_slash_editable_install_refuses() -> None:
"""Editable installs must not invoke `perform_install_extra` from the TUI.
+72 -7
View File
@@ -1715,7 +1715,10 @@ class TestInstallExtraSubcommand:
# the handler's `isatty()` refusal check.
mock_stdin.read.return_value = ""
command_mock = MagicMock(
return_value=f"uv tool install -U 'deepagents-code[{extra}]'",
return_value=(
"curl -LsSf https://langch.in/dcode | "
f"DEEPAGENTS_CODE_EXTRAS={extra} bash"
),
)
if command_side_effect is not None:
command_mock.side_effect = command_side_effect
@@ -1743,6 +1746,10 @@ class TestInstallExtraSubcommand:
"deepagents_code.update_check.install_extra_command",
command_mock,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
command_mock,
),
patch(
"deepagents_code.update_check.perform_install_extra",
new_callable=AsyncMock,
@@ -1800,10 +1807,19 @@ class TestInstallExtraSubcommand:
perform_return: tuple[bool, str] = (True, ""),
perform_side_effect: BaseException | None = None,
command_side_effect: BaseException | None = None,
command_return: str | None = None,
recovery_side_effect: BaseException | None = None,
recovery_return: str | None = None,
input_reply: str = "n",
) -> tuple[int, MagicMock, MagicMock]:
"""Invoke `cli_main()` with `--install` and capture console output.
`install_extra_command` and `install_extra_recovery_command` share one
mock by default (the realistic "both resolve identically" case). Pass
`recovery_return` or `recovery_side_effect` to drive the recovery
command independently — e.g. to exercise the path where the initial
command resolves but the recovery command raises.
Returns:
`(exit_code, perform_mock, console_mock)` — *console_mock* is a
`MagicMock` substituted for `deepagents_code.main.console`,
@@ -1826,11 +1842,22 @@ class TestInstallExtraSubcommand:
perform_mock.side_effect = perform_side_effect
else:
perform_mock.return_value = perform_return
command_mock = MagicMock(
return_value=f"uv tool install -U 'deepagents-code[{extra}]'",
default_script_cmd = (
f"curl -LsSf https://langch.in/dcode | DEEPAGENTS_CODE_EXTRAS={extra} bash"
)
command_mock = MagicMock(return_value=command_return or default_script_cmd)
if command_side_effect is not None:
command_mock.side_effect = command_side_effect
# Default: recovery resolves to the same command. Only split into its
# own mock when a test drives the recovery command independently.
if recovery_return is not None or recovery_side_effect is not None:
recovery_mock = MagicMock(
return_value=recovery_return or default_script_cmd
)
if recovery_side_effect is not None:
recovery_mock.side_effect = recovery_side_effect
else:
recovery_mock = command_mock
with (
patch.object(sys, "argv", argv),
patch.object(sys, "stdin", mock_stdin),
@@ -1848,6 +1875,10 @@ class TestInstallExtraSubcommand:
"deepagents_code.update_check.install_extra_command",
command_mock,
),
patch(
"deepagents_code.update_check.install_extra_recovery_command",
recovery_mock,
),
patch(
"deepagents_code.update_check.perform_install_extra",
perform_mock,
@@ -1874,7 +1905,7 @@ class TestInstallExtraSubcommand:
assert "Installed extra 'quickjs'" in text
def test_failure_renders_log_path_and_manual_command(self) -> None:
"""A failed install surfaces both the log path and the manual uv command."""
"""A failed install surfaces both the log path and manual script command."""
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_return=(False, "resolver: conflict"),
@@ -1884,9 +1915,41 @@ class TestInstallExtraSubcommand:
assert "Install failed" in text
assert "resolver: conflict" in text
assert "/tmp/deepagents-install.log" in text
assert "uv tool install -U 'deepagents-code" in text
assert "curl -LsSf https://langch.in/dcode" in text
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in text
assert "quickjs" in text
def test_failure_escapes_uv_recovery_command_markup(self) -> None:
"""Failed uv recovery commands preserve extras rendered by Rich."""
command = "uv tool install -U 'deepagents-code[quickjs]'"
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_return=(False, "resolver: conflict"),
command_return=command,
)
assert code == 1
text = self._printed_text(console_mock)
assert "deepagents-code\\[quickjs]" in text
def test_failure_recovery_command_error_keeps_prior_command(self) -> None:
"""A recovery-command error on a failed install keeps the prior command.
The command resolved before the failure is shown instead of crashing.
"""
resolved = "uv tool install -U 'deepagents-code[quickjs]'"
code, _perform, console_mock = self._run_install_capture(
"quickjs",
perform_return=(False, "resolver: conflict"),
command_return=resolved,
recovery_side_effect=ValueError("bad receipt"),
)
assert code == 1
text = self._printed_text(console_mock)
assert "Install failed" in text
# Falls back to the install_extra_command value resolved before the
# failure, with its bracket escaped for Rich.
assert "deepagents-code\\[quickjs]" in text
def test_keyboard_interrupt_exits_130(self) -> None:
"""Ctrl-C during install exits 130 with an Aborted message."""
code, _perform, console_mock = self._run_install_capture(
@@ -1907,7 +1970,8 @@ class TestInstallExtraSubcommand:
assert "RuntimeError" in text
assert "disk full" in text
assert "/tmp/deepagents-install.log" in text
assert "uv tool install -U 'deepagents-code" in text
assert "curl -LsSf https://langch.in/dcode" in text
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in text
assert "quickjs" in text
def test_command_generation_exception_uses_literal_fallback(self) -> None:
@@ -1922,7 +1986,8 @@ class TestInstallExtraSubcommand:
assert "RuntimeError" in text
assert "metadata broken" in text
assert "Run manually: " in text
assert "uv tool install -U 'deepagents-code\\[quickjs]'" in text
assert "curl -LsSf https://langch.in/dcode" in text
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in text
def test_interactive_decline_aborts(self) -> None:
"""Interactive TTY + reply 'n' to unknown extra aborts with exit 1."""
+203 -26
View File
@@ -25,6 +25,7 @@ from deepagents_code.update_check import (
ShadowedDcode,
ToolRequirementIntrospectionError,
_extract_release_times,
_install_extra_uv_tool_command,
_latest_from_releases,
_parse_version,
_uv_tool_bin_dir,
@@ -54,6 +55,7 @@ from deepagents_code.update_check import (
get_sdk_release_time,
get_seen_version,
install_extra_command,
install_extra_recovery_command,
install_extras_command,
install_package_command,
is_auto_update_enabled,
@@ -2464,48 +2466,120 @@ class TestDependencyRefreshSupported:
class TestInstallExtraCommand:
"""`install_extra_command` builds the uv tool install string."""
"""`install_extra_command` builds the promoted install-script string."""
def test_basic(self) -> None:
"""Single-quoted bracket form, with `-U` to reinstall."""
"""Extras are passed to the install script through its environment."""
assert (
install_extras_command(["quickjs"])
== "uv tool install -U 'deepagents-code[quickjs]'"
== "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=quickjs bash"
)
def test_provider_extra(self) -> None:
assert (
install_extras_command(["fireworks"])
== "uv tool install -U 'deepagents-code[fireworks]'"
== "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=fireworks bash"
)
def test_installed_extra_names_missing_distribution_returns_empty(self) -> None:
"""Display-only introspection stays forgiving when metadata is absent."""
assert installed_extra_names("does-not-exist-pkg-xyz-abc") == set()
def test_install_extra_command_refuses_missing_distribution(self) -> None:
"""Reinstall commands must not drop extras when metadata is unavailable."""
with pytest.raises(ExtrasIntrospectionError, match="cannot preserve"):
def test_install_extra_command_ignores_missing_distribution(self) -> None:
"""Display-only commands tolerate missing distribution metadata."""
assert (
install_extra_command("quickjs", distribution_name="missing-dcode-test")
== "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=quickjs bash"
)
def test_no_installed_extras_from_clean_metadata(
self, tmp_path, monkeypatch
) -> None:
"""Clean metadata with no installed optional deps is distinct from failure."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(
tmp_path,
"deepagents-code",
requires=('definitely-absent-dcode-test-quickjs-xyz; extra == "quickjs"',),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == set()
assert (
install_extra_command("quickjs", distribution_name="deepagents-code")
== "uv tool install -U 'deepagents-code[quickjs]'"
install_extra_command("quickjs") == "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=quickjs bash"
)
def test_install_extra_command_refuses_invalid_metadata(
def test_install_extra_command_preserves_detected_extras(
self, tmp_path, monkeypatch
) -> None:
"""Install-script guidance keeps existing extras when metadata reveals them."""
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(
tmp_path,
"deepagents-code",
requires=(
'definitely-present-dcode-test-nvidia; extra == "nvidia"',
'definitely-absent-dcode-test-quickjs-xyz; extra == "quickjs"',
),
)
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
install_extra_command("quickjs") == "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=nvidia,quickjs bash"
)
def test_recovery_command_preserves_uv_receipt_context(
self, tmp_path, monkeypatch
) -> None:
"""UV recovery guidance matches the automatic context-preserving install."""
_write_uv_receipt(
tmp_path,
'{ name = "deepagents-code" }, { name = "langchain-custom" }',
python="/opt/Python 3.13/bin/python",
)
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(
tmp_path,
"deepagents-code",
requires=('definitely-present-dcode-test-nvidia; extra == "nvidia"',),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.setattr(
"deepagents_code.update_check.detect_install_method", lambda: "uv"
)
assert install_extra_recovery_command("quickjs") == (
"uv tool install -U --python '/opt/Python 3.13/bin/python' "
"'deepagents-code[nvidia,quickjs]' --with langchain-custom"
)
def test_recovery_command_uses_script_for_non_uv_without_receipt(
self, tmp_path, monkeypatch
) -> None:
"""Unsupported install recovery does not require uv receipt introspection."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }, "bad"')
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.setattr(
"deepagents_code.update_check.detect_install_method", lambda: "other"
)
monkeypatch.setattr(
"deepagents_code.extras_info.installed_extra_names",
lambda _distribution_name="deepagents-code": set(),
)
assert install_extra_recovery_command("quickjs") == (
"curl -LsSf https://langch.in/dcode | DEEPAGENTS_CODE_EXTRAS=quickjs bash"
)
def test_uv_install_extra_command_refuses_invalid_metadata(
self, tmp_path, monkeypatch
) -> None:
"""Malformed optional-dependency metadata must not drop existing extras."""
@@ -2517,10 +2591,15 @@ class TestInstallExtraCommand:
monkeypatch.syspath_prepend(str(tmp_path))
with pytest.raises(ExtrasIntrospectionError, match="Could not parse"):
install_extra_command("quickjs", distribution_name="deepagents-code")
_install_extra_uv_tool_command(
"quickjs", distribution_name="deepagents-code"
)
def test_preserves_installed_extras(self, tmp_path, monkeypatch) -> None:
def test_uv_install_extra_command_preserves_installed_extras(
self, tmp_path, monkeypatch
) -> None:
"""Installing a new extra keeps already-installed extras selected."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(
tmp_path,
@@ -2530,31 +2609,43 @@ class TestInstallExtraCommand:
'definitely-absent-dcode-test-baseten-xyz; extra == "baseten"',
),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
install_extra_command("baseten", distribution_name="deepagents-code")
_install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
)
== "uv tool install -U 'deepagents-code[baseten,nvidia]'"
)
def test_dedupes_existing_extra(self, tmp_path, monkeypatch) -> None:
def test_uv_install_extra_command_dedupes_existing_extra(
self, tmp_path, monkeypatch
) -> None:
"""Installing an already-present extra does not duplicate it."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(
tmp_path,
"deepagents-code",
requires=('definitely-present-dcode-test-nvidia; extra == "nvidia"',),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
assert (
install_extra_command("nvidia", distribution_name="deepagents-code")
_install_extra_uv_tool_command(
"nvidia", distribution_name="deepagents-code"
)
== "uv tool install -U 'deepagents-code[nvidia]'"
)
def test_drops_composite_extras(self, tmp_path, monkeypatch) -> None:
def test_uv_install_extra_command_drops_composite_extras(
self, tmp_path, monkeypatch
) -> None:
"""Composite extras are not echoed back into uv reinstall commands."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(tmp_path, "definitely-present-dcode-test-openai")
_write_dist_info(
@@ -2565,27 +2656,55 @@ class TestInstallExtraCommand:
'definitely-present-dcode-test-openai; extra == "all-providers"',
),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
install_extra_command("baseten", distribution_name="deepagents-code")
_install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
)
== "uv tool install -U 'deepagents-code[baseten,nvidia]'"
)
def test_uv_install_extra_command_preserves_receipt_python_and_with_packages(
self, tmp_path, monkeypatch
) -> None:
"""Installing an extra preserves the uv tool interpreter and `--with` deps."""
_write_uv_receipt(
tmp_path,
'{ name = "deepagents-code" }, { name = "langchain-custom" }',
python="/opt/Python 3.13/bin/python",
)
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
_write_dist_info(
tmp_path,
"deepagents-code",
requires=('definitely-present-dcode-test-nvidia; extra == "nvidia"',),
)
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
command = _install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
)
assert command == (
"uv tool install -U --python '/opt/Python 3.13/bin/python' "
"'deepagents-code[baseten,nvidia]' --with langchain-custom"
)
def test_sorts_extras_deterministically(self) -> None:
assert (
install_extras_command({"quickjs", "baseten", "nvidia"})
== "uv tool install -U 'deepagents-code[baseten,nvidia,quickjs]'"
== "curl -LsSf https://langch.in/dcode | "
"DEEPAGENTS_CODE_EXTRAS=baseten,nvidia,quickjs bash"
)
def test_rejects_shell_metacharacters(self) -> None:
assert not is_valid_extra_name("quickjs']; touch /tmp/pwned; '")
with pytest.raises(ValueError, match="Invalid extra name"):
install_extra_command(
"quickjs']; touch /tmp/pwned; '",
distribution_name="missing-dcode-test",
)
install_extra_command("quickjs']; touch /tmp/pwned; '")
with pytest.raises(ValueError, match="Invalid extra name"):
install_extras_command(["quickjs", "bad;name"])
@@ -2726,6 +2845,41 @@ class TestPerformInstallExtra:
assert success is False
assert "Unsupported install method" in output
@pytest.mark.parametrize(
("method", "needle"),
[
("brew", "Homebrew install detected"),
("other", "Unsupported install method detected"),
],
)
async def test_non_uv_install_refuses_before_reading_uv_receipt(
self,
method: InstallMethod,
needle: str,
tmp_path,
monkeypatch,
) -> None:
"""Unsupported install guidance wins over uv receipt introspection errors."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }, "bad"')
monkeypatch.setattr("sys.prefix", str(tmp_path))
# Isolate from the host's installed extras so the script command is
# deterministic — install_extra_command merges real distribution
# metadata, which would otherwise leak the dev env's extras in.
monkeypatch.setattr(
"deepagents_code.extras_info.installed_extra_names",
lambda _distribution_name="deepagents-code": set(),
)
with patch(
"deepagents_code.update_check.detect_install_method",
return_value=method,
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert needle in output
assert "ToolRequirementIntrospectionError" not in output
assert "curl -LsSf https://langch.in/dcode" in output
assert "DEEPAGENTS_CODE_EXTRAS=quickjs bash" in output
async def test_invalid_extra_refuses_before_detecting_install(self) -> None:
"""Malformed forced extras must never reach command construction."""
with patch(
@@ -2751,7 +2905,7 @@ class TestPerformInstallExtra:
return_value="/usr/bin/uv",
),
patch(
"deepagents_code.update_check.install_extra_command",
"deepagents_code.update_check._install_extra_uv_tool_command",
return_value="printf 'ok\\n'",
),
):
@@ -2759,6 +2913,29 @@ class TestPerformInstallExtra:
assert success is True
assert output == "ok"
async def test_uv_receipt_failure_is_reported(self, tmp_path, monkeypatch) -> None:
"""A malformed uv receipt is reported instead of dropping install context."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }, "bad"')
monkeypatch.setattr("sys.prefix", str(tmp_path))
with (
patch(
"deepagents_code.update_check.detect_install_method",
return_value="uv",
),
patch(
"deepagents_code.update_check.shutil.which",
return_value="/usr/bin/uv",
),
patch(
"deepagents_code.extras_info.installed_extra_names",
return_value=frozenset(),
),
):
success, output = await perform_install_extra("quickjs")
assert success is False
assert "ToolRequirementIntrospectionError" in output
assert "non-table requirement" in output
async def test_uv_missing_returns_actionable_error(self) -> None:
"""When `uv` is not on PATH, surface a clear error before exec."""
with (
@@ -2962,7 +3139,7 @@ class TestRunInstallSubprocessFailureModes:
return_value="/usr/bin/uv",
),
patch(
"deepagents_code.update_check.install_extra_command",
"deepagents_code.update_check._install_extra_uv_tool_command",
return_value="sleep 5",
),
):
@@ -2987,7 +3164,7 @@ class TestRunInstallSubprocessFailureModes:
return_value="/usr/bin/uv",
),
patch(
"deepagents_code.update_check.install_extra_command",
"deepagents_code.update_check._install_extra_uv_tool_command",
return_value="uv tool install -U 'deepagents-code[quickjs]'",
),
patch("asyncio.create_subprocess_shell", side_effect=_raise),
@@ -3010,7 +3187,7 @@ class TestRunInstallSubprocessFailureModes:
return_value="/usr/bin/uv",
),
patch(
"deepagents_code.update_check.install_extra_command",
"deepagents_code.update_check._install_extra_uv_tool_command",
return_value="sh -c 'printf boom 1>&2; exit 1'",
),
):