Update Healer Man server

This commit is contained in:
phenom committed 2026-09-19 14:33:15 -04:00
1 parent a05a79eb83
commit f8314e7de1
14 files changed
+529 -108

No files matched your search

+15 -9
View File
@@ -41,17 +41,23 @@ def vite_node_command() -> Path:
return executable
def runtime_session_command(source: str, level: int) -> list[str]:
return [
str(vite_node_command()),
"--mode",
"balance-lab",
str(RUNTIME),
"session",
"--source",
source,
"--level",
str(level),
]
class RuntimeSession:
def __init__(self, source: str, level: int, workspace: Path) -> None:
command = [
str(vite_node_command()),
str(RUNTIME),
"session",
"--source",
source,
"--level",
str(level),
]
command = runtime_session_command(source, level)
self.process = subprocess.Popen(
command,
cwd=ROOT,
+327 -72
View File
@@ -1,4 +1,4 @@
"""Visual release manager for Healer Man with explicit safety gates.
"""Visual Git, TrueNAS, and optional APK release manager with safety gates.
The GUI intentionally leaves the TrueNAS portion as copyable shell commands:
the app must be stopped and started from the TrueNAS Apps UI around that block.
@@ -71,6 +71,8 @@ OVERRIDABLE_TEST_LABELS = frozenset(
ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
FAILURE_OUTPUT_LINE_LIMIT = 160
FAILURE_OUTPUT_CHARACTER_LIMIT = 20_000
DEFAULT_GIT_TRUENAS_ONLY = True
DEFAULT_RUN_TESTS = True
class ReleaseError(RuntimeError):
@@ -104,15 +106,27 @@ class ReleaseVersion:
version_code: int
def full_release_checks(npm: str) -> tuple[tuple[list[str], str], ...]:
"""Commands that must succeed before a release can be staged."""
return (
def full_release_checks(
npm: str,
*,
include_android: bool = True,
include_tests: bool = True,
) -> tuple[tuple[list[str], str], ...]:
"""Commands that must succeed before an update can be staged."""
test_checks = (
([npm, "run", "content:test"], "Content tests"),
([npm, "run", "server:test"], "Server tests"),
([npm, "test"], "Application tests"),
([npm, "run", "test:shelved"], "Shelved-mode application tests"),
)
required_checks = (
([npm, "run", "content:publish"], "Publish verified 54-dungeon content manifest"),
([npm, "run", "build"], "Production build"),
)
checks = (test_checks if include_tests else ()) + required_checks
if not include_android:
return checks
return checks + (
([npm, "run", "android:sync"], "Android web build and Capacitor sync"),
)
@@ -645,7 +659,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
)
ttk.Label(
title,
text="Validate → inspect → commit → push → publish signed APK",
text="Validate → inspect → commit → push → deploy, with optional signed APK",
style="Sub.TLabel",
).grid(row=1, column=0, sticky="w", pady=(2, 0))
self.refresh_button = ttk.Button(title, text="Refresh", command=self.refresh)
@@ -665,6 +679,12 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.next_version_var = tk.StringVar()
self.next_code_var = tk.StringVar()
self.commit_message_var = tk.StringVar()
self.git_truenas_only_var = tk.BooleanVar(
value=DEFAULT_GIT_TRUENAS_ONLY
)
self.run_tests_var = tk.BooleanVar(value=DEFAULT_RUN_TESTS)
self.release_mode_hint_var = tk.StringVar()
self.test_mode_hint_var = tk.StringVar()
ttk.Label(release_panel, text="Current", style="Muted.Panel.TLabel").grid(
row=1, column=0, sticky="w"
@@ -682,25 +702,31 @@ def launch_gui(*, smoke_test: bool = False) -> int:
ttk.Label(release_panel, text="Bump", style="Muted.Panel.TLabel").grid(
row=2, column=0, sticky="w", pady=(12, 0)
)
bump_box = ttk.Combobox(
self.bump_box = ttk.Combobox(
release_panel,
textvariable=self.bump_var,
values=("Patch", "Minor", "Major"),
state="readonly",
width=10,
)
bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(12, 0))
bump_box.bind("<<ComboboxSelected>>", lambda _event: self._suggest_versions())
self.bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(12, 0))
self.bump_box.bind("<<ComboboxSelected>>", lambda _event: self._suggest_versions())
ttk.Label(release_panel, text="Next version", style="Muted.Panel.TLabel").grid(
row=3, column=0, sticky="w", pady=(9, 0)
)
ttk.Entry(release_panel, textvariable=self.next_version_var, width=15).grid(
self.next_version_entry = ttk.Entry(
release_panel, textvariable=self.next_version_var, width=15
)
self.next_version_entry.grid(
row=3, column=1, sticky="ew", padx=(8, 20), pady=(9, 0)
)
ttk.Label(release_panel, text="Next code", style="Muted.Panel.TLabel").grid(
row=3, column=2, sticky="w", pady=(9, 0)
)
ttk.Entry(release_panel, textvariable=self.next_code_var, width=12).grid(
self.next_code_entry = ttk.Entry(
release_panel, textvariable=self.next_code_var, width=12
)
self.next_code_entry.grid(
row=3, column=3, sticky="ew", padx=(8, 0), pady=(9, 0)
)
ttk.Label(release_panel, text="Commit message", style="Muted.Panel.TLabel").grid(
@@ -709,7 +735,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
ttk.Entry(release_panel, textvariable=self.commit_message_var).grid(
row=4, column=1, columnspan=3, sticky="ew", padx=(8, 0), pady=(9, 0)
)
ttk.Label(release_panel, text="Release notes", style="Muted.Panel.TLabel").grid(
ttk.Label(release_panel, text="Release notes (APK only)", style="Muted.Panel.TLabel").grid(
row=5, column=0, sticky="nw", pady=(9, 0)
)
self.release_notes = tk.Text(
@@ -727,12 +753,42 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.release_notes.grid(
row=5, column=1, columnspan=3, sticky="nsew", padx=(8, 0), pady=(9, 0)
)
self.release_mode_toggle = ttk.Checkbutton(
release_panel,
text="Git + TrueNAS only (skip Android version bump and APK publication)",
variable=self.git_truenas_only_var,
command=self._update_release_mode,
)
self.release_mode_toggle.grid(
row=6, column=0, columnspan=4, sticky="w", pady=(12, 0)
)
ttk.Label(
release_panel,
textvariable=self.release_mode_hint_var,
style="Muted.Panel.TLabel",
wraplength=610,
).grid(row=7, column=0, columnspan=4, sticky="w", pady=(4, 0))
self.run_tests_toggle = ttk.Checkbutton(
release_panel,
text="Run test suites (recommended)",
variable=self.run_tests_var,
command=self._update_test_mode,
)
self.run_tests_toggle.grid(
row=8, column=0, columnspan=4, sticky="w", pady=(10, 0)
)
ttk.Label(
release_panel,
textvariable=self.test_mode_hint_var,
style="Muted.Panel.TLabel",
wraplength=610,
).grid(row=9, column=0, columnspan=4, sticky="w", pady=(4, 0))
secrets_panel = ttk.Frame(shell, style="Panel.TFrame", padding=15)
secrets_panel.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
secrets_panel.columnconfigure(1, weight=1)
ttk.Label(
secrets_panel, text="Signing and Gitea", style="Panel.TLabel",
secrets_panel, text="Signing and Gitea (APK only)", style="Panel.TLabel",
font=("Segoe UI Semibold", 12),
).grid(row=0, column=0, columnspan=3, sticky="w")
ttk.Label(
@@ -869,6 +925,54 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.retry_button.grid(row=0, column=1, padx=7)
self.cancel_button = ttk.Button(actions, text="Cancel", command=self.cancel, state="disabled")
self.cancel_button.grid(row=0, column=2, padx=(7, 0))
self._update_release_mode()
self._update_test_mode()
def _update_release_mode(self) -> None:
git_truenas_only = self.git_truenas_only_var.get()
self.bump_box.configure(state="disabled" if git_truenas_only else "readonly")
entry_state = "disabled" if git_truenas_only else "normal"
self.next_version_entry.configure(state=entry_state)
self.next_code_entry.configure(state=entry_state)
if git_truenas_only:
self.release_button.configure(text="Validate and Push Git + TrueNAS Update")
self.release_mode_hint_var.set(
"Default mode: keeps the current Android version, skips Android sync and APK "
"publication, and prepares the pinned TrueNAS deployment commands after push."
)
else:
self.release_button.configure(text="Validate, Push, and Publish APK")
self.release_mode_hint_var.set(
"APK mode: bumps the app and Android versions, runs Android sync, and publishes "
"the signed APK after the Git push."
)
self._suggest_versions()
current_notes = self.release_notes.get("1.0", "end").strip()
server_default = "Healer Man Git and TrueNAS server update."
apk_default = (
f"Healer Man {self.next_version_var.get()} app and Android update."
)
generated_apk_notes = re.fullmatch(
r"Healer Man \d+\.\d+\.\d+ app and Android update\.",
current_notes,
)
if git_truenas_only and (not current_notes or generated_apk_notes):
self._set_text(self.release_notes, server_default)
elif not git_truenas_only and (
not current_notes or current_notes == server_default
):
self._set_text(self.release_notes, apk_default)
def _update_test_mode(self) -> None:
if self.run_tests_var.get():
self.test_mode_hint_var.set(
"Runs content, server, normal-application, and shelved-application tests."
)
else:
self.test_mode_hint_var.set(
"Skips those four test suites. Candidate safety checks, content publication, "
"the production build, and staged-file review still run."
)
def _command_text_widget(self, notebook: ttk.Notebook) -> tk.Text:
frame = ttk.Frame(notebook)
@@ -938,7 +1042,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.last_commit = current_commit()
self.current_version_var.set(package_version)
self.current_code_var.set(str(release_version.version_code))
self._suggest_versions()
self._update_release_mode()
change_count = len(
[line for line in status.splitlines() if not line.startswith("##")]
)
@@ -956,10 +1060,6 @@ def launch_gui(*, smoke_test: bool = False) -> int:
generate_truenas_verify_commands(),
readonly=True,
)
if not self.release_notes.get("1.0", "end").strip():
self.release_notes.insert(
"1.0", f"Healer Man {self.next_version_var.get()} app and Android update."
)
self.status_var.set("Ready")
except Exception as exc:
self.status_var.set("Configuration error")
@@ -973,7 +1073,10 @@ def launch_gui(*, smoke_test: bool = False) -> int:
next_code = suggest_version_code(next_version, release.version_code)
self.next_version_var.set(next_version)
self.next_code_var.set(str(next_code))
self.commit_message_var.set(f"Release Healer Man {next_version}")
if self.git_truenas_only_var.get():
self.commit_message_var.set("Update Healer Man server")
else:
self.commit_message_var.set(f"Release Healer Man {next_version}")
except Exception as exc:
self.status_var.set(str(exc))
@@ -1037,32 +1140,84 @@ def launch_gui(*, smoke_test: bool = False) -> int:
def start_full_release(self) -> None:
if self.busy:
return
publish_apk = not self.git_truenas_only_var.get()
run_tests = self.run_tests_var.get()
test_summary = (
"The four test suites will run."
if run_tests
else "The four test suites will be skipped; safety checks and the production build still run."
)
try:
version = self._requested_version()
credentials = self._credentials()
toolchain_ready, toolchain_detail = android_toolchain_status()
if not toolchain_ready:
raise ReleaseError(toolchain_detail)
commit_message = self.commit_message_var.get().strip()
notes = self.release_notes.get("1.0", "end").strip()
if not commit_message:
raise ReleaseError("Commit message is required.")
if not notes:
raise ReleaseError("Release notes are required.")
version: ReleaseVersion | None = None
credentials: dict[str, str] | None = None
notes = ""
if publish_apk:
version = self._requested_version()
credentials = self._credentials()
toolchain_ready, toolchain_detail = android_toolchain_status()
if not toolchain_ready:
raise ReleaseError(toolchain_detail)
notes = self.release_notes.get("1.0", "end").strip()
if not notes:
raise ReleaseError("Release notes are required.")
except Exception as exc:
messagebox.showerror("Cannot start release", str(exc), parent=self.root)
return
if not publish_apk:
if not messagebox.askyesno(
"Start Git + TrueNAS update?",
"Run the server validation and production build, review and commit the exact "
"changed files, then push main?\n\nThe Android version will not change and no "
"APK will be built or published. The Deploy tab will be pinned to the new commit.\n\n"
+ test_summary,
parent=self.root,
):
return
self._begin_worker(
"Validating Git + TrueNAS update",
lambda: self._full_release_worker(
None,
commit_message,
"",
None,
publish_apk=False,
run_tests=run_tests,
),
(
"Git main is published without changing the Android version or publishing "
"an APK.\n\nUse the TrueNAS Deploy tab after stopping the app, then the "
"Verify tab after restarting it."
),
)
return
assert version is not None
assert credentials is not None
if not messagebox.askyesno(
"Start full release?",
f"Prepare Healer Man {version.version_name} (Android code {version.version_code})?\n\n"
"The app will bump versions, run all release checks, then show the exact staged files "
"before it commits, pushes main, and publishes the signed APK.",
"before it commits, pushes main, and publishes the signed APK.\n\n"
+ test_summary,
parent=self.root,
):
return
self._begin_worker(
"Validating release",
lambda: self._full_release_worker(version, commit_message, notes, credentials),
lambda: self._full_release_worker(
version,
commit_message,
notes,
credentials,
publish_apk=True,
run_tests=run_tests,
),
(
"Git main and the signed APK release are published.\n\nUse the TrueNAS Deploy "
"tab after stopping the app, then the Verify tab after restarting it."
),
)
def start_apk_only(self) -> None:
@@ -1095,9 +1250,18 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self._begin_worker(
"Publishing current APK",
lambda: self._apk_only_worker(release_version, notes, credentials),
(
"The signed APK release is published for the current Git commit.\n\nUse the "
"TrueNAS Deploy tab only if that commit is not already deployed."
),
)
def _begin_worker(self, label: str, target: Callable[[], None]) -> None:
def _begin_worker(
self,
label: str,
target: Callable[[], None],
success_message: str,
) -> None:
self.busy = True
self.cancel_event.clear()
self.status_var.set(label)
@@ -1105,11 +1269,21 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.release_button.configure(state="disabled")
self.retry_button.configure(state="disabled")
self.refresh_button.configure(state="disabled")
self.release_mode_toggle.configure(state="disabled")
self.run_tests_toggle.configure(state="disabled")
self.cancel_button.configure(state="normal")
self._append_log(f"\n=== {label} ===")
threading.Thread(target=self._worker_wrapper, args=(target,), daemon=True).start()
threading.Thread(
target=self._worker_wrapper,
args=(target, success_message),
daemon=True,
).start()
def _worker_wrapper(self, target: Callable[[], None]) -> None:
def _worker_wrapper(
self,
target: Callable[[], None],
success_message: str,
) -> None:
try:
target()
except ReleaseCancelled as exc:
@@ -1117,7 +1291,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
except Exception as exc:
self.events.put(("done", "error", str(exc)))
else:
self.events.put(("done", "success", "Release workflow completed."))
self.events.put(("done", "success", success_message))
def _preflight_git(self, *, require_clean: bool) -> None:
if git_output("branch", "--show-current") != "main":
@@ -1137,10 +1311,13 @@ def launch_gui(*, smoke_test: bool = False) -> int:
def _full_release_worker(
self,
version: ReleaseVersion,
version: ReleaseVersion | None,
commit_message: str,
notes: str,
credentials: dict[str, str],
credentials: dict[str, str] | None,
*,
publish_apk: bool,
run_tests: bool,
) -> None:
self._preflight_git(require_clean=False)
self._log_event(
@@ -1153,11 +1330,21 @@ def launch_gui(*, smoke_test: bool = False) -> int:
f"({len(candidate_paths)} changed path(s), including untracked files)"
)
npm = shutil.which("npm")
powershell = shutil.which("powershell.exe") or shutil.which("powershell")
if not npm or not powershell:
raise ReleaseError("npm and Windows PowerShell must be available on PATH.")
if not npm:
raise ReleaseError("npm must be available on PATH.")
powershell = None
if publish_apk:
if version is None or credentials is None:
raise ReleaseError("APK publication requires version and signing details.")
powershell = shutil.which("powershell.exe") or shutil.which("powershell")
if not powershell:
raise ReleaseError("Windows PowerShell must be available on PATH.")
managed_files = (PACKAGE_FILE, PACKAGE_LOCK_FILE, RELEASE_VERSION_FILE)
managed_files = (
(PACKAGE_FILE, PACKAGE_LOCK_FILE, RELEASE_VERSION_FILE)
if publish_apk
else ()
)
backups = {
path: path.read_bytes() if path.exists() else None for path in managed_files
}
@@ -1165,16 +1352,32 @@ def launch_gui(*, smoke_test: bool = False) -> int:
committed = False
base_commit = current_commit()
try:
self._run_command(
[npm, "version", version.version_name, "--no-git-tag-version"],
"Bump package version",
)
write_release_version(version)
self._log_event(
f"Recorded Android version {version.version_name} / {version.version_code}."
)
if publish_apk:
assert version is not None
self._run_command(
[npm, "version", version.version_name, "--no-git-tag-version"],
"Bump package version",
)
write_release_version(version)
self._log_event(
f"Recorded Android version {version.version_name} / {version.version_code}."
)
else:
self._log_event(
"Git + TrueNAS mode: keeping the current Android version and "
"skipping APK publication."
)
if not run_tests:
self._log_event(
"⚠ Test suites disabled: skipping content, server, normal-application, "
"and shelved-application tests."
)
for command, label in full_release_checks(npm):
for command, label in full_release_checks(
npm,
include_android=publish_apk,
include_tests=run_tests,
):
try:
self._run_command(command, label)
except ReleaseCommandError as failure:
@@ -1221,15 +1424,49 @@ def launch_gui(*, smoke_test: bool = False) -> int:
if not staged_paths:
raise ReleaseError("There are no staged changes to release.")
stat = git_output("diff", "--cached", "--stat")
if publish_apk:
assert version is not None
review_heading = (
f"Version: {version.version_name}\n"
f"Android code: {version.version_code}\n"
)
review_question = "Commit, push main, and publish the APK?"
approval_title = "Review staged release"
approval_heading = "Inspect every staged path before publishing"
approve_label = "Commit, Push, and Publish"
cancel_label = "Cancel release"
else:
current_android = read_release_version()
review_heading = (
"Mode: Git + TrueNAS only\n"
f"Android version remains: {current_android.version_name} "
f"(code {current_android.version_code})\n"
)
review_question = (
"Commit and push main without publishing an APK? The TrueNAS Deploy tab "
"will be pinned to the new commit."
)
approval_title = "Review staged Git + TrueNAS update"
approval_heading = "Inspect every staged path before pushing"
approve_label = "Commit and Push"
cancel_label = "Cancel update"
review_heading += (
f"Test suites: {'run' if run_tests else 'SKIPPED'}\n"
)
review = (
f"Version: {version.version_name}\n"
f"Android code: {version.version_code}\n"
f"Commit: {commit_message}\n\n"
review_heading
+ f"Commit: {commit_message}\n\n"
f"Staged paths ({len(staged_paths)}):\n"
+ "\n".join(staged_paths)
+ f"\n\nDiff summary:\n{stat}\n\nCommit, push main, and publish the APK?"
+ f"\n\nDiff summary:\n{stat}\n\n{review_question}"
)
approved = self._request_approval(
review,
title=approval_title,
heading=approval_heading,
approve_label=approve_label,
cancel_label=cancel_label,
)
approved = self._request_approval(review)
if not approved:
raise ReleaseCancelled("Release stopped at the staged-file review.")
@@ -1244,7 +1481,11 @@ def launch_gui(*, smoke_test: bool = False) -> int:
f"Remote main is {remote_commit or 'unknown'}, expected {release_commit}."
)
self.events.put(("commit", release_commit))
self._publish_apk(powershell, version, notes, credentials)
if publish_apk:
assert powershell is not None
assert version is not None
assert credentials is not None
self._publish_apk(powershell, version, notes, credentials)
except Exception:
if current_commit() != base_commit:
committed = True
@@ -1257,18 +1498,31 @@ def launch_gui(*, smoke_test: bool = False) -> int:
stderr=subprocess.DEVNULL,
check=False,
)
for path, content in backups.items():
if content is None:
if path.exists():
path.unlink()
else:
path.write_bytes(content)
self._log_event("Restored version files because no release commit was created.")
if publish_apk:
for path, content in backups.items():
if content is None:
if path.exists():
path.unlink()
else:
path.write_bytes(content)
self._log_event(
"Restored version files because no release commit was created."
)
else:
self._log_event(
"No update commit was created; reviewed files were left in the working tree."
)
else:
self._log_event(
"The release commit exists. Version files were kept; use "
"'Publish Current APK Only' after correcting the publication problem."
)
if publish_apk:
self._log_event(
"The release commit exists. Version files were kept; use "
"'Publish Current APK Only' after correcting the publication problem."
)
else:
self._log_event(
"The update commit exists locally; resolve the push problem without "
"rewriting the commit."
)
raise
def _apk_only_worker(
@@ -1519,27 +1773,28 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.release_button.configure(state="normal")
self.retry_button.configure(state="normal")
self.refresh_button.configure(state="normal")
self.release_mode_toggle.configure(state="normal")
self.run_tests_toggle.configure(state="normal")
self.cancel_button.configure(state="disabled")
self.keystore_password_var.set("")
self.key_password_var.set("")
if self.remember_token_var.get() and SAVED_GITEA_TOKEN_FILE.is_file():
self.token_var.set(read_saved_gitea_token())
if result == "success":
self.status_var.set("Release completed")
self._append_log("\n✓ Release workflow completed successfully.")
self.status_var.set("Workflow completed")
self._append_log("\n✓ Workflow completed successfully.")
messagebox.showinfo(
"Release complete",
"Git main and the signed APK release are published.\n\n"
"Use the TrueNAS Deploy tab after stopping the app, then the Verify tab after restarting it.",
"Workflow complete",
detail,
parent=self.root,
)
elif result == "cancelled":
self.status_var.set("Release cancelled")
self.status_var.set("Workflow cancelled")
self._append_log(f"\nCancelled: {detail}")
else:
self.status_var.set("Release failed")
self.status_var.set("Workflow failed")
self._append_log(f"\nERROR: {detail}")
messagebox.showerror("Release failed", detail, parent=self.root)
messagebox.showerror("Workflow failed", detail, parent=self.root)
self.refresh()
def _close(self) -> None:
+26
View File
@@ -22,6 +22,10 @@ class VersionTests(unittest.TestCase):
class SafetyTests(unittest.TestCase):
def test_safe_update_options_are_enabled_by_default(self):
self.assertTrue(release_manager.DEFAULT_GIT_TRUENAS_ONLY)
self.assertTrue(release_manager.DEFAULT_RUN_TESTS)
def test_detects_outputs_and_secrets(self):
paths = [
"src/game/example.ts",
@@ -54,6 +58,28 @@ class SafetyTests(unittest.TestCase):
self.assertLess(publish_index, commands.index(["npm", "run", "build"]))
self.assertLess(publish_index, commands.index(["npm", "run", "android:sync"]))
def test_git_and_truenas_checks_skip_only_the_android_stage(self):
checks = release_manager.full_release_checks("npm", include_android=False)
commands = [command for command, _label in checks]
self.assertIn(["npm", "run", "content:publish"], commands)
self.assertIn(["npm", "run", "build"], commands)
self.assertNotIn(["npm", "run", "android:sync"], commands)
def test_disabled_test_suites_keep_safety_and_build_checks(self):
checks = release_manager.full_release_checks(
"npm",
include_android=False,
include_tests=False,
)
commands = [command for command, _label in checks]
self.assertEqual(
commands,
[
["npm", "run", "content:publish"],
["npm", "run", "build"],
],
)
def test_only_test_failures_may_be_overridden(self):
checks = release_manager.full_release_checks("npm")
overridable = {
+6
View File
@@ -6,12 +6,18 @@ from scripts.balance_lab import (
automatic_parties,
coverage_parties,
random_party,
runtime_session_command,
search_bosses,
tuning_label,
)
class BalanceLabControllerTests(unittest.TestCase):
def test_headless_runtime_uses_the_no_watch_vite_mode(self) -> None:
command = runtime_session_command("wow", 40)
self.assertEqual(command[1:3], ["--mode", "balance-lab"])
self.assertEqual(command[-4:], ["--source", "wow", "--level", "40"])
def test_random_party_is_repeatable_and_complete(self) -> None:
builds = [
{"id": "tank-a", "role": "tank"},