mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
feat(code): unify config show/list around effective values (#4174)
`dcode config show` now lists effective values and their source by default (with `list`/`ls` as aliases); use `--verbose` for option descriptions and where to set each one. --- `config list` previously printed a static catalog while `config show` printed effective values — but in every comparable CLI (`git config --list`, `aws configure list`, `gcloud config list`, `npm config ls`) `list` means "show effective values", so the split defied expectations and the two views had unrelated formatting. This makes `config show` the single effective-value+source view (rendered as an aligned table, fixing the manual `:<22` padding wobble), adds `-v/--verbose/--all` to fold in descriptions + how-to-set, and makes `list`/`ls` an alias of `show`. Credential redaction is preserved and env/TOML values stay escaped. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"""CLI commands for the `config` group: inspect the configuration surface.
|
||||
|
||||
`config list` prints the static manifest (every tunable option, its type,
|
||||
default, and where it can be set). `config show` resolves each option against
|
||||
the app credential store (for credentials), the live environment, and
|
||||
`config.toml`, reporting the effective value and which source provided it.
|
||||
`config show` (aliased as `config list`/`ls`) resolves each option against the
|
||||
app credential store (for credentials), the live environment, and `config.toml`,
|
||||
reporting the effective value and which source provided it, matching what
|
||||
`git config --list` / `aws configure list` users expect. Adding `--verbose`/`--all`
|
||||
folds in each option's description and where it can be set (the static catalog).
|
||||
`config get <key>` does the same for a single option. `config path` prints the
|
||||
on-disk config locations.
|
||||
|
||||
@@ -25,13 +26,13 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from deepagents_code.output import write_json
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from deepagents_code.config_manifest import ConfigOption
|
||||
from deepagents_code.output import OutputFormat
|
||||
@@ -79,8 +80,19 @@ def setup_config_parser(
|
||||
add_output_args(config_parser)
|
||||
config_sub = config_parser.add_subparsers(dest="config_command")
|
||||
|
||||
def _add_verbose_arg(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
"--all",
|
||||
dest="verbose",
|
||||
action="store_true",
|
||||
help="Also show each option's description and where to set it",
|
||||
)
|
||||
|
||||
show_parser = config_sub.add_parser(
|
||||
"show",
|
||||
aliases=["list", "ls"],
|
||||
help="Show effective config values and their source",
|
||||
add_help=False,
|
||||
)
|
||||
@@ -89,21 +101,9 @@ def setup_config_parser(
|
||||
"--help",
|
||||
action=make_help_action(_lazy_ui_help("show_config_help")),
|
||||
)
|
||||
_add_verbose_arg(show_parser)
|
||||
add_output_args(show_parser)
|
||||
|
||||
list_parser = config_sub.add_parser(
|
||||
"list",
|
||||
aliases=["ls"],
|
||||
help="List all available config options",
|
||||
add_help=False,
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"-h",
|
||||
"--help",
|
||||
action=make_help_action(_lazy_ui_help("show_config_help")),
|
||||
)
|
||||
add_output_args(list_parser)
|
||||
|
||||
get_parser = config_sub.add_parser(
|
||||
"get",
|
||||
help="Show the effective value and source for one option",
|
||||
@@ -199,7 +199,7 @@ def _resolve(
|
||||
toml_data: dict[str, Any],
|
||||
*,
|
||||
stored: _StoredCredentialView | None = None,
|
||||
) -> tuple[bool, str, Any]:
|
||||
) -> tuple[bool, str, object]:
|
||||
"""Resolve an option for display, reporting what the runtime actually reads.
|
||||
|
||||
Credential options follow runtime precedence: a present `DEEPAGENTS_CODE_`
|
||||
@@ -323,6 +323,30 @@ def _missing_extra_hint(option: ConfigOption) -> bool:
|
||||
return importlib.util.find_spec(option.dependency_module) is None
|
||||
|
||||
|
||||
class ResolvedOption(NamedTuple):
|
||||
"""An option paired with its resolved effective value, for display.
|
||||
|
||||
Bundles the four values that always travel together through the render
|
||||
helpers as one named record, so they can't be reordered or misaligned at a
|
||||
call site the way a bare positional tuple can.
|
||||
"""
|
||||
|
||||
option: ConfigOption
|
||||
"""The option being described."""
|
||||
|
||||
is_set: bool
|
||||
"""`False` when `value` came from the option's typed default."""
|
||||
|
||||
source: str
|
||||
"""Where the effective value came from (e.g. `env (...)`, `stored`, `default`)."""
|
||||
|
||||
value: object
|
||||
"""The effective value; `None` when unset.
|
||||
|
||||
Redacted for secrets before display.
|
||||
"""
|
||||
|
||||
|
||||
# --- Commands ---------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -333,14 +357,18 @@ def _show_json_row(
|
||||
source: str,
|
||||
value: object,
|
||||
store_error: str | None,
|
||||
include_catalog: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""Build one `config show --json` row, redacting secrets and flagging store errors.
|
||||
"""Build one show/list --json row, redacting secrets and flagging store errors.
|
||||
|
||||
Returns:
|
||||
A JSON-serializable row. Redacted options report presence only (`value`
|
||||
is `None`); a `store_error` key is added to credential rows when
|
||||
the `/auth` store was unreadable, so a corrupt store is
|
||||
distinguishable from an empty one in the bug-report artifact.
|
||||
distinguishable from an empty one in the bug-report artifact. When
|
||||
`include_catalog` is set the static catalog fields (summary, type,
|
||||
default, ...) are folded in so `config list --json` consumers stay
|
||||
unbroken.
|
||||
"""
|
||||
row: dict[str, Any] = {
|
||||
"key": option.key,
|
||||
@@ -351,14 +379,37 @@ def _show_json_row(
|
||||
# Redact secret values: report presence only.
|
||||
"value": None if option.redacted else value,
|
||||
}
|
||||
if include_catalog:
|
||||
row.update(
|
||||
{
|
||||
"summary": option.summary,
|
||||
"type": option.type,
|
||||
"default": option.default,
|
||||
"env_var": option.env_var,
|
||||
"toml_path": option.toml_path,
|
||||
"cli_flag": option.cli_flag,
|
||||
}
|
||||
)
|
||||
if store_error and option.group == "Credentials":
|
||||
row["store_error"] = store_error
|
||||
return row
|
||||
|
||||
|
||||
def _run_show(output_format: OutputFormat) -> int:
|
||||
def _run_show(output_format: OutputFormat, *, verbose: bool, list_mode: bool) -> int:
|
||||
"""Resolve every option and print its effective value and source.
|
||||
|
||||
With `verbose`, each option also lists its description and where it can be
|
||||
set (the catalog detail formerly served by `config list`).
|
||||
|
||||
Args:
|
||||
output_format: `text` for the rendered view, `json` for a machine-
|
||||
readable payload.
|
||||
verbose: Fold each option's description and how-to-set into the output.
|
||||
list_mode: `True` when invoked as `config list`/`ls` rather than
|
||||
`config show`. It selects the `config list` JSON envelope label and,
|
||||
for backward compatibility, includes the static catalog fields in
|
||||
`config list --json` even without `verbose`.
|
||||
|
||||
Returns:
|
||||
Process exit code (`0` on success).
|
||||
"""
|
||||
@@ -373,98 +424,117 @@ def _run_show(output_format: OutputFormat) -> int:
|
||||
# re-parsing `auth.json` per credential option.
|
||||
stored = _load_stored_credentials()
|
||||
|
||||
options = get_config_options()
|
||||
resolved = [(opt, *_resolve(opt, toml_data, stored=stored)) for opt in options]
|
||||
resolved = [
|
||||
ResolvedOption(opt, *_resolve(opt, toml_data, stored=stored))
|
||||
for opt in get_config_options()
|
||||
]
|
||||
|
||||
if output_format == "json":
|
||||
label = "config list" if list_mode else "config show"
|
||||
# `config list --json` was the catalog endpoint; keep its catalog fields
|
||||
# so existing consumers stay unbroken (now additive alongside effective
|
||||
# values). `config show --json` stays effective-only unless `--verbose`.
|
||||
include_catalog = verbose or list_mode
|
||||
write_json(
|
||||
"config show",
|
||||
label,
|
||||
[
|
||||
_show_json_row(
|
||||
opt,
|
||||
is_set=is_set,
|
||||
source=source,
|
||||
value=value,
|
||||
row.option,
|
||||
is_set=row.is_set,
|
||||
source=row.source,
|
||||
value=row.value,
|
||||
store_error=stored.error,
|
||||
include_catalog=include_catalog,
|
||||
)
|
||||
for opt, is_set, source, value in resolved
|
||||
for row in resolved
|
||||
],
|
||||
)
|
||||
return 0
|
||||
|
||||
if verbose:
|
||||
_print_show_verbose(resolved, store_error=stored.error)
|
||||
else:
|
||||
_print_show_table(resolved, store_error=stored.error)
|
||||
return 0
|
||||
|
||||
|
||||
def _print_store_warning(store_error: str | None) -> None:
|
||||
"""Print a warning when the `/auth` credential store was unreadable."""
|
||||
if not store_error:
|
||||
return
|
||||
from rich.markup import escape
|
||||
|
||||
from deepagents_code.config import console
|
||||
|
||||
console.print(f"[yellow]Warning:[/yellow] {escape(store_error)}", highlight=False)
|
||||
console.print()
|
||||
|
||||
|
||||
def _print_show_table(
|
||||
resolved: Sequence[ResolvedOption],
|
||||
*,
|
||||
store_error: str | None = None,
|
||||
) -> None:
|
||||
"""Render the compact effective-value table, grouped by section."""
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from deepagents_code.config import console
|
||||
from deepagents_code.config_manifest import iter_groups
|
||||
|
||||
console.print()
|
||||
_print_store_warning(store_error)
|
||||
for group in iter_groups(row.option for row in resolved):
|
||||
console.print(f"[bold]{group}[/bold]")
|
||||
table = Table.grid(padding=(0, 2))
|
||||
table.add_column()
|
||||
table.add_column()
|
||||
table.add_column(style="dim")
|
||||
for row in resolved:
|
||||
if row.option.group != group:
|
||||
continue
|
||||
display = _display_value(row.option, is_set=row.is_set, value=row.value)
|
||||
# `display`/`source` may contain markup from env/TOML; `Text` cells
|
||||
# render literally, so values can't break the table.
|
||||
table.add_row(
|
||||
Text(f" {row.option.key}"),
|
||||
Text(display),
|
||||
Text(_source_label(row.source, option=row.option)),
|
||||
)
|
||||
console.print(table, highlight=False)
|
||||
console.print()
|
||||
|
||||
|
||||
def _print_show_verbose(
|
||||
resolved: Sequence[ResolvedOption],
|
||||
*,
|
||||
store_error: str | None = None,
|
||||
) -> None:
|
||||
"""Render the effective value plus description and how-to-set per option."""
|
||||
from rich.markup import escape
|
||||
|
||||
from deepagents_code.config import console
|
||||
from deepagents_code.config_manifest import iter_groups
|
||||
|
||||
console.print()
|
||||
if stored.error:
|
||||
console.print(
|
||||
f"[yellow]Warning:[/yellow] {escape(stored.error)}", highlight=False
|
||||
)
|
||||
console.print()
|
||||
for group in iter_groups(options):
|
||||
_print_store_warning(store_error)
|
||||
for group in iter_groups(row.option for row in resolved):
|
||||
console.print(f"[bold]{group}[/bold]")
|
||||
for opt, is_set, source, value in resolved:
|
||||
if opt.group != group:
|
||||
for row in resolved:
|
||||
if row.option.group != group:
|
||||
continue
|
||||
display = _display_value(opt, is_set=is_set, value=value)
|
||||
source_label = _source_label(source, option=opt)
|
||||
# `display` and `source_label` may contain Rich markup from env/TOML
|
||||
# or terminal metadata; escape them so values can't break rendering.
|
||||
display_text = escape(display)
|
||||
source_text = escape(source_label)
|
||||
display = _display_value(row.option, is_set=row.is_set, value=row.value)
|
||||
# `display`/`source` may carry markup from env/TOML; escape them.
|
||||
console.print(
|
||||
f" {opt.key:<34} {display_text:<22} [dim]{source_text}[/dim]",
|
||||
f" [cyan]{row.option.key}[/cyan] {escape(display)} "
|
||||
f"[dim]{escape(_source_label(row.source, option=row.option))}[/dim]",
|
||||
highlight=False,
|
||||
)
|
||||
console.print(f" {row.option.summary}", highlight=False, style="dim")
|
||||
console.print(
|
||||
f" {_sources_line(row.option)}", highlight=False, style="dim"
|
||||
)
|
||||
console.print()
|
||||
return 0
|
||||
|
||||
|
||||
def _run_list(output_format: OutputFormat) -> int:
|
||||
"""Print the static catalog of available options (no resolution).
|
||||
|
||||
Returns:
|
||||
Process exit code (`0` on success).
|
||||
"""
|
||||
from deepagents_code.config_manifest import get_config_options
|
||||
|
||||
options = get_config_options()
|
||||
if output_format == "json":
|
||||
write_json(
|
||||
"config list",
|
||||
[
|
||||
{
|
||||
"key": opt.key,
|
||||
"group": opt.group,
|
||||
"summary": opt.summary,
|
||||
"type": opt.type,
|
||||
"default": opt.default,
|
||||
"redacted": opt.redacted,
|
||||
"env_var": opt.env_var,
|
||||
"toml_path": opt.toml_path,
|
||||
"cli_flag": opt.cli_flag,
|
||||
}
|
||||
for opt in options
|
||||
],
|
||||
)
|
||||
return 0
|
||||
|
||||
from deepagents_code.config import console
|
||||
from deepagents_code.config_manifest import iter_groups
|
||||
|
||||
console.print()
|
||||
for group in iter_groups(options):
|
||||
console.print(f"[bold]{group}[/bold]")
|
||||
for opt in options:
|
||||
if opt.group != group:
|
||||
continue
|
||||
console.print(f" [cyan]{opt.key}[/cyan] [dim]({opt.type})[/dim]")
|
||||
console.print(f" {opt.summary}", highlight=False)
|
||||
console.print(f" {_sources_line(opt)}", highlight=False, style="dim")
|
||||
console.print()
|
||||
return 0
|
||||
|
||||
|
||||
def _run_get(key: str, output_format: OutputFormat) -> int:
|
||||
@@ -565,11 +635,12 @@ def run_config_command(args: argparse.Namespace) -> int:
|
||||
"""
|
||||
output_format: OutputFormat = getattr(args, "output_format", "text")
|
||||
command = getattr(args, "config_command", None)
|
||||
verbose: bool = getattr(args, "verbose", False)
|
||||
|
||||
if command == "show":
|
||||
return _run_show(output_format)
|
||||
if command in {"list", "ls"}:
|
||||
return _run_list(output_format)
|
||||
if command in {"show", "list", "ls"}:
|
||||
return _run_show(
|
||||
output_format, verbose=verbose, list_mode=command in {"list", "ls"}
|
||||
)
|
||||
if command == "get":
|
||||
return _run_get(args.key, output_format)
|
||||
if command == "path":
|
||||
@@ -585,7 +656,7 @@ def run_config_command(args: argparse.Namespace) -> int:
|
||||
|
||||
|
||||
def _sources_line(option: ConfigOption) -> str:
|
||||
"""Render a compact 'set via' line for `config list`.
|
||||
"""Render a compact 'set via' line for the verbose (`--verbose`) view.
|
||||
|
||||
Returns:
|
||||
A human-readable description of where the option can be set.
|
||||
|
||||
@@ -668,12 +668,13 @@ def show_config_help() -> None:
|
||||
console.print(" dcode config <command> [options]")
|
||||
console.print()
|
||||
console.print("[bold]Commands:[/bold]", style=theme.PRIMARY)
|
||||
console.print(" show Show effective values and their source")
|
||||
console.print(" list|ls List all available options")
|
||||
console.print(" show|list|ls Show effective values and their source")
|
||||
console.print(" get <key> Show one option's value and source")
|
||||
console.print(" path Show config file locations")
|
||||
console.print()
|
||||
_print_option_section()
|
||||
_print_option_section(
|
||||
" -v, --verbose, --all Also show each option's description and how to set it",
|
||||
)
|
||||
console.print()
|
||||
console.print(
|
||||
" Credentials are reported as set/not set only; values are never printed.",
|
||||
@@ -682,7 +683,7 @@ def show_config_help() -> None:
|
||||
console.print()
|
||||
console.print("[bold]Examples:[/bold]", style=theme.PRIMARY)
|
||||
console.print(" dcode config show")
|
||||
console.print(" dcode config list --json")
|
||||
console.print(" dcode config show --verbose")
|
||||
console.print(" dcode config get interpreter.memory_limit_mb")
|
||||
console.print(" dcode config path")
|
||||
console.print()
|
||||
|
||||
@@ -518,6 +518,25 @@ def test_run_show_json_flags_unreadable_store(stored_auth_dir, capsys):
|
||||
assert all("store_error" not in r for r in rows if r["group"] != "Credentials")
|
||||
|
||||
|
||||
def test_run_show_text_warns_on_unreadable_store(stored_auth_dir, capsys):
|
||||
"""`config show` text output warns when the credential store is unreadable.
|
||||
|
||||
Guards the `_print_store_warning` call in both text renderers: without it a
|
||||
corrupt store would look identical to an empty one in the interactive view —
|
||||
the silent failure this warning exists to prevent.
|
||||
"""
|
||||
stored_auth_dir.mkdir(parents=True, exist_ok=True)
|
||||
(stored_auth_dir / "auth.json").write_text("{ not json", encoding="utf-8")
|
||||
for verbose in (False, True):
|
||||
args = argparse.Namespace(
|
||||
config_command="show", output_format="text", verbose=verbose
|
||||
)
|
||||
assert run_config_command(args) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "Warning" in out
|
||||
assert "unreadable" in out
|
||||
|
||||
|
||||
def test_run_get_text_warns_on_unreadable_store(stored_auth_dir, capsys):
|
||||
"""`config get` text output shows a warning banner for an unreadable store."""
|
||||
stored_auth_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1209,6 +1228,21 @@ def test_config_show_text_survives_markup_in_value(monkeypatch) -> None:
|
||||
assert run_config_command(args) == 0
|
||||
|
||||
|
||||
def test_config_show_verbose_text_survives_markup_in_value(monkeypatch) -> None:
|
||||
"""The verbose text path escapes markup in values so rendering can't break.
|
||||
|
||||
`_print_show_verbose` renders with markup enabled and relies on manual
|
||||
`escape()`; the compact table path uses `Text` cells, so it needs its own
|
||||
guard.
|
||||
"""
|
||||
monkeypatch.setenv(
|
||||
_env_vars.EXTERNAL_EVENT_SOCKET_PATH,
|
||||
"/tmp/sock[/]oops",
|
||||
)
|
||||
args = argparse.Namespace(config_command="show", output_format="text", verbose=True)
|
||||
assert run_config_command(args) == 0
|
||||
|
||||
|
||||
# --- Command smoke (text paths) ---------------------------------------------
|
||||
|
||||
|
||||
@@ -1219,11 +1253,47 @@ def test_run_show_text_returns_zero() -> None:
|
||||
|
||||
|
||||
def test_run_list_text_returns_zero() -> None:
|
||||
"""The default (text) `config list` rendering path runs without error."""
|
||||
"""`config list` aliases the effective-value view and renders without error."""
|
||||
args = argparse.Namespace(config_command="list", output_format="text")
|
||||
assert run_config_command(args) == 0
|
||||
|
||||
|
||||
def test_run_show_verbose_text_shows_descriptions(capsys) -> None:
|
||||
"""`config show --verbose` folds in each option's description and how-to-set.
|
||||
|
||||
A plain exit-code check would still pass if the verbose path silently
|
||||
regressed to the compact table, so assert the distinguishing content — an
|
||||
option's summary and its `set via` line — is actually rendered.
|
||||
"""
|
||||
opt = get_option("interpreter.memory_limit_mb")
|
||||
assert opt is not None
|
||||
args = argparse.Namespace(config_command="show", output_format="text", verbose=True)
|
||||
assert run_config_command(args) == 0
|
||||
# Normalize whitespace so Rich soft-wrapping at the test console width can't
|
||||
# break the substring match.
|
||||
rendered = " ".join(capsys.readouterr().out.split())
|
||||
assert " ".join(opt.summary.split()) in rendered
|
||||
assert "set via" in rendered
|
||||
|
||||
|
||||
def test_config_parser_wires_aliases_and_verbose_flag(monkeypatch) -> None:
|
||||
"""Real argparse wiring: `config list --all --json` parses as verbose list JSON.
|
||||
|
||||
Every other command test builds a `Namespace` directly, so this is the only
|
||||
guard that the `list`/`ls` aliases and the `-v`/`--verbose`/`--all` flag are
|
||||
actually registered on the parser.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from deepagents_code.main import parse_args
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["dcode", "config", "list", "--all", "--json"])
|
||||
ns = parse_args()
|
||||
assert ns.config_command == "list"
|
||||
assert ns.verbose is True
|
||||
assert ns.output_format == "json"
|
||||
|
||||
|
||||
def test_run_get_text_returns_zero() -> None:
|
||||
"""The default (text) `config get` rendering path runs without error."""
|
||||
args = argparse.Namespace(
|
||||
@@ -1465,8 +1535,47 @@ def test_run_path_json_reports_existence(monkeypatch, tmp_path, capsys) -> None:
|
||||
assert row["path"] == str(cfg)
|
||||
|
||||
|
||||
def test_run_list_json_serializes_catalog(capsys) -> None:
|
||||
"""`config list --json` serializes the catalog without error."""
|
||||
def test_run_show_json_reports_effective_values(capsys) -> None:
|
||||
"""`config show --json` reports effective values without catalog fields."""
|
||||
import json
|
||||
|
||||
args = argparse.Namespace(config_command="show", output_format="json")
|
||||
assert run_config_command(args) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["command"] == "config show"
|
||||
rows = payload["data"]
|
||||
assert all(
|
||||
{"key", "group", "source", "set", "redacted", "value"} <= set(r) for r in rows
|
||||
)
|
||||
assert all("type" not in r for r in rows)
|
||||
|
||||
|
||||
def test_run_show_verbose_json_serializes_catalog(capsys) -> None:
|
||||
"""`config show --verbose --json` folds the catalog into each row."""
|
||||
import json
|
||||
|
||||
args = argparse.Namespace(config_command="show", output_format="json", verbose=True)
|
||||
assert run_config_command(args) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["command"] == "config show"
|
||||
rows = payload["data"]
|
||||
assert any(
|
||||
r["key"] == "interpreter.memory_limit_mb" and r["default"] == 64 for r in rows
|
||||
)
|
||||
assert all(
|
||||
{"key", "type", "default", "redacted", "env_var", "toml_path", "cli_flag"}
|
||||
<= set(r)
|
||||
for r in rows
|
||||
)
|
||||
|
||||
|
||||
def test_run_list_json_preserves_catalog(capsys) -> None:
|
||||
"""`config list --json` keeps catalog fields for backward compatibility.
|
||||
|
||||
`list` was the machine-readable catalog endpoint, so its JSON must stay
|
||||
additive: effective value/source plus the original catalog fields, even
|
||||
without `--verbose`.
|
||||
"""
|
||||
import json
|
||||
|
||||
args = argparse.Namespace(config_command="list", output_format="json")
|
||||
@@ -1484,6 +1593,39 @@ def test_run_list_json_serializes_catalog(capsys) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_run_ls_json_uses_list_label(capsys) -> None:
|
||||
"""The `ls` alias shares the `config list` JSON envelope label and catalog."""
|
||||
import json
|
||||
|
||||
args = argparse.Namespace(config_command="ls", output_format="json")
|
||||
assert run_config_command(args) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload["command"] == "config list"
|
||||
assert all("type" in r for r in payload["data"])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("stored_auth_dir")
|
||||
def test_run_list_json_redacts_stored_secret_value(capsys) -> None:
|
||||
"""`config list --json` redacts a stored secret like `config show --json`.
|
||||
|
||||
`list` newly resolves effective values (it was a static catalog before), so
|
||||
the redaction invariant must be proven on this path too.
|
||||
"""
|
||||
import json
|
||||
|
||||
from deepagents_code import auth_store
|
||||
|
||||
auth_store.set_stored_key("anthropic", "from-store")
|
||||
args = argparse.Namespace(config_command="list", output_format="json")
|
||||
assert run_config_command(args) == 0
|
||||
raw = capsys.readouterr().out
|
||||
rows = json.loads(raw)["data"]
|
||||
row = next(r for r in rows if r["key"] == "credentials.anthropic")
|
||||
assert row["set"] is True
|
||||
assert row["value"] is None
|
||||
assert "from-store" not in raw
|
||||
|
||||
|
||||
# --- Provider/credential drift ----------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user