hokusai/tests/test_config.py
Konstantin Fickel 7503672942
All checks were successful
Continuous Integration / Build Package (push) Successful in 25s
Continuous Integration / Lint, Check & Test (push) Successful in 44s
feat: add content targets and loop expansion for target templates
Content targets write literal text to files via 'content:' field,
without requiring an AI provider or API keys. They are not archived
when overwritten.

Loop expansion allows defining 'loops:' at the top level with named
lists of values. Targets with [var] in their name are expanded via
cartesian product. Variables are substituted in all string fields.
Explicit targets override expanded ones. Escaping: \[var] -> [var].
Expansion happens at config load time so the rest of the system
(builder, graph, state) sees only expanded targets.
2026-02-21 18:39:13 +01:00

131 lines
4.6 KiB
Python

"""Integration tests for hokusai.config."""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from hokusai.config import ContentTargetConfig, GenerateTargetConfig, load_config
class TestLoadConfig:
"""Test loading and validating YAML config files end-to-end."""
def test_minimal_config(self, project_dir: Path) -> None:
config_path = project_dir / "test.hokusai.yaml"
_ = config_path.write_text(
yaml.dump({"targets": {"out.txt": {"prompt": "hello"}}})
)
config = load_config(config_path)
assert "out.txt" in config.targets
target = config.targets["out.txt"]
assert isinstance(target, GenerateTargetConfig)
assert target.prompt == "hello"
assert config.defaults.text_model == "pixtral-large-latest"
assert config.defaults.image_model == "flux-2-pro"
def test_full_config_with_all_fields(self, project_dir: Path) -> None:
raw = {
"defaults": {
"text_model": "custom-text",
"image_model": "custom-image",
},
"targets": {
"banner.png": {
"prompt": "A wide banner",
"model": "flux-dev",
"width": 1920,
"height": 480,
"inputs": ["ref.png"],
"reference_images": ["ref.png"],
"control_images": ["ctrl.png"],
},
"story.md": {
"prompt": "Write a story",
"inputs": ["banner.png"],
},
},
}
config_path = project_dir / "full.hokusai.yaml"
_ = config_path.write_text(yaml.dump(raw, default_flow_style=False))
config = load_config(config_path)
assert config.defaults.text_model == "custom-text"
assert config.defaults.image_model == "custom-image"
banner = config.targets["banner.png"]
assert isinstance(banner, GenerateTargetConfig)
assert banner.model == "flux-dev"
assert banner.width == 1920
assert banner.height == 480
assert banner.reference_images == ["ref.png"]
assert banner.control_images == ["ctrl.png"]
story = config.targets["story.md"]
assert isinstance(story, GenerateTargetConfig)
assert story.model is None
assert story.inputs == ["banner.png"]
def test_empty_targets_rejected(self, project_dir: Path) -> None:
config_path = project_dir / "empty.hokusai.yaml"
_ = config_path.write_text(yaml.dump({"targets": {}}))
with pytest.raises(Exception, match="At least one target"):
_ = load_config(config_path)
def test_missing_prompt_rejected(self, project_dir: Path) -> None:
config_path = project_dir / "bad.hokusai.yaml"
_ = config_path.write_text(yaml.dump({"targets": {"out.txt": {}}}))
with pytest.raises(Exception):
_ = load_config(config_path)
def test_config_with_loops(self, project_dir: Path) -> None:
config_path = project_dir / "test.hokusai.yaml"
_ = config_path.write_text(
yaml.dump(
{
"loops": {"a": [1, 2]},
"targets": {"file-[a].txt": {"content": "Value [a]"}},
}
)
)
config = load_config(config_path)
assert "file-1.txt" in config.targets
assert "file-2.txt" in config.targets
assert "file-[a].txt" not in config.targets
t1 = config.targets["file-1.txt"]
assert isinstance(t1, ContentTargetConfig)
assert t1.content == "Value 1"
def test_config_loops_normalize_values_to_strings(self, project_dir: Path) -> None:
config_path = project_dir / "test.hokusai.yaml"
_ = config_path.write_text(
yaml.dump(
{
"loops": {"x": [True, 3.14]},
"targets": {"out-[x].txt": {"content": "[x]"}},
}
)
)
config = load_config(config_path)
assert "out-True.txt" in config.targets
assert "out-3.14.txt" in config.targets
def test_content_target(self, project_dir: Path) -> None:
config_path = project_dir / "test.hokusai.yaml"
_ = config_path.write_text(
yaml.dump({"targets": {"file.txt": {"content": "ABC"}}})
)
config = load_config(config_path)
target = config.targets["file.txt"]
assert isinstance(target, ContentTargetConfig)
assert target.content == "ABC"