A saved run config follows the update: paths into a removed Control, engine or DeepSeek version folder are pointed at the current one (#4891)

Setup writes absolute paths into the run config (cwd and --expert-profile into the Control bundle that ran it, the picture reader and library folders into
an engine package, the DeepSeek package). An update keeps the new Control/engine and the one before, so the second update after setup removed the folder
the config names: the model showed as not installed and a start failed ("Qwen breaks each time Strata is updated").

strata_control/cfg_paths.py is the one resolver: a string that names a file in components/control/<ver>, components/engine/<ver>-<kind> or
engine-dsv41/<ver>-<kind> whose version folder is gone is mapped to the same file in the active Control / the newest engine of its own kind. Paths the
person typed, packs, models, output paths (--expert-profile-save, log) and folders that still exist are left alone.
- profiles.read_run_config applies it in memory, so the doctor, the model list, the launch plan and the host see the current folders.
- runtime.start writes it once into every saved run config, one line in the host log, keeps a caller's expected source digest valid, and stops with the
  file named when it exists in no current version.
Tests: tests/terminal/test_cfg_paths.py (real update_control twice, real runtime.start for the Terminal and the app, DeepSeek); 9 of 11 fail without the wiring.
This commit is contained in:
Niko1221 committed 2026-11-02 08:08:13 +01:00
1 parent 2fb096b938
commit d0d31a4191
4 files changed
+486

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
"""Paths in a saved run config that name a version folder of Strata's own components, and what to do when an update has removed that folder.
Setup writes absolute paths into the run config: ``cwd`` and ``--expert-profile`` into the Control bundle that ran it
(``<components>/control/<version>/...``), the picture reader and library folders into an engine package
(``<components>/engine/<version>-<platform>-<backend>/...``), the DeepSeek engine into ``engine-dsv41/<version>-.../``. An update puts the new version
beside the old one and removes the versions older than the one before, so the second update after a model was set up deleted the folders its config
names: the model showed as "not installed" and a start failed (#4891; "Qwen breaks each time Strata is updated").
One resolver, used by both sides:
* ``resolve_config(cfg)`` (pure; ``profiles.read_run_config`` calls it, so the doctor, the model list, the launch plan and the runtime host see the
current folders without a file being written);
* ``repair(configs)`` (runtime.start calls it for the profile being started and every saved profile): the same, written once into the file, one plain
line for each file it changed.
Only paths in Strata's own folders are touched (``components/control``, ``components/engine``, ``engine-dsv41``), and only when the version folder they
name is GONE; a path the person typed themselves, a model or pack folder, or a version folder that still exists is left exactly as it is. A path whose
file is nowhere in the current version either is not guessed: ``unresolved`` names it.
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
_SEP = r"[\\/]"
_COMPONENT = re.compile(rf"^(?P<head>.*?{_SEP}components{_SEP})(?P<kind>control|engine){_SEP}(?P<ver>[^\\/]+)(?P<rest>(?:{_SEP}.*)?)$", re.S)
_DSV41 = re.compile(rf"^(?P<head>.*?{_SEP}engine-dsv41{_SEP})(?P<ver>[^\\/]+)(?P<rest>(?:{_SEP}.*)?)$", re.S)
_VERSION = re.compile(r"^\d+(?:\.\d+)+")
SKIP_KEYS = ("log",) # paths the engine writes, not reads
SKIP_FLAGS = ("--expert-profile-save",)
def _key(name: str) -> tuple:
head = str(name).split("-")[0]
return tuple(int(x) for x in head.split(".") if x.isdigit()), str(name)
def _kind_of(name: str) -> str:
return str(name).split("-", 1)[1] if "-" in str(name) else ""
def _newest(folder: Path, kind: str | None = None) -> Path | None:
try:
found = [d for d in folder.iterdir() if d.is_dir() and _VERSION.match(d.name) and (kind is None or _kind_of(d.name) == kind)]
except OSError:
return None
return max(found, key=lambda d: _key(d.name), default=None)
def _active_control(control: Path) -> Path | None:
try:
data = json.loads((control / "active.json").read_text(encoding="utf-8-sig"))
folder = control / str(data["folder"])
if folder.is_dir():
return folder
except (OSError, ValueError, KeyError, TypeError):
pass
return _newest(control)
def _components() -> Path:
from strata_control import engine_component as ec
return ec.components_dir()
def resolve_path(value: str) -> tuple[str, bool] | None:
"""For a string that names a file in a version folder of Strata's own that is gone: (the same file in the current version, True) when it is there,
(value, False) when it is nowhere; None when the string is not such a path or its version folder still exists (nothing to do)."""
if not isinstance(value, str) or len(value) < 8 or "\n" in value:
return None
m = _COMPONENT.match(value)
if m:
old = Path(m["head"] + m["kind"] + os.sep + m["ver"])
if old.exists() or not _VERSION.match(m["ver"]):
return None
root = _components() / m["kind"]
current = _active_control(root) if m["kind"] == "control" else _newest(root, _kind_of(m["ver"]))
rest = m["rest"].lstrip("\\/")
else:
m = _DSV41.match(value)
if not m:
return None
old = Path(m["head"] + m["ver"])
if old.exists() or not _VERSION.match(m["ver"]):
return None
current = _newest(Path(m["head"]), _kind_of(m["ver"]))
rest = m["rest"].lstrip("\\/")
if current is None:
return value, False
new = current / rest if rest else current
return (str(new), True) if new.exists() else (value, False)
def _walk(obj, found: list, lost: list, key: str = ""):
if isinstance(obj, str):
if key in SKIP_KEYS:
return obj
res = resolve_path(obj)
if res is None:
return obj
(found if res[1] else lost).append((obj, res[0]))
return res[0]
if isinstance(obj, list):
out, skip = [], False
for item in obj:
out.append(item if skip else _walk(item, found, lost, key))
skip = isinstance(item, str) and item in SKIP_FLAGS
return out
if isinstance(obj, dict):
return {k: _walk(v, found, lost, str(k)) for k, v in obj.items()}
return obj
def resolve_config(cfg: dict) -> tuple[dict, list, list]:
"""(the config with every path of a removed version folder pointed at the current one, [(old, new)], [(old, "") that exist nowhere])."""
found: list = []
lost: list = []
return _walk(cfg, found, lost), found, [(old, "") for old, _ in lost]
def repair(configs) -> tuple[list[str], list[str]]:
"""For a START: every run config in `configs` is resolved and, when a path changed, written once (atomically, nothing else changed).
Returns (one plain line for each file changed, one plain line for each path that exists nowhere). Never raises."""
notes: list[str] = []
problems: list[str] = []
seen = set()
for c in configs:
c = Path(c)
if c in seen:
continue
seen.add(c)
try:
cfg = json.loads(c.read_text(encoding="utf-8-sig"))
except (OSError, ValueError):
continue
if not isinstance(cfg, dict):
continue
new, found, lost = resolve_config(cfg)
for old, _ in lost:
problems.append(f"{c.name}: {old} was removed by an update and no file of that name is in the current version; run setup again for this model")
if not found:
continue
try:
from strata_control import profiles
profiles._atomic(c, json.dumps(new, indent=2).encode("utf-8"))
except (OSError, ValueError):
continue
notes.append(f"{c.name}: {len(found)} path{'s' if len(found) != 1 else ''} named folders of an earlier Strata version that an update removed ("
+ ", ".join(sorted({Path(o).name or o for o, _ in found})[:3]) + "); they now name the current version.")
return notes, problems
+3
View File
@@ -137,6 +137,9 @@ def read_run_config(path: Path) -> dict:
cfg = json.loads(path.read_text(encoding="utf-8-sig"))
except (OSError, ValueError) as exc:
raise ProfileError(f"cannot read run config ({type(exc).__name__})", 5) from None
if isinstance(cfg, dict): # paths into a version folder of Strata's own that an update removed: the current one
from strata_control import cfg_paths
cfg = cfg_paths.resolve_config(cfg)[0]
if not isinstance(cfg, dict) or not isinstance(cfg.get("args"), list) or not all(
isinstance(arg, str) for arg in cfg["args"]
):
+33
View File
@@ -515,6 +515,36 @@ def _check_start_candidate(name: str, expected_profile_revision: str | None,
return plan, digest
def _repair_config_paths(name: str) -> tuple[str | None, str | None]:
"""Before a start: every path of the saved run configs that names a version folder of Strata's own that an update removed (the Control bundle
that ran setup: cwd, --expert-profile; an engine package: the picture reader, library folders; the DeepSeek package) is pointed at the current
version and written once (strata_control/cfg_paths.py; the same resolver the config reader applies in memory). One line of what it did goes to
the profile's host log. Returns (the profile's own run config digest before, after) when it changed that file, for a caller that planned the
start on the old digest. A path that exists in no current version stops the start with the file named."""
try:
from strata_control import cfg_paths, dsv41_paths
profile, _ = profiles.load(name)
own = Path(profile["source_config"])
before = hashlib.sha256(own.read_bytes()).hexdigest()
configs = [own, *[c for c in dsv41_paths.saved_configs() if c != own]]
notes, problems = cfg_paths.repair(configs)
if notes:
_, _, log_path = _paths(name)
if log_path.parent.is_dir():
with open(log_path, "ab") as stream:
for note in notes:
stream.write(f"{time.strftime('%Y-%m-%dT%H:%M:%S')} config: {note}\n".encode("utf-8"))
mine = [p for p in problems if p.startswith(own.name + ":")]
after = hashlib.sha256(own.read_bytes()).hexdigest()
if mine:
raise ProfileError(mine[0], 6)
return (before, after) if after != before else (None, None)
except ProfileError:
raise
except Exception: # noqa: BLE001 - a start never fails because of this
return None, None
def _repair_dsv41_paths(name: str) -> None:
"""Before a start: a saved profile that runs the DeepSeek engine from an engine-dsv41/ folder an update has replaced is pointed at the newest
installed package of its own kind (every saved profile's run config, so the next start of another one is repaired too). Local only: it never
@@ -566,6 +596,9 @@ def start(name: str, *, wait: float = 300, shutdown_timeout: float = 30,
if expected_source_sha256 is not None and (not isinstance(expected_source_sha256, str) or
not re.fullmatch(r"[0-9a-f]{64}", expected_source_sha256)):
raise ProfileError("expected source digest must be 64 lowercase hex characters")
changed_from, changed_to = _repair_config_paths(name)
if changed_from is not None and expected_source_sha256 == changed_from:
expected_source_sha256 = changed_to # (the caller planned on the file as it was; Strata rewrote it, the caller did not)
_repair_dsv41_paths(name)
plan, _ = _check_start_candidate(name, expected_profile_revision, expected_source_sha256)
_engine_preflight(plan)
+298
View File
@@ -0,0 +1,298 @@
"""#4891 ("Qwen breaks each time Strata is updated"): a saved run config names folders of one version of Strata's own components (the Control bundle that
ran setup: cwd, --expert-profile; an engine package: the picture reader, library folders; the DeepSeek package). The update keeps the new Control and
the one before it, so the SECOND update after setup removes the folder the config names; the model then showed as "not installed" and a start failed.
The updates here are real (`updater.run_update(control=True)` unpacks and prunes Control folders; `prune_engines` / `dsv41_paths.prune` remove the
older engine packages); the starts are real `runtime.start` calls with a fake engine, the way the app (with --die-with-pid) and the Terminal start.
"""
from __future__ import annotations
import json
import os
import shutil
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from strata_control import cfg_paths, doctor, dsv41_paths, engine_component as ec, profiles, runtime, updater # noqa: E402
from tests.terminal.test_updater import PLATFORM, UpdaterCase # noqa: E402
WINDOWS = os.name == "nt"
FAKE_ENGINE = ("#!" + sys.executable + "\nimport sys\n"
"print('READY 4096 stop', flush=True)\n"
"for line in sys.stdin:\n"
" if line.strip() == 'QUIT': break\n"
" if line.startswith(('GEN ', 'BGEN ')):\n"
" print('T 97', flush=True)\n"
" print('DONE 1 1 1.0 1.0 length 0 0 0', flush=True)\n")
class StaleConfigPaths(UpdaterCase):
# (UpdaterCase's own tests are not repeated)
def setUp(self):
super().setUp()
self.control = self.components / "control"
self.engine_root = self.components / "engine"
self.ds_root = self.root / "data" / "engine-dsv41"
self.sequence = 0
# Control 0.1.44 is what ran setup: its data/ holds the shipped expert profile
self.install_control("0.1.44")
(self.control / "active.json").write_text(json.dumps({"version": "0.1.44", "folder": "0.1.44", "sha256": "0" * 64}))
# the engine package that setup (or an earlier update) wrote into the config, and the DeepSeek package
self.engine_kind = f"{PLATFORM}-cuda-cuda13"
(self.root / "packs" / "iq3_s").mkdir(parents=True)
self.old_engine = self.make_engine_folder(self.engine_root, "0.1.44")
self.old_ds = self.make_ds_folder("0.1.49")
self.tokenizer = str(self.root / "tokenizer")
self.mine = self.root / "mine" / "my-profile.bin" # a path the person typed themselves
self.configs = {}
# ------------------------------------------------------------------ the world
def install_control(self, version):
folder = self.control / version
(folder / "data").mkdir(parents=True)
(folder / "strata.py").write_text(f"print('strata {version} (python stub)')\n")
(folder / "data" / "expert-profile.bin").write_bytes(b"profile-" + version.encode())
return folder
def make_engine_folder(self, root, version, script_name=None):
folder = root / f"{version}-{self.engine_kind}"
folder.mkdir(parents=True)
name = script_name or ("engine.cmd" if WINDOWS else "engine.py")
script = folder / "engine.py"
script.write_text(FAKE_ENGINE, encoding="utf-8")
script.chmod(0o700)
if WINDOWS:
(folder / "engine.cmd").write_bytes(f'@echo off\r\n"{sys.executable}" "%~dp0engine.py" %*\r\n'.encode())
(folder / "strata-vision").write_bytes(b"reader " + version.encode())
return folder
def make_ds_folder(self, version):
folder = self.ds_root / f"{version}-{self.engine_kind}"
folder.mkdir(parents=True)
(folder / dsv41_paths.exe_name()).write_bytes(b"deepseek " + version.encode())
(folder / dsv41_paths.exe_name()).chmod(0o755)
return folder
def engine_exe(self, folder):
return folder / ("engine.cmd" if WINDOWS else "engine.py")
def publish_control(self, version):
bundle = self.root / f"bundle-{version}"
(bundle / "strata_control").mkdir(parents=True)
(bundle / "data").mkdir()
(bundle / "data" / "expert-profile.bin").write_bytes(b"profile-" + version.encode())
(bundle / "strata.py").write_text(f"print('strata {version} (python stub)')\n")
(bundle / "strata_control" / "__init__.py").write_text("")
from feed_tool import build_feed, pack_control
shutil.rmtree(self.release, ignore_errors=True)
(self.release / "control").mkdir(parents=True)
pack_control(bundle, self.release / "control", version=version, platform=PLATFORM)
self.sequence += 1
build_feed(self.release, key=self.key, sequence=self.sequence, control_dir=self.release / "control")
def update_to(self, version):
self.publish_control(version)
out = updater.run_update(self.ctx(), engine=False, control=True)
self.assertEqual(out["results"][0]["status"], "updated", out)
def two_updates(self):
"""Setup ran on Control 0.1.44. Two updates later (0.1.45, then 0.1.46) Control keeps 0.1.46 and 0.1.45, and the engine and DeepSeek packages
are replaced the way their updates do it."""
self.update_to("0.1.45")
self.assertTrue((self.control / "0.1.44").is_dir(), "one update keeps the folder before it")
self.update_to("0.1.46")
self.assertFalse((self.control / "0.1.44").exists(), "the second update removes the folder setup wrote into the config")
new_engine = self.make_engine_folder(self.engine_root, "0.1.46")
updater.prune_engines(self.components, {new_engine.name})
self.assertFalse(self.old_engine.exists())
new_ds = self.make_ds_folder("0.1.50")
with mock.patch.object(dsv41_paths, "in_use", return_value=False):
dsv41_paths.prune(self.ds_root, new_ds / dsv41_paths.exe_name())
self.assertFalse(self.old_ds.exists())
return new_engine, new_ds
def qwen_config(self, name, vision=False, **extra):
cfg = {"exe": str(self.engine_exe(self.old_engine)), "backend": "cuda", "tokenizer": self.tokenizer,
"cwd": str(self.control / "0.1.44"), "model_name": "fake", "host": "127.0.0.1", "port": 0,
"lib_dirs": [str(self.old_engine)],
"args": ["--pack", str(self.root / "packs" / "iq3_s"),
"--expert-profile", str(self.control / "0.1.44" / "data" / "expert-profile.bin"),
"--expert-profile-save", str(self.control / "0.1.44" / "data" / "learned.bin"),
"--mtp", "2", "--lookup-file", str(self.mine)]}
if vision: # (a real start would run the picture reader: only the read-side tests ask for it)
cfg["vision"] = {"exe": str(self.old_engine / "strata-vision")}
cfg.update(extra)
path = self.root / "run" / f"{name}.json"
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
profiles.create(name, str(path))
self.configs[name] = path
return path
def deepseek_config(self, name):
exe = self.old_ds / dsv41_paths.exe_name()
cfg = {"exe": str(exe), "arch": "deepseek41", "backend": "cuda", "tokenizer": self.tokenizer, "cwd": str(self.control / "0.1.44"),
"model_name": "deepseek", "host": "127.0.0.1", "port": 0, "lib_dirs": [str(self.old_ds)],
"args": ["--pack", str(self.root / "packs" / "ds"), "--max-context", "8192"]}
path = self.root / "run" / f"{name}.json"
path.parent.mkdir(exist_ok=True)
path.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
profiles.create(name, str(path))
self.configs[name] = path
return path
def cfg_of(self, path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def without_output_path(self, path):
"""The config's text without the one path the engine WRITES (--expert-profile-save), which is deliberately not looked for."""
return Path(path).read_text(encoding="utf-8").replace(json.dumps(str(self.control / "0.1.44" / "data" / "learned.bin"))[1:-1], "")
def host_log_lines(self, name, word="config:"):
_, _, log_path = runtime._paths(name)
return [l for l in log_path.read_text(encoding="utf-8").splitlines() if word in l] if log_path.exists() else []
def stop(self, name):
try:
if runtime._read_state(name) is not None:
runtime.stop(name, timeout=12)
except Exception: # noqa: BLE001
pass
# ------------------------------------------------------------------ what an update does
def test_the_folders_the_config_names_are_gone_after_two_updates_and_the_model_looks_missing(self):
path = self.qwen_config("qwen", vision=True)
self.two_updates()
cfg = self.cfg_of(path)
self.assertFalse(Path(cfg["cwd"]).exists())
self.assertFalse(Path(cfg["args"][cfg["args"].index("--expert-profile") + 1]).exists())
report = doctor.inspect(path)
stale = [c for c in report["components"] if c["status"] != "ok" and c["name"] in ("expert-profile", "vision/exe", "engine")]
self.assertEqual(stale, [], "the doctor (and so the model list) must follow the current folders: " + json.dumps(stale))
def test_reading_the_config_follows_the_current_version_in_memory_and_writes_nothing(self):
path = self.qwen_config("qwen", vision=True)
new_engine, _ = self.two_updates()
before = path.read_bytes()
cfg = profiles.read_run_config(path)
active = self.control / "0.1.46"
self.assertEqual(Path(cfg["cwd"]), active)
self.assertEqual(Path(cfg["args"][cfg["args"].index("--expert-profile") + 1]), active / "data" / "expert-profile.bin")
self.assertEqual(Path(cfg["vision"]["exe"]), new_engine / "strata-vision")
self.assertEqual([Path(d) for d in cfg["lib_dirs"]], [new_engine])
self.assertEqual(Path(cfg["exe"]), self.engine_exe(new_engine))
self.assertEqual(path.read_bytes(), before)
def test_the_paths_the_person_typed_and_the_data_are_left_alone(self):
path = self.qwen_config("qwen")
self.two_updates()
cfg = profiles.read_run_config(path)
args = cfg["args"]
self.assertEqual(args[args.index("--pack") + 1], str(self.root / "packs" / "iq3_s"))
self.assertEqual(args[args.index("--lookup-file") + 1], str(self.mine)) # (typed, outside Strata's folders, does not exist: not touched)
self.assertEqual(args[args.index("--mtp") + 1], "2")
self.assertEqual(args[args.index("--expert-profile-save") + 1], str(self.control / "0.1.44" / "data" / "learned.bin"),
"a path the engine writes is not a file to look for")
self.assertEqual(cfg["tokenizer"], self.tokenizer)
self.assertEqual(cfg["model_name"], "fake")
def test_a_folder_that_still_exists_is_not_replaced_by_a_newer_one(self):
path = self.qwen_config("qwen")
self.update_to("0.1.45") # 0.1.44 is still there (the version before the newest)
before = path.read_bytes()
cfg = profiles.read_run_config(path)
self.assertEqual(cfg["cwd"], str(self.control / "0.1.44"))
self.assertEqual(cfg_paths.repair([path]), ([], []))
self.assertEqual(path.read_bytes(), before)
# ------------------------------------------------------------------ starts
def test_a_terminal_start_rewrites_the_config_once_and_says_so_in_one_line(self):
path = self.qwen_config("term")
new_engine, _ = self.two_updates()
self.addCleanup(self.stop, "term")
started = runtime.start("term", wait=15)
self.assertTrue(started["ok"], started)
cfg = self.cfg_of(path)
self.assertEqual(cfg["cwd"], str(self.control / "0.1.46"))
self.assertEqual(cfg["exe"], str(self.engine_exe(new_engine)))
self.assertNotIn("0.1.44", self.without_output_path(path))
self.assertEqual(len(self.host_log_lines("term")), 1, self.host_log_lines("term"))
self.assertIn("earlier Strata version", self.host_log_lines("term")[0])
runtime.stop("term", timeout=12)
again = runtime.start("term", wait=15)
self.assertTrue(again["ok"], again)
self.assertEqual(len(self.host_log_lines("term")), 1, "nothing is rewritten the second time")
def test_an_app_start_with_die_with_pid_does_the_same(self):
path = self.qwen_config("app-1")
self.two_updates()
self.addCleanup(self.stop, "app-1")
started = runtime.start("app-1", wait=15, die_with_pid=os.getpid())
self.assertTrue(started["ok"], started)
self.assertEqual(self.cfg_of(path)["cwd"], str(self.control / "0.1.46"))
def test_a_start_planned_on_the_old_file_is_not_refused_because_strata_rewrote_it(self):
import hashlib
path = self.qwen_config("app-2")
self.two_updates()
self.addCleanup(self.stop, "app-2")
planned = hashlib.sha256(path.read_bytes()).hexdigest()
started = runtime.start("app-2", wait=15, expected_source_sha256=planned)
self.assertTrue(started["ok"], started)
def test_every_saved_profile_is_repaired_by_one_start(self):
a, b = self.qwen_config("one"), self.qwen_config("two")
self.two_updates()
self.addCleanup(self.stop, "one")
self.assertTrue(runtime.start("one", wait=15)["ok"])
self.assertNotIn("0.1.44", self.without_output_path(b), "the Models page reads the other profiles' configs too")
def test_deepseek_cwd_exe_and_libraries_follow_the_update_too(self):
path = self.deepseek_config("ds")
_, new_ds = self.two_updates()
report = doctor.inspect(path)
self.assertEqual([c for c in report["components"] if c["name"] == "engine" and c["status"] != "ok"], [])
notes, problems = cfg_paths.repair([path])
self.assertEqual(problems, [])
self.assertEqual(len(notes), 1)
cfg = self.cfg_of(path)
self.assertEqual(cfg["exe"], str(new_ds / dsv41_paths.exe_name()))
self.assertEqual(cfg["lib_dirs"], [str(new_ds)])
self.assertEqual(cfg["cwd"], str(self.control / "0.1.46"))
self.assertEqual(cfg["args"], ["--pack", str(self.root / "packs" / "ds"), "--max-context", "8192"])
def test_deepseek_start_goes_through_the_same_repair(self):
path = self.deepseek_config("ds-app")
self.two_updates()
with mock.patch.object(runtime, "_check_start_candidate", side_effect=RuntimeError("stop after the repair")), \
mock.patch.object(runtime, "_engine_preflight"):
with self.assertRaises(RuntimeError):
runtime.start("ds-app", wait=1, die_with_pid=os.getpid())
cfg = self.cfg_of(path)
self.assertIn("0.1.50", cfg["exe"])
self.assertEqual(cfg["cwd"], str(self.control / "0.1.46"))
# ------------------------------------------------------------------ a file that is nowhere
def test_a_file_the_new_version_no_longer_has_stops_the_start_with_its_name(self):
path = self.qwen_config("lost")
self.two_updates()
(self.control / "0.1.46" / "data" / "expert-profile.bin").unlink()
(self.control / "0.1.45" / "data" / "expert-profile.bin").unlink()
with self.assertRaises(profiles.ProfileError) as caught:
runtime.start("lost", wait=5)
text = str(caught.exception)
self.assertIn("expert-profile.bin", text)
self.assertIn("removed by an update", text)
self.assertIn("run setup again", text)
self.assertIn("0.1.44", path.read_text(encoding="utf-8"), "nothing is guessed: the file is left as it was")
if __name__ == "__main__":
unittest.main()