From 48d5b3744398bfbb0cbf62ed985a60fc7b326bd0 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:08:54 +0530 Subject: [PATCH 1/3] Add active component identity reports to basectl version --- .ai-context/COMMANDS.md | 5 +- CHANGELOG.md | 4 + base_init.sh | 78 +------ bin/basectl | 41 +--- cli/bash/commands/basectl/README.md | 35 +++ cli/bash/commands/basectl/basectl.sh | 47 +--- .../basectl/subcommands/setup_common.sh | 9 + .../commands/basectl/subcommands/version.sh | 92 ++++++++ .../basectl/tests/runtime-dispatch.bats | 2 +- cli/bash/commands/basectl/tests/version.bats | 122 +++++++++++ cli/python/base_version/__init__.py | 0 cli/python/base_version/report.py | 200 ++++++++++++++++++ cli/python/base_version/tests/test_report.py | 126 +++++++++++ docs/inspection-json.md | 8 +- lib/base/base_bash_libs_runtime.sh | 80 +++++++ lib/shell/completions/basectl_completion.sh | 2 +- lib/shell/completions/basectl_completion.zsh | 2 +- lib/shell/completions/tests/completions.bats | 11 + tests/base_init.bats | 2 + 19 files changed, 709 insertions(+), 157 deletions(-) create mode 100644 cli/bash/commands/basectl/subcommands/version.sh create mode 100644 cli/bash/commands/basectl/tests/version.bats create mode 100644 cli/python/base_version/__init__.py create mode 100644 cli/python/base_version/report.py create mode 100644 cli/python/base_version/tests/test_report.py create mode 100644 lib/base/base_bash_libs_runtime.sh diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 4755cc62..c809b805 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -222,7 +222,10 @@ are documented in `docs/inspection-json.md`. - `basectl update [project]` - update Base or a named project using the configured Git checkout or Homebrew-managed Base handoff, then run setup for the selected project. -- `basectl version` - show the installed Base version. +- `basectl version` - show the installed Base version without provider bootstrap. + `--all` reports selected Base/base-cli/base-bash-libs identity, paths, Git revision + and dirty state; `--all --json` emits a v1 inspection envelope. Detailed + inspection needs Python 3 but tolerates missing providers and the Base venv. - `basectl help` - show command help. ## Command Implementation Pattern diff --git a/CHANGELOG.md b/CHANGELOG.md index ce02bc98..56f4fc02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ numeric next development line is tracked in `DEVELOPMENT_VERSION`. ## [Unreleased] +- Added `basectl version --all` and `--all --json` to inspect active component + versions, provider paths, Git revisions, and dirty state even when providers + or the Base Python environment are unavailable. + - Added an optional repository-owned review policy for `basectl repo configure`. Teams can request approving reviews and code-owner review without changing the no-configuration behavior of existing solo repositories; stronger diff --git a/base_init.sh b/base_init.sh index 74127b5c..6e96237d 100755 --- a/base_init.sh +++ b/base_init.sh @@ -146,78 +146,6 @@ base_init_resolve_home() { printf '%s\n' "$source_dir" } -base_init_homebrew_prefix() { - case "$BASE_HOME" in - */opt/base/libexec) - printf '%s\n' "${BASE_HOME%/opt/base/libexec}" - ;; - */Cellar/base/*/libexec) - printf '%s\n' "${BASE_HOME%%/Cellar/base/*}" - ;; - esac -} - -base_init_bash_libs_dir_is_usable() { - local candidate="${1:-}" - - [[ -n "$candidate" ]] || return 1 - [[ -f "$candidate/std/lib_std.sh" ]] -} - -base_init_report_missing_bash_libs() { - local candidate - local homebrew_prefix - - base_init_error "Base reusable Bash libraries were not found." - - candidate="$BASE_HOME/../base-bash-libs/lib/bash" - base_init_error "Tried sibling base-bash-libs checkout at '$candidate'." - - homebrew_prefix="$(base_init_homebrew_prefix || true)" - if [[ -n "$homebrew_prefix" ]]; then - candidate="$homebrew_prefix/opt/base-bash-libs/libexec/lib/bash" - base_init_error "Tried Homebrew base-bash-libs package at '$candidate'." - fi - - base_init_error "Clone basefoundry/base-bash-libs next to Base, install it with 'brew install basefoundry/base/base-bash-libs', or set BASE_BASH_LIBS_DIR to a compatible lib/bash directory." -} - -base_init_set_bash_libs_contract() { - local candidate - local homebrew_prefix - local explicit_dir="${BASE_BASH_LIBS_DIR:-}" - - if [[ -n "$explicit_dir" ]]; then - base_init_bash_libs_dir_is_usable "$explicit_dir" || { - base_init_error "BASE_BASH_LIBS_DIR '$explicit_dir' does not contain std/lib_std.sh." - return 1 - } - BASE_BASH_LIBS_DIR="$(cd -L -- "$explicit_dir" && pwd -L)" || return 1 - BASE_BASH_LIBS_SOURCE=explicit - return $? - fi - - candidate="$BASE_HOME/../base-bash-libs/lib/bash" - if base_init_bash_libs_dir_is_usable "$candidate"; then - BASE_BASH_LIBS_DIR="$(cd -L -- "$candidate" && pwd -L)" || return 1 - BASE_BASH_LIBS_SOURCE=sibling - return $? - fi - - homebrew_prefix="$(base_init_homebrew_prefix || true)" - if [[ -n "$homebrew_prefix" ]]; then - candidate="$homebrew_prefix/opt/base-bash-libs/libexec/lib/bash" - if base_init_bash_libs_dir_is_usable "$candidate"; then - BASE_BASH_LIBS_DIR="$(cd -L -- "$candidate" && pwd -L)" || return 1 - BASE_BASH_LIBS_SOURCE=homebrew - return $? - fi - fi - - base_init_report_missing_bash_libs - return 1 -} - base_init_export_contract() { local base_home base_os base_platform base_host base_host_env uname_os @@ -360,7 +288,13 @@ import_base_lib() { } base_init_main() { + local init_path + base_init_require_bash || return 1 + init_path="$(base_init_resolve_path "${BASH_SOURCE[0]}")" || return 1 + # Load definitions from this bootstrap, even when diagnosing a stale BASE_HOME. + # shellcheck source=lib/base/base_bash_libs_runtime.sh + source "${init_path%/*}/lib/base/base_bash_libs_runtime.sh" || return 1 base_init_export_contract || return 1 base_init_source_stdlib "$@" || return 1 base_init_require_bash_libs_version || return 1 diff --git a/bin/basectl b/bin/basectl index 6894413d..3bf47ce6 100755 --- a/bin/basectl +++ b/bin/basectl @@ -71,25 +71,6 @@ basectl_print_version() { printf 'basectl %s\n' "$(base_read_version "$base_home")" } -basectl_version_usage() { - cat <<'EOF' -Usage: - basectl version - -Purpose: - Show the installed Base version. - -Options: - -h, --help Show this help text. -EOF -} - -basectl_version_usage_error() { - printf 'ERROR: %s\n' "$*" >&2 - printf "Run 'basectl version --help' for usage.\n" >&2 - return 2 -} - basectl_proc_translated() { local translated @@ -355,24 +336,10 @@ main() { if [[ "${1:-}" == "version" ]]; then shift - case "${1:-}" in - -h|--help|help) - (($# == 1)) || { - basectl_version_usage_error "version does not accept arguments." - return $? - } - basectl_version_usage - return 0 - ;; - "") - basectl_print_version "$base_home" - return 0 - ;; - *) - basectl_version_usage_error "version does not accept arguments." - return $? - ;; - esac + # shellcheck source=cli/bash/commands/basectl/subcommands/version.sh + source "$base_home/cli/bash/commands/basectl/subcommands/version.sh" + basectl_version_main "$base_home" "$@" + return $? fi if [[ $# -eq 0 && -t 0 && -t 1 ]]; then diff --git a/cli/bash/commands/basectl/README.md b/cli/bash/commands/basectl/README.md index c703cf2a..e28425f9 100644 --- a/cli/bash/commands/basectl/README.md +++ b/cli/bash/commands/basectl/README.md @@ -295,3 +295,38 @@ such command directories exist. Optional utility CLIs such as `caff` and - `basectl version` prints the installed Base version from the repo-root `VERSION` file. - basectl-specific bootstrap subcommands live under `cli/bash/commands/basectl/subcommands/`. - basectl tests live under `cli/bash/commands/basectl/tests/`. + +### Component versions + +`basectl version` and `basectl --version` keep the concise, dependency-independent +Base identity. Use `basectl version --all` to inspect Base, base-cli and +base-bash-libs, or `basectl version --all --json` for automation. + +The detailed report includes selected provider source, resolved location, version, +Git commit and dirty state (including untracked files), plus the Base Python path +selected by `base-wrapper`, respecting `BASE_SETUP_VENV_DIR`. It uses the same +explicit/sibling/installed provider precedence as runtime startup. Source versions +come from the selected checkout, never a shadowed wheel's metadata. Git identity +is only read at the component root, so an enclosing workspace repository cannot +be mistaken for the component. Packaged Bash releases can use embedded metadata. + +These are observed component identities, not release-pinned expectations or a +compatibility verdict. The [release BOM](../../../../docs/release-bom.md) records +the tested release combination; a source override may intentionally differ. +Inspection locates provider files without importing or executing provider code. +Use `basectl check base` for readiness and compatibility checks. + +Detailed inspection uses a stdlib-only Python 3 bootstrap diagnostic and remains +available when the Base venv or providers are missing or incompatible. Missing +providers appear as `unavailable`, unreadable identity as `unknown`, and unknown +JSON values as `null`. Partial reports exit 0 with envelope status `warn`; a report +is not a health gate. If no Python 3 interpreter is available, detailed inspection +fails with an actionable error; simple version output still works. No installation +or network access is performed. Paths in the report may contain local usernames. + +JSON uses the inspection v1 envelope (`schema_version`, `command: "version"`, +`status`, `data`, `error`). `data.components` contains records with `name`, `source`, +`path`, `version`, `revision`, `dirty`, `status`, and `detail`. `data.python` contains +`path` and `exists` (executable availability, not a health verdict). Consumers must +allow additive fields. Invalid CLI arguments exit 2 and write usage errors to stderr. +The Bash identity reader is also used in the existing check/doctor provider message. diff --git a/cli/bash/commands/basectl/basectl.sh b/cli/bash/commands/basectl/basectl.sh index c9f0fcac..2f00ca8f 100644 --- a/cli/bash/commands/basectl/basectl.sh +++ b/cli/bash/commands/basectl/basectl.sh @@ -15,9 +15,11 @@ BASECTL_REQUIRED_HOME_FILES=( lib/bash/runtime/bashrc lib/bash/runtime/command_protocol.sh lib/bash/version/lib_version.sh + lib/base/base_bash_libs_runtime.sh bin/basectl bin/base-wrapper cli/bash/commands/basectl/basectl.sh + cli/bash/commands/basectl/subcommands/version.sh ) readonly BASECTL_REQUIRED_HOME_FILES @@ -107,7 +109,7 @@ Diagnostics and maintenance: Other: version - Show the installed Base version. + Show the installed Base version; --all includes component providers. help Show this help text. @@ -473,49 +475,8 @@ basectl_do_workspace() { base_workspace_subcommand_main "$@" } -basectl_source_version_library() { - local version_lib="$BASE_HOME/lib/bash/version/lib_version.sh" - - [[ -f "$version_lib" ]] || { - basectl_error "Base version library '$version_lib' was not found." - return 1 - } - - # shellcheck source=/dev/null - source "$version_lib" -} - basectl_do_version() { - case "${1:-}" in - "") - ;; - -h|--help|help) - (($# == 1)) || { - basectl_error "version does not accept arguments." - printf "Run 'basectl version --help' for usage.\n" >&2 - return 2 - } - cat <<'EOF' -Usage: - basectl version - -Purpose: - Show the installed Base version. - -Options: - -h, --help Show this help text. -EOF - return 0 - ;; - *) - basectl_error "version does not accept arguments." - printf "Run 'basectl version --help' for usage.\n" >&2 - return 2 - ;; - esac - - basectl_source_version_library || return 1 - printf 'basectl %s\n' "$(base_read_version "$BASE_HOME")" + "$BASE_HOME/bin/basectl" version "$@" } basectl_should_start_shell() { diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 18ca7dc3..f2564384 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -445,6 +445,15 @@ setup_base_bash_libraries_status() { } setup_base_bash_libraries_check_message() { + local python_bin summary + + python_bin="$(command -v python3 || true)" + # Same stdlib-only identity reader as version --all; retain source guidance + # below when Python is unavailable during early setup diagnostics. + if [[ -n "$python_bin" ]] && summary="$("$python_bin" -I -S "$BASE_HOME/cli/python/base_version/report.py" \ + --bash-summary "${BASE_BASH_LIBS_SOURCE:-unknown}" "${BASE_BASH_LIBS_DIR:-}" 2>/dev/null)"; then + printf '%s; ' "$summary" + fi case "${BASE_BASH_LIBS_SOURCE:-unknown}" in explicit) printf "Base is using reusable Bash libraries from explicit BASE_BASH_LIBS_DIR '%s'.\n" "${BASE_BASH_LIBS_DIR:-unknown}" diff --git a/cli/bash/commands/basectl/subcommands/version.sh b/cli/bash/commands/basectl/subcommands/version.sh new file mode 100644 index 00000000..c6480620 --- /dev/null +++ b/cli/bash/commands/basectl/subcommands/version.sh @@ -0,0 +1,92 @@ +# shellcheck shell=bash +# Version inspection deliberately runs before provider-dependent bootstrap. + +basectl_version_usage() { + cat <<'USAGE' +Usage: + basectl version [--all [--json]] + +Purpose: + Show the installed Base version. Use --all to inspect selected component providers. + +Options: + --all Show Base, base-cli, and base-bash-libs identity and source paths. + --json Emit a versioned JSON report (requires --all). + -h, --help Show this help text. + +Detailed inspection requires Python 3, but does not require working providers. +Unavailable providers are reported without suppressing other components. +USAGE +} + +basectl_version_usage_error() { + printf 'ERROR: %s\n' "$*" >&2 + printf "Run 'basectl version --help' for usage.\n" >&2 + return 2 +} + +basectl_version_all() ( + # Subshell: runtime selection must not mutate a caller's loaded contract. + local base_home="$1" output_format="$2" + local cli_root cli_kind cli_error="" bash_error="" python_bin selected_python + local base_version candidate + BASE_HOME="$base_home" + # shellcheck source=lib/base/base_cli_runtime.sh + source "$base_home/lib/base/base_cli_runtime.sh" || return 1 + # shellcheck source=lib/base/base_bash_libs_runtime.sh + source "$base_home/lib/base/base_bash_libs_runtime.sh" || return 1 + base_init_error() { printf 'ERROR: %s\n' "$*" >&2; } + + cli_root="$(base_cli_runtime_source_root 2>&1)" || { cli_error="$cli_root"; cli_root=""; } + cli_kind="$(base_cli_runtime_source_kind)" || cli_kind=unavailable + # Capture errors without a temporary file; resolve in this subshell after success. + bash_error="$(base_init_set_bash_libs_contract 2>&1)" + if [[ -z "$bash_error" ]]; then + base_init_set_bash_libs_contract || return 1 + fi + selected_python="${BASE_SETUP_VENV_DIR:-$HOME/.base.d/base/.venv}/bin/python" + python_bin="" + for candidate in "$selected_python" "$(command -v python3 || true)" /usr/bin/python3; do + if [[ -x "$candidate" ]] && "$candidate" -I -S -c 'import importlib.metadata' >/dev/null 2>&1; then + python_bin="$candidate" + break + fi + done + [[ -n "$python_bin" ]] || { + printf 'ERROR: Detailed version inspection requires Python 3. Use basectl version for Base alone.\n' >&2 + return 1 + } + base_version="$(base_read_version "$base_home")" + # Bootstrap diagnostic exception: stdlib-only reporter must survive missing + # venvs and incompatible providers, so it cannot use base-wrapper. + "$python_bin" -I -S "$base_home/cli/python/base_version/report.py" \ + "$base_home" "$base_version" "$output_format" "$selected_python" \ + "$cli_kind" "$cli_root" "$cli_error" \ + "${BASE_BASH_LIBS_SOURCE:-unavailable}" "${BASE_BASH_LIBS_DIR:-}" "$bash_error" +) + +basectl_version_main() { + local base_home="$1" all=0 output_format=text arg + shift + if [[ "$#" -eq 1 && ( "$1" == -h || "$1" == --help || "$1" == help ) ]]; then + basectl_version_usage + return 0 + fi + for arg in "$@"; do + case "$arg" in + --all) all=1 ;; + --json) output_format=json ;; + *) basectl_version_usage_error "Unknown version argument '$arg'."; return $? ;; + esac + done + if [[ "$all" -eq 0 ]]; then + [[ "$output_format" == text ]] || { + basectl_version_usage_error '--json requires --all.' + return $? + } + basectl_print_version "$base_home" + return $? + fi + basectl_source_version_library "$base_home" + basectl_version_all "$base_home" "$output_format" +} diff --git a/cli/bash/commands/basectl/tests/runtime-dispatch.bats b/cli/bash/commands/basectl/tests/runtime-dispatch.bats index e533102d..f3271d3f 100644 --- a/cli/bash/commands/basectl/tests/runtime-dispatch.bats +++ b/cli/bash/commands/basectl/tests/runtime-dispatch.bats @@ -634,7 +634,7 @@ EOF run_basectl version nonsense [ "$status" -eq 2 ] - [ "${lines[0]}" = "ERROR: version does not accept arguments." ] + [ "${lines[0]}" = "ERROR: Unknown version argument 'nonsense'." ] [ "${lines[1]}" = "Run 'basectl version --help' for usage." ] [[ "$output" != *"basectl $(head -n 1 "$BASE_REPO_ROOT/VERSION")"* ]] } diff --git a/cli/bash/commands/basectl/tests/version.bats b/cli/bash/commands/basectl/tests/version.bats new file mode 100644 index 00000000..4c3e7267 --- /dev/null +++ b/cli/bash/commands/basectl/tests/version.bats @@ -0,0 +1,122 @@ +#!/usr/bin/env bats + +load ../../../../../tests/test_helper.sh + +setup() { + setup_test_tmpdir + unset BASE_BASH_LIBS_DIR BASE_CLI_SOURCE_DIR BASE_SETUP_VENV_DIR + TEST_INSTALL="$TEST_TMPDIR/base" + mkdir -p "$TEST_INSTALL/bin" "$TEST_INSTALL/lib/bash/version" \ + "$TEST_INSTALL/lib/base" "$TEST_INSTALL/cli/python" \ + "$TEST_INSTALL/cli/bash/commands/basectl/subcommands" + cp "$BASE_REPO_ROOT/bin/basectl" "$TEST_INSTALL/bin/" + cp "$BASE_REPO_ROOT/lib/bash/version/lib_version.sh" "$TEST_INSTALL/lib/bash/version/" + cp "$BASE_REPO_ROOT/lib/base/"*runtime.sh "$TEST_INSTALL/lib/base/" + cp "$BASE_REPO_ROOT/cli/bash/commands/basectl/subcommands/version.sh" "$TEST_INSTALL/cli/bash/commands/basectl/subcommands/" + cp -R "$BASE_REPO_ROOT/cli/python/base_version" "$TEST_INSTALL/cli/python/" + printf '1.2.3\n' > "$TEST_INSTALL/VERSION" + export HOME="$TEST_TMPDIR/home" + mkdir -p "$HOME" +} + +make_providers() { + mkdir -p "$TEST_TMPDIR/base-cli/lib/python/base_cli" "$TEST_TMPDIR/base-bash-libs/lib/bash/std" + printf 'raise RuntimeError("do not import")\n' > "$TEST_TMPDIR/base-cli/lib/python/base_cli/__init__.py" + printf 'exit 99\n' > "$TEST_TMPDIR/base-bash-libs/lib/bash/std/lib_std.sh" + printf '4.5.6\n' > "$TEST_TMPDIR/base-cli/VERSION" + printf '2.3.4\n' > "$TEST_TMPDIR/base-bash-libs/VERSION" +} + +@test "simple version survives absent providers and venv" { + run "$TEST_INSTALL/bin/basectl" version + [ "$status" -eq 0 ] + [ "$output" = 'basectl 1.2.3' ] + run "$TEST_INSTALL/bin/basectl" --version + [ "$status" -eq 0 ] + [ "$output" = 'basectl 1.2.3' ] +} + +@test "detailed version reports siblings without executing providers" { + make_providers + run "$TEST_INSTALL/bin/basectl" version --all + [ "$status" -eq 0 ] + [[ "$output" == *'base-cli: 4.5.6'* ]] + [[ "$output" == *'base-bash-libs: 2.3.4'* ]] + [[ "$output" == *'source: sibling'* ]] + [[ "$output" == *'(unavailable)'* ]] +} + +@test "detailed JSON is a single partial report with missing providers" { + run "$TEST_INSTALL/bin/basectl" version --json --all + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; r=json.load(sys.stdin); assert r["schema_version"] == 1; assert r["status"] == "warn"; assert len(r["data"]["components"]) == 3; assert r["data"]["components"][1]["status"] == "unavailable"' +} + +@test "explicit overrides win and paths with spaces are preserved" { + make_providers + mv "$TEST_TMPDIR/base-cli" "$TEST_TMPDIR/other cli" + mv "$TEST_TMPDIR/base-bash-libs" "$TEST_TMPDIR/other libs" + make_providers + printf '8.8.8\n' > "$TEST_TMPDIR/other cli/VERSION" + run env BASE_CLI_SOURCE_DIR="$TEST_TMPDIR/other cli/lib/python" \ + BASE_BASH_LIBS_DIR="$TEST_TMPDIR/other libs/lib/bash" \ + "$TEST_INSTALL/bin/basectl" version --all --json + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; c=json.load(sys.stdin)["data"]["components"]; assert c[1]["source"] == c[2]["source"] == "explicit"; assert c[1]["version"] == "8.8.8"; assert "other cli" in c[1]["path"]' +} + +@test "broken explicit overrides do not fall back to valid siblings" { + make_providers + run env BASE_CLI_SOURCE_DIR="$TEST_TMPDIR/missing" BASE_BASH_LIBS_DIR="$TEST_TMPDIR/missing" \ + "$TEST_INSTALL/bin/basectl" version --all --json + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; c=json.load(sys.stdin)["data"]["components"]; assert c[1]["status"] == c[2]["status"] == "unavailable"; assert c[1]["version"] is None' +} + +@test "broken sibling CLI does not fall back to installed metadata" { + mkdir -p "$TEST_TMPDIR/base-cli" + run "$TEST_INSTALL/bin/basectl" version --all --json + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; c=json.load(sys.stdin)["data"]["components"]; assert c[1]["status"] == "unavailable"; assert "sibling" in c[1]["detail"]' +} + +@test "version JSON requires all and unknown arguments fail" { + run "$TEST_INSTALL/bin/basectl" version --json + [ "$status" -eq 2 ] + [[ "$output" == *'--json requires --all'* ]] + run "$TEST_INSTALL/bin/basectl" version --all unexpected + [ "$status" -eq 2 ] + run "$TEST_INSTALL/bin/basectl" --version --all + [ "$status" -eq 2 ] +} + +@test "Homebrew layout uses the runtime Bash provider resolver" { + local prefix="$TEST_TMPDIR/brew" + mkdir -p "$prefix/opt/base" "$prefix/opt/base-bash-libs/libexec/lib/bash/std" + mv "$TEST_INSTALL" "$prefix/opt/base/libexec" + printf '2.9.0\n' > "$prefix/opt/base-bash-libs/libexec/VERSION" + touch "$prefix/opt/base-bash-libs/libexec/lib/bash/std/lib_std.sh" + run "$prefix/opt/base/libexec/bin/basectl" version --all --json + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; c=json.load(sys.stdin)["data"]["components"][2]; assert c["source"] == "homebrew"; assert c["version"] == "2.9.0"' +} + +@test "selected Python respects the Base setup venv override" { + run env BASE_SETUP_VENV_DIR="$TEST_TMPDIR/custom venv" "$TEST_INSTALL/bin/basectl" version --all --json + [ "$status" -eq 0 ] + printf '%s' "$output" | python3 -c 'import json,sys; p=json.load(sys.stdin)["data"]["python"]; assert p["path"].endswith("custom venv/bin/python"); assert p["exists"] is False' +} + +@test "working Base Python can report versions when PATH Python is broken" { + local python_bin + python_bin="$(command -v python3)" + mkdir -p "$TEST_TMPDIR/venv/bin" "$TEST_TMPDIR/mockbin" + ln -s "$python_bin" "$TEST_TMPDIR/venv/bin/python" + printf '#!/bin/sh\nexit 91\n' > "$TEST_TMPDIR/mockbin/python3" + chmod +x "$TEST_TMPDIR/mockbin/python3" + make_providers + run env BASE_SETUP_VENV_DIR="$TEST_TMPDIR/venv" PATH="$TEST_TMPDIR/mockbin:$PATH" \ + "$TEST_INSTALL/bin/basectl" version --all + [ "$status" -eq 0 ] + [[ "$output" == *'base-cli: 4.5.6'* ]] +} diff --git a/cli/python/base_version/__init__.py b/cli/python/base_version/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cli/python/base_version/report.py b/cli/python/base_version/report.py new file mode 100644 index 00000000..51fd757c --- /dev/null +++ b/cli/python/base_version/report.py @@ -0,0 +1,200 @@ +"""Stdlib-only bootstrap inspection; never import the providers being diagnosed.""" +from __future__ import annotations + +import importlib.metadata +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys + +# Bootstrap inspection cannot import base_cli.ExitCode. +SUCCESS = 0 + + +def read_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except (OSError, UnicodeError): + return "" + + +def first_line(path: Path) -> str | None: + lines = read_text(path).splitlines() + return (lines[0].strip() or None) if lines else None + + +def git_identity(root: Path) -> dict: + """Do not accidentally report an enclosing workspace repository's identity.""" + result = {"revision": None, "dirty": None} + if not (root / ".git").exists(): + return result + try: + env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} + env["GIT_OPTIONAL_LOCKS"] = "0" + revision = subprocess.run( + ["git", "-C", str(root), "rev-parse", "--verify", "HEAD"], + capture_output=True, text=True, timeout=5, env=env, check=False, + ) + status = subprocess.run( + ["git", "-C", str(root), "status", "--porcelain", "--untracked-files=all"], + capture_output=True, text=True, timeout=5, env=env, check=False, + ) + if revision.returncode == 0: + result["revision"] = revision.stdout.strip() + if status.returncode == 0: + result["dirty"] = bool(status.stdout) + except (OSError, subprocess.TimeoutExpired): + pass + return result + + +def component(name: str, source: str, path: str | None, version: str | None = None) -> dict: + return {"name": name, "source": source, "path": path, "version": version, + "revision": None, "dirty": None, "status": "unknown", "detail": None} + + +def source_identity(record: dict, root: Path) -> dict: + record["version"] = first_line(root / "VERSION") + record.update(git_identity(root)) + record["status"] = "available" if record["version"] else "unknown" + if not record["version"]: + record["detail"] = "Selected source has no readable VERSION; installed metadata is not used." + return record + + +def probe_installed(base_home: str) -> dict: + # Match base-wrapper's isolated control-plane path without importing base_cli. + sys.path.insert(0, str(Path(base_home) / "cli/python")) + spec = importlib.util.find_spec("base_cli") + if spec is None or not spec.origin: + return {"path": None, "version": None, "detail": "base_cli is not discoverable in the selected Python environment."} + origin = Path(spec.origin).resolve() + version = None + try: + distribution = importlib.metadata.distribution("base-cli") + # Metadata from a shadowed wheel must never identify another source tree. + if Path(distribution.locate_file("base_cli/__init__.py")).resolve() == origin: + version = distribution.version + except importlib.metadata.PackageNotFoundError: + pass + return {"path": str(origin), "version": version, "detail": None} + + +def python_component(base_home: Path, python: str, kind: str, source: str, error: str) -> dict: + record = component("base-cli", kind, str(Path(source).resolve()) if source else None) + if error: + record.update(status="unavailable", detail=error) + return record + if source: + root = Path(source).resolve() + record["path"] = str((root / "base_cli/__init__.py").resolve()) + # Only the documented lib/python layout has an unambiguous repo VERSION. + if root.name == "python" and root.parent.name == "lib": + return source_identity(record, root.parent.parent) + record["detail"] = "Selected source has no recognized package metadata layout." + return record + try: + process = subprocess.run( + [python, "-I", str(Path(__file__).resolve()), "--probe", str(base_home)], + capture_output=True, text=True, timeout=10, check=False, + ) + if process.returncode != 0: + record.update(status="unavailable", detail="Selected Python could not inspect base-cli.") + return record + observed = json.loads(process.stdout) + record.update(observed) + if not record["path"]: + record["status"] = "unavailable" + else: + package = Path(record["path"]).parent + if package.parent.name == "python" and package.parent.parent.name == "lib": + record["source"] = "installed-editable" + source_identity(record, package.parents[2]) + else: + record["status"] = "available" if record["version"] else "unknown" + if not record["version"]: + record["detail"] = "Import location has no matching base-cli distribution metadata." + except (OSError, subprocess.TimeoutExpired, ValueError): + record.update(status="unavailable", detail="Selected Python is missing, unusable, or did not return a valid inspection.") + return record + + +def bash_component(kind: str, source: str, error: str) -> dict: + record = component("base-bash-libs", kind, str(Path(source).resolve()) if source else None) + if error or not source: + record.update(status="unavailable", detail=error or "No Bash provider was selected.") + return record + # Follow a symlinked stdlib just as base-bash-libs does when locating metadata. + stdlib = (Path(source) / "std/lib_std.sh").resolve() + if len(stdlib.parents) < 4: + record["detail"] = "Selected stdlib has no package root metadata layout." + return record + root = stdlib.parents[3] + source_identity(record, root) + metadata = {} + for line in read_text(stdlib.parents[1] / "base-bash-libs.release").splitlines(): + key, separator, value = line.partition("=") + if separator: + metadata[key] = value + if not os.access(root / "VERSION", os.R_OK): + record["version"] = metadata.get("version") or None + if record["revision"] is None: + record["revision"] = metadata.get("commit") if metadata.get("commit") != "unknown" else None + record["dirty"] = {"clean": False, "dirty": True}.get(metadata.get("dirty_state")) + if record["version"]: + record.update(status="available", detail=None) + return record + + +def build_report(arguments: list[str]) -> dict: + home, version, _, python, cli_kind, cli_root, cli_error, bash_kind, bash_root, bash_error = arguments + base = component("base", "checkout" if (Path(home) / ".git").exists() else "installation", str(Path(home).resolve()), version if version != "unknown" else None) + base.update(git_identity(Path(home))) + base["status"] = "available" if version != "unknown" else "unknown" + components = [base, python_component(Path(home), python, cli_kind, cli_root, cli_error), + bash_component(bash_kind, bash_root, bash_error)] + python_available = os.access(python, os.X_OK) + return {"schema_version": 1, "command": "version", "status": "ok" if python_available and all( + item["status"] == "available" for item in components) else "warn", + "data": {"python": {"path": str(Path(python).absolute()), "exists": python_available}, + "components": components}, "error": None} + + +def render_component(item: dict) -> str: + identity = item["version"] or item["status"] + if item["revision"]: + identity += f" (git {item['revision'][:12]}" + if item["dirty"] is not None: + identity += ", dirty" if item["dirty"] else ", clean" + identity += ")" + lines = [f"{item['name']}: {identity}", + f" source: {item['source']} path: {item['path'] or 'unavailable'}"] + if item["detail"]: + lines.append(f" {item['detail']}") + return "\n".join(lines) + + +def render_text(report: dict) -> str: + lines = [render_component(item) for item in report["data"]["components"]] + python = report["data"]["python"] + lines.append(f"Python: {python['path']}" + ("" if python["exists"] else " (unavailable)")) + return "\n".join(lines) + + +def main(arguments: list[str]) -> int: + if len(arguments) == 3 and arguments[0] == "--bash-summary": + item = bash_component(arguments[1], arguments[2], "") + print(render_component(item).splitlines()[0]) + return SUCCESS + if len(arguments) == 2 and arguments[0] == "--probe": + print(json.dumps(probe_installed(arguments[1]), sort_keys=True)) + return SUCCESS + report = build_report(arguments) + print(json.dumps(report, sort_keys=True) if arguments[2] == "json" else render_text(report)) + return SUCCESS + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/cli/python/base_version/tests/test_report.py b/cli/python/base_version/tests/test_report.py new file mode 100644 index 00000000..cf8938c4 --- /dev/null +++ b/cli/python/base_version/tests/test_report.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +import sys + +from base_version.report import bash_component, build_report, git_identity, main, python_component, render_text + + +def source(tmp_path, name, layout, version="7.8.9"): + root = tmp_path / name + path = root / layout + path.mkdir(parents=True) + (root / "VERSION").write_text(version) + return root, path + + +def git(root, *args): + return subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True, text=True).stdout.strip() + + +def test_source_version_does_not_use_installed_metadata(tmp_path): + root, path = source(tmp_path, "base-cli", "lib/python") + (path / "base_cli").mkdir() + # Inspection must not execute this provider. + (path / "base_cli/__init__.py").write_text("raise RuntimeError('broken provider')") + item = python_component(tmp_path, "/missing/python", "explicit", str(path), "") + assert item["version"] == "7.8.9" + assert item["status"] == "available" + (root / "VERSION").unlink() + item = python_component(tmp_path, sys.executable, "explicit", str(path), "") + assert item["version"] is None + assert item["status"] == "unknown" + + +def test_missing_providers_give_partial_json(tmp_path): + report = build_report([str(tmp_path), "1.2.3", "json", "/missing/python", "pip", "", "", "unavailable", "", "not found"]) + assert report["status"] == "warn" + assert report["error"] is None + assert [item["status"] for item in report["data"]["components"]] == ["available", "unavailable", "unavailable"] + assert json.loads(json.dumps(report)) == report + assert "base: 1.2.3" in render_text(report) + + +def test_git_revision_dirty_and_no_parent_leak(tmp_path): + git(tmp_path, "init") + git(tmp_path, "config", "user.email", "fixture@example.invalid") + git(tmp_path, "config", "user.name", "Fixture") + (tmp_path / "VERSION").write_text("1.0.0") + git(tmp_path, "add", ".") + git(tmp_path, "commit", "-m", "fixture") + assert git_identity(tmp_path) == {"revision": git(tmp_path, "rev-parse", "HEAD"), "dirty": False} + nested = tmp_path / "nested" + nested.mkdir() + assert git_identity(nested) == {"revision": None, "dirty": None} + (nested / "untracked").touch() + assert git_identity(tmp_path)["dirty"] is True + + +def test_bash_embedded_metadata_and_symlink(tmp_path): + root, path = source(tmp_path, "libs", "lib/bash/std") + (path / "lib_std.sh").write_text("exit 99") + (root / "VERSION").unlink() + (path.parent / "base-bash-libs.release").write_text("version=2.1.0\ncommit=" + "a" * 40 + "\ndirty_state=clean\n") + link = tmp_path / "linked" + link.symlink_to(path.parent, target_is_directory=True) + item = bash_component("homebrew", str(link), "") + assert item["version"] == "2.1.0" + assert item["revision"] == "a" * 40 + assert item["dirty"] is False + assert item["path"] == str(path.parent) + + +def test_invalid_override_is_not_replaced(tmp_path): + item = python_component(tmp_path, sys.executable, "unavailable", "", "invalid explicit root") + assert item["status"] == "unavailable" + assert item["detail"] == "invalid explicit root" + assert item["version"] is None + + +def test_installed_probe_uses_selected_environment_without_import(tmp_path): + venv = tmp_path / "venv" + subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(venv)], check=True) + python = venv / "bin/python" + site = Path(subprocess.check_output([str(python), "-I", "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"], text=True).strip()) + (site / "base_cli").mkdir() + (site / "base_cli/__init__.py").write_text("raise RuntimeError('must not import')") + metadata = site / "base_cli-9.8.7.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text("Metadata-Version: 2.1\nName: base-cli\nVersion: 9.8.7\n") + item = python_component(tmp_path, str(python), "pip", "", "") + assert item["version"] == "9.8.7" + assert item["path"] == str(site / "base_cli/__init__.py") + assert item["status"] == "available" + # A .pth source override in the selected interpreter must not inherit wheel metadata. + root, path = source(tmp_path, "editable", "lib/python", "3.2.1") + (path / "base_cli").mkdir() + (path / "base_cli/__init__.py").write_text("raise RuntimeError('must not import')") + (site / "override.pth").write_text(f"import sys; sys.path.insert(0, {str(path)!r})\n") + item = python_component(tmp_path, str(python), "pip", "", "") + assert item["version"] == "3.2.1" + assert item["source"] == "installed-editable" + + +def test_git_absent_preserves_version(tmp_path, monkeypatch): + _, path = source(tmp_path, "libs", "lib/bash/std") + (path / "lib_std.sh").touch() + monkeypatch.setenv("PATH", "/nonexistent") + assert bash_component("explicit", str(path.parent), "")["version"] == "7.8.9" + + +def test_empty_version_does_not_claim_embedded_release(tmp_path): + root, path = source(tmp_path, "libs", "lib/bash/std", "\n9.9.9") + (path / "lib_std.sh").touch() + (path.parent / "base-bash-libs.release").write_text("version=2.1.0\n") + item = bash_component("explicit", str(path.parent), "") + assert item["version"] is None + assert item["status"] == "unknown" + + +def test_bash_summary_fits_the_single_line_check_protocol(tmp_path, capsys): + _, path = source(tmp_path, "libs", "lib/bash/std") + (path / "lib_std.sh").touch() + assert main(["--bash-summary", "explicit", str(path.parent)]) == 0 + assert capsys.readouterr().out == "base-bash-libs: 7.8.9\n" diff --git a/docs/inspection-json.md b/docs/inspection-json.md index 975f2ca0..0b736b0a 100644 --- a/docs/inspection-json.md +++ b/docs/inspection-json.md @@ -9,6 +9,9 @@ that are useful in CI, release gates, agent handoffs, and dashboards. Use - `basectl gh issue readiness` - `basectl gh branch stale` +`basectl version --all --json` also uses this envelope; it selects JSON with +`--json`. + Text remains the default. JSON mode writes exactly one JSON document to stdout and never mixes ANSI formatting or human prose into that stream. Upstream tools may still write diagnostics to stderr. @@ -35,7 +38,7 @@ The five top-level keys and their types are stable: - `schema_version` is the integer `1` for this payload family. - `command` is one of `repo check`, `release check`, - `gh issue readiness`, or `gh branch stale`. + `gh issue readiness`, `gh branch stale`, or `version`. - `status` is `ok`, `warn`, or `error`. - `data` is a command-specific object. - `error` is `null` for a completed inspection. A controlled usage, @@ -74,6 +77,9 @@ Serialization does not change command policy or exit status: - `gh issue readiness` returns nonzero for both partial and not-ready results. - `gh branch stale` returns zero when stale branches are findings; its payload uses `status: "warn"` when the result list is non-empty. +- `version --all --json` returns zero for completed partial reports (`warn`). + It reports observed identities rather than compatibility. Its option errors + return `2` with stderr text; see the [command reference](../cli/bash/commands/basectl/README.md#component-versions). - controlled usage errors return `2`; environment and upstream failures retain their command failure status. diff --git a/lib/base/base_bash_libs_runtime.sh b/lib/base/base_bash_libs_runtime.sh new file mode 100644 index 00000000..ef8e7b16 --- /dev/null +++ b/lib/base/base_bash_libs_runtime.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Shared provider selection for runtime bootstrap and dependency-independent inspection. +# shellcheck disable=SC2034 +# Callers supply BASE_HOME and base_init_error; no provider code is sourced here. + +[[ -n "${_base_bash_libs_runtime_sourced:-}" ]] && return 0 +_base_bash_libs_runtime_sourced=1 +readonly _base_bash_libs_runtime_sourced + +base_init_homebrew_prefix() { + case "$BASE_HOME" in + */opt/base/libexec) + printf '%s\n' "${BASE_HOME%/opt/base/libexec}" + ;; + */Cellar/base/*/libexec) + printf '%s\n' "${BASE_HOME%%/Cellar/base/*}" + ;; + esac +} + +base_init_bash_libs_dir_is_usable() { + local candidate="${1:-}" + + [[ -n "$candidate" ]] || return 1 + [[ -f "$candidate/std/lib_std.sh" ]] +} + +base_init_report_missing_bash_libs() { + local candidate + local homebrew_prefix + + base_init_error "Base reusable Bash libraries were not found." + + candidate="$BASE_HOME/../base-bash-libs/lib/bash" + base_init_error "Tried sibling base-bash-libs checkout at '$candidate'." + + homebrew_prefix="$(base_init_homebrew_prefix || true)" + if [[ -n "$homebrew_prefix" ]]; then + candidate="$homebrew_prefix/opt/base-bash-libs/libexec/lib/bash" + base_init_error "Tried Homebrew base-bash-libs package at '$candidate'." + fi + + base_init_error "Clone basefoundry/base-bash-libs next to Base, install it with 'brew install basefoundry/base/base-bash-libs', or set BASE_BASH_LIBS_DIR to a compatible lib/bash directory." +} + +base_init_set_bash_libs_contract() { + local candidate + local homebrew_prefix + local explicit_dir="${BASE_BASH_LIBS_DIR:-}" + + if [[ -n "$explicit_dir" ]]; then + base_init_bash_libs_dir_is_usable "$explicit_dir" || { + base_init_error "BASE_BASH_LIBS_DIR '$explicit_dir' does not contain std/lib_std.sh." + return 1 + } + BASE_BASH_LIBS_DIR="$(cd -L -- "$explicit_dir" && pwd -L)" || return 1 + BASE_BASH_LIBS_SOURCE=explicit + return $? + fi + + candidate="$BASE_HOME/../base-bash-libs/lib/bash" + if base_init_bash_libs_dir_is_usable "$candidate"; then + BASE_BASH_LIBS_DIR="$(cd -L -- "$candidate" && pwd -L)" || return 1 + BASE_BASH_LIBS_SOURCE=sibling + return $? + fi + + homebrew_prefix="$(base_init_homebrew_prefix || true)" + if [[ -n "$homebrew_prefix" ]]; then + candidate="$homebrew_prefix/opt/base-bash-libs/libexec/lib/bash" + if base_init_bash_libs_dir_is_usable "$candidate"; then + BASE_BASH_LIBS_DIR="$(cd -L -- "$candidate" && pwd -L)" || return 1 + BASE_BASH_LIBS_SOURCE=homebrew + return $? + fi + fi + + base_init_report_missing_bash_libs + return 1 +} diff --git a/lib/shell/completions/basectl_completion.sh b/lib/shell/completions/basectl_completion.sh index 0b4726a3..b9028cb5 100644 --- a/lib/shell/completions/basectl_completion.sh +++ b/lib/shell/completions/basectl_completion.sh @@ -945,7 +945,7 @@ _base_basectl_completion() { _base_basectl_completion_project_or_options "--dry-run -v -h --help" "$cur" ;; version) - _base_basectl_completion_compgen "-h --help" "$cur" + _base_basectl_completion_compgen "--all --json -h --help" "$cur" ;; help) _base_basectl_completion_help diff --git a/lib/shell/completions/basectl_completion.zsh b/lib/shell/completions/basectl_completion.zsh index 40a572b5..34577012 100644 --- a/lib/shell/completions/basectl_completion.zsh +++ b/lib/shell/completions/basectl_completion.zsh @@ -1204,7 +1204,7 @@ _base_basectl_completion() { fi ;; version) - _arguments '(-h --help)'{-h,--help}'[Show help text]' + _arguments '--all[Show all component identities]' '--json[Emit JSON; requires --all]' '(-h --help)'{-h,--help}'[Show help text]' ;; help) local -a help_words diff --git a/lib/shell/completions/tests/completions.bats b/lib/shell/completions/tests/completions.bats index a7b752ca..784d4d89 100644 --- a/lib/shell/completions/tests/completions.bats +++ b/lib/shell/completions/tests/completions.bats @@ -1098,3 +1098,14 @@ EOF [[ "$output" == now=* ]] [ ! -e "$date_called" ] } + +@test "version completion offers detailed and JSON inspection" { + run bash_completion_candidates basectl version -- + [ "$status" -eq 0 ] + [[ "$output" == *"--all"* ]] + [[ "$output" == *"--json"* ]] + run zsh_completion_specs basectl version "" + [ "$status" -eq 0 ] + [[ "$output" == *"--all["* ]] + [[ "$output" == *"--json["* ]] +} diff --git a/tests/base_init.bats b/tests/base_init.bats index 34d39001..f7273687 100644 --- a/tests/base_init.bats +++ b/tests/base_init.bats @@ -21,6 +21,8 @@ create_minimal_base_home() { "$base_home/lib/bash/runtime" \ "$base_home/lib/shell" + mkdir -p "$base_home/lib/base" + cp "$BASE_REPO_ROOT/lib/base/base_bash_libs_runtime.sh" "$base_home/lib/base/" cp "$BASE_REPO_ROOT/base_init.sh" "$base_home/base_init.sh" cp "$BASE_REPO_ROOT/lib/bash/runtime/command_protocol.sh" "$base_home/lib/bash/runtime/command_protocol.sh" } From 44d261441aed94766f5a8480695451f0826a61a8 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:11:36 +0530 Subject: [PATCH 2/3] Apply lint conventions to component version inspection --- cli/python/base_version/report.py | 11 ++++++++--- cli/python/base_version/tests/test_report.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cli/python/base_version/report.py b/cli/python/base_version/report.py index 51fd757c..5306bc4e 100644 --- a/cli/python/base_version/report.py +++ b/cli/python/base_version/report.py @@ -69,7 +69,8 @@ def probe_installed(base_home: str) -> dict: sys.path.insert(0, str(Path(base_home) / "cli/python")) spec = importlib.util.find_spec("base_cli") if spec is None or not spec.origin: - return {"path": None, "version": None, "detail": "base_cli is not discoverable in the selected Python environment."} + return {"path": None, "version": None, + "detail": "base_cli is not discoverable in the selected Python environment."} origin = Path(spec.origin).resolve() version = None try: @@ -117,7 +118,8 @@ def python_component(base_home: Path, python: str, kind: str, source: str, error if not record["version"]: record["detail"] = "Import location has no matching base-cli distribution metadata." except (OSError, subprocess.TimeoutExpired, ValueError): - record.update(status="unavailable", detail="Selected Python is missing, unusable, or did not return a valid inspection.") + record.update(status="unavailable", + detail="Selected Python is missing, unusable, or did not return a valid inspection.") return record @@ -150,7 +152,10 @@ def bash_component(kind: str, source: str, error: str) -> dict: def build_report(arguments: list[str]) -> dict: home, version, _, python, cli_kind, cli_root, cli_error, bash_kind, bash_root, bash_error = arguments - base = component("base", "checkout" if (Path(home) / ".git").exists() else "installation", str(Path(home).resolve()), version if version != "unknown" else None) + base = component( + "base", "checkout" if (Path(home) / ".git").exists() else "installation", + str(Path(home).resolve()), version if version != "unknown" else None, + ) base.update(git_identity(Path(home))) base["status"] = "available" if version != "unknown" else "unknown" components = [base, python_component(Path(home), python, cli_kind, cli_root, cli_error), diff --git a/cli/python/base_version/tests/test_report.py b/cli/python/base_version/tests/test_report.py index cf8938c4..c3b6f4bb 100644 --- a/cli/python/base_version/tests/test_report.py +++ b/cli/python/base_version/tests/test_report.py @@ -35,7 +35,9 @@ def test_source_version_does_not_use_installed_metadata(tmp_path): def test_missing_providers_give_partial_json(tmp_path): - report = build_report([str(tmp_path), "1.2.3", "json", "/missing/python", "pip", "", "", "unavailable", "", "not found"]) + report = build_report([ + str(tmp_path), "1.2.3", "json", "/missing/python", "pip", "", "", "unavailable", "", "not found", + ]) assert report["status"] == "warn" assert report["error"] is None assert [item["status"] for item in report["data"]["components"]] == ["available", "unavailable", "unavailable"] @@ -83,7 +85,9 @@ def test_installed_probe_uses_selected_environment_without_import(tmp_path): venv = tmp_path / "venv" subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(venv)], check=True) python = venv / "bin/python" - site = Path(subprocess.check_output([str(python), "-I", "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"], text=True).strip()) + site = Path(subprocess.check_output( + [str(python), "-I", "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"], text=True, + ).strip()) (site / "base_cli").mkdir() (site / "base_cli/__init__.py").write_text("raise RuntimeError('must not import')") metadata = site / "base_cli-9.8.7.dist-info" @@ -94,7 +98,7 @@ def test_installed_probe_uses_selected_environment_without_import(tmp_path): assert item["path"] == str(site / "base_cli/__init__.py") assert item["status"] == "available" # A .pth source override in the selected interpreter must not inherit wheel metadata. - root, path = source(tmp_path, "editable", "lib/python", "3.2.1") + _, path = source(tmp_path, "editable", "lib/python", "3.2.1") (path / "base_cli").mkdir() (path / "base_cli/__init__.py").write_text("raise RuntimeError('must not import')") (site / "override.pth").write_text(f"import sys; sys.path.insert(0, {str(path)!r})\n") @@ -111,7 +115,7 @@ def test_git_absent_preserves_version(tmp_path, monkeypatch): def test_empty_version_does_not_claim_embedded_release(tmp_path): - root, path = source(tmp_path, "libs", "lib/bash/std", "\n9.9.9") + _, path = source(tmp_path, "libs", "lib/bash/std", "\n9.9.9") (path / "lib_std.sh").touch() (path.parent / "base-bash-libs.release").write_text("version=2.1.0\n") item = bash_component("explicit", str(path.parent), "") From d8479e907570a140abf84d931ef41e7e086f88d6 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:38:06 +0530 Subject: [PATCH 3/3] Guard optional Zsh version completion coverage --- lib/shell/completions/tests/completions.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/shell/completions/tests/completions.bats b/lib/shell/completions/tests/completions.bats index 784d4d89..11f0dbea 100644 --- a/lib/shell/completions/tests/completions.bats +++ b/lib/shell/completions/tests/completions.bats @@ -1099,11 +1099,16 @@ EOF [ ! -e "$date_called" ] } -@test "version completion offers detailed and JSON inspection" { +@test "Bash version completion offers detailed and JSON inspection" { run bash_completion_candidates basectl version -- [ "$status" -eq 0 ] [[ "$output" == *"--all"* ]] [[ "$output" == *"--json"* ]] +} + +@test "Zsh version completion offers detailed and JSON inspection" { + command -v zsh >/dev/null 2>&1 || skip "zsh is not available" + run zsh_completion_specs basectl version "" [ "$status" -eq 0 ] [[ "$output" == *"--all["* ]]