Skip to content

comfy-test.toml reference

The config file lives at the root of your nodepack (custom_nodes/ComfyUI-MyPack/comfy-test.toml)

Unknown keys are a hard error.

Minimal file

[test]
levels = ["syntax", "install", "registration", "execution"]

[test.lanes]
lanes = ["linux-cpu", "windows-cpu"]

[test.workflows]
cpu = "all"

[test]

Key Type Default Meaning
levels list see below Which levels to run; a set, not a sequence (levels). The only thing that decides what a run does -- no lane and no flag overrides it. Listing execution or execution_light on a pack with no workflows is an error.
comfyui_version string "latest" ComfyUI ref to test against: latest (the default branch's HEAD), a tag, a branch, or a full 40-character commit SHA. The resolved version and commit are both recorded in results -- see pinning to a commit.
python_version string or list "3.13" A single version pins it; a list draws one at random per run. COMFY_TEST_PYTHON_VERSION overrides both. Supported: 3.10, 3.11, 3.12, 3.13 -- anything else is a hard error (ADR-0005).
torch_version string newest complete triple Pins torch + the matching torchvision/torchaudio. A version, "latest" to opt out, or an explicit "t/tv/ta" triple. An unavailable triple aborts at config parse -- see torch, torchvision and torchaudio.
extra_pip_indices list [] Extra pip indexes for the whole test venv, added as --extra-index-url alongside the PyTorch index and pypi.org. For private mirrors and Artifactory proxies -- see below.
res int 1080 Capture resolution (viewport height) for screenshots and video.
custom string none Path to a run(ctx) hook, relative to your pack, for the custom level. Setting it enables that level automatically.

Default levels: syntax, install, registration, instantiation, static_capture, validation, execution. The opt-in levels -- coverage, warnings, hazards, javascript, execution_light and custom -- must be listed explicitly (except custom, above).

Pinning ComfyUI to a commit

[test]
comfyui_version = "latest"                                     # default branch HEAD
comfyui_version = "v0.3.60"                                    # a tag
comfyui_version = "master"                                     # a branch
comfyui_version = "37ac9ff44ffd1e4cc4b481cee550ced67608ec3a"   # a commit

All four are fetched the same way and cost the same -- one commit over the wire. A SHA is the only one of them that cannot move: ComfyUI's version string bumps only on releases, so many different HEADs report the same 0.33.0, and latest is a different commit every day.

An abbreviated SHA is rejected

comfyui_version = "37ac9ff" fails at config parse. This is a git limitation, not a comfy-test one: abbreviations are expanded against objects you already have, and a fresh clone has none -- so the remote is asked for a ref literally named 37ac9ff, which does not exist. Paste the full 40 characters.

Six hex characters or fewer are treated as a branch name, since that is below the length git itself would accept as an abbreviation.

Pinning ComfyUI does not pin its frontend

comfyui_version fixes the ComfyUI source tree. It does not by itself fix everything that tree installs at runtime -- see ComfyUI versioning.

Where to declare a package index

Three files can point pip at an index, and they cover different environments. Picking the wrong one produces a test that passes while real installs fail.

Declare it in Applies to Reaches your users?
requirements.txt (--extra-index-url line) ComfyUI's main venv yes -- Manager installs this file on a real install
comfy-env.toml [pypi-options] extra-index-urls comfy-env's isolated pixi envs yes, on a real install
comfy-test.toml extra_pip_indices the test venv only no

Your pack's own dependencies belong in requirements.txt. A --extra-index-url line there is honoured by uv and by every real user installing through ComfyUI-Manager. Putting that index only in comfy-test.toml means comfy-test resolves the dependency and your users cannot -- a green run that proves the opposite of what it looks like.

extra_pip_indices exists for the installs that cannot read a requirements.txt, and it is the only lever for them:

  • the pinned torch triple, installed before any requirements file exists
  • ComfyUI's own requirements.txt, which is not yours to edit
  • peer packs pulled in via comfy-env's [node_packs], which are other repositories' files

That is why it is described as infrastructure: a mirror for the whole environment, not a place to declare where your package comes from.

macOS resolves pack requirements differently

The macOS lane overrides the index-routed install path with plain uv, so extra indexes may not reach your pack's own requirements there. Verify on that lane before depending on it.

Choosing interpreters

[test]
python_version = "3.12"                    # pin one
python_version = ["3.10", "3.13"]          # draw one at random per run

The default is a fixed 3.13, not a draw. An unpinned random interpreter meant a re-run could go green with no fix -- the single most confusing behaviour the tool had. Widening is now a deliberate act: give a list and you opt into the variance, and provenance.python_version records which one ran.

A list is the right choice for a nightly or dispatch lane, where sampling the matrix over many runs is the point. Pin a single version for pre-merge CI, where a reproducible red is worth more than coverage.

There is no name key

Your pack is always identified by its directory name as installed under custom_nodes/ -- config_file.py sets it from the directory unconditionally and never reads a name from the TOML.

This is deliberate: ComfyUI itself identifies a pack by that directory, so a config-supplied name could only ever disagree with reality. Older examples showed name = "..."; it does nothing, and can be deleted.

[test] timeout is not read

Per-workflow timeout is [test.workflows] timeout; the [test] key of the same name is a setup timeout that the parser does not consult.

[test.lanes]

[test.lanes]
lanes = ["linux-cpu", "windows-cuda", "macos-desktop"]

An allowlist of ids from the lane table; unknown tokens are an error, and there are no per-lane booleans (ADR-0008).

Per-lane overrides use the lane's config key:

[test.windows_portable]
comfyui_portable_version = "v0.3.60"
skip_workflow = true
Key Type Default Meaning
enabled bool true Off switch for a listed lane.
skip_workflow bool false Run the pipeline but not the workflows. The supported way to keep execution in levels while a particular lane runs none.
comfyui_portable_version string none Pin the portable bundle (portable kinds only).

[test.workflows]

[test.workflows]
cpu = { exclude = ["heavy"] }   # selection is always per accelerator
cuda = "all"
timeout = 3600
Key Type Default Meaning
cpu / cuda / rocm "all", list, or { exclude = [...] } [] Which workflows run on that accelerator.
timeout int 3600 Per-workflow timeout, seconds.

There is no workflows key

Selection is always per accelerator. Workflows themselves are auto-discovered from the folders ComfyUI recognises -- there is no key that lists them. A workflows = [...] entry is an unknown key and aborts the run before anything is built.

The gpu key does not exist either, and is specifically diagnosed in the error message: it is the typo behind ADR-0006.

Selecting workflows

Three forms, per accelerator:

[test.workflows]
cuda = "all"                              # everything discovered
cpu  = ["basic", "upscale"]               # exactly these two
cpu  = { exclude = ["heavy_sdxl"] }       # everything except these

The .json suffix is optional -- "basic" and "basic.json" are the same thing.

The usual shape is a CUDA lane running the lot and a CPU lane skipping what needs a GPU:

[test.workflows]
cuda = "all"
cpu  = { exclude = ["flux_full", "video_interpolation", "sdxl_refiner"] }
timeout = 3600

!name was removed -- there is one way to exclude

The older per-item spelling is now a hard error:

cpu = ["basic", "!heavy"]   # rejected

It looked like "run basic, skip heavy" and never meant that: a single ! entry switched the whole list to everything except, the include was dropped on the floor, and you got every workflow on a CPU lane.

A table cannot express that mistake -- a selection names either what to run or what to skip, never both. The error prints the replacement for you.

An empty list means nothing runs on that accelerator -- it is not a synonym for "all". But omitting both cpu and cuda is different again: with neither configured the skip filter is disabled entirely and every discovered workflow runs.

[test.coverage]

[test.coverage]
inputs = { GeomPackLoadMesh = { file_path = "3d/cube.glb" } }

Declares inputs so the coverage level can account for nodes that need values to be exercised.

There is no [test.javascript] section

The javascript level takes no configuration. Your pack gets one namespace, derived from its published identity in pyproject.toml -- [tool.comfy] DisplayName if present, otherwise [project] name with any comfyui- prefix stripped. Nothing is declared in comfy-test.toml.

A pack that ships several namespaces (usually vendored JS that kept its old prefix) must rename that JS. There is no longer an escape hatch for it (ADR-0014).