* feat(inspect): add a declarative inspection node semantics table
* fix(inspect): bound and isolate the semantics table
Bound the semantics loader: at most 1 MiB is read, containers may nest
at most eight levels, and a RecursionError from the JSON parser becomes
a SemanticsError instead of escaping.
The table is no longer loaded at import. get_semantics() reads it on
first use and caches only a valid result, and the resolver imports it
lazily, only for a profile with semantics on. The default ingestion
profile never imports the module, so a missing or corrupt table cannot
break scan, monitor or admin import. Inspection of a prompt graph with
an unusable table is unresolved with the reason "Inspection node
semantics are unavailable" and an error log, rather than a crash.
INSPECTION_PROFILE now has semantics on; no strategy consumes the table
yet, so resolution is unchanged.
* feat(inspect): resolve guider-based samplers
Under INSPECTION_PROFILE the resolver discovers samplers from the
semantics table and follows SamplerCustomAdvanced through its guider
link (Reroutes allowed) to a listed guider:
- CFGGuider: positive and negative, so the pair resolves.
- BasicGuider: positive only. The state is "absent" with the reason
"Guider has no negative input", the existing missing-negative path.
- Any other guider, including DualCFGGuider (D17), is unsupported-node
and names that guider.
Guider hops draw visits, scan and depth from the existing budgets. A
SamplerCustomAdvanced upstream in latent ancestry is an earlier stage,
so the result stays multistage-ambiguous. Positive and negative
provenance record guider_id. FaceDetailer (detailer-stage) is not a
candidate yet: deciding it needs output ancestry. DEFAULT_PROFILE never
loads the table and is unchanged.
Vector changes (inspection profile only, default vectors unchanged):
- f07b: no-sampler -> resolved (CFGGuider).
- f17: no-sampler -> unsupported-node (FluxGuidance, Task 2.3).
- f01b: no-sampler -> unsupported-node (EmptyFlux2LatentImage latent,
Task 2.5).
- f01: no-sampler -> ambiguous (24 SamplerCustomAdvanced branches).
The no-sampler tests (resolver and inspect route) now use a sampler
class the inspector does not know, because f17 has a supported one.
* feat(inspect): trace conditioning through supported pass-through and selector nodes
* feat(inspect): treat detailers as sampler stages
* fix(inspect): choose selectors that share one condition together
* fix(inspect): bound detailer stage chains by the depth limit
* feat(inspect): support common text encoders and string sources
* fix(inspect): keep pair execution evidence behind unreadable string sources
* feat(inspect): accept any non-sampler latent origin
* fix(inspect): queue each node once in breadth-first ancestry walks
The latent and detailer image walks queued every link before checking
whether its node was already seen, and popped from the list head. A dense
shared fan-in grew the queue far past the visit budget before any visit
was charged. Both walks now use a deque and enqueue each node at most
once, charging every examined edge as scanned work; outcomes are
unchanged.
* fix(inspect): treat unlisted pipe and embed samplers as possible stages
The latent and detailer ancestry walks only flagged unlisted nodes with a
linked latent_image, guider, positive or negative input, so easy-use pipe
samplers, Impact pipe detailers and embed-driven video samplers were walked
through and the final sampler's prompt resolved silently. A separate
stage-shaped input set (adding pipes, embeds and custom-sampler parts)
now makes such nodes unsupported-node, named.
* fix(inspect): walk each sampler's latent ancestry once per resolution
Every selector branch run and candidate re-walked the same latent
ancestry, so conditioning-carrying origins such as WanImageToVideo turned
a three-selector graph from ambiguous into work-limit. The walk outcome is
now cached per sampler; branch runs of one candidate pay for it once and
other candidates are charged its visit cost, as the text memo does. Walks
that ran out of work are not cached. Every link input is still walked.
* fix(inspect): treat unlisted model-driven samplers as possible stages
Unlisted samplers that link a model with conditioning or a latent under
other input names (XlabsSampler, generic video samplers) were still walked
through, so the final sampler's prompt resolved silently. An unlisted node
with a linked model plus conditioning, neg_conditioning, samples or latent
is now unsupported-node, named; model patches such as LoraLoader and
ModelSampling nodes are not.
* feat(inspect): compute facts from the resolved sampler
* fix(inspect): merge facts across sampler branches when ambiguous
* fix(inspect): merge facts over unresolved stages and share stage inputs via the table
* feat(inspect): explain why a prompt could not be resolved
* feat(inspect): port legacy parser rules the new resolver missed (PromptManager)
R1: before 3.2.3 a save patch overwrote every PromptManager node's saved
text with the last-run node's prompt. The legacy viewer recognizes the
signature (comfy-metadata.js createContext: two or more PromptManager
workflow nodes with one API text and different widget values). Inspection
now refuses those nodes as malformed instead of resolving to another
node's prompt; the inspector hands the parsed workflow to the resolver for
this check only. Lands before the new pass-throughs (R6) that would
otherwise make these graphs resolve to the wrong text.
R7: this repository's node classes up to 07a5af3 (PromptManagerV2,
Positive, Negative, Detailer, V2Text, PositiveText, NegativeText) resolve
with their own outputs and their exact strip and ", " join.
The default (ingestion) profile ignores both.
* feat(inspect): port legacy parser rules the new resolver missed (A1111)
R3: an accumulating batch saver repeats the A1111 parameters record once
per batch item, identical except "Batch index: N". The bounded PNG reader
now keeps the last copy (Pillow's rule, which #165 relied on) instead of
calling the file unreadable. Any other difference, including a missing or
second batch index, is still a conflict, and decoding the copies draws from
the duplicate role budget.
R4: parameters text that starts with "Negative prompt:" has an empty
positive (#165), instead of showing the delimiter line as the positive.
R5: the settings footer may be followed by sd-dynamic-prompts' one-line
"Template:" and "Negative Template:" records, and may be "Steps: N" alone
when there is no full footer.
R10: commas inside A1111's quoted settings values no longer split them, so
an "ADetailer prompt" with a comma keeps the other facts known.
The facts' A1111 reader mirrors R4, R5 and R10; the default (ingestion)
profile keeps the historical split. Also from the Task 2.7 review: a guard
test fails on any uncoded unresolved reason without a user line (M1), and
the encoder bounds role lines to the client's 512 code units (M4).
* feat(inspect): port legacy parser rules the new resolver missed (installed nodes)
R6 and R9 for nodes whose sources are installed beside ComfyUI, each
entry's input names and output slots read from the node's code (the task
report lists file and line):
- detailer stages UltimateSDUpscale (image), UltimateSDUpscaleNoUpscale
(upscaled_image) and DetailerForEach (image), judged with their upstream
samplers like FaceDetailer;
- pass-throughs ControlNetInpaintingAliMamaApply and LTXVConditioning
(positive 0, negative 1) and FluxKontextMultiReferenceLatentMethod;
- encoder Text to Conditioning, strings TextInput and DisplayText;
- facts model pass-throughs Lora Loader (LoraManager), Power Lora Loader
(rgthree), ModelPatchTorchSettings, CFGNorm, PerturbedAttentionGuidance.
Impact Pack encodes a detailer's non-empty wildcard instead of its positive
input, so FaceDetailer and DetailerForEach now list it as a prompt
override: a literal wildcard is decided at run time and a linked one names
its source, instead of resolving to the positive input. The overwritten
PromptManager refusal (R1) still holds behind the new pass-throughs. The
default profile does not read the table.
* feat(inspect): port legacy parser rules the new resolver missed (upstream nodes)
R6 and R9 for nodes not installed beside ComfyUI here. Their input names
and output slots come from each pack's source at the commit the saved
workflows record in the node's cnr_id and ver properties (the task report
lists repository, commit, file and line):
- samplers ClownsharKSampler_Beta (RES4LYF), KSampler (Efficient) and
ttN KSampler_v2 (latent input "latent");
- pass-throughs SeedVarianceEnhancer and ApplyInstantIDAdvanced (positive
1, negative 2; output 0 is the model);
- selector Switch conditioning [Crystools];
- encoder smZ CLIPTextEncode: its text when with_SDXL is false; when true
the connected CLIP decides at run time which inputs are read, so the
prompt is known only when text, text_g and text_l agree;
- strings ttN text, CR Text, CR Prompt Text and TextBoxMira;
- facts model pass-through CR Apply LoRA Stack.
Inputs that replace what a sampler reads are not followed: RES4LYF options
(may carry a guider, and with sigmas replace the settings), an Efficiency
script and a ttN XY plot leave the prompt unsupported and the facts
unresolved. The overwritten PromptManager refusal (R1) still holds behind
SeedVarianceEnhancer. The default profile does not read the table.
* test(inspect): cover batch copies past the role budget and orphan widgets
* fix(inspect): match the batch index only in the settings footer
Review I-1: duplicate A1111 parameters copies were compared after blanking
"Batch index: N" anywhere in the record, so copies whose prompts differ
only by that phrase were accepted. The field is now blanked only inside
the record's one "Steps: N, ..." footer line, which must hold it exactly
once; any other difference stays a conflict.
Review M-4: the inspector parses the workflow record only when the API
graph can show the overwritten-PromptManager signature (a PromptManager
class, or a node with PromptManager's search_text input).
* fix(inspect): refuse overwritten PromptManager text inside subgraphs
Controller ruling on review M-1: PromptManager nodes inside subgraphs run
as "<instance>:<inner>" API IDs, nested instances chaining their IDs. The
overwritten-text check now walks subgraph instances (bounded in depth and
nodes) and reads a text widget stored by name, so these nodes are refused
like top-level ones, anchored or not. A definition without an instance
runs nothing and is ignored.
Review M-5: the historical adapters skip an explicit null prepend or append,
as the 07a5af3 code's "if prepend_text:" did.
* test(inspect): import unittest one way in the inspection suites
Code scanning flagged the mixed 'import unittest' and 'from unittest import
mock' forms. Use 'import unittest' plus 'import unittest.mock' everywhere.
* fix(history): name the recorded run's current-file prompts apart from its frozen fields
Now that the inspector resolves more saved files, the Recorded run dialog can
hold two regions named "Positive prompt" (and two of each Copy/Send action):
the frozen run field and the inline current file's prompt. Assistive
technology could not tell them apart, and the real-host chronology test read
two <pre> elements.
Prompt blocks take an optional context label. The inline current file inside
a recorded run names its regions "Current file positive prompt" and its
actions "Copy/Send current file positive prompt"; the visible headings stay
as in the concept. The real-host test now also scopes its frozen-field reads
to the "Frozen run prompt texts" section, and a disposable-server workflow
pins the distinct names with a resolved current file.
* fix(inspect): drop preview-only stages from unanchored candidates
A stage whose outputs reach only non-file outputs (PreviewImage and the other
non_file_outputs) cannot have written a saved file, yet it stayed an
unanchored candidate, so a preview-only FaceDetailer made the saved branch
ambiguous (f01b).
The inspection profile now drops such stages before detailer absorption,
using two linear backward passes over the graph (charged like the consumer
map; a failed pass keeps every stage). A stage without consumers, or one that
also reaches any other terminal, is kept, and nothing is dropped when every
stage is preview-only. Raw plus detailed double saves both write files and
stay ambiguous. A file in the host temp folder may itself be a preview, so it
is inspected with PREVIEW_INSPECTION_PROFILE, which keeps those stages.
Workspace facts follow the same choice: an absent or unresolved result that
names a sampler merges facts across that sampler's stages only, so the
dropped detailer's settings no longer blur the saved file's facts.
f01b now resolves (absent negative, guider has none); its vector is updated
and the expected failure removed. The default profile is unchanged:
differential against 39dcc40 identical over 160,256 cases. Over 60,054
inspection cases no previously resolved or absent result changed.
* fix(history): name the current file's prompt pair button apart from the frozen run's
The recorded run dialog nests the current-file inspector, whose footer repeated the "Send prompt pair" name. Name it from the prompt context, scope the real-host test to the frozen region, and add a dialog-wide duplicate-name check.
48 KiB
Testing and merge confidence
Routine changes should merge after the required checks pass and the diff is reviewed. Add a regression test for each bug; prove it fails before the fix.
What runs
| Layer | Real components | Replaced boundaries |
|---|---|---|
| Python tests | Domain logic, SQLite, API handlers, filesystem checks | ComfyUI host and ML runtimes where needed |
| JavaScript tests | Frontend modules and controller methods | DOM/network fixtures for focused edge cases |
| Viewer checks | Production Viewer.js and frontend methods in Chromium desktop/touch | Controlled images and page fixtures |
| Real ComfyUI | Pinned CPU host, actual extension, node definitions, widgets, routes, SQLite and real execution lifecycle | Stock UI cases isolate host writes; the execution suite queues deterministic test-only nodes without models |
| Page workflows | Production HTML, startup, click handlers, HTTP routes, SQLite, PNG files, thumbnails | ComfyUI host services and the tagging model |
Page workflows cover save/edit/reload, duplicate-edit rejection, gallery image identity, Tags selection and viewer cleanup, delayed responses, auto-tag review and persistence, and infinite scrolling without replacing existing cards. Data safety workflows download and upload actual SQLite backups, verify restored prompts/tags/image links, reject corrupt uploads without changing live data, and exercise cancellation and network-failure recovery for restores and deletion. Duplicate cleanup verifies only the selected file disappears; prompt deletion verifies the other prompt and original image files survive. They run in Chromium, Firefox, WebKit, and Chromium touch emulation. No GPU, model downloads, running ComfyUI instance, or personal configuration is required. The server binds to localhost and creates disposable directories and a database; its reset endpoint exists only in the test harness.
Run locally
Use a Python environment containing requirements-dev.txt and Node.js 24:
python -m unittest discover -s tests -q
npm ci --prefix tools/vendor --ignore-scripts
npx --prefix tools/vendor playwright install --with-deps chromium firefox webkit
npm run --prefix tools/vendor test
npm run --prefix tools/vendor test:coverage
npm run --prefix tools/vendor test:workflows -- --project firefox --project webkit
PM_TEST_PYTHON selects the Python executable; PM_TEST_PORT selects the local
port (default 8765). Set these environment variables when Python is outside PATH
or the port is occupied. Test servers are never reused. On unsupported Linux
distributions, browser system dependencies may need manual installation. For a
quick browser run, use test:workflows -- --project chromium.
CI runs the browser jobs inside the pinned image
mcr.microsoft.com/playwright:v<@playwright/test version>-noble
(tests/vendor/browser-image.test.mjs keeps the tag in step with the lockfile).
Concept comparisons measure geometry, and geometry follows the fonts a browser
falls back to: the concept names Inter, which neither the image nor a stock
runner ships. On a plain ubuntu-latest runner the fallback is DejaVu Sans,
whose wider metrics make the concept's own Tags table overflow a 390px viewport,
so the 390px comparisons are only meaningful in the pinned image. To reproduce CI,
run the suites in that image.
Managed real ComfyUI
The shared engine comes from an exact commit of
ComfyAssets Toolkit, recorded
in tools/vendor/package.json and its integrity-checked lockfile. The local
adapter sets PromptManager's database/configuration and monitoring policy;
source discovery, coverage mapping/gates and process lifecycle are shared code.
KikoTools remains the reference and is unchanged by this integration.
Use a dedicated disposable ComfyUI checkout at the revision in
tools/vendor/comfyui-runtime.json. With a separate Python environment, install
that checkout's requirements, this repo's requirements-dev.txt, and the exact
CPU torch/torchvision pins from the JSON (using PyTorch's CPU wheel index).
Install Chromium using the Playwright command above, then run:
PM_COMFY_ROOT=/path/to/disposable/ComfyUI \
PM_TEST_PYTHON=/path/to/cpu-env/bin/python \
npm run --prefix tools/vendor test:comfyui
CI performs this setup automatically. PM_COMFY_URL can select another
http://127.0.0.1:PORT/ address (default port 8189). This is an owned managed
server: a running host is never reused. Do not point it at your live checkout.
The launcher verifies the upstream SHA and clean tracked files, snapshots only
tracked PromptManager files, and creates temporary user/input/output/database/
configuration/log directories. Personal untracked files are excluded. It runs
CPU/offline with only PromptManager enabled and removes owned state on shutdown.
The upstream checkout must also be disposable: ComfyUI may initialize local
runtime files there. Browser host-settings writes are intercepted; actual
PromptManager routes use the disposable host and database.
real-comfyui-reports retains startup logs, version attachments, screenshots and
traces for 14 days. Update upstream/frontend/CPU pins together after reviewing
and running both startup projects; never replace the SHA with a moving branch.
Update the toolkit via an exact reviewed commit archive and commit both npm
manifest and lockfile. No toolkit or npm installation is needed by end users.
Coverage and failures
Python CI enforces 85% combined statement/branch coverage. Frontend coverage merges
Node V8 unit coverage with Chromium page-workflow coverage through c8. Every
web/js/**/*.js and web/js/**/*.mjs file appears, including untouched files at zero coverage. The
controller tests provide V8 filenames so their executed code is counted too.
tools/vendor/frontend-coverage-baseline.json holds a floor for every file and
metric. CI rejects regressions, missing measurements, and new files without a
reviewed baseline. These floors reflect current coverage, with two percentage
points of allowance for runtime/timing differences; they do not claim 80% coverage
of the large controllers. New files need at least 80% line coverage when added via
the baseline updater. Raise floors with npm run --prefix tools/vendor coverage:update; it reruns the suites and cannot lower an existing floor.
Review baseline changes like code changes.
The viewerjs check runs on every PR, including backend-only changes. It is an
aggregate job that passes only when the frontend coverage (chromium and touch)
job and both page workflows (firefox) and page workflows (webkit) jobs
succeed; the browsers run on separate runners in parallel. GitHub artifacts
named frontend-test-reports contain HTML coverage, a JSON summary, and the
Chromium/touch failure screenshots/traces; browser-traces-firefox and
browser-traces-webkit hold the other browsers' traces. Open traces with
Playwright's show-trace tool.
The real-host suite covers both Classic and Nodes 2.0 startup, switching both ways, actual database prompt selection, execution-text serialization, workflow reload, widget/style cleanup, node schemas and standalone-page opening. It does not certify every existing feature, arbitrary host versions, GPU/model changes, cross-pack installation or visual design changes. Those still need targeted validation.
Windows portability preflight
Before changing paths, SQLite, streams, restore or cleanup:
- Follow the boundary's existing native canonical/identity contract;
Path.resolve()alone does not fold Windows case. Keep lexical aliases distinct from resolved targets. Cover mixed case, drive and UNC spellings, plus foreign-namespace denial with an allowed native control. See Python's normcase documentation. - Close every owned database/model connection, worker and stream handle before
unlink or
TemporaryDirectory.cleanup(). Await worker/stream retirement. A SQLitewith connmanages transactions, not connection closure, andaddCleanupruns after teardown; use explicitfinallycleanup when teardown removes files first. - Windows can reject unlink of an open file; Unix can remove its directory entry while the handle remains usable. Preserve the shared behavior assertions and exercise the actual native branch, then prove handle release and successful cleanup. Do not hide failures with blanket skips or retries. See Python's remove/unlink semantics.
- Concurrent SQLite tests must not depend on how fast the host commits.
Managed connections use WAL with
synchronous=NORMAL(seeconfigure_connection) so commits do not hold the write lock across a flush. Make real overlap deterministic (for example hold the first writer open withFirstWriterHold) and give peers that must all succeed the ordinary busy timeout; cross-thread handoffs wait on events with generous ceilings, never a short wall clock (#242). - Run native Windows CI early for these changes and include a Windows impact and
evidence note in the PR. All twelve OS/Python combinations and every applicable
exact-head check must pass before merge. Portable
ntpath/PureWindowsPathcontrols do not establish native filesystem, locking or cleanup success.
Required merge checks
.github/branch-protection.json records the required checks: all twelve Python/OS
combinations, Python coverage, formatting, frontend modules, viewerjs, real ComfyUI compatibility,
and bandit (high severity).
The branch must be current with main, and administrators follow the same checks.
It does not require an additional human approval.
A repository administrator applies the configuration after the new workflow is passing, then verifies it:
gh api --method PUT repos/ComfyAssets/ComfyUI_PromptManager/branches/main/protection \
--input .github/branch-protection.json
gh api repos/ComfyAssets/ComfyUI_PromptManager/branches/main/protection/required_status_checks
Update the manifest whenever required job names change. Keep the frontend check free of path filters so a required check is never silently skipped.
Unified workspace
The opt-in /prompt_manager/workspace page opens the native Images workspace.
Overview retains real /prompt_manager/stats counts. Both use the shipped toolkit. Existing admin, gallery and metadata URLs remain
available while replacement workspaces are developed. Browser smoke tests in
tests/browser/workspace.spec.mjs cover navigation/history, keyboard skip links,
mobile layout, theme persistence/fallback, loading/retry and stale responses.
The Images cases in tests/browser/workspace-images.spec.mjs additionally cover
server-side filters, cursor append/retry, Grid/List/Masonry, bounded DOM, keyboard
navigation, restored browsing context, Viewer.js cleanup and actual video playback.
tests/test_api_workspace_images.py checks signed cursors, source identities,
legacy compatibility and header-only dimensions. Frontend stream/layout units
cover cancellation, malformed responses and bounded visibility lookup. These run
automatically in the existing four-browser-project workflow and frontend coverage job. tests/comfyui/workspace.spec.mjs verifies the page against the
managed host with both renderer startup configurations.
The shared inline and collection inspector reads the current rooted file's supported
embedded records, exact generation facts and recorded or inferred prompt evidence.
tests/browser/full-inspector.spec.mjs covers original/metadata/workflow downloads,
response limits, source changes, nested sending, matched dark/light and mobile
geometry, and the loaded-collection viewer. The viewer never fetches another page
or preloads all originals. Its explicit slideshow waits 3.5 seconds after image
readiness, pauses at native video without advancing on ended, and resumes on a
manually selected image only while prior intent remains. Pause/Stop, inspector
reading/copy/download/send, hidden document, media failure, close, query/route
disposal and pagehide cancel that intent; returning visibility or closing the sender
does not resume it. Stop leaves native video playback alone. Video uses only the
existing normalized gallerySettings autoplay/mute/loop values, read without writes;
denied/malformed storage falls back to autoplay off, mute on and loop on. Rejected
autoplay keeps native controls usable.
The inspector does not establish historical file identity, infer run membership,
edit image tags, export all EXIF, or load/execute a workflow. Saved vocabulary is
labelled by its recipe or positive-prompt scope. Limited raw evidence is omitted
atomically, with JSON exports unavailable and the rooted original still downloadable.
The inspection API uses one bounded read with per-record budgets: role records
(prompt, parameters, the pair execution record) share 2 MiB and exceeding it
fails the read; the workflow record has its own 4 MiB and past it only the workflow
becomes record format limited (no raw text, graph limited, roles and facts
unaffected); other text chunks are skipped unread; byte-identical duplicates are
accepted within a bounded read budget, while differing duplicates (and any duplicate
role record over that budget) still fail. Other read bounds are unchanged: 8 MiB
non-PNG header, 10,000 PNG chunks and a three-second ffprobe. Resolution under
INSPECTION_PROFILE bounds work, not graph size: 2,000 visits per candidate chain,
20,000 in total, 64 link/JSON-container levels, 100,000 resolved characters, and a
two 20,000-node ceilings: one over the top-level entries of the API prompt, and one
over the workflow graph's UI nodes plus subgraph-definition nodes (past it the
workflow graph and its facts become limited). The serialized
response stays under 8 MiB by omitting, in order, the workflow graph, then the prompt
graph, then all raw records atomically. Retry does not raise these limits. Ingestion
(scan, live monitor, admin import) and queue-time run counting keep the default
profile, including its 500-node gate. Only INSPECTION_PROFILE reads the node
semantics table web/js/inspection-semantics.json (at most 1 MiB and eight nesting
levels, loaded on first use). With it, SamplerCustomAdvanced follows its guider
link (through Reroutes) to CFGGuider (positive and negative) or BasicGuider
(positive only; the negative is absent, "Guider has no negative input"). Any
other guider, such as DualCFGGuider, is unsupported-node. Conditioning is
traced through the table's pass-throughs (FluxGuidance, ReferenceLatent, the
ConditioningSet* nodes, ControlNet apply nodes, InpaintModelConditioning and
WanImageToVideo by output slot). ConditioningZeroOut is an explicit empty text
with source zeroed-conditioning. easy ifElse with a literal boolean follows that
branch only; with a linked boolean both branches are traced, equal texts resolve and
different texts make the sampler ambiguous with one candidate per choice (at most
32, else work-limit). ConditioningCombine/Concat/Average resolve only when every
part has the same text, otherwise unresolved with composite naming the node.
Results that depend on a forced choice are never memoized. A FaceDetailer
(detailer-stage) is a sampler stage: the samplers reached through its image
ancestry are compared with it. Equal texts resolve with the detailer as the sampler;
different texts are ambiguous with multistage-differs and one candidate per
stage; an unreadable stage makes the chain unresolved. Unanchored, a sampler
whose output only feeds detailers (or previews) is not a candidate of its own.
A sampler's latent ancestry is walked through every link input of non-sampler
nodes within the same visit and depth budgets: with no earlier sampler it is a
single stage whatever the origin (VAEEncode, EmptyLatentBatch, EmptyFlux2LatentImage,
EmptyHunyuanLatentVideo, SetLatentNoiseMask, InpaintModelConditioning or
WanImageToVideo slot 2). An earlier sampler (img2img from a decode, hires fix) stays
unresolved "Multistage sampler ancestry is ambiguous" until stage texts are
compared; an unlisted node with sampler-shaped inputs (positive, negative, guider,
latent_image, a pipe, video embeds, custom-sampler parts, or a model link together
with conditioning, neg_conditioning, samples or latent) is unsupported-node
naming it, here and in a detailer's image ancestry; a latent that is not a link or
reaches its own sampler is malformed; running out of budget is work-limit or
depth-limit, never single stage. Each sampler's walk runs once per resolution;
selector branch runs share it and other candidates are charged its visit cost.
Text encoders: single encoders (the Qwen image encoders, TextEncodeQwenImage21
slot 0 prompt and slot 1 negative_prompt, KikoTextEncode) read one string
input unchanged. parts-equal encoders (CLIPTextEncodeSDXL, Flux, SD3) resolve when
every part is identical or exactly one part is non-empty (exact "", whitespace is
text); otherwise composite. String sources reproduce the pinned KJNodes code from
literal settings only: StringConstant is its string; StringConstantMultiline
with a literal true strip_newlines is replace("\n", "").strip();
JoinStringMulti keeps string_1 (even empty), appends string_2..string_N
(N = inputcount) after the literal delimiter unless missing or exactly "", and
ignores later inputs. A linked setting is unsupported-node; a list output
(return_list true or linked) and PromptSearchList are runtime-value ("Text is
decided at run time"), never one list item. Unverified text nodes (LoRAfromText,
RegexReplace, XYZPrompt, ImpactWildcardProcessor) stay unsupported-node. Every
intermediate text is length-checked and charged to the scanned-character budget. If
the table cannot be loaded, graph inspection is unresolved ("Inspection node
semantics are unavailable") and ingestion is unaffected, because it never imports the table.
tools/inspection_corpus.py --root DIR --out JSONL [--workers N] [--baseline JSONL] [--summary TXT] [--code TREE] measures a media folder read-only against the #240
invariants H1 to H8. It runs the inspection route pipeline and the workspace JS facts
of --code (default: its own tree), opens media only for reading, follows no
symlinks, imports no database, and refuses --out/--summary inside the root or a
code tree. Its rows hold statuses, sizes, timings and SHA-256 digests of resolved
role texts, never file names or prompt text unless --with-paths is given; digests
of short prompts can still be guessed, so the JSONL stays local and only the
aggregate summary goes into a PR body. Run it on the base commit first and pass that
JSONL as --baseline for H1 and H8 (2x the baseline p99, never below 50 ms). It
exits 1 when an invariant fails, including the harness line: a tool exception or
facts that were not computed (no Node.js) fail the run. Unreadable or unsupported
files are inspector outcomes, not harness errors. Never point it at a live install's folders from an
automated job. Installing the Python inspection/source-scope changes requires a
maintainer ComfyUI restart. Automated tests use disposable hosts and never restart
the running personal ComfyUI.
Run the focused page checks with:
npm run --prefix tools/vendor test:workflows -- workspace.spec.mjs workspace-images.spec.mjs
node --test tests/js/workspace-*.test.js
Use the disposable fixture and managed-host prerequisites above. A maintainer
live check needs a ComfyUI restart after adding the Python route, then opening
/prompt_manager/workspace; the mockup preview server is a separate design tool.
Prompt sending and cross-pack compatibility
tests/js/prompt-bridge-contracts.test.js runs the Toolkit's shared protocol
contracts against the actual checked-in browser asset. Adapter and lifecycle
units cover role isolation, connected inputs, stale previews, callback recovery,
serialization normalization and cached-page restoration. Browser tab workflows
exercise real BroadcastChannel delivery; tests/comfyui/prompt-bridge.spec.mjs
checks the real widgets, execution serialization and renderer transitions.
The existing required real-ComfyUI job also runs test:compat: PromptManager
alone, pinned KikoTools alone and both together, each starting in Classic and
Nodes 2.0. It copies tracked source into disposable hosts, preserves independent
configuration and checks edits/reload with both packs present. KikoTools remains
on its current embedded foundation; this does not adopt the Toolkit in that pack.
For local runs, use a clean KikoTools checkout at the revision in
tools/vendor/cross-pack-runtime.json, plus the managed-host prerequisites above:
PM_COMFY_ROOT=/path/to/pinned/comfyui PM_KIKO_ROOT=/path/to/pinned/kikotools \
npm run --prefix tools/vendor test:compat
The compatibility runner reserves loopback ports 8193–8195; PM_COMPAT_PORT
selects another consecutive unprivileged range. Never run automated tests against
the maintainer's running ComfyUI. This JavaScript host extension needs a full
browser refresh after deployment; its first slice adds no gallery send controls.
Workspace prompt inspection and sending
tests/test_api_prompt_inspector.py covers rooted physical-file identity,
execution/graph/parameter evidence, saved associations, ambiguity, empty negatives,
read-only behavior, bounded PNG/video metadata and corrupt inputs. Run it with
python -m unittest discover -s tests -p test_api_prompt_inspector.py in a disposable
checkout. tests/js/prompt-inspector.test.js and prompt-sender.test.js cover
request races, explicit destination selection, preview tickets and uncertain
acknowledgements. Browser workflows exercise the same controls in Images and
Viewer, including nested modal focus, mobile layouts and per-role sending.
tests/comfyui/prompt-sender.spec.mjs sends through the actual workspace UI into
real Classic and Nodes 2.0 widgets and verifies workflow serialization without
loading a workflow or queueing generation.
Durable execution ledger
npm run --prefix tools/vendor test:executions runs 46 real execution cases
(23 each in Classic and Nodes 2.0, retaining all 42 prior execution cases),
alongside the existing 80 stock host cases (the original 78 plus the native
editor host case) and six pack compatibility cases. Run
these suites serially because they share report cleanup and owned host resources. Use the same pinned disposable PM_COMFY_ROOT,
PM_TEST_PYTHON and Playwright browser installation as test:comfyui:
PM_COMFY_ROOT=/path/to/disposable/ComfyUI \
PM_TEST_PYTHON=/path/to/cpu-env/bin/python \
PLAYWRIGHT_BROWSERS_PATH=/path/to/playwright-browsers \
npm run --prefix tools/vendor test:executions
This suite reserves http://127.0.0.1:8196/ and never attaches to an existing
server. Each renderer worker launches a CPU/offline host with independent
configuration, database, user and output directories. The shared launcher installs
tracked snapshots of PromptManager and a tiny temporary Git-tracked fixture pack.
Only that owned test pack provides cached text, deliberate failure, an
interruptible queue gate, deterministic CLIP/model/latent and CPU tensor inputs,
and control routes.
It replaces external sampler inference and trigger injection only inside the
disposable host; real registered PromptManager classes, queue, executor and cache
remain active. Its cache control resets the real executor on its worker to exercise
Classic, LRU, RAM-pressure and disabled caching; production has no test nodes or
endpoints. Cleanup always releases the gate, stops the host and removes both
owned directories. Logs and Playwright failures appear under
tools/vendor/test-results/executions/ and the existing CI artifact upload.
The request-only case creates no browser or WebSocket client. Other cases verify
real execution_cached node lists after a failed job, both renderer switches and
workflow reload, repeated public UUIDs, interruption, graph rejection, and queued
removal before dequeue. Actual host history is checked alongside finite polling
of SQLite after the background writer. The read helper uses SQLite read-only
mode, exact UUID job/session filters and a 256-row limit; absent ledger schema is
explicit, while missing/corrupt databases and other read errors fail the suite.
Sensitive diagnostic and extra-data sentinels must never appear in lifecycle
rows. Exact prompt fields are deliberately persisted only in context snapshots.
The same read-only helper calls the internal get_execution_contexts and
get_execution_outputs readers for requested run IDs and inspects their snapshot
identities and current usage counts. Its outputs map is keyed by immutable run
ID, alongside the existing rows, captures, usage and snapshotIds results.
execution-outputs.spec.mjs runs actual stock SaveImage and PreviewImage nodes
from tiny CPU tensors. It compares each attempt's persisted descriptors with that
attempt's final host history, including A,A,B batches, multiple output nodes,
cached locators, reused public UUIDs, PNG metadata disabled, graphs without PM
nodes and text-only outputs. The pinned SaveImage returns an IMAGE socket; linking
it to the owned failure/gate node orders the partial-run tests explicitly. A held
downstream gate proves completed image work does not finalize the run early.
Other cases verify real output capture across startup in each renderer, switching
both ways and workflow reload. PM usage counts retain their existing per-job
behavior. References identify reported locators only: file identity is unverified,
new generation and context membership are unknown, and temp references are
transient. The stock suite excludes both execution files; discovery retains its
80 cases (the original 78 plus the native editor host case) and the six pack
cases unchanged.
For a meaningful historical failure, create a separate disposable detached
checkout at fdba6fd0126a8a40cbd6f3e50ed4c5de8991efd4 and pass its path to
launchExecutionHost({ repository }). That base also predates the #211 config
import fix: after confirming an actual headless job succeeded, use
readExecutionRuns on its explicitly constructed owned path
host.stateDirectory/custom_nodes/promptmanager/prompts.db. It reports
schemaAvailable: false and empty rows, failing the expected succeeded-row
assertion. This historical inspection stays outside the ordinary suite, which
requires the configured database path. The launcher also accepts
PM_EXECUTION_REPOSITORY; omit it for the implementation worktree. Stage new
files first because snapshots use git ls-files. Never reset the implementation
checkout or point this harness at a live ComfyUI checkout.
The package-qualified config import also corrects #211: the configured database path is honored when PromptManager runs as a ComfyUI custom node. Follow the database path compatibility and safe backup sequence before restarting an existing deployment.
The ledger records only new dequeued jobs and observed outcomes. Existing
counters, prompts, pairs and image links are preserved, with no reconstructed old
runs. Schema v2 migrates automatically when the configured database opens, adding
immutable prompt-context tables without backfilling old runs. The internal reader
returns not_captured for preexisting runs; a new capture can instead be complete,
partial, ambiguous or unavailable. Keep the existing database backup/restore
procedure: restored history changes storage epoch, and old library navigation
hints must never be rebound to unrelated current rows.
Context cases verify A,A,B as three jobs/two combinations/one positive snapshot
when every positive field matches; all five registered PM variants; exact raw
whitespace and Unicode; known empty negatives; Text negative roles and crossed
pairs; fully cached repeats after trigger configuration changes; successful source
text before downstream failure/interruption; unknown cached dynamic raw; unsupported
transforms and duplicate list invocation ambiguity. Fresh values use
runtime_return, cached effective STRING outputs use host_cache_output, and
submitted literals use graph inference. SQL NULL means unknown; empty text is
known. Text is never trimmed or normalized by capture. Cache availability and
inferred sampler roles prove neither sampler execution nor consumption, and list
indices do not identify generated images. External trigger configuration changes
must not rewrite the effective text of an existing cached result. The cache matrix
also checks that positive/pair usage increments once per dequeued job while a
negative-only component retains its existing zero standalone-use count.
Fault injection for restore fencing, inherited async capability revocation, queued
finalization, epoch and generation revocation, provider-local cache entries, eviction/reset, malformed or
oversized payloads and missing host interfaces lives in the focused Python
test_execution*.py suites. These use disposable storage and mocked host interfaces;
they are distinct from the pinned real executor/cache checks above. Run-owned
output descriptors do not create gallery image associations or a chronological
timeline. Deploying the Python changes
requires a ComfyUI restart. After restart, queue three new jobs A,A,B with identical
positive fields and two negative recipes; existing counters should advance as usual,
and internal context inspection should find three attempts and two combinations.
For a live output check after that manual restart:
- Queue SaveImage and PreviewImage with a batch of two, then submit an unchanged cached repeat and a changed tensor/input under new jobs.
- Inspect each run's internal output capture against its final ComfyUI history; cached references may repeat, while the attempts remain distinct.
- Switch Classic to Nodes 2.0 and back, reload the workflow and queue again.
- Verify prompt/pair counters and gallery links retain their existing behavior; output references alone do not prove file bytes or prompt membership.
Automated checks use only managed disposable hosts. They never restart or upgrade the personal ComfyUI server.
Chronological Library history (#209)
#/library/{positive_id} retains saved negative selection, the full selected
negative directly beneath its positive, acknowledged role/pair sending, and one
pair-filtered shared gallery. Recorded executions are a separate projection.
Selecting an execution, anchor, Raw/Effective mode, or reported occurrence does
not select a saved variant or refetch its gallery. A,A,B means three attempts,
two frozen contexts, and one exact raw-positive group. Different composition or
evidence can produce different frozen contexts despite equal-looking previews.
The versioned public reads are:
| Route | Meaning |
|---|---|
GET /prompt_manager/library/{positive_id}/history |
Exact frozen raw-positive group; compact chronological windows |
GET /prompt_manager/history/runs/{run_id} |
Frozen fields, per-field provenance, capture and outcome for a known UUID |
GET /prompt_manager/history/runs/{run_id}/outputs |
Ordered host-reported occurrences, including repeated locations |
GET /prompt_manager/history/runs/{run_id}/output/current/{media,thumbnail,inspection} |
Authorized current file at that reported location, using the returned epoch/scope URL |
Run and output reads accept the returned epoch; current-file routes require
the returned epoch, current scope and occurrence coordinates. Run integers are
decimal strings, preserving SQLite's full signed 64-bit values. Windows and
receipt pages default to 40 with a 1–100 limit. Cursors are opaque, authenticated,
scope-bound and capped at 4096 bytes. Run JSON has an 8 MiB UTF-8 limit and receipt
pages a 1 MiB limit; exceeding a limit fails explicitly instead of truncating
exact evidence. The unchanged capture limits remain 16 anchors/256 occurrences.
A cursor freezes its upper ordering boundary, not an application-wide snapshot:
each page has one SQLite read transaction and a count as of that read. Later
pages can observe completed capture, deletion or other current changes. Newer
runs require Refresh history. Unknown positive raw text cannot enter an exact
Library group but remains readable through its known run UUID. Unknown (null),
known empty ("") and whitespace are distinct. Complete-zero reported outputs
means the host reported none; missing capture means unknown, and partial or
conflicting capture remains incomplete. Receipts prove reported locations, not
historical bytes, newly generated files or context-output ownership. Cached
attempts can repeat exactly the same receipts.
Each nonempty history page also returns an authenticated resume_cursor, including
an exhausted page with no continuation. Re-entry uses resume instead of cursor
to read from that prior upper boundary, then restores at most the prior number of
40-run pages. It retains horizontal/vertical scroll and the visible anchor as
in-memory visit intent. The API validates epoch and exact saved-text scope again;
DTOs and current candidates are never saved as visit intent. Explicit Refresh
loads the newest window/count and revalidates the selected run's exact raw-positive
membership, context and receipt. A valid selection outside that window has an
explicit label and View selected run control.
Every receipt can be selected, including missing/error candidates. Retry current
availability rereads its existing bounded output page; a reduced response limit
may require several reads within that one already loaded page. Earlier rows,
context/mode, exact selection and saved gallery remain. Recovery actions use the
shared client's strict smaller-limit, reload-outputs, retry-read and
refresh-history vocabulary. Active inspection epoch/identity failures immediately
reach the owning history recovery, clearing stale recorded actions before reload.
Restore acceptance depends on merged #223: incompatible online storage replacement must safely refuse without harming another owner's WAL/connection. History fencing does not repair unsafe replacement. For accepted replacement, the read transaction ends before a fresh-connection epoch check; the final epoch/generation validation is the publication linearization point. Browser generations also ignore late success/error callbacks after route/scope changes. Old cursors, epoch and current-scope URLs fail with bounded recovery instructions. Restored numeric IDs never repair an original navigation hint. Saved Library details are revalidated separately, and a changed owner requires explicit reload.
Current media/thumbnail responses use an already-opened, validated canonical
file handle, bounded streaming and Cache-Control: no-store, including HEAD and
range behavior. Inspection is a current metadata observation with pre/post file
facts, canonical reauthorization and a fresh scope/epoch check; it is not pinned
historical bytes or an OS filesystem snapshot. A deterministic change during the
metadata read is rejected. A later request can legitimately observe replacement
bytes at the same path without changing the immutable receipt.
The focused acceptance command retains all four projects:
PM_TEST_PORT=8766 npm run --prefix tools/vendor test:workflows -- \
workspace-history.spec.mjs workspace-visual.spec.mjs \
--project chromium --project touch --project firefox --project webkit
npm run --prefix tools/vendor test:executions
npm run --prefix tools/vendor test:comfyui
npm run --prefix tools/vendor test:compat
Use the pinned disposable prerequisites above; managed suites run serially. The real execution helpers compare SQLite captures to these public routes for stock SaveImage/PreviewImage batches, multiple nodes, metadata disabled, cached headless repeats, partial failure and interruption. Additional cases verify the actual PM A,A,B group, unknown-positive exclusion, and exact frozen Raw/Effective pair sends through the acknowledged host bridge after Classic/Nodes 2.0 startup, both switches and workflow reload, without queuing or loading a workflow on send. All original 42 execution, 78 stock-host and six compatibility cases remain.
The browser fixture uses actual APIs/SQLite/files, fixed owned backup snapshots, one-shot bounded publication barriers and fixed filesystem/configuration commands. Reset and cleanup release barriers, restore seams and revoke commands. It never attaches to a personal host. The canonical HTML and its six required assets are served from a fixed test-only allowlist; the original concept index/app/style bytes are hash-pinned and unmodified. At 1600×1100 and 390×844 in both themes, with fixed locale/UTC/reduced motion and ready fonts, tests capture unmasked concept/production PNG pairs and geometry, and pin actual branch padding, surfaces, 2px role lines, 57px rhythm, staggering, colors, selected focus and native dialog behavior. Controls and actions are checked independently of screenshots. Keyboard focus/return, coarse targets, 200% layout, route return and persisted page lifecycle recovery retain their separate behavioral tests.
Initial production acceptance requires maintainer inspection of paired PNGs and geometry; never update snapshots automatically or mask content to bless a gap. Data-dependent text/images/counts and the clearly labeled saved/frozen/current distinctions are documented in the comparison helper. Broader Images card activation/Info/tag/favorite/select parity and shared Viewer stage/toolbar polish and shared role-block spacing/padding/glyph polish remain outside #209. The responsive production Viewer keeps its toolbar in the media panel and prompt details readable rather than reproducing the concept's mobile toolbar collision. These are explicit remaining visual differences, not a whole-application or full-gallery/Viewer pixel-parity claim.
Preserve raw logs, failed traces, paired PNGs, geometry, runtime versions and
source hashes before later managed commands clear their output directories.
Frontend baseline updates are additive through coverage:update only after
every previous metric floor and each new file's 80% line requirement pass; rerun
the coverage gate using the committed baseline. Python retains its 85% combined
statement/branch gate. Deploying the history Python changes requires a maintainer
ComfyUI restart; automated acceptance never restarts the personal server.
Native recipe editor (#234)
The Library page edits recipes in one native modal (dialog.library-editor)
instead of sending users to the legacy dashboard. It is frontend only: it consumes
the Library/pair routes, epoch tokens, null-clears and conflict codes defined by
the backend change (#233) and does not add or change a route, so this section does
not repeat them. The editor reads fresh, no-store state before it opens.
The four dialog modes match the approved concept's shell: New prompt, Edit recipe metadata, Create text revision and Add a negative variant. Pair Archive… and Restore are one reviewed action with Show archived in the saved-variants list; archiving a pair never touches its positive prompt, its siblings, history or images. Export downloads the full v1 recipe export. A failed Save keeps focus inside the dialog and keeps every entry.
Recovery is exercised against real failure responses, not mocks of the controller:
- a conflict response keeps the draft and explains the newer saved state; a storage-changed response writes nothing, and only New prompt re-reads in place: every other dialog offers only Reload Library, because an old numeric owner is never resolved after a restore;
- an unknown outcome (no response) keeps the draft and offers an explicit Retry that resends the identical request once, which the server deduplicates; an Edit metadata retry instead reloads the latest saved values for review and resends nothing;
- a confirmed receipt whose follow-up refresh fails offers a read-only refresh retry and never re-sends the write.
The concept comparison (library-editor-canonical-comparison.mjs) inventories the
concept dialog and the production dialog in the same browser: geometry, CSS tokens,
colors and a control inventory (labels, field types, options, actions and their
danger/primary classes). Only the documented differences (the prototype's Save
label and demo note) are mapped. The mutation check proves the comparison fails when
a token, label or control is changed, so it cannot pass vacuously.
PNG baselines are intentionally absent: toHaveScreenshot is not used for the
editor. Paired concept/production captures and the artifact-only states
(library-editor-visual.spec.mjs) are attached as test artifacts for maintainer
acceptance; a baseline is added only after the maintainer accepts them.
Focused commands:
PM_TEST_PORT=8766 npm run --prefix tools/vendor test:workflows -- \
workspace-library-editor.spec.mjs library-editor-visual.spec.mjs \
--project chromium --project touch --project firefox --project webkit
npm run --prefix tools/vendor test:comfyui # library-sender.spec.mjs host regression
native editor preview sends exact saved roles across renderer transitions opens
the real workspace, creates a prompt with an explicitly empty negative, and sends the
selected pair through the acknowledged host bridge in Classic and Nodes 2.0, across
both switches and a workflow reload. It asserts the actual widgets and serialized
workflow, no queue or workflow load, unchanged history and pair metadata, and that
a connected destination rejects the preview without changing either role. It needs
the pinned disposable ComfyUI, so it runs in CI's "real ComfyUI compatibility" job.
Deploying this change needs no Python change and no ComfyUI restart: the diff for
the editor UI touches only HTML, CSS and JavaScript (plus test fixtures). A browser
refresh is enough. New editor modules have additive coverage floors in
tools/vendor/frontend-coverage-baseline.json; none were lowered.
Native vocabulary backend (#236)
Routes under /prompt_manager/vocabulary/: GET tags and GET categories
(bounded pages, signed epoch-scoped cursors), POST tags (create or reuse an exact
name), PATCH tags/{id} (rename), POST tags/{id}/delete, GET tags/{id}/impact,
POST categories/preview and POST categories/apply. Every command carries
expected_epoch; tag commands also carry the exact {id, name} identity, and
category apply carries the preview's impact_token.
Error contract: command validation failures are 400 with code: null; reads
return {success: false, error} with no code, and impact on an unknown id is 404. A stale epoch is 409
storage-changed; a changed or missing identity, or a changed category projection,
is 409 vocabulary-conflict with a generic message. Both 409 bodies include the
current library_epoch. Nothing is written on any refusal.
Count semantics: a count is the number of eligible Library parents carrying the value
directly or through one active saved pair, exactly what list_library(tags=[name]) or
list_library(category=value) returns. Unused registry tags report 0 and are kept by
get_all_tags, suggest_tags, backup and restore, duplicate cleanup, orphan pruning
and owner deletion (TestRetainedTags in tests/test_database_backup.py). Restore
rotates the epoch, so a pre-restore expected_epoch is refused with storage-changed.
Owner rows and their timestamps never change on a tag command; a category apply only
touches rows carrying the exact source.
Two legacy routes change additively: GET /tags now lists unused registry tags, and
/tags/suggest without context appends unused names at count 0. Unused tags become
deletable only through the native Delete tag (vocabulary UI PR), not on the legacy
tags page.
Both token functions (library_edit_token, category_impact_token) share
canonical_token in database/library_tokens.py; TestSharedTokenHashing pins their
outputs.
Deploying this backend needs a ComfyUI restart (Python and route changes).
Native vocabulary UI (#236)
#/tags ("Tags & categories", page title "Tags") is a native workspace route; it no
longer hands off to the legacy tag manager. That page stays reachable from Overview's
"Open existing tag manager" card for tag search and sort, the untagged entry and
multi-tag AND/OR browsing; bulk prompt actions (delete, tag, category) stay on the
legacy admin page. The screen consumes only the /prompt_manager/vocabulary/* routes
above and adds no route, so PR B needs no ComfyUI restart: a browser refresh is enough.
Screen: a + Create tag heading action, a Tag vocabulary table (columns Tag, Prompts,
Images shown as "—" with hidden "Not available yet", and Actions with Browse and
Rename / merge), and a Categories panel whose category rows end with the Manage
category row (Review). Lists load 100 rows at a time, append when scrolled into view
and keep Load more tags / Load more categories while more remain. Prompts counts
are the Library Browse totals.
Dialogs (vocabulary-dialog.mjs): Create tag, Rename or merge tag (renaming to
an existing name merges; it carries a Delete tag… confirmation that names the
links removed) and Manage categories (Category select, Preview, then Apply).
Names with commas are rejected on these native routes only.
Guarded states, each exercised against the real server in
tests/browser/workspace-vocabulary.spec.mjs: stale epoch (storage-changed) and
vocabulary-conflict write nothing and re-read; an unknown outcome keeps the dialog
open, re-reads and never resends a delete; an unconfirmed Delete offers Retry that only
re-reads the impact; a failed list read shows Retry with "Nothing was changed."; a
failed Load more moves keyboard focus to its Retry; a write that lands after its dialog
was force-closed or left with Back still re-reads the lists and drops the cached Library
shelf; Manage categories reads up to 1,000 categories and says so when capped.
Browse on a tag or category row opens Library with exactly that Tags or Category
filter (the concept puts the name in the search text instead); the Prompts count equals
the number of cards Browse shows.
Test files: tests/js/workspace-vocabulary-state.test.js (strict parsing, stale and
abort handling, late writes after dispose, sentences), tests/js/workspace-state.test.js
(route/title), tests/browser/workspace-vocabulary.spec.mjs (screen, dialogs, Browse,
mobile, the concept geometry/token/control comparison and its mutation check),
tests/browser/vocabulary-canonical-comparison.mjs (inventory mapping),
tests/browser/vocabulary-visual.spec.mjs (paired capture artifacts) and the
focused node tag suggestions include a newly created tag case in
tests/browser/focused-node.spec.mjs. The fixture is
tests/browser/workspace_vocabulary_fixture.py. The screen is not host-facing, so no
tests/comfyui case was added.
node --test tests/js/workspace-vocabulary-state.test.js tests/js/workspace-state.test.js
PM_TEST_PORT=8766 npm run --prefix tools/vendor test:workflows -- \
workspace-vocabulary.spec.mjs vocabulary-visual.spec.mjs focused-node.spec.mjs \
--project chromium --project touch --project firefox --project webkit
Recorded deviations from the concept (maintainer acceptance): no Auto-tag images
button and an Images "—" cell until the image-annotation slice; bounded lists with
Load more; title "Tags"; Manage categories adds a Category select and Preview before
Apply; "Apply" instead of "Apply in prototype"; Delete tag… inside Rename or merge;
Browse sets the exact filter; singular "1 positive prompt"; the production list appends
fixture-only tags after the concept's sample tags. Paired concept/production captures
for both themes and widths are attached as test artifacts; no PNG baseline is added
until acceptance. Not compared: hover states and the dialog focus ring (the page-level
keyboard ring and link ring offsets are asserted). No coverage floor was lowered;
vocabulary-state.mjs, vocabulary.mjs and vocabulary-dialog.mjs have additive
floors in frontend-coverage-baseline.json.