79c25a329f
The previous followup fix (e9654518, then2afb0126) only applied the bundled INI to HelloImgui's runtime state via `imgui.load_ini_settings_from_memory`, called from the `post_init` callback. That callback fires AFTER HelloImgui has already: 1. loaded user prefs from disk 2. loaded imgui settings from disk (via imgui.load_ini_settings_from_disk) 3. set up the dockspace tree By the time post_init fires, HelloImgui has already discarded the empty on-disk INI's data and built its dock state. The load_ini_settings_from_memory apply in post_init ended up being SILENTLY DISCARDED for [Docking][Data] entries with orphaned DockSpace IDs. Empirical evidence: manual launch test (sloppy.py without --enable-test-hooks) after2afb0126produced a saved manualslop_layout.ini of 3072 bytes with 2 DockNode entries, but those DockNodes were created at RUNTIME, not loaded from the bundled INI's literal IDs. The imgui core loader rejected the literal IDs from the bundled INI because the runtime IDs didn't match. Fix: add `_install_default_layout_pre_run_result` to App.run entry, called BEFORE `_run_immapp_result`. It writes the bundled INI to cwd if cwd's INI is missing/empty/small, so when HelloImgui's load_user_pref / load_ini_settings_from_disk runs, it reads my bundled INI as the initial state. The literal DockSpace ID 0xAFC85805 (= runtime-generated MainDockSpace 2949142533) matches, the DockNode IDs 0x00000001/0x00000002 match (because HelloImgui restores dock IDs from INI), and per-window DockId references apply to the matching DockNodes. The post_init live-session apply (imgui.load_ini_settings_from_memory) is now mostly redundant for first-launch: HelloImgui reads the bundled INI on its initial load. But it's still there for any edge case where HelloImgui's load_ini_settings_from_disk reads an INI after the pre-run write somehow fails, AND it covers the "user manually wiped cwd INI mid-session" case. Test changes: - _assert_live_session_apply renamed to _assert_install_applied -- the primary path is now pre-run, and the test accepts either "[GUI] pre-run installed default layout:" or "[GUI] installed default layout: ... (and applied to live session)" - Updated test 1 and 2 to use the new helper name Empirical verification (re-run of 18s manual launch): - Before launch: cwd INI absent - During launch: [GUI] pre-run installed default layout: ...layouts/default.ini -> ...manualslop_layout.ini - During launch: [GUI] visible-by-default windows: AI Settings, Diagnostics, Discussion Hub, Files & Media, Log Management, Operations Hub, Project Settings, Response, Theme - After force-kill: cwd/manualslop_layout.ini is 3072 bytes containing [Docking][Data] with DockSpace ID=0xAFC85805 + DockNode ID=0x00000001 (CentralNode=1, SizeRef=481,1172) + DockNode ID=0x00000002 (SizeRef=1197,1172) + 8 [Window][...] entries with DockId=0x00000001,N or DockId=0x00000002,N + 0 stale window names - 17/17 tests pass
234 lines
7.5 KiB
Python
234 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from typing import TextIO
|
|
|
|
import pytest
|
|
|
|
|
|
_PROJECT_ROOT: Path = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
_SCRIPTS_DIR: Path = _PROJECT_ROOT / "tests" / "artifacts" / "tier2_state" / "default_layout_install_20260629"
|
|
_GUI_SCRIPT: str = str(_PROJECT_ROOT / "sloppy.py")
|
|
_INI_FILENAME: str = "manualslop_layout.ini"
|
|
|
|
|
|
def _spawn_sloppy_for(workspace: Path, log_suffix: str) -> subprocess.Popen:
|
|
_SCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
log_path: Path = _SCRIPTS_DIR / f"test_{log_suffix}.log"
|
|
log_file: TextIO = open(log_path, "w", encoding="utf-8", errors="replace")
|
|
env: dict[str, str] = os.environ.copy()
|
|
env["PYTHONPATH"] = str(_PROJECT_ROOT.absolute())
|
|
args: list[str] = ["uv", "run", "python", "-u", _GUI_SCRIPT]
|
|
creation: int = subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0
|
|
proc: subprocess.Popen = subprocess.Popen(
|
|
args,
|
|
stdout=log_file,
|
|
stderr=log_file,
|
|
text=True,
|
|
cwd=str(workspace.absolute()),
|
|
env=env,
|
|
creationflags=creation,
|
|
)
|
|
log_file.close()
|
|
return proc
|
|
|
|
|
|
def _read_launch_log(log_suffix: str) -> str:
|
|
log_path: Path = _SCRIPTS_DIR / f"test_{log_suffix}.log"
|
|
if not log_path.exists():
|
|
return ""
|
|
try:
|
|
return log_path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def _assert_install_applied(log_suffix: str) -> None:
|
|
text: str = _read_launch_log(log_suffix)
|
|
install_msgs: list[str] = [l for l in text.splitlines() if "pre-run installed" in l or "applied to live session" in l]
|
|
assert install_msgs, (
|
|
f"install was not invoked for this run; expected one of "
|
|
f"'[GUI] pre-run installed default layout: ...' or "
|
|
f"'[GUI] installed default layout: ... (and applied to live session)' "
|
|
f"in stderr, got install-related lines: "
|
|
f"{[l for l in text.splitlines() if 'default layout' in l]!r}"
|
|
)
|
|
|
|
|
|
def _terminate(process: subprocess.Popen) -> None:
|
|
if process.poll() is not None:
|
|
return
|
|
pid: int | None = process.pid
|
|
if pid is None:
|
|
return
|
|
try:
|
|
if os.name == "nt":
|
|
subprocess.run(
|
|
["taskkill", "/F", "/T", "/PID", str(pid)],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
check=False,
|
|
)
|
|
else:
|
|
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
process.wait(timeout=2)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _read_ini(workspace: Path) -> str:
|
|
ini_path: Path = workspace / _INI_FILENAME
|
|
if not ini_path.exists():
|
|
return ""
|
|
try:
|
|
return ini_path.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def _has_window_with_collapsed_zero(text: str) -> bool:
|
|
for line in text.splitlines():
|
|
if line.startswith("[Window][") and line.rstrip().endswith("]"):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _has_docking_block_with_docknodes(text: str) -> bool:
|
|
if "[Docking][Data]" not in text:
|
|
return False
|
|
if "DockSpace" not in text:
|
|
return False
|
|
docknode_count: int = 0
|
|
for line in text.splitlines():
|
|
if line.strip().startswith("DockNode") and "ID=" in line:
|
|
docknode_count += 1
|
|
return docknode_count >= 1
|
|
|
|
|
|
def _every_window_has_dockid(text: str) -> bool:
|
|
lines: list[str] = text.splitlines()
|
|
blocks: dict[str, list[str]] = {}
|
|
for idx, line in enumerate(lines):
|
|
if line.startswith("[Window][") and line.rstrip().endswith("]"):
|
|
block: list[str] = []
|
|
for next_line in lines[idx + 1:]:
|
|
if next_line.startswith("[") and "][" in next_line:
|
|
break
|
|
block.append(next_line)
|
|
blocks[line] = block
|
|
if not blocks:
|
|
return False
|
|
has_dockid: bool = True
|
|
for header, block in blocks.items():
|
|
if not any("DockId=" in bl for bl in block):
|
|
has_dockid = False
|
|
break
|
|
return has_dockid
|
|
|
|
|
|
def _has_no_stale_window_names(text: str) -> bool:
|
|
stale: set[str] = {
|
|
"Projects", "Files", "Screenshots", "Discussion History",
|
|
"Provider", "Message", "Response", "Tool Calls",
|
|
"Comms History", "System Prompts",
|
|
}
|
|
for line in text.splitlines():
|
|
if line.startswith("[Window][") and line.rstrip().endswith("]"):
|
|
name: str = line[len("[Window]["):-1]
|
|
if name in stale:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _workspace_for(tmp_path: Path, test_name: str) -> Path:
|
|
return tmp_path / f"_default_layout_install_{os.getpid()}_{test_name}"
|
|
|
|
|
|
def _start_subprocess_in(workspace: Path, log_suffix: str) -> subprocess.Popen:
|
|
proc: subprocess.Popen = _spawn_sloppy_for(workspace, log_suffix)
|
|
time.sleep(5.0)
|
|
return proc
|
|
|
|
|
|
def _assert_installed_default(workspace: Path) -> None:
|
|
ini_path: Path = workspace / _INI_FILENAME
|
|
assert ini_path.exists(), f"expected {ini_path} to exist after launch"
|
|
text: str = _read_ini(workspace)
|
|
assert "[Window][Project Settings]" in text, (
|
|
f"installed INI missing [Window][Project Settings]; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
assert _has_window_with_collapsed_zero(text), (
|
|
f"installed INI has no [Window][...] entry; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
assert _has_docking_block_with_docknodes(text), (
|
|
f"installed INI missing [Docking][Data] block with DockSpace + >=1 DockNode children; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
assert _every_window_has_dockid(text), (
|
|
f"installed INI does not have a DockId= line following every [Window][...] header; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
assert _has_no_stale_window_names(text), (
|
|
f"installed INI contains a window name from _STALE_WINDOW_NAMES -- _diag_layout_state will emit a stale-name warning; got first 400 chars: {text[:400]!r}"
|
|
)
|
|
|
|
|
|
def test_default_layout_installed_when_ini_missing(tmp_path: Path) -> None:
|
|
workspace: Path = _workspace_for(tmp_path, "ini_missing")
|
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
ini_path: Path = workspace / _INI_FILENAME
|
|
if ini_path.exists():
|
|
ini_path.unlink()
|
|
proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_missing")
|
|
try:
|
|
_assert_installed_default(workspace)
|
|
_assert_install_applied("ini_missing")
|
|
finally:
|
|
_terminate(proc)
|
|
shutil.rmtree(workspace, ignore_errors=True)
|
|
|
|
|
|
def test_default_layout_installed_when_ini_empty(tmp_path: Path) -> None:
|
|
workspace: Path = _workspace_for(tmp_path, "ini_empty")
|
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
ini_path: Path = workspace / _INI_FILENAME
|
|
ini_path.write_bytes(b"\n\n\n\n\n")
|
|
proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_empty")
|
|
try:
|
|
_assert_installed_default(workspace)
|
|
_assert_install_applied("ini_empty")
|
|
finally:
|
|
_terminate(proc)
|
|
shutil.rmtree(workspace, ignore_errors=True)
|
|
|
|
|
|
def test_default_layout_NOT_installed_when_layout_present(tmp_path: Path) -> None:
|
|
workspace: Path = _workspace_for(tmp_path, "ini_custom")
|
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
ini_path: Path = workspace / _INI_FILENAME
|
|
custom_lines: list[str] = ["[Window][CustomPanelPersistent]", "Pos=10,10", "DockId=0xDEAD", ""]
|
|
for i in range(32):
|
|
custom_lines.append(f"; padding line {i}")
|
|
custom_lines.append("[Window][Filler]")
|
|
custom_lines.append(f"Pos={i},{i}")
|
|
custom_lines.append("Size=100,100")
|
|
content: str = "\n".join(custom_lines) + "\n"
|
|
ini_path.write_text(content, encoding="utf-8")
|
|
assert ini_path.stat().st_size >= 1000, "custom INI must be at least 1000 bytes for this test"
|
|
proc: subprocess.Popen = _start_subprocess_in(workspace, "ini_custom")
|
|
try:
|
|
text: str = _read_ini(workspace)
|
|
assert "[Window][CustomPanelPersistent]" in text, (
|
|
f"custom INI should be preserved (no overwrite), but content changed: {text[:400]!r}"
|
|
)
|
|
assert "DockId=0xDEAD" in text, "custom DockId entry should survive install"
|
|
finally:
|
|
_terminate(proc)
|
|
shutil.rmtree(workspace, ignore_errors=True)
|