refactor(domain): strip live filesystem I/O from VOs and entities

DDD-pure cleanup — entities and value objects no longer query the world
at read time.

  FilePath: drop .exists() / .is_file() / .is_dir(). The VO is now a
    pure address; ask the injected FilesystemScanner for live state.
  Movie:    drop .has_file() / .is_downloaded(). Invariant: when the
    application sets file_path, it has already constated the file
    exists; downstream readers trust the snapshot.
  Episode:  same — drop .has_file() / .is_downloaded().
  SubtitlePlacer: drop the pre-check .exists() calls. The placer now
    attempts os.link() and reports FileNotFoundError / FileExistsError
    as skip reasons. Removes a TOCTOU race as a bonus.

Tests adjusted: the FilePath VO method tests are gone (the methods are
gone), test_has_file_false_when_no_path replaced by a plain assertion
on file_path is None. Placer tests are unchanged — the skip-reason
strings ('not found', 'already exists') match the new try/except paths.

The 'snapshot value objects' pattern (ProbedMediaInfo, TmdbMovieInfo)
that this cleanup enables is documented in refactor_domain_io.md, to
be applied when a future use case actually needs richer metadata —
not now, no speculative VOs.
This commit is contained in:
2026-05-19 14:58:59 +02:00
parent e6ee700825
commit 9556bf9e08
6 changed files with 9 additions and 57 deletions
-17
View File
@@ -72,23 +72,6 @@ class TestFilePath:
with pytest.raises(ValidationError):
FilePath(123) # type: ignore
def test_exists_true(self, tmp_path):
p = FilePath(tmp_path)
assert p.exists()
def test_exists_false(self, tmp_path):
p = FilePath(tmp_path / "nonexistent")
assert not p.exists()
def test_is_file(self, tmp_path):
f = tmp_path / "file.txt"
f.write_text("x")
assert FilePath(f).is_file()
assert not FilePath(tmp_path).is_file()
def test_is_dir(self, tmp_path):
assert FilePath(tmp_path).is_dir()
def test_str(self, tmp_path):
p = FilePath(tmp_path)
assert str(p) == str(tmp_path)
+2 -3
View File
@@ -157,10 +157,9 @@ class TestEpisode:
assert filename.startswith("S01E05")
assert "Gray.Matter" in filename
def test_has_file_false_when_no_path(self):
def test_file_path_unset_by_default(self):
e = self._ep()
assert not e.has_file()
assert not e.is_downloaded()
assert e.file_path is None
def test_str_format(self):
e = self._ep(season=2, episode=3, title="Bit by a Dead Bee")