diff --git a/novelforge/agents/chapter/pipeline.py b/novelforge/agents/chapter/pipeline.py index 262fd00..3891a7b 100644 --- a/novelforge/agents/chapter/pipeline.py +++ b/novelforge/agents/chapter/pipeline.py @@ -186,7 +186,11 @@ def run_chapter_rhythm_classifier( "chapter_num": chapter_num, "failure_summary": failure_summary, }) - return {PASS_FAILURE_KEY: failure_summary} + return { + PASS_FAILURE_KEY: failure_summary, + "recommended_shape_for_this_chapter": "", + "recommendation_reason": "", + } # --------------------------------------------------------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index c345ff6..3c76a77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -227,6 +227,14 @@ def _canned_llm_response(messages, *, action="", json_mode=False): ] }) + # Chapter rhythm classifier (returns JSON with a named rhythm shape) + if "classifying" in act or ("rhythm" in act and "chapter" in act): + return json.dumps({ + "detected_patterns": [], + "recommended_shape_for_this_chapter": "lyrical-interlude", + "recommendation_reason": "Default rhythm for test purposes.", + }) + if "revision" in act: return "The revised chapter content with improvements." diff --git a/tests/test_chapter_agent_failures.py b/tests/test_chapter_agent_failures.py index 24011ce..f8f38fb 100644 --- a/tests/test_chapter_agent_failures.py +++ b/tests/test_chapter_agent_failures.py @@ -180,10 +180,19 @@ def test_rhythm_classifier_returns_dict_with_pass_failure_key(self, monkeypatch) assert "boom" in result[PASS_FAILURE_KEY] def test_rhythm_classifier_fallback_is_otherwise_empty(self, monkeypatch): - """The fallback dict must not carry false business data (only PASS_FAILURE_KEY).""" + """The fallback dict must carry PASS_FAILURE_KEY and safe-default business keys.""" _patch_chapter_call_llm(monkeypatch, _raise_runtime("x")) result = run_chapter_rhythm_classifier(**_CHAPTER_KWARGS_STR, title="Novel") - assert set(result.keys()) == {PASS_FAILURE_KEY} + assert PASS_FAILURE_KEY in result + # Minimal safe defaults must be present so callers can .get() without KeyError + assert "recommended_shape_for_this_chapter" in result + assert "recommendation_reason" in result + # No spurious keys beyond the three above + assert set(result.keys()) == { + PASS_FAILURE_KEY, + "recommended_shape_for_this_chapter", + "recommendation_reason", + } # --------------------------------------------------------------------------- diff --git a/tests/test_illustration_job.py b/tests/test_illustration_job.py index 9fe7bba..b6b1114 100644 --- a/tests/test_illustration_job.py +++ b/tests/test_illustration_job.py @@ -160,14 +160,17 @@ def test_warning_logged_when_novel_progress_entry_missing( # Patch progress_manager.update so that the first call (linking # illustration_token onto the novel entry) raises KeyError, simulating # a deleted entry. - original_update = pm.update + # NOTE: patch the *class* method, not the instance, to avoid leaving a + # stale instance attribute on the singleton after monkeypatch teardown + # (which would bypass class-level spies installed by other tests). + original_class_update = type(pm).update - def _raise_on_novel_token(tok, data): + def _raise_on_novel_token(self, tok, data): if "illustration_token" in data: raise KeyError(tok) - return original_update(tok, data) + return original_class_update(self, tok, data) - monkeypatch.setattr(pm, "update", _raise_on_novel_token) + monkeypatch.setattr(type(pm), "update", _raise_on_novel_token) token = "deleted-novel-link" _done_novel(token)