---
phase: 19-testing
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - tests/test_utils_json.py
  - tests/test_utils_yaml.py
  - tests/test_utils_slugify.py
  - tests/test_utils_journal.py
autonomous: true
requirements:
  - TEST-04

must_haves:
  truths:
    - "read_json handles valid JSON, invalid JSON, missing file, empty file"
    - "write_json writes nested data, creates parent dirs, handles empty dict"
    - "read_jsonl handles valid JSONL, empty file, missing file, malformed lines"
    - "write_jsonl writes rows correctly, handles empty list"
    - "yaml_quote handles regular strings, quotes, backslashes, booleans, None"
    - "yaml_block handles multi-line text, empty input, None input"
    - "yaml_list handles string lists, empty lists, None, mixed values"
    - "slugify_filename strips illegal chars, truncates at 120, falls back to 'untitled'"
    - "_extract_year finds 4-digit years in strings, returns '' on no match"
    - "load_journal_db loads valid zoterostyle.json, returns {} on missing/bad file"
    - "lookup_impact_factor prefers SCI IF from DB, falls back to extra field match"
  artifacts:
    - path: "tests/test_utils_json.py"
      provides: "Unit tests for read_json, write_json, read_jsonl, write_jsonl"
      min_lines: 60
    - path: "tests/test_utils_yaml.py"
      provides: "Unit tests for yaml_quote, yaml_block, yaml_list"
      min_lines: 60
    - path: "tests/test_utils_slugify.py"
      provides: "Unit tests for slugify_filename, _extract_year"
      min_lines: 40
    - path: "tests/test_utils_journal.py"
      provides: "Unit tests for load_journal_db, lookup_impact_factor"
      min_lines: 60
  key_links:
    - from: "tests/test_utils_json.py"
      to: "paperforge/worker/_utils.py"
      via: "from paperforge.worker._utils import read_json, write_json, read_jsonl, write_jsonl"
    - from: "tests/test_utils_yaml.py"
      to: "paperforge/worker/_utils.py"
      via: "from paperforge.worker._utils import yaml_quote, yaml_block, yaml_list"
    - from: "tests/test_utils_slugify.py"
      to: "paperforge/worker/_utils.py"
      via: "from paperforge.worker._utils import slugify_filename, _extract_year"
    - from: "tests/test_utils_journal.py"
      to: "paperforge/worker/_utils.py"
      via: "from paperforge.worker._utils import load_journal_db, lookup_impact_factor"
---

<objective>
Create 4 dedicated unit test files covering ALL `paperforge/worker/_utils.py` functions.

Purpose: Close TEST-04 — the last testing gap in v1.4 code health phase. These tests validate the shared utility module that every worker depends on.
Output: 4 test files, ~220+ total test lines, 0 new failures in existing suite.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
</execution_context>

<context>
@paperforge/worker/_utils.py
@tests/conftest.py
@tests/test_smoke.py
@tests/test_selection_sync_pdf.py
</context>

<interfaces>
Functions under test (from paperforge/worker/_utils.py):

```python
def read_json(path: Path) -> dict | list: ...
def write_json(path: Path, data) -> None: ...
def read_jsonl(path: Path) -> list[dict]: ...
def write_jsonl(path: Path, rows) -> None: ...
def yaml_quote(value: str) -> str: ...
def yaml_block(value: str) -> list[str]: ...
def yaml_list(key: str, values) -> list[str]: ...
def slugify_filename(text: str) -> str: ...
def _extract_year(value: str) -> str: ...
def load_journal_db(vault: Path) -> dict[str, dict]: ...
def lookup_impact_factor(journal_name: str, extra: str, vault: Path) -> str: ...
```

Test patterns to follow:
- `test_selection_sync_pdf.py` uses `_make_vault(tmp_path)` for isolated vault creation
- `test_repair.py` uses `_make_vault(tmp_path)` + manual file writes
- `test_ocr_state_machine.py` uses `_make_vault(tmp_path)` + `_mock_paths()`
- All use `tmp_path` pytest fixture for temporary directories
- Use `conftest.py`'s `test_vault` fixture only if a full vault with paperforge.json is needed (e.g., load_journal_db)
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Create test_utils_json.py — JSON I/O unit tests</name>
  <files>tests/test_utils_json.py</files>
  <action>
Create `tests/test_utils_json.py` with 4 test classes (one per function) using `tmp_path`:

```python
"""Unit tests for _utils.py JSON I/O functions.

Covers: read_json, write_json, read_jsonl, write_jsonl
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from paperforge.worker._utils import read_json, read_jsonl, write_json, write_jsonl


class TestReadJson:
    """read_json(path) -> dict|list"""

    def test_reads_valid_json(self, tmp_path: Path) -> None:
        p = tmp_path / "data.json"
        p.write_text('{"key": "value"}', encoding="utf-8")
        assert read_json(p) == {"key": "value"}

    def test_reads_list_json(self, tmp_path: Path) -> None:
        p = tmp_path / "list.json"
        p.write_text('[1, 2, 3]', encoding="utf-8")
        assert read_json(p) == [1, 2, 3]

    def test_invalid_json_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.json"
        p.write_text("{invalid}", encoding="utf-8")
        with pytest.raises(json.JSONDecodeError):
            read_json(p)

    def test_missing_file_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "nope.json"
        with pytest.raises(FileNotFoundError):
            read_json(p)

    def test_empty_file_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "empty.json"
        p.write_text("", encoding="utf-8")
        with pytest.raises(json.JSONDecodeError):
            read_json(p)


class TestWriteJson:
    """write_json(path, data)"""

    def test_writes_dict(self, tmp_path: Path) -> None:
        p = tmp_path / "out.json"
        write_json(p, {"a": 1, "b": 2})
        assert json.loads(p.read_text(encoding="utf-8")) == {"a": 1, "b": 2}

    def test_writes_nested_data(self, tmp_path: Path) -> None:
        p = tmp_path / "nested.json"
        data = {"items": [{"id": 1}, {"id": 2}]}
        write_json(p, data)
        assert json.loads(p.read_text(encoding="utf-8")) == data

    def test_creates_parent_dirs(self, tmp_path: Path) -> None:
        p = tmp_path / "a" / "b" / "c" / "deep.json"
        write_json(p, {"key": "val"})
        assert p.exists()
        assert json.loads(p.read_text(encoding="utf-8")) == {"key": "val"}

    def test_writes_pretty_indent(self, tmp_path: Path) -> None:
        p = tmp_path / "pretty.json"
        write_json(p, {"x": 1})
        text = p.read_text(encoding="utf-8")
        assert "{\n" in text, "should be indented with newlines"
        assert "  " in text, "should use 2-space indent"

    def test_writes_empty_dict(self, tmp_path: Path) -> None:
        p = tmp_path / "empty.json"
        write_json(p, {})
        assert json.loads(p.read_text(encoding="utf-8")) == {}


class TestReadJsonl:
    """read_jsonl(path) -> list[dict]"""

    def test_reads_valid_jsonl(self, tmp_path: Path) -> None:
        p = tmp_path / "data.jsonl"
        p.write_text('{"a": 1}\n{"b": 2}\n', encoding="utf-8")
        assert read_jsonl(p) == [{"a": 1}, {"b": 2}]

    def test_skips_empty_lines(self, tmp_path: Path) -> None:
        p = tmp_path / "sparse.jsonl"
        p.write_text('{"a": 1}\n\n{"b": 2}\n', encoding="utf-8")
        assert read_jsonl(p) == [{"a": 1}, {"b": 2}]

    def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
        p = tmp_path / "nope.jsonl"
        assert read_jsonl(p) == []

    def test_empty_file_returns_empty(self, tmp_path: Path) -> None:
        p = tmp_path / "empty.jsonl"
        p.write_text("", encoding="utf-8")
        assert read_jsonl(p) == []

    def test_malformed_line_raises(self, tmp_path: Path) -> None:
        p = tmp_path / "bad.jsonl"
        p.write_text('{"ok": 1}\n{broken}\n', encoding="utf-8")
        with pytest.raises(json.JSONDecodeError):
            read_jsonl(p)


class TestWriteJsonl:
    """write_jsonl(path, rows)"""

    def test_writes_rows(self, tmp_path: Path) -> None:
        p = tmp_path / "out.jsonl"
        write_jsonl(p, [{"a": 1}, {"b": 2}])
        lines = p.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == 2
        assert json.loads(lines[0]) == {"a": 1}
        assert json.loads(lines[1]) == {"b": 2}

    def test_handles_empty_list(self, tmp_path: Path) -> None:
        p = tmp_path / "empty.jsonl"
        write_jsonl(p, [])
        text = p.read_text(encoding="utf-8")
        assert text == "" or text.strip() == ""

    def test_creates_parent_dirs(self, tmp_path: Path) -> None:
        p = tmp_path / "x" / "y" / "out.jsonl"
        write_jsonl(p, [{"k": "v"}])
        assert p.exists()
        assert json.loads(p.read_text(encoding="utf-8").strip().splitlines()[0]) == {"k": "v"}
```

Use only stdlib + pytest imports. Do NOT import from conftest.
  </action>
  <verify>
    <automated>python -m pytest tests/test_utils_json.py -x -q</automated>
  </verify>
  <done>All JSON I/O tests pass: read/write JSON, read/write JSONL, edge cases (empty, missing, malformed, nested dirs)</done>
</task>

<task type="auto">
  <name>Task 2: Create test_utils_yaml.py — YAML helpers unit tests</name>
  <files>tests/test_utils_yaml.py</files>
  <action>
Create `tests/test_utils_yaml.py` with 3 test classes covering all YAML helper functions:

```python
"""Unit tests for _utils.py YAML helper functions.

Covers: yaml_quote, yaml_block, yaml_list
"""

from __future__ import annotations

from pathlib import Path

import pytest

from paperforge.worker._utils import yaml_block, yaml_list, yaml_quote


class TestYamlQuote:
    """yaml_quote(value: str) -> str"""

    def test_plain_string(self) -> None:
        assert yaml_quote("hello") == '"hello"'

    def test_string_with_quotes(self) -> None:
        assert yaml_quote('say "hello"') == '"say \\"hello\\""'

    def test_string_with_backslash(self) -> None:
        assert yaml_quote("a\\b") == '"a\\\\b"'

    def test_boolean_true(self) -> None:
        assert yaml_quote(True) == "true"

    def test_boolean_false(self) -> None:
        assert yaml_quote(False) == "false"

    def test_none_becomes_empty_quoted(self) -> None:
        assert yaml_quote(None) == '""'  # str(None) -> "None" -> hmm

    def test_empty_string(self) -> None:
        assert yaml_quote("") == '""'

    def test_int_converts_to_string(self) -> None:
        assert yaml_quote(42) == '"42"'

    def test_string_with_only_special_yaml_chars(self) -> None:
        assert yaml_quote(": ") == '": "'


class TestYamlBlock:
    """yaml_block(value: str) -> list[str]"""

    def test_single_line(self) -> None:
        result = yaml_block("Hello world")
        assert result[0] == "abstract: |-"
        assert "Hello world" in result[1]

    def test_multi_line(self) -> None:
        text = "Line 1\nLine 2\nLine 3"
        result = yaml_block(text)
        assert result[0] == "abstract: |-"
        assert len(result) == 4  # header + 3 lines
        assert "  Line 1" in result[1]
        assert "  Line 2" in result[2]
        assert "  Line 3" in result[3]

    def test_empty_string(self) -> None:
        result = yaml_block("")
        assert len(result) == 2
        assert "abstract: |-" in result[0]

    def test_none_input(self) -> None:
        result = yaml_block(None)  # type: ignore[arg-type]
        assert len(result) == 2
        assert "abstract: |-" in result[0]

    def test_whitespace_only(self) -> None:
        result = yaml_block("   ")
        assert len(result) == 2  # treated as empty after strip
        assert "abstract: |-" in result[0]

    def test_trailing_newline_stripped(self) -> None:
        result = yaml_block("Hello\n")
        assert len(result) == 2  # stripped -> single line
        assert "  Hello" in result[1]

    def test_lines_are_indented_with_two_spaces(self) -> None:
        result = yaml_block("Test")
        assert result[1].startswith("  ")


class TestYamlList:
    """yaml_list(key: str, values) -> list[str]"""

    def test_list_of_strings(self) -> None:
        result = yaml_list("tags", ["a", "b", "c"])
        assert result[0] == "tags:"
        assert "  - " in result[1] and "a" in result[1]
        assert "  - " in result[2] and "b" in result[2]
        assert "  - " in result[3] and "c" in result[3]

    def test_empty_list(self) -> None:
        result = yaml_list("tags", [])
        assert result == ["tags: []"]

    def test_none_values_filtered(self) -> None:
        result = yaml_list("tags", ["a", None, "c"])
        assert len(result) == 3  # 3 items minus None = 2, plus header = 3 lines
        assert "a" in result[1]
        assert "c" in result[2]

    def test_all_none_returns_empty_list(self) -> None:
        result = yaml_list("tags", [None, None])
        assert result == ["tags: []"]

    def test_none_input(self) -> None:
        result = yaml_list("tags", None)  # type: ignore[arg-type]
        assert result == ["tags: []"]

    def test_string_with_quotes_in_list(self) -> None:
        result = yaml_list("items", ['hello "world"'])
        assert len(result) == 2
        assert '\\"' in result[1]  # quote should be escaped
```

Important note: For `yaml_quote(None)` — the actual behavior is `str(None)` which produces `"None"` in quotes. The test should match actual behavior, not desired behavior. Let me check what the function does:
```python
def yaml_quote(value: str) -> str:
    if isinstance(value, bool):
        return "true" if value else "false"
    return '"' + str(value or "").replace("\\", "\\\\").replace('"', '\\"') + '"'
```
So `yaml_quote(None)` → `str(None or "")` → `str("")` → `'""'`. Wait, `None or ""` is `""`. So it returns `'""'`. The test for `test_none_becomes_empty_quoted` should assert `== '""'`.

Write the test_none_becomes_empty_quoted as:
```python
    def test_none_becomes_empty_quoted(self) -> None:
        assert yaml_quote(None) == '""'
```
  </action>
  <verify>
    <automated>python -m pytest tests/test_utils_yaml.py -x -q</automated>
  </verify>
  <done>All YAML helper tests pass: yaml_quote handles booleans/specials, yaml_block handles multi-line/empty, yaml_list handles filtering/empty</done>
</task>

<task type="auto">
  <name>Task 3: Create test_utils_slugify.py — slugify and year extraction tests</name>
  <files>tests/test_utils_slugify.py</files>
  <action>
Create `tests/test_utils_slugify.py` with 2 test classes:

```python
"""Unit tests for _utils.py slugify and year extraction.

Covers: slugify_filename, _extract_year
"""

from __future__ import annotations

import pytest

from paperforge.worker._utils import _extract_year, slugify_filename


class TestSlugifyFilename:
    """slugify_filename(text: str) -> str"""

    def test_normal_text_passes_through(self) -> None:
        assert slugify_filename("Hello World") == "Hello World"

    def test_removes_colons(self) -> None:
        assert ":" not in slugify_filename("A: B")

    def test_removes_angle_brackets(self) -> None:
        result = slugify_filename("<tag> value")
        assert "<" not in result and ">" not in result

    def test_removes_windows_illegal_chars(self) -> None:
        result = slugify_filename('a<b>c:d"e/f\\g|h?i*j')
        # None of these should survive: < > : " / \ | ? *
        for char in '<>:"/\\|?*':
            assert char not in result, f"char {char!r} should be removed"

    def test_truncates_to_120_chars(self) -> None:
        long_text = "x" * 200
        result = slugify_filename(long_text)
        assert len(result) == 120

    def test_truncation_at_120_respects_no_illegal_chars(self) -> None:
        long_text = "a" * 50 + "<>" + "b" * 100
        result = slugify_filename(long_text)
        assert len(result) <= 120
        assert "<" not in result

    def test_empty_string_falls_back_to_untitled(self) -> None:
        assert slugify_filename("") == "untitled"

    def test_only_special_chars_falls_back_to_untitled(self) -> None:
        assert slugify_filename("<>:\"/\\|?*") == "untitled"

    def test_whitespace_stripped(self) -> None:
        result = slugify_filename("  hello  ")
        assert result == "hello"
        assert result == result.strip()

    def test_unicode_text_preserved(self) -> None:
        result = slugify_filename("研究论文")
        assert "研究" in result

    def test_none_input(self) -> None:
        assert slugify_filename(None) == "untitled"  # type: ignore[arg-type]


class TestExtractYear:
    """_extract_year(value: str) -> str"""

    def test_finds_2024(self) -> None:
        assert _extract_year("2024-03-15") == "2024"

    def test_finds_1999(self) -> None:
        assert _extract_year("1999") == "1999"

    def test_no_year_returns_empty(self) -> None:
        assert _extract_year("n/a") == ""

    def test_empty_string_returns_empty(self) -> None:
        assert _extract_year("") == ""

    def test_none_input_returns_empty(self) -> None:
        assert _extract_year(None) == ""  # type: ignore[arg-type]

    def test_1900_is_rejected(self) -> None:
        # _extract_year only matches 19xx or 20xx
        assert _extract_year("1900") == "1900"  # actually 19xx IS matched

    def test_1800_is_rejected(self) -> None:
        assert _extract_year("1800") == ""

    def test_year_in_middle_of_text(self) -> None:
        assert _extract_year("Published in 2023 by Elsevier") == "2023"
```
  </action>
  <verify>
    <automated>python -m pytest tests/test_utils_slugify.py -x -q</automated>
  </verify>
  <done>All slugify/year tests pass: illegal chars removed, 120-char truncation, year extraction from various formats</done>
</task>

<task type="auto">
  <name>Task 4: Create test_utils_journal.py — journal DB unit tests</name>
  <files>tests/test_utils_journal.py</files>
  <action>
Create `tests/test_utils_journal.py` with 2 test classes. These tests need a vault with `paperforge.json` and optionally a `zoterostyle.json`. Use `tmp_path` to create minimal vaults — do NOT use the `test_vault` fixture from conftest.py since that creates a full fixture vault (heavy for unit tests).

IMPORTANT: `_utils.py` caches `_JOURNAL_DB` globally across calls within the same process. Tests that check `load_journal_db` loading behavior must account for this cache. Reset the cache between test scenarios by directly setting `from paperforge.worker import _utils; _utils._JOURNAL_DB = None`.

```python
"""Unit tests for _utils.py journal database functions.

Covers: load_journal_db, lookup_impact_factor

NOTE: _utils._JOURNAL_DB is a module-level cache. Tests must reset it
between scenarios by setting _utils._JOURNAL_DB = None before each test.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from paperforge.worker import _utils


def _make_vault(tmp_path: Path) -> Path:
    """Create minimal vault with paperforge.json."""
    vault = tmp_path / "vault"
    vault.mkdir()
    cfg = {
        "system_dir": "99_System",
        "resources_dir": "03_Resources",
        "literature_dir": "Literature",
        "control_dir": "LiteratureControl",
        "base_dir": "05_Bases",
        "skill_dir": ".opencode/skills",
    }
    (vault / "paperforge.json").write_text(json.dumps(cfg), encoding="utf-8")
    return vault


def _reset_cache() -> None:
    """Reset the module-level _JOURNAL_DB cache between tests."""
    _utils._JOURNAL_DB = None


class TestLoadJournalDb:
    """load_journal_db(vault: Path) -> dict[str, dict]"""

    def test_loads_valid_zoterostyle(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        zstyle_path = zstyle_dir / "zoterostyle.json"
        data = {"Journal A": {"rank": {"sciif": "5.2"}}}
        zstyle_path.write_text(json.dumps(data), encoding="utf-8")

        result = _utils.load_journal_db(vault)
        assert result == data

    def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        result = _utils.load_journal_db(vault)
        assert result == {}

    def test_malformed_json_returns_empty(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        (zstyle_dir / "zoterostyle.json").write_text("{broken", encoding="utf-8")

        result = _utils.load_journal_db(vault)
        assert result == {}

    def test_cache_returns_same_object(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        zstyle_path = zstyle_dir / "zoterostyle.json"
        zstyle_path.write_text(json.dumps({"J": {}}), encoding="utf-8")

        first = _utils.load_journal_db(vault)
        second = _utils.load_journal_db(vault)
        assert first is second  # same cached object


class TestLookupImpactFactor:
    """lookup_impact_factor(journal_name, extra, vault) -> str"""

    def test_finds_sciif_in_db(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        zstyle_path = zstyle_dir / "zoterostyle.json"
        zstyle_path.write_text(
            json.dumps({"J Bone Joint Surg Am": {"rank": {"sciif": "5.2"}}}),
            encoding="utf-8",
        )

        # Pre-warm cache
        _utils.load_journal_db(vault)

        result = _utils.lookup_impact_factor("J Bone Joint Surg Am", "", vault)
        assert result == "5.2"

    def test_fallback_to_extra_field(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        extra = "Some text 影响因子: 3.14 more text"
        result = _utils.lookup_impact_factor("Unknown Journal", extra, vault)
        assert result == "3.14"

    def test_extra_field_chinese_colon(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        result = _utils.lookup_impact_factor("Unknown", "影响因子：2.718", vault)
        assert result == "2.718"

    def test_empty_journal_returns_empty(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        result = _utils.lookup_impact_factor("", "anything", vault)
        assert result == ""

    def test_no_match_returns_empty(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        result = _utils.lookup_impact_factor("Nonexistent Journal", "", vault)
        assert result == ""

    def test_db_rank_sciif_int_converted_to_str(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        zstyle_path = zstyle_dir / "zoterostyle.json"
        zstyle_path.write_text(
            json.dumps({"Test J": {"rank": {"sciif": 4}}}, ensure_ascii=False),
            encoding="utf-8",
        )
        _utils.load_journal_db(vault)

        result = _utils.lookup_impact_factor("Test J", "", vault)
        assert result == "4"

    def test_db_rank_sciif_missing_falls_to_extra(self, tmp_path: Path) -> None:
        _reset_cache()
        vault = _make_vault(tmp_path)
        zstyle_dir = vault / "99_System" / "Zotero"
        zstyle_dir.mkdir(parents=True)
        zstyle_path = zstyle_dir / "zoterostyle.json"
        zstyle_path.write_text(
            json.dumps({"Test J": {"rank": {"not_sciif": "x"}}}),
            encoding="utf-8",
        )
        _utils.load_journal_db(vault)

        result = _utils.lookup_impact_factor("Test J", "影响因子: 1.5", vault)
        assert result == "1.5"
```
  </action>
  <verify>
    <automated>python -m pytest tests/test_utils_journal.py -x -q</automated>
  </verify>
  <done>All journal DB tests pass: DB loading with/without file, cache behavior, SCI IF lookup, extra field fallback, edge cases</done>
</task>

</tasks>

<verification>
After all 4 tasks:
```bash
python -m pytest tests/test_utils_json.py tests/test_utils_yaml.py tests/test_utils_slugify.py tests/test_utils_journal.py -x -q
```
All 4 files pass individually. No regressions in existing test suite.

```bash
python -m pytest tests/ -x -q
```
Must show 205+ total tests (existing 205 + 4 new test files).
</verification>

<success_criteria>
1. `tests/test_utils_json.py` exists with 5 test classes, all passing
2. `tests/test_utils_yaml.py` exists with 3 test classes, all passing
3. `tests/test_utils_slugify.py` exists with 2 test classes, all passing
4. `tests/test_utils_journal.py` exists with 2 test classes, all passing
5. Total test count: 205 + ~30 new = 235+ tests, 0 failures, 0 errors
</success_criteria>

<output>
After completion, create `.planning/phases/19-testing/19-01-SUMMARY.md`
</output>
