b617a8b674
comfy-dev's explicit-over-implicit call: arbo now sends train_id, so the
worker no longer derives the loras/trained/{train_id}/ namespace from
output_dir.parent (which coupled it to arbo's handoff layout). train_id is
optional + path-safe-validated; when present it wins, else the path
derivation remains as the fallback. Wired through TrainRequest ->
validate_request -> published_relative_path -> _publish_lora. 18 tests green.
163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
"""Tests for the fixed-invocation builder — the INV-T7 enforcement surface.
|
|
|
|
These assert that (a) a valid request produces exactly the proven §4.6 command shape, and
|
|
(b) every out-of-bounds / unsafe / mis-fit request is REJECTED before any argv is produced.
|
|
Pure functions, no GPU, no subprocess — runnable anywhere.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from worker import config
|
|
from worker.invocation import InvalidTrainRequest, build_command, published_relative_path
|
|
|
|
|
|
def _req(**over):
|
|
base = {
|
|
"dataset_dir": "/worktank/arbo/train/t123/dataset",
|
|
"base_model_path": "/opt/fluxgym/models/sdxl/base.safetensors",
|
|
"output_dir": "/worktank/arbo/train/t123/out",
|
|
"output_name": "char_t123",
|
|
"trigger": "ohwx",
|
|
"subject_class": "woman",
|
|
"repeats": 10,
|
|
"tier": "balanced",
|
|
"device_index": 0,
|
|
"seed": 42,
|
|
}
|
|
base.update(over)
|
|
return base
|
|
|
|
|
|
def _flag_value(argv, flag):
|
|
return argv[argv.index(flag) + 1]
|
|
|
|
|
|
# ---- happy path ------------------------------------------------------------------------
|
|
|
|
def test_balanced_on_3090_builds_proven_shape():
|
|
argv, env, params = build_command(_req(tier="balanced", device_index=0))
|
|
assert str(config.ACCELERATE_BIN) == argv[0]
|
|
assert "launch" == argv[1]
|
|
assert str(config.SDXL_TRAIN_SCRIPT) in argv
|
|
# tier table (§4.6): balanced -> 1500 steps, dim 32, res 768
|
|
assert _flag_value(argv, "--max_train_steps") == "1500"
|
|
assert _flag_value(argv, "--network_dim") == "32"
|
|
assert _flag_value(argv, "--resolution") == "768,768"
|
|
# the load-bearing lean flags + caption gotcha
|
|
assert "--network_train_unet_only" in argv
|
|
assert "--gradient_checkpointing" in argv
|
|
assert _flag_value(argv, "--caption_extension") == ".txt"
|
|
assert _flag_value(argv, "--optimizer_type") == "adamw8bit"
|
|
# env: PCI_BUS_ID ordering + the assigned device + allocator
|
|
assert env["CUDA_DEVICE_ORDER"] == "PCI_BUS_ID"
|
|
assert env["CUDA_VISIBLE_DEVICES"] == "0"
|
|
assert env["PYTORCH_CUDA_ALLOC_CONF"] == "expandable_segments:True"
|
|
assert params["steps"] == 1500
|
|
|
|
|
|
def test_quality_on_a6000_ok_1024():
|
|
argv, env, _ = build_command(_req(tier="quality", device_index=1))
|
|
assert _flag_value(argv, "--resolution") == "1024,1024"
|
|
assert _flag_value(argv, "--max_train_steps") == "3000"
|
|
assert env["CUDA_VISIBLE_DEVICES"] == "1"
|
|
|
|
|
|
def test_paths_and_names_flow_through():
|
|
argv, _, _ = build_command(_req(output_name="my_char", trigger="ohwx"))
|
|
assert _flag_value(argv, "--output_name") == "my_char"
|
|
assert _flag_value(argv, "--train_data_dir") == "/worktank/arbo/train/t123/dataset"
|
|
|
|
|
|
# ---- rejections (INV-T7 / §4.6 tier-device fit) ----------------------------------------
|
|
|
|
def test_quality_on_3090_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="quality"):
|
|
build_command(_req(tier="quality", device_index=0))
|
|
|
|
|
|
def test_unknown_tier_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="tier"):
|
|
build_command(_req(tier="ultra"))
|
|
|
|
|
|
def test_bad_device_index_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="device_index"):
|
|
build_command(_req(device_index=3))
|
|
|
|
|
|
def test_dataset_dir_outside_handoff_root_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="dataset_dir"):
|
|
build_command(_req(dataset_dir="/etc/passwd"))
|
|
|
|
|
|
def test_path_traversal_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="dataset_dir"):
|
|
build_command(_req(dataset_dir="/worktank/arbo/train/../../etc/shadow"))
|
|
|
|
|
|
def test_base_model_outside_allowed_roots_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="base_model_path"):
|
|
build_command(_req(base_model_path="/home/someone/evil.safetensors"))
|
|
|
|
|
|
def test_published_relative_path():
|
|
# Phase 2 publish: derive {train_id} from the handoff output_dir -> ComfyUI-relative loras path
|
|
assert published_relative_path(
|
|
"/worktank/arbo/train/392cf898ac03/output", "sindra_lora"
|
|
) == "trained/392cf898ac03/sindra_lora.safetensors"
|
|
|
|
|
|
def test_published_relative_path_explicit_train_id_wins():
|
|
# explicit train_id (arbo now sends it) takes precedence over the path-derived fallback
|
|
assert published_relative_path(
|
|
"/worktank/arbo/train/whatever/output", "sindra_lora", train_id="abc123"
|
|
) == "trained/abc123/sindra_lora.safetensors"
|
|
|
|
|
|
def test_train_id_optional_and_validated():
|
|
# absent -> OK (falls back to derivation); present+safe -> OK; present+unsafe -> 422
|
|
build_command(_req()) # no train_id key
|
|
build_command(_req(train_id="392cf898ac03"))
|
|
with pytest.raises(InvalidTrainRequest, match="train_id"):
|
|
build_command(_req(train_id="../../etc"))
|
|
|
|
|
|
def test_storetank_checkpoint_allowed():
|
|
# the canonical SDXL store (2026-06-13 move) — arbo dispatches base_model_path from here
|
|
argv, _, _ = build_command(
|
|
_req(base_model_path="/storetank/arbo/models/checkpoints/albedobaseXL_v31Large.safetensors")
|
|
)
|
|
assert _flag_value(argv, "--pretrained_model_name_or_path") == \
|
|
"/storetank/arbo/models/checkpoints/albedobaseXL_v31Large.safetensors"
|
|
|
|
|
|
def test_unsafe_output_name_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="output_name"):
|
|
build_command(_req(output_name="a; rm -rf /"))
|
|
|
|
|
|
def test_unsafe_trigger_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="trigger"):
|
|
build_command(_req(trigger="$(whoami)"))
|
|
|
|
|
|
def test_repeats_out_of_range_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="repeats"):
|
|
build_command(_req(repeats=0))
|
|
with pytest.raises(InvalidTrainRequest, match="repeats"):
|
|
build_command(_req(repeats=1000))
|
|
|
|
|
|
def test_relative_dataset_dir_rejected():
|
|
with pytest.raises(InvalidTrainRequest, match="absolute"):
|
|
build_command(_req(dataset_dir="relative/path"))
|
|
|
|
|
|
def test_alpha_scales_with_dim():
|
|
_, _, params = build_command(_req(tier="fast"))
|
|
# fast -> dim 16; alpha = dim * ratio (0.5, matching Sindra) = 8
|
|
argv, _, _ = build_command(_req(tier="fast"))
|
|
assert _flag_value(argv, "--network_dim") == "16"
|
|
assert _flag_value(argv, "--network_alpha") == "8"
|
|
assert params["dim"] == 16
|