1 Commits

Author SHA1 Message Date
b891f338cd Update bal/__init__.py
v0.6.0: bump __init__ version
2026-07-17 17:38:55 +00:00
10 changed files with 32 additions and 358 deletions

View File

@@ -2329,184 +2329,3 @@ The request was to make the failure message clearer (no timeout change).
- `ruff`: no new errors. - `ruff`: no new errors.
**Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation). **Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation).
---
## 43. v0.6.0 - Version bump for the official repository release
**Date:** 2026-07-17
**Context:** the plugin content of this version is IDENTICAL to v0.5.18 - no code
changes. A release was published on the official repository
(bitcoinafterlife/bal-electrum-plugin) tagged "v0.6.0", but the internal version
files still read "0.5.18" (a human oversight: the release tag was not matched by
a version bump in the code), so Electrum displayed "0.5.18" after installing it.
This entry aligns the internal version with the intended "0.6.0" release tag by
bumping `bal/VERSION`, `bal/manifest.json`, `bal/__init__.py` and
`bal/core/plugin_base.py` from 0.5.18 to 0.6.0. No functional changes.
**Verification:** full test suite against Electrum 4.7.2 and 4.8.0 (same 266
passed / 2 pre-existing unrelated failures as v0.5.18); `ruff` clean.
**Outcome:** DONE.
---
## 44. Repo: inheritance-logic test suite + first BAL CLI (no plugin version change)
**Date:** 2026-07-20
**Scope note:** this entry adds REPOSITORY files only (`tests/`, `bal_cli.py`).
The plugin package `bal/` is byte-for-byte unchanged, so the plugin version
stays **0.6.0** on purpose (no zip rebuild, no version bump - avoiding any new
tag/code mismatch).
**1) New logic test suite - `tests/test_inheritance_scenarios.py` (23 tests):**
- Multi-heir distribution via `Heirs.prepare_lists`: 50/50 percent split of
(balance - fees); percentages normalized to their SUM (30+30 behaves like
50/50); fixed heirs paid first with percents sharing the remainder; fixed
amounts exceeding the balance scaled down proportionally (`onlyfixed`).
- Dust rules: below-dust heirs marked `DUST:` while valid heirs keep building;
the leftover redistribution can LIFT dust fixed heirs above the threshold
(pinned: 100:200 on 1M -> 333333/666666); all-dust wills refused
(`HeirAmountIsDustException`); `BalanceTooLowException` when balance < fees.
- Will-executor fees: one pseudo-heir per distinct locktime; fees larger than
the balance raise `WillExecutorFeeException`.
- Expired-heir exclusion vs `from_locktime`; grouping of heirs by locktime.
- Heir add/remove/locktime-change all flag the will as changed
(`Will._same_heirs`); the `Heirs` mapping persists on add/remove.
- Will expiry (`Will.check_will_expired`): past locktime raises
`WillExpiredException`, future/boundary (== now) does not, non-VALID items
are ignored.
- Locktime parsing: int passthrough, `<n>d` -> future midnight, `1y` == `365d`.
**2) First BAL CLI - `bal_cli.py` (offline iteration):**
- Reuses `bal.core` directly with Electrum as a library (no Qt, headless).
- Commands: `heirs list/add/remove/export/import`, `status`.
- Safety: **testnet by default**, mainnet only with an explicit `--mainnet`
flag (active network always printed); encrypted wallets via `--password` or
`BAL_WALLET_PASSWORD` env var; address/locktime validation (rejects wrong
network and past locktimes); guaranteed process termination (electrum leaves
non-daemon threads even offline - the CLI stops the wallet and event loop,
then hard-exits with the proper exit code, so machine callers never hang).
- Planned next iteration: `will build/sign/push/check` with an explicit
`--yes` confirmation flag for automation.
- End-to-end smoke test `tests/test_cli_smoke.py`: creates a REAL testnet
wallet via electrum-as-library in a subprocess, then drives
add/list/reject-invalid-address/reject-past-locktime/export/remove/status.
**Verification:**
- Full suite against **Electrum 4.7.2**: `290 passed`; against **4.8.0**:
`290 passed` (same 2 pre-existing, unrelated `baltx_fees` failures in both).
- `ruff`: no new errors on the three new files.
**Outcome:** DONE.
---
## 45. Repo: CLI will commands (build / sign / push / check) - no plugin version change
**Date:** 2026-07-21
**Goal:** complete the headless workflow started in entry #44, so a machine can
run the whole inheritance cycle without the Qt GUI.
**What was added (`bal_cli.py`):**
- `will build` - builds the will transactions offline through the same
`bal.core` pipeline the GUI uses (`Heirs.get_transactions` -> `WillItem` ->
`Will.update_will` / `normalize_will`), storing them as "New".
- `will sign` - signs the valid, incomplete transactions with the wallet
password (`--password` / `BAL_WALLET_PASSWORD`). Prints a summary and asks
for confirmation; `--yes` skips it for automation. Chained will inputs
(spending a previous will's change) are patched exactly as the GUI does.
Watching-only wallets and hardware keystores are rejected with a clear
message (hardware devices need physical confirmation).
- `will push` - sends signed transactions to the selected will-executors via
`Willexecutors.push_transactions_parallel`; `--yes` and `--force` supported;
updates PUSHED / PUSH_FAIL statuses.
- `will check` - read-only report with **exit codes for scripts**: 0 valid,
complete and pushed; 2 no will stored; 3 EXPIRED; 4 not fully signed;
5 signed but not pushed; 6 heirs changed since the will was built.
- `_CliBalPlugin`: minimal stand-in exposing only what `bal.core` needs
headless (`WILLEXECUTORS`, `NO_WILLEXECUTOR`, `get_decimal_point`).
**Three real bugs found and fixed while implementing this:**
1. `Will.only_valid()` returns a GENERATOR - `len()` on it raised
`TypeError`; now wrapped in `list()`.
2. `copy.deepcopy(tx)` fails on Electrum 4.8 (`cannot pickle '_thread.RLock'`);
signing now re-parses the transaction from its serialization instead.
3. **Silent will loss on save**: values loaded from the wallet DB are
`StoredDict`s carrying the DB lock, and `JsonDB.put` deep-copies its value
and returns False *silently* when that fails - the will appeared saved but
was never written. Persistence now normalizes through a
`json.dumps`/`loads` round-trip (which also validates serializability) and
exits with an explicit error if anything is non-serializable.
Also added: pre-build validation of the selected executor addresses for the
active network (refreshed from the server when possible), and a guard that
catches exception sentinels embedded by `heirs.py` in a heir entry when an
output cannot be built.
**Verification:**
- New `test_cli_will_cycle` in `tests/test_cli_smoke.py`: creates a REAL
testnet wallet, funds it offline with a handmade transaction, then drives
`build -> check(4) -> sign --yes -> check(5) -> push (aborted at the
confirmation prompt) -> status`. No network access.
- Full suite: **291 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
pre-existing, unrelated `baltx_fees` failures in both).
- `ruff`: all checks passed.
**Note:** repository-only change (CLI + tests). The plugin version is
unchanged; the Qt plugin code is untouched.
**Outcome:** DONE.
---
## 46. v0.6.1 - Version read from manifest.json (single source of truth) - PR #4
**Date:** 2026-07-22
**Goal (Truman):** stop keeping the version in four places (`bal/VERSION`,
`bal/manifest.json`, `bal/__init__.py`, `bal/core/plugin_base.py`) synced by a
pre-commit hook. The version must be read at runtime from `manifest.json` only.
**What changed:**
- `bal/core/plugin_base.py`: new module function `get_version()` reads the
`"version"` field from `bal/manifest.json` using `importlib.resources`
(zip-safe: works both extracted and from inside a zip, which is how Electrum
loads external plugins via `zipimport`; no hand-built paths, so no Windows
backslash-in-zip issue). The value is cached. `BalPlugin` now exposes a
`version` **property** returning `get_version()`; the hardcoded
`__version__` class attribute and the old `version()` method that read the
`VERSION` file are removed.
- The three call sites that printed the version now use the property
(`self.bal_window.bal_plugin.version` in `bal/gui/qt/widgets.py` and
`bal/gui/qt/dialogs.py`) or `get_version()` where no plugin instance is
available (`bal/core/willexecutors.py`, the HTTP user-agent, a static
method).
- **Removed the `bal/VERSION` file** (the plugin package now ships 36 files
instead of 37).
- `HANDOFF.md`: updated the four references that told maintainers to edit
`bal/VERSION` / keep four files in sync - now they point to
`manifest.json` as the single source of truth. (Historical version
references in `CHANGELOG.md` and `.agent_memory_tasks.md` are left intact on
purpose - they describe past events.)
**Verification:**
- New `tests/test_version_source.py` (7 tests): version equals the manifest
field; the `version` property matches `get_version()`; no hardcoded
`__version__` remains; no `bal/VERSION` file remains; the value is cached;
and - the key one - the version is read correctly when `bal` is imported
FROM INSIDE A ZIP in a child interpreter (the real Electrum/Windows
scenario).
- Full suite: **298 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
pre-existing, unrelated `baltx_fees` failures in both).
- `ruff`: clean.
- Also verified the built distribution zip reports the manifest version via
`get_version()` (bumped to 0.6.1 in this change).
**Note:** repository change targeting PR #4 (branch
`bitcoinafterlife-patch-5`). No functional change to inheritance behavior.
**Outcome:** DONE.

View File

@@ -19,7 +19,7 @@
services) can be paid a fee to broadcast the inheritance when due. The owner services) can be paid a fee to broadcast the inheritance when due. The owner
periodically proves they are alive ("check-alive"); if the deadline passes, periodically proves they are alive ("check-alive"); if the deadline passes,
the inheritance becomes spendable. the inheritance becomes spendable.
- **Current version:** see the `"version"` field of `bal/manifest.json` (the single source of truth; read at runtime via `get_version()` in `bal/core/plugin_base.py`). - **Current version:** see `bal/VERSION` (last shipped: **0.4.8**).
--- ---
@@ -55,10 +55,11 @@ These are non-negotiable. They come from the owner directly.
``` ```
bal/ <- the plugin package (this is what ships in the ZIP) bal/ <- the plugin package (this is what ships in the ZIP)
__init__.py <- package docstring (no version here anymore) __init__.py <- __version__ (one of 4 version files)
manifest.json <- plugin manifest, "version" field (SINGLE SOURCE OF TRUTH for the version) VERSION <- plain-text version (one of 4 version files)
manifest.json <- plugin manifest, "version" field (one of 4)
core/ core/
plugin_base.py <- get_version() reads the version from manifest.json (zip-safe) plugin_base.py <- __version__ "AUTOMATICALLY GENERATED" (one of 4)
heirs.py <- HEIRS + transaction building (prepare_lists, heirs.py <- HEIRS + transaction building (prepare_lists,
prepare_transactions, buildTransactions). CORE LOGIC. prepare_transactions, buildTransactions). CORE LOGIC.
will.py <- Will/WillItem, validation (check_amounts, check_will), will.py <- Will/WillItem, validation (check_amounts, check_will),
@@ -114,11 +115,13 @@ find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc"
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
``` ```
**Bump version — ONE file only (single source of truth):** **Bump version — there are FOUR files, keep them in sync:**
``` ```
bal/core/plugin_base.py -> __version__ = "X.Y.Z" # AUTOMATICALLY GENERATED DO NOT EDIT
bal/__init__.py -> __version__ = "X.Y.Z"
bal/VERSION -> X.Y.Z
bal/manifest.json -> "version": "X.Y.Z", bal/manifest.json -> "version": "X.Y.Z",
``` ```
The code reads this at runtime via `get_version()` in `bal/core/plugin_base.py` (exposed as the `BalPlugin.version` property), so there is nothing else to keep in sync. There is no longer a `bal/VERSION` file nor a hardcoded `__version__`.
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner **IMPORTANT for the owner when testing:** after installing a ZIP, the owner
must **fully restart Electrum** (not just reload the plugin) — Electrum's must **fully restart Electrum** (not just reload the plugin) — Electrum's
@@ -289,7 +292,7 @@ must **fully restart Electrum** (not just reload the plugin) — Electrum's
## 7. How to resume (checklist for the next AI) ## 7. How to resume (checklist for the next AI)
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`. 1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
2. Confirm the environment: `git status`, current branch, and the `"version"` field of `bal/manifest.json`. 2. Confirm the environment: `git status`, current branch, `bal/VERSION`.
3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8). 3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
4. Talk to the owner in **Italian**, write everything else in **English**. 4. Talk to the owner in **Italian**, write everything else in **English**.
5. For any change: present a PLAN, wait for "OK" (R4), then implement, test, 5. For any change: present a PLAN, wait for "OK" (R4), then implement, test,

1
bal/VERSION Normal file
View File

@@ -0,0 +1 @@
0.5.18

View File

@@ -36,7 +36,4 @@ The plugin supports Electrum 4.7.2 and 4.8.0 with PyQt6. Electrum 4.8.0 removed
available and adapts, so both releases keep working. available and adapts, so both releases keep working.
""" """
# The plugin version is NOT defined here. It lives only in ``bal/manifest.json`` __version__ = "0.6.0"
# (the single source of truth) and is read at runtime via ``get_version()`` in
# ``bal/core/plugin_base.py`` (exposed as the ``BalPlugin.version`` property).
# Keeping a hardcoded ``__version__`` here would just be a stale duplicate.

View File

@@ -21,7 +21,6 @@ serialised together with the wallet file.
This module performs **no** GUI work and imports nothing from PyQt / electrum.gui. This module performs **no** GUI work and imports nothing from PyQt / electrum.gui.
""" """
import json
import os import os
import platform import platform
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
@@ -34,45 +33,6 @@ from electrum.transaction import tx_from_any
_logger = get_logger(__name__) _logger = get_logger(__name__)
# --------------------------------------------------------------------------- #
# Plugin version - single source of truth
# --------------------------------------------------------------------------- #
# The version lives ONLY in bal/manifest.json (the file Electrum itself reads).
# We used to hardcode it in four files and keep them in sync with a pre-commit
# hook; reading it from the manifest removes that duplication.
#
# importlib.resources is used on purpose: it reads a data file bundled inside
# the ``bal`` package and works identically whether the plugin runs from an
# extracted directory or from INSIDE a zip (Electrum loads external plugins via
# zipimport). It never builds a path by hand, so there is no os.path.join
# backslash issue on Windows inside a zip.
_VERSION_CACHE = None
def get_version():
"""Return the plugin version from ``bal/manifest.json`` (cached).
Zip-safe and independent of the current working directory. Falls back to
``"unknown"`` if the manifest cannot be read, so importing the plugin never
fails just because of version lookup.
"""
global _VERSION_CACHE
if _VERSION_CACHE is None:
try:
import importlib.resources
data = (
importlib.resources.files("bal")
.joinpath("manifest.json")
.read_text(encoding="utf-8")
)
_VERSION_CACHE = json.loads(data)["version"]
except Exception as e: # noqa: BLE001 - never break import over version
_logger.error(f"failed to read version from manifest.json: {e}")
_VERSION_CACHE = "unknown"
return _VERSION_CACHE
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Wallet-DB registration # Wallet-DB registration
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@@ -160,6 +120,9 @@ class BalPlugin(BasePlugin):
layer (or unit tests) can use the plugin logic without importing Qt. layer (or unit tests) can use the plugin logic without importing Qt.
""" """
_version = None
__version__ = "0.5.18" # AUTOMATICALLY GENERATED DO NOT EDIT
# Command used to open an .ics calendar file, per operating system. # Command used to open an .ics calendar file, per operating system.
default_app = { default_app = {
"Linux": "xdg-open", "Linux": "xdg-open",
@@ -175,11 +138,18 @@ class BalPlugin(BasePlugin):
# Default geometry hint for some dialogs (kept from the original code). # Default geometry hint for some dialogs (kept from the original code).
SIZE = (159, 97) SIZE = (159, 97)
@property
def version(self): def version(self):
"""Plugin version, read from ``bal/manifest.json`` (single source of """Return the plugin version, read once from the ``VERSION`` file."""
truth). See :func:`get_version`.""" if not self._version:
return get_version() try:
f = ""
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
f = str(fi.read())
self._version = f.strip()
except Exception as e:
_logger.error(f"failed to get version: {e}")
self._version = "unknown"
return self._version
def __init__(self, parent, config, name): def __init__(self, parent, config, name):
self.logger = get_logger(__name__) self.logger = get_logger(__name__)

View File

@@ -23,7 +23,7 @@ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.network import Network from electrum.network import Network
from .plugin_base import BalPlugin, get_version from .plugin_base import BalPlugin
# Per-request timeout (seconds) for interactive operations (ping / info / # Per-request timeout (seconds) for interactive operations (ping / info /
# list download). These fail fast (no retries) so a dead server does not # list download). These fail fast (no retries) so a dead server does not
@@ -272,7 +272,7 @@ class Willexecutors:
raise Exception("You are offline.") raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}") _logger.debug(f"<-- {method} {url} {data}")
headers = {} headers = {}
headers["user-agent"] = f"BalPlugin v:{get_version()}" headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
headers["Content-Type"] = "text/plain" headers["Content-Type"] = "text/plain"
if not handle_response: if not handle_response:
handle_response = Willexecutors.handle_response handle_response = Willexecutors.handle_response

View File

@@ -1533,7 +1533,7 @@ class BalBuildWillDialog(BalDialog):
"BEGIN:VCALENDAR", "BEGIN:VCALENDAR",
"VERSION:2.0", "VERSION:2.0",
"PRODID:-//Bitcoin After Life//Electrum Plugin/" "PRODID:-//Bitcoin After Life//Electrum Plugin/"
f"{self.bal_window.bal_plugin.version}", f"{BalPlugin.__version__}",
] ]
total = len(offsets) total = len(offsets)

View File

@@ -1153,7 +1153,7 @@ class WillSettingsWidget(QWidget):
lines = [ lines = [
"BEGIN:VCALENDAR", "BEGIN:VCALENDAR",
"VERSION:2.0", "VERSION:2.0",
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{self.bal_window.bal_plugin.version}", f"PRODID:-//Bitcoin After Life//Electrum Plugin/{BalPlugin.__version__}",
] ]
# One separate VEVENT per reminder offset (its own date in the calendar). # One separate VEVENT per reminder offset (its own date in the calendar).
@@ -1284,7 +1284,7 @@ class WillSettingsWidget(QWidget):
"BEGIN:VCALENDAR", "BEGIN:VCALENDAR",
"VERSION:2.0", "VERSION:2.0",
"PRODID:-//Bitcoin After Life//Electrum Plugin/" "PRODID:-//Bitcoin After Life//Electrum Plugin/"
f"{self.bal_window.bal_plugin.version}", f"{BalPlugin.__version__}",
] ]
total = len(offsets) total = len(offsets)

View File

@@ -1,7 +1,7 @@
{ {
"name": "bal", "name": "bal",
"fullname": "Bitcoin After Life", "fullname": "Bitcoin After Life",
"version": "0.6.1", "version": "0.5.18",
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
"author": "Svatantrya", "author": "Svatantrya",
"licence": "MIT", "licence": "MIT",

View File

@@ -1,116 +0,0 @@
"""Tests for the single-source-of-truth plugin version.
The plugin version must come ONLY from ``bal/manifest.json`` (no hardcoded
copies, no ``bal/VERSION`` file), and it must be readable both from an extracted
package and from INSIDE a zip (the way Electrum loads external plugins via
zipimport). These tests lock that behavior.
Run:
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
python3 -m pytest tests/test_version_source.py -q
"""
import json
import os
import subprocess
import sys
import zipfile
import bal
from bal.core.plugin_base import BalPlugin, get_version
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_MANIFEST = os.path.join(_REPO, "bal", "manifest.json")
def _manifest_version():
with open(_MANIFEST, encoding="utf-8") as f:
return json.load(f)["version"]
def test_get_version_matches_manifest():
"""get_version() returns exactly the manifest 'version' field."""
assert get_version() == _manifest_version()
def test_get_version_is_not_unknown():
"""The manifest must be readable - never the 'unknown' fallback here."""
assert get_version() != "unknown"
# a plausible semantic version, e.g. 0.6.0
assert get_version()[0].isdigit()
def test_version_property_matches_get_version():
"""BalPlugin.version is a property returning the same value."""
assert isinstance(BalPlugin.version, property)
# A bare object works as ``self`` because the property ignores instance
# state and simply delegates to get_version().
class _Dummy:
version = BalPlugin.version
assert _Dummy().version == get_version()
def test_no_hardcoded_class_version():
"""The old hardcoded BalPlugin.__version__ constant is gone."""
assert "__version__" not in vars(BalPlugin)
def test_no_version_file_in_package():
"""The bal/VERSION file has been removed (manifest is the only source)."""
assert not os.path.exists(os.path.join(_REPO, "bal", "VERSION"))
def test_get_version_cached():
"""Second call returns the cached value (same object)."""
v1 = get_version()
v2 = get_version()
assert v1 == v2
assert v1 is v2 # cached string, identical object
def test_version_readable_from_inside_zip(tmp_path):
"""The version must be readable when 'bal' is imported from a zip.
Electrum loads external plugins via zipimport, so this is the real
production scenario (and the one that used to break on Windows with
os.path.join). We build a zip of the bal package and read the version in a
fresh interpreter whose only path to 'bal' is that zip.
"""
bal_dir = os.path.dirname(os.path.abspath(bal.__file__))
zip_path = tmp_path / "bal_plugin.zip"
with zipfile.ZipFile(zip_path, "w") as z:
for root, _dirs, files in os.walk(bal_dir):
for fn in files:
if fn.endswith(".pyc"):
continue
full = os.path.join(root, fn)
# arcname keeps the leading 'bal/' package prefix
arc = os.path.join(
"bal", os.path.relpath(full, bal_dir)
)
z.write(full, arc)
# electrum must stay importable in the child, but 'bal' must resolve ONLY
# from the zip. So: drop the on-disk repo (and this package's dir) from the
# child's path, prepend the zip, and run from a neutral working directory.
repo_real = os.path.realpath(_REPO)
bal_parent_real = os.path.realpath(os.path.dirname(bal_dir))
filtered = [
p for p in sys.path
if p and os.path.realpath(p) not in (repo_real, bal_parent_real)
]
child_path = os.pathsep.join([str(zip_path)] + filtered)
env = dict(os.environ, PYTHONPATH=child_path, QT_QPA_PLATFORM="offscreen")
code = (
"import bal, os;"
"assert 'bal_plugin.zip' in bal.__file__.replace(os.sep, '/'),"
" 'bal not loaded from zip: ' + bal.__file__;"
"from bal.core.plugin_base import get_version;"
"print(get_version())"
)
out = subprocess.run(
[sys.executable, "-c", code], env=env, capture_output=True, text=True,
cwd=str(tmp_path),
)
assert out.returncode == 0, f"child failed: {out.stderr}"
assert out.stdout.strip() == _manifest_version()