Create the alignment cache directory before the first model call

The client writes each answer as it arrives, so a cache directory that was
never created turns every model call into an error - and the fallback would
quietly turn the whole book into a mechanical split while the run still
reported success.

The directory is now created by the command line entry point, and the client
is constructed with it rather than having it assigned afterwards. A test
pins it.

45 tests, ruff clean.
This commit is contained in:
Umbra Tools
2026-11-06 19:31:41 -08:00
parent 996fe37f4f
commit f0c2317ca0
2 changed files with 73 additions and 2 deletions

View File

@@ -692,8 +692,13 @@ def main(argv: list[str] | None = None) -> int:
llm = None
if not args.no_llm:
llm = LLM(base_url=args.base_url) if args.base_url else LLM()
llm.cache_dir = args.run_dir / "cache"
# The cache directory has to exist before the first call: the client
# writes the answer as it arrives, and a missing directory would turn
# every model answer into an error - which the fallback would then
# quietly turn into a mechanical split for the whole book.
cache_dir = args.run_dir / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
llm = LLM(base_url=args.base_url, cache_dir=cache_dir) if args.base_url else LLM(cache_dir=cache_dir)
selected = select_chapters(book, args.chapters)
chapters: list[tuple[dict, list[ParagraphAlignment]]] = []

View File

@@ -331,5 +331,71 @@ class DeliveryTests(unittest.TestCase):
self.assertNotIn("text", collapsed[0])
class CommandTests(unittest.TestCase):
def test_the_cache_directory_is_created_before_the_first_call(self) -> None:
"""A missing cache directory would fail every model answer.
The client writes each answer as it arrives, so a cache directory that
was never created turns every model call into an error - and the
fallback would quietly turn the whole book into a mechanical split.
"""
import json
import tempfile
from unittest import mock
from align import main
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
book = root / "book.json"
book.write_text(
json.dumps(
{
"target_language": "zh-CN",
"chapters": [
{
"id": "ch-1",
"index": 1,
"title": "One",
"title_zh": "一",
"matter": "body",
"paragraphs": [],
}
],
}
),
encoding="utf-8",
)
bundle = root / "bundle.jsonl"
bundle.write_text("", encoding="utf-8")
run_dir = root / "runs"
run_dir.mkdir()
seen: dict[str, object] = {}
class StubLLM:
def __init__(self, **kwargs: object) -> None:
seen.update(kwargs)
with mock.patch.object(sys.modules["align"], "LLM", StubLLM):
exit_code = main(
[
"--book",
str(book),
"--run-dir",
str(run_dir),
"--out",
str(root / "delivery.json"),
"--bundle",
str(bundle),
]
)
self.assertEqual(exit_code, 0)
cache_dir = seen.get("cache_dir")
self.assertIsNotNone(cache_dir)
assert cache_dir is not None
self.assertTrue(Path(cache_dir).is_dir())
if __name__ == "__main__":
unittest.main()