* feat(system): read-only health check and truthful overview counts POST /prompt_manager/system/health runs the health check off-loop as a read registry job of kind "health" (one at a time, beside a scan, not cancellable, no file outcomes) and keeps the last report; GET returns it or null. The check reads one snapshot on its own mode=ro connection, closed in finally: integrity_check(100), foreign_key_check, stored hashes against utils.hashing, duplicate pair rows, the legacy consistency queries (now shared with check_consistency), and image links whose file is gone, counted only under readable sources. An unavailable source, or the absent Needs-review queue, makes its check unavailable and the report incomplete instead of producing a count. GET /prompt_manager/system/overview returns Library-eligible positive prompts and active negative variants, indexed images, the last health summary or null, backup null, the Needs-review count null, the effective sources with whether they were auto-detected, and the thumbnail plan's missing count. GET /prompt_manager/system/statistics lists runs and average rating per category of Library parents, with the /stats totals. * feat(system): native System route, drafts and shared review dialog - #/system/<section> renders the concept's System shell: section nav, settings layout and Save row with truthful status (D92); each section not delivered yet hands off to its legacy tools (D94). Overview's System card opens #/system/overview (Q12). - A module-level draft store saves browser, then host (expected_revision), then runtime; failed domains stay dirty, a stale revision is a conflict. - System row builders (setting, operation, section nav) on the scope. - openReviewDialog and backupNotice extracted from the scan review, which is now a thin caller with identical markup. - Overview images gain a "rated" count (images whose linked prompt is rated); the missing-files health check reports per source, so partial availability is visible. * feat(system): native Overview with health report and statistics System > Overview renders the concept's metrics and its Library health and Workspace preferences surfaces from GET /system/overview: real counts, "Not checked" until a health check ran, no invented review count, backup or thumbnail status (D93). Explore opens "Usage statistics" with the real runs and ratings per category and the Library totals (D96); the statistics route now also returns rated_images. Maintenance shows the concept's notice with Run health checks above its hand-off: the read-only check runs as a registry read job and opens the "Health report" with a chip per real result, missing files per source (D95). A load failure shows Retry and no zeros; late answers never repaint a newer section. Adds the System concept comparison (shell, Overview, both dialogs, the notice, mutation checks), the System workflow spec and its fixture hooks, and the TESTING.md "Native System" section. * test(system): coverage floors for the native Overview Adds the system-overview.mjs baseline and raises the system.mjs floors to their measured coverage; no other floor changes. * fix(system): keep health reports per database and off the event loop - A restore that replaces the database forgets the stored health report, and a check that started before the swap is not kept (database generation stamped at start). - The Overview no longer walks every source for the missing-thumbnail count; the page reads /images/thumbnails/status on its own. - Image sources for the health check and the Overview resolve in the executor; a failure to start the check answers the coded envelope. - Any exception fails only its own check; missing files reads its rows in the snapshot and stats the files after it ended. - The read-only URI keeps Windows UNC paths (empty authority). - An integrity count at SQLite's limit is flagged as capped. * fix(system): thumbnail row read, tracked health job and guarded host save - The Overview's thumbnail row is its own read of the thumbnail status: "Checking thumbnails…" until it answers, unavailable when it fails. - The health job is tracked in the Activity store (new store.track) and followed through its event stream instead of polling every 150 ms. - A host save before a read returned a revision fails without a request; System reads the host values on mount and after a conflict. - "100+ problems found" when the integrity count is capped. - D-NEW-S2-5 is D123 everywhere; the unused Overview hand-off is gone. - The concept spec builds the health report from the concept's own rows and asserts the full unmapped path list; surface tops pin the 18px offset. - TESTING.md: D96's reason, the read-only (WAL/SHM) standard, the UNC URI and the new behavior. Coverage floors raised for the touched files. * fix(system): make two health tests portable to Windows - Windows forbids "?" in file names, so the URI-character open test uses "#", "%" and a space there. - ntpath.realpath on Python 3.11+ returns a NUL-byte path instead of raising, so the bad-path test raises as POSIX does to reach the missing-files failure path on every platform.
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""System test hooks for the browser tests (#249 System S2 Task 6).
|
|
|
|
The data is the concept-shaped real fixture (``POST /__test__/concept/reset``,
|
|
``workspace_concept_fixture.py``): its parents, saved pairs, images, ratings
|
|
and runs are what the Overview, the Statistics dialog and the health check
|
|
read. These per-server hooks only change what a System test needs on top:
|
|
|
|
- ``POST /__test__/system/reset`` restores the configured sources, forgets the
|
|
session's health report and ends every injected failure;
|
|
- ``POST /__test__/system/fail`` ``{path, times}`` answers the next ``times``
|
|
requests for ``path`` (a ``/prompt_manager/system/`` route) with the routes'
|
|
500 error envelope;
|
|
- ``POST /__test__/system/missing-source`` configures one more source that
|
|
does not exist (an unmounted drive);
|
|
- ``POST /__test__/system/missing-file`` deletes the newest linked image's
|
|
file from disk, leaving its database row (a missing-file link);
|
|
- ``POST /__test__/system/unthumbnailed`` ``{count}`` writes ``count`` plain
|
|
PNGs with no thumbnail into the output folder.
|
|
|
|
Delays use the shared request holds (``request_hold.py``).
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from aiohttp import web
|
|
from PIL import Image
|
|
|
|
FAILABLE_PREFIX = "/prompt_manager/system/"
|
|
MAX_UNTHUMBNAILED = 50
|
|
|
|
|
|
def register_system_commands(app, routes, api, root: Path):
|
|
from py.config import GalleryConfig
|
|
|
|
configured = list(GalleryConfig.MONITORING_DIRECTORIES)
|
|
failures: dict[str, int] = {}
|
|
|
|
@web.middleware
|
|
async def inject_failures(request, handler):
|
|
left = failures.get(request.path, 0)
|
|
if left <= 0:
|
|
return await handler(request)
|
|
failures[request.path] = left - 1
|
|
return web.json_response(
|
|
{"success": False, "code": None, "error": "Injected failure"}, status=500
|
|
)
|
|
|
|
app.middlewares.append(inject_failures)
|
|
|
|
def reset():
|
|
failures.clear()
|
|
GalleryConfig.MONITORING_DIRECTORIES = list(configured)
|
|
api._health_report = None
|
|
|
|
@routes.post("/__test__/system/reset")
|
|
async def system_reset(request):
|
|
reset()
|
|
return web.json_response({"success": True})
|
|
|
|
@routes.post("/__test__/system/fail")
|
|
async def system_fail(request):
|
|
data = await request.json()
|
|
path, times = data.get("path"), data.get("times", 1)
|
|
if (
|
|
not isinstance(path, str)
|
|
or not path.startswith(FAILABLE_PREFIX)
|
|
or isinstance(times, bool)
|
|
or not isinstance(times, int)
|
|
or not 1 <= times <= 10
|
|
):
|
|
raise web.HTTPBadRequest()
|
|
failures[path] = times
|
|
return web.json_response({"success": True})
|
|
|
|
@routes.post("/__test__/system/missing-source")
|
|
async def missing_source(request):
|
|
absent = str(root / "unmounted-drive")
|
|
if os.path.exists(absent):
|
|
raise web.HTTPConflict()
|
|
GalleryConfig.MONITORING_DIRECTORIES = [*configured, absent]
|
|
return web.json_response(
|
|
{"success": True, "configured": len(GalleryConfig.MONITORING_DIRECTORIES)}
|
|
)
|
|
|
|
@routes.post("/__test__/system/missing-file")
|
|
async def missing_file(request):
|
|
with api.db.model.get_connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT file_path FROM generated_images ORDER BY id DESC LIMIT 1"
|
|
).fetchone()
|
|
if row is None:
|
|
raise web.HTTPNotFound()
|
|
Path(row[0]).unlink()
|
|
api.invalidate_gallery_cache()
|
|
return web.json_response({"success": True, "filename": Path(row[0]).name})
|
|
|
|
@routes.post("/__test__/system/unthumbnailed")
|
|
async def unthumbnailed(request):
|
|
data = await request.json()
|
|
count = data.get("count")
|
|
if (
|
|
isinstance(count, bool)
|
|
or not isinstance(count, int)
|
|
or not 1 <= count <= MAX_UNTHUMBNAILED
|
|
):
|
|
raise web.HTTPBadRequest()
|
|
folder = root / "output" / "unthumbnailed"
|
|
folder.mkdir(exist_ok=True)
|
|
for number in range(count):
|
|
with Image.new("RGB", (64, 64), "#2b3442") as image:
|
|
image.save(folder / f"plain_{number + 1:03}.png")
|
|
api.invalidate_gallery_cache()
|
|
return web.json_response({"success": True})
|
|
|
|
return reset
|