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.
131 lines
4.6 KiB
Python
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"
|