Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
215 lines
10 KiB
Python
215 lines
10 KiB
Python
"""Guards for the KDE theme's silent-failure modes.
|
|
|
|
Every check here corresponds to something that broke without producing an error
|
|
message. They are static reads of the scripts and packages because the failures
|
|
are configuration-shaped -- there is nothing to import and nothing that raises.
|
|
"""
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
KDE = REPO / "assets" / "themes" / "KDE"
|
|
INSTALLER = KDE / "install-plasma.sh"
|
|
LNF = KDE / "look-and-feel" / "com.nexusos.desktop"
|
|
ICONS = REPO / "assets" / "themes" / "NexusOS-icons"
|
|
THEME_INSTALLER = REPO / "assets" / "themes" / "install-theme.sh"
|
|
|
|
|
|
def test_look_and_feel_is_copied_never_symlinked():
|
|
"""KPackage skips symlinked package directories without a word, so a
|
|
symlinked Global Theme simply never appears in System Settings."""
|
|
text = INSTALLER.read_text()
|
|
assert "cp -rL" in text, "look-and-feel/wallpaper must be copied into place"
|
|
for line in text.splitlines():
|
|
if line.strip().startswith("ln -s"):
|
|
assert "look-and-feel" not in line and "wallpapers" not in line, \
|
|
f"KPackage package dir must not be symlinked: {line.strip()}"
|
|
|
|
|
|
def test_plasmashell_restart_is_detached_from_the_callers_stdout():
|
|
"""The restarted shell outlives the script. Inheriting stdout keeps the
|
|
caller's pipe open forever, which hangs `ncp restore` after a successful
|
|
apply."""
|
|
text = INSTALLER.read_text()
|
|
restart = [l for l in text.splitlines()
|
|
if "kstart5 plasmashell" in l and not l.strip().startswith("#")]
|
|
assert restart, "no plasmashell restart found"
|
|
for line in restart:
|
|
assert "setsid" in line, f"restart not detached: {line.strip()}"
|
|
assert "</dev/null" in line and ">/dev/null" in line, \
|
|
f"restart still holds the caller's stdio: {line.strip()}"
|
|
|
|
|
|
def test_splash_renders_without_the_stage_signal():
|
|
"""A splash gated on `stage == 2` shows a blank coloured screen if that
|
|
signal never arrives -- what `ksplashqml --test` does."""
|
|
qml = (LNF / "contents" / "splash" / "Splash.qml").read_text()
|
|
content = qml[qml.index("id: content"):]
|
|
body = content[:content.index("OpacityAnimator")]
|
|
assert "opacity: 0" not in body, "splash content starts invisible"
|
|
assert "introAnimation" not in qml, "visibility still gated on a stage change"
|
|
|
|
|
|
def test_sddm_theme_is_configured_in_exactly_one_place():
|
|
"""boot-branding.sh and install-plasma.sh both deploy the SDDM theme; two
|
|
different config files meant the setting could disagree with itself."""
|
|
for script in (INSTALLER, REPO / "bin" / "boot-branding.sh"):
|
|
text = script.read_text()
|
|
stray = re.findall(r"/etc/sddm\.conf(?!\.d)", text)
|
|
assert not stray, f"{script.name} writes bare /etc/sddm.conf; use conf.d"
|
|
|
|
|
|
def test_restore_desktop_stage_covers_plasma_as_well_as_xfce():
|
|
"""The desktop stage used to bail out entirely without xfconf-query, so a
|
|
Plasma box got no theme back from `ncp restore` at all."""
|
|
text = (REPO / "bin" / "restore-linux.sh").read_text()
|
|
assert "install-plasma.sh" in text, "restore never invokes the Plasma installer"
|
|
assert "--no-sddm" in text, "restore should leave SDDM to boot-branding.sh"
|
|
# The XFCE check must not be able to skip the Plasma branch or the branding.
|
|
assert "exit 0\nfi\n" not in text.split("desktop stage")[1][:900], \
|
|
"XFCE guard still exits the whole stage"
|
|
|
|
|
|
def test_global_theme_package_is_well_formed():
|
|
meta = json.loads((LNF / "metadata.json").read_text())
|
|
assert meta["KPlugin"]["Id"] == LNF.name, "package Id must match its directory"
|
|
assert "Plasma/LookAndFeel" in meta["KPlugin"]["ServiceTypes"]
|
|
|
|
defaults = (LNF / "contents" / "defaults").read_text()
|
|
# Every component the Global Theme selects has to exist in the repo.
|
|
assert "ColorScheme=NexusOS" in defaults
|
|
assert (KDE / "plasma" / "NexusOS").is_dir()
|
|
assert (KDE / "aurorae" / "NexusOS").is_dir()
|
|
assert (KDE / "wallpaper" / "NexusOS" / "metadata.json").is_file()
|
|
assert "Theme=com.nexusos.desktop" in defaults, "splash not wired to this package"
|
|
|
|
|
|
def test_patterned_backgrounds_are_referenced_as_raster_not_svg():
|
|
"""QtSvg is SVG Tiny 1.2 and has no <pattern>, so the brushed-metal and
|
|
machine-line textures vanish and the gradient renders flat. Every QML that
|
|
shows that artwork must load the rasterized PNG."""
|
|
qml_files = [
|
|
KDE / "sddm" / "NexusOS-QML" / "Main.qml",
|
|
LNF / "contents" / "splash" / "Splash.qml",
|
|
]
|
|
for f in qml_files:
|
|
# Only the source: lines -- the comments deliberately mention the SVG,
|
|
# since that is the file you edit and re-rasterize.
|
|
sources = [l for l in f.read_text().splitlines()
|
|
if "source:" in l and not l.strip().startswith("//")]
|
|
bg = [l for l in sources if "background" in l]
|
|
assert bg, f"{f.name} loads no background"
|
|
for line in bg:
|
|
assert "background.png" in line, \
|
|
f"{f.name} loads a patterned SVG through QtSvg: {line.strip()}"
|
|
|
|
|
|
def _defaults_sections():
|
|
"""Parse the look-and-feel defaults into {section: {key: value}}."""
|
|
out, section = {}, None
|
|
for line in (LNF / "contents" / "defaults").read_text().splitlines():
|
|
line = line.strip()
|
|
if line.startswith("["):
|
|
section = line
|
|
out[section] = {}
|
|
elif line and "=" in line and section:
|
|
k, v = line.split("=", 1)
|
|
out[section][k] = v
|
|
return out
|
|
|
|
|
|
def test_lock_screen_theme_names_a_look_and_feel_package():
|
|
"""Plasma 5.27 draws the lock screen from a look-and-feel package, and
|
|
[Greeter]Theme names that package. A bare theme name (the old "NexusOS")
|
|
is not one, so Plasma falls back to Breeze without saying anything."""
|
|
greeter = _defaults_sections()["[kscreenlockerrc][Greeter]"]
|
|
assert greeter["Theme"].endswith(".desktop"), \
|
|
f"lock theme must be a look-and-feel package id, got {greeter['Theme']!r}"
|
|
|
|
installer = INSTALLER.read_text()
|
|
lock_lines = [l for l in installer.splitlines()
|
|
if "kscreenlockerrc" in l and "--key Theme" in l]
|
|
assert lock_lines, "installer never sets the lock screen theme"
|
|
for line in lock_lines:
|
|
assert ".desktop" in line, f"installer sets a non-package lock theme: {line.strip()}"
|
|
|
|
# And the lock wallpaper is the raster metal background.
|
|
assert "background.png" in installer, "lock wallpaper not set to the metal raster"
|
|
|
|
|
|
def _index_theme():
|
|
"""Parse index.theme into (header dict, list of declared directories)."""
|
|
header, section, dirs = {}, None, []
|
|
for line in (ICONS / "index.theme").read_text().splitlines():
|
|
line = line.strip()
|
|
if line.startswith("[") and line != "[Icon Theme]":
|
|
section = line.strip("[]")
|
|
dirs.append(section)
|
|
elif "=" in line and not line.startswith("#") and section is None:
|
|
k, v = line.split("=", 1)
|
|
header[k.strip()] = v.strip()
|
|
return header, dirs
|
|
|
|
|
|
def test_every_declared_icon_directory_exists():
|
|
"""index.theme declared 124 directories against 21 real ones, most copied
|
|
from Papirus. A declared-but-missing directory is dead weight the icon
|
|
loader walks on every lookup."""
|
|
_, dirs = _index_theme()
|
|
missing = [d for d in dirs if not (ICONS / d).is_dir()]
|
|
assert not missing, f"index.theme declares directories that do not exist: {missing}"
|
|
|
|
listed = set(_index_theme()[0].get("Directories", "").split(","))
|
|
assert listed == set(dirs), "Directories= and the [section] list disagree"
|
|
|
|
|
|
def test_icon_theme_inherits_a_recolourable_parent():
|
|
"""Papirus hardcodes its blues, so nothing the colour scheme does can reach
|
|
them and the un-themed surface stayed blue forever. Breeze's icons carry
|
|
ColorScheme-* classes that Plasma recolours from the active scheme."""
|
|
header, _ = _index_theme()
|
|
parents = [p.strip() for p in header["Inherits"].split(",")]
|
|
assert not parents[0].lower().startswith("papirus"), \
|
|
"primary parent cannot recolour from the colour scheme"
|
|
assert parents[0] == "breeze-dark", f"expected breeze-dark first, got {parents[0]!r}"
|
|
assert parents[-1] == "hicolor", "hicolor must remain the last-resort fallback"
|
|
|
|
|
|
def test_icon_theme_is_installed_where_qt_looks():
|
|
"""~/.icons is the GTK/XFCE legacy path. Qt/KF5 searches XDG data dirs only,
|
|
so installing there alone meant Plasma never found the theme and every icon
|
|
fell back to Breeze without a word."""
|
|
text = THEME_INSTALLER.read_text()
|
|
links = [l for l in text.splitlines()
|
|
if l.strip().startswith("link ") and "NexusOS-icons" in l]
|
|
assert any(".local/share/icons" in l for l in links), \
|
|
"icon theme is not installed to an XDG data dir; Plasma will not see it"
|
|
assert any(".icons/NexusOS" in l for l in links), \
|
|
"dropping ~/.icons would break the XFCE session and GTK apps"
|
|
|
|
|
|
def test_inherits_check_reads_only_the_primary_parent():
|
|
"""The installer compared the whole comma-separated Inherits value against a
|
|
directory name, so a valid multi-parent list warned that an installed
|
|
fallback was missing."""
|
|
text = THEME_INSTALLER.read_text()
|
|
inh = [l for l in text.splitlines() if "INH=" in l and "Inherits" in l]
|
|
assert inh, "inheritance check not found"
|
|
assert any("-f1" in l for l in inh), \
|
|
"inheritance check still treats the whole Inherits list as one theme name"
|
|
|
|
|
|
def test_panel_layout_is_portable():
|
|
"""The panel script sets the launcher icon by absolute path. Hardcoding
|
|
this box's home would give any other clone or user a missing icon, so the
|
|
path is a placeholder the installer substitutes."""
|
|
js = (KDE / "panel-layout.js").read_text()
|
|
assert "/home/" not in js, "panel-layout.js hardcodes a home directory"
|
|
assert "__NEXUS_ROOT__" in js, "no placeholder for the repo path"
|
|
|
|
installer = INSTALLER.read_text()
|
|
assert "__NEXUS_ROOT__" in installer, "installer never substitutes the repo path"
|
|
# Rewriting the panel wholesale on every restore would wipe later additions.
|
|
assert "PANEL_MARKER" in installer, "panel layout is not guarded by a marker"
|