feat(taskflow): add core task API, storage persistence, csv export, stats page, and test coverage
This commit is contained in:
83
tests/test_repository_atomicity.py
Normal file
83
tests/test_repository_atomicity.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.storage import DATA_FORMAT_VERSION, StoredTask
|
||||
from app.storage.repository import JsonFileTaskRepository
|
||||
|
||||
|
||||
def make_task(title: str = "Task") -> StoredTask:
|
||||
return StoredTask(
|
||||
id=uuid4(),
|
||||
title=title,
|
||||
created_at=datetime.now(UTC),
|
||||
started_at=None,
|
||||
done_at=None,
|
||||
)
|
||||
|
||||
|
||||
def test_interrupted_save_before_rename_keeps_original_data_intact(tmp_path, monkeypatch):
|
||||
file_path = tmp_path / "tasks.json"
|
||||
repo = JsonFileTaskRepository(file_path)
|
||||
original_task = make_task("Original")
|
||||
repo.create_task(original_task)
|
||||
|
||||
replacement_called = False
|
||||
real_replace = Path.replace
|
||||
|
||||
def failing_replace(self: Path, target: Path):
|
||||
nonlocal replacement_called
|
||||
replacement_called = True
|
||||
raise RuntimeError("simulated crash before rename")
|
||||
|
||||
monkeypatch.setattr(Path, "replace", failing_replace)
|
||||
|
||||
with pytest.raises(RuntimeError, match="simulated crash before rename"):
|
||||
repo.create_task(make_task("New task"))
|
||||
|
||||
assert replacement_called is True
|
||||
|
||||
reloaded_repo = JsonFileTaskRepository(file_path)
|
||||
assert reloaded_repo.list_tasks() == [original_task]
|
||||
|
||||
tmp_file = tmp_path / "tasks.json.tmp"
|
||||
assert tmp_file.exists()
|
||||
|
||||
monkeypatch.setattr(Path, "replace", real_replace)
|
||||
|
||||
|
||||
def test_normal_save_replaces_file_with_complete_payload(tmp_path):
|
||||
file_path = tmp_path / "tasks.json"
|
||||
repo = JsonFileTaskRepository(file_path)
|
||||
task_1 = make_task("One")
|
||||
task_2 = make_task("Two")
|
||||
|
||||
repo.create_task(task_1)
|
||||
repo.create_task(task_2)
|
||||
|
||||
payload = json.loads(file_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert payload["version"] == DATA_FORMAT_VERSION
|
||||
assert len(payload["tasks"]) == 2
|
||||
assert {item["id"] for item in payload["tasks"]} == {str(task_1.id), str(task_2.id)}
|
||||
|
||||
|
||||
def test_stale_tmp_file_does_not_break_startup(tmp_path):
|
||||
file_path = tmp_path / "tasks.json"
|
||||
repo_1 = JsonFileTaskRepository(file_path)
|
||||
task = make_task("Persisted")
|
||||
repo_1.create_task(task)
|
||||
|
||||
stale_tmp = tmp_path / "tasks.json.tmp"
|
||||
stale_tmp.write_text('{"version": 999, "tasks": "broken"}', encoding="utf-8")
|
||||
|
||||
repo_2 = JsonFileTaskRepository(file_path)
|
||||
|
||||
assert repo_2.list_tasks() == [task]
|
||||
assert stale_tmp.exists()
|
||||
|
||||
Reference in New Issue
Block a user