ad31341a4d
Extracts the per-name YAML-list storage logic out of tensors/characters.py into a new generic tensors.fragments.FragmentLibrary class, then layers two kind-specific function-style modules on top: - tensors.characters (unchanged public API, now thin wrapper) - tensors.scenes (new): save_scene / load_scene / list_scenes / delete_scene / scene_path / resolve_scene, stored in ~/.local/share/tensors/scenes/ CLI additions: - tsr scene save -o <name> 'elem1, elem2, ...' - tsr scene list / show / delete Wired into generate + template + style-sweep: - tsr generate -S <name> / --scene-prompt 'elems' injects scene elements into the positive prompt right after the character block (order: quality_prefix -> character -> scene -> rating -> user) - tsr template -S <name> / --scene-prompt 'elems' embeds a 'scene' list field in the dumped JSON template (named + inline merged, deduped) - style-sweep templates accept 'scene' (str|list) and 'scene_prompt' keys - generate --input JSON honors both 'scene' (str=name or list=inline) and 'scene_prompt' keys FragmentLibrary uses kind-aware error messages (e.g. "Invalid scene name" vs "Invalid character name") so users see the right context. 28 new tests cover the generic library + scenes. Bumps version to 0.1.24 (supersedes v0.1.23 which failed to publish due to a transient Sigstore network error).
97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
"""Tests for the scene library (tensors.scenes).
|
|
|
|
Mirrors test_characters.py — the underlying implementation is shared so the
|
|
focus here is verifying that scene-specific exports route through correctly
|
|
and that monkeypatching SCENES_DIR works the same way.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def scene_env(tmp_path, monkeypatch):
|
|
"""Redirect SCENES_DIR at fresh tmp_path per test."""
|
|
from tensors import scenes as scene_mod
|
|
|
|
scene_dir = tmp_path / "scenes"
|
|
monkeypatch.setattr(scene_mod, "SCENES_DIR", scene_dir)
|
|
return scene_mod, scene_dir
|
|
|
|
|
|
class TestSaveLoad:
|
|
def test_save_creates_dir_and_file(self, scene_env):
|
|
scene_mod, scene_dir = scene_env
|
|
path = scene_mod.save_scene("penthouse", ["luxury penthouse", "volumetric lighting"])
|
|
|
|
assert path == scene_dir / "penthouse.yml"
|
|
assert path.is_file()
|
|
assert path.read_text() == '- "luxury penthouse"\n- "volumetric lighting"\n'
|
|
|
|
def test_load_roundtrip(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
elements = ["a", "b with spaces", "c, with, commas", 'd "quotes"']
|
|
scene_mod.save_scene("mixed", elements)
|
|
|
|
assert scene_mod.load_scene("mixed") == elements
|
|
|
|
def test_load_missing_raises(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
with pytest.raises(FileNotFoundError, match=r"Scene 'nope' not found"):
|
|
scene_mod.load_scene("nope")
|
|
|
|
|
|
class TestListAndDelete:
|
|
def test_list_empty_when_dir_missing(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
assert scene_mod.list_scenes() == []
|
|
|
|
def test_list_sorted_names(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
scene_mod.save_scene("zeta", ["x"])
|
|
scene_mod.save_scene("alpha", ["y"])
|
|
|
|
assert scene_mod.list_scenes() == ["alpha", "zeta"]
|
|
|
|
def test_delete_existing(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
scene_mod.save_scene("doomed", ["x"])
|
|
|
|
assert scene_mod.delete_scene("doomed") is True
|
|
assert scene_mod.list_scenes() == []
|
|
|
|
def test_delete_missing(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
assert scene_mod.delete_scene("ghost") is False
|
|
|
|
|
|
class TestResolveScene:
|
|
def test_named_plus_inline_dedup(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
scene_mod.save_scene("base", ["a", "b"])
|
|
|
|
assert scene_mod.resolve_scene(scene="base", scene_prompt="b, c") == ["a", "b", "c"]
|
|
|
|
def test_extra_appends_last(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
|
|
assert scene_mod.resolve_scene(scene_prompt="a, b", extra=["b", "c"]) == ["a", "b", "c"]
|
|
|
|
def test_no_args_empty(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
assert scene_mod.resolve_scene() == []
|
|
|
|
|
|
class TestNameValidation:
|
|
@pytest.mark.parametrize("bad_name", ["", "foo/bar", "../etc", "with space", "a$b"])
|
|
def test_invalid_names_rejected(self, scene_env, bad_name):
|
|
scene_mod, _ = scene_env
|
|
with pytest.raises(ValueError, match=r"Invalid scene name"):
|
|
scene_mod.save_scene(bad_name, ["x"])
|
|
|
|
def test_scene_path_validates(self, scene_env):
|
|
scene_mod, _ = scene_env
|
|
with pytest.raises(ValueError, match=r"Invalid scene name"):
|
|
scene_mod.scene_path("bad name")
|