diff --git a/.coveragerc b/.coveragerc index 7383d158a..58342c6c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,3 +11,7 @@ omit = */__pycache__/* */site-packages/* config*.py + +[report] +omit = + */__pycache__/* diff --git a/tests/test_generate_full.py b/tests/test_generate_full.py index 2d7c2c7ce..8aaa5750e 100644 --- a/tests/test_generate_full.py +++ b/tests/test_generate_full.py @@ -73,48 +73,45 @@ def test_generate_output_ndjson(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - # Create a test generator in muse directory (with explicit sources) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" - test_generator = muse_dir / "test_gen.md" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + test_generator = tmp_path / "test_gen.md" test_generator.write_text( '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + # Mock the underlying generation function in think.models + import think.models - config = { - "name": "test_gen", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - events = run_generator_with_config(mod, config, monkeypatch) + config = { + "name": "test_gen", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } - # Should have start and finish events - assert len(events) >= 2 - assert events[0]["event"] == "start" - assert events[0]["name"] == "test_gen" + events = run_generator_with_config(mod, config, monkeypatch) - # Find finish event - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert finish_events[0]["result"] == MOCK_RESULT["text"] + # Should have start and finish events + assert len(events) >= 2 + assert events[0]["event"] == "start" + assert events[0]["name"] == "test_gen" - finally: - if test_generator.exists(): - test_generator.unlink() + # Find finish event + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert finish_events[0]["result"] == MOCK_RESULT["text"] def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): @@ -122,9 +119,11 @@ def test_generate_hook_invoked_with_context(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - # Create the hook file in muse/ directory - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" - hook_file = muse_dir / "test_hook.py" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + hook_file = tmp_path / "test_hook.py" hook_file.write_text(""" def post_process(result, context): import json @@ -145,56 +144,47 @@ def post_process(result, context): return None """) - # Create generator with hook (new format, with explicit sources) - test_generator = muse_dir / "hooked_gen.md" + test_generator = tmp_path / "hooked_gen.md" test_generator.write_text( '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "test_hook"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "hooked_gen", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } - - events = run_generator_with_config(mod, config, monkeypatch) - - # Should have start and finish events - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - - # Read captured context - captured_path = tmp_path / "20240101" / "agents" / "context_captured.json" - captured = json.loads(captured_path.read_text()) - - assert captured["day"] == "20240101" - assert captured["segment"] is None - # span_mode is a bool in the new config structure - assert captured["span"] is False - assert captured["name"] == "hooked_gen" - assert captured["has_transcript"] is True - assert captured["has_hook"] is True # Frontmatter fields now directly in config - - finally: - # Clean up test files - if hook_file.exists(): - hook_file.unlink() - if test_generator.exists(): - test_generator.unlink() + # Mock the underlying generation function in think.models + import think.models + + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + + config = { + "name": "hooked_gen", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } + + events = run_generator_with_config(mod, config, monkeypatch) + + # Should have start and finish events + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + + # Read captured context + captured_path = tmp_path / "20240101" / "agents" / "context_captured.json" + captured = json.loads(captured_path.read_text()) + + assert captured["day"] == "20240101" + assert captured["segment"] is None + # span_mode is a bool in the new config structure + assert captured["span"] is False + assert captured["name"] == "hooked_gen" + assert captured["has_transcript"] is True + assert captured["has_hook"] is True # Frontmatter fields now directly in config def test_generate_without_hook_succeeds(tmp_path, monkeypatch): @@ -202,44 +192,41 @@ def test_generate_without_hook_succeeds(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - # Create generator without hook (with explicit sources) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" - test_generator = muse_dir / "nohook_gen.md" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + test_generator = tmp_path / "nohook_gen.md" test_generator.write_text( '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nNo hook prompt' ) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + # Mock the underlying generation function in think.models + import think.models - config = { - "name": "nohook_gen", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - events = run_generator_with_config(mod, config, monkeypatch) + config = { + "name": "nohook_gen", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } - # Should have start and finish events - assert len(events) >= 2 - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert finish_events[0]["result"] == MOCK_RESULT["text"] + events = run_generator_with_config(mod, config, monkeypatch) - finally: - if test_generator.exists(): - test_generator.unlink() + # Should have start and finish events + assert len(events) >= 2 + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert finish_events[0]["result"] == MOCK_RESULT["text"] def test_generate_error_event_on_missing_generator(tmp_path, monkeypatch): @@ -272,35 +259,32 @@ def test_generate_skipped_on_no_input(tmp_path, monkeypatch): day_dir = day_path("20240101") day_dir.mkdir(parents=True, exist_ok=True) - # Create a test generator (with explicit sources) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" - test_generator = muse_dir / "empty_gen.md" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + test_generator = tmp_path / "empty_gen.md" test_generator.write_text( '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - try: - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "empty_gen", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - events = run_generator_with_config(mod, config, monkeypatch) + config = { + "name": "empty_gen", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } - # Should have start and finish with skipped - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert finish_events[0].get("skipped") == "no_input" + events = run_generator_with_config(mod, config, monkeypatch) - finally: - if test_generator.exists(): - test_generator.unlink() + # Should have start and finish with skipped + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert finish_events[0].get("skipped") == "no_input" def test_named_hook_resolution(tmp_path, monkeypatch): diff --git a/tests/test_output_hooks.py b/tests/test_output_hooks.py index 26436e153..9768bb88f 100644 --- a/tests/test_output_hooks.py +++ b/tests/test_output_hooks.py @@ -163,15 +163,17 @@ def test_output_hook_invocation(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - # Create generator with hook in muse directory - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" + # Use tmp_path as muse directory to avoid polluting real muse/ + import think.muse - prompt_file = muse_dir / "hooked_test.md" + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + prompt_file = tmp_path / "hooked_test.md" prompt_file.write_text( '{\n "type": "generate",\n "title": "Hooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "hooked_test"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - hook_file = muse_dir / "hooked_test.py" + hook_file = tmp_path / "hooked_test.py" hook_file.write_text(""" def post_process(result, context): # Verify context has expected fields @@ -181,41 +183,34 @@ def post_process(result, context): return result + "\\n\\n## Hook was here" """) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "hooked_test", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } - - events = run_generator_with_config(mod, config, monkeypatch) - - # Find finish event - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - - content = finish_events[0]["result"] - assert "## Original Result" in content - assert "## Hook was here" in content - - finally: - if hook_file.exists(): - hook_file.unlink() - if prompt_file.exists(): - prompt_file.unlink() + # Mock the underlying generation function in think.models + import think.models + + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + + config = { + "name": "hooked_test", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } + + events = run_generator_with_config(mod, config, monkeypatch) + + # Find finish event + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + + content = finish_events[0]["result"] + assert "## Original Result" in content + assert "## Hook was here" in content def test_output_hook_returns_none(tmp_path, monkeypatch): @@ -223,50 +218,45 @@ def test_output_hook_returns_none(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) - prompt_file = muse_dir / "noop_test.md" + prompt_file = tmp_path / "noop_test.md" prompt_file.write_text( '{\n "type": "generate",\n "title": "Noop",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "noop_test"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - hook_file = muse_dir / "noop_test.py" + hook_file = tmp_path / "noop_test.py" hook_file.write_text(""" def post_process(result, context): return None # Signal to use original """) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "noop_test", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } - - events = run_generator_with_config(mod, config, monkeypatch) - - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert finish_events[0]["result"] == MOCK_RESULT["text"] - - finally: - if hook_file.exists(): - hook_file.unlink() - if prompt_file.exists(): - prompt_file.unlink() + # Mock the underlying generation function in think.models + import think.models + + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + + config = { + "name": "noop_test", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } + + events = run_generator_with_config(mod, config, monkeypatch) + + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert finish_events[0]["result"] == MOCK_RESULT["text"] def test_output_hook_error_fallback(tmp_path, monkeypatch): @@ -274,51 +264,46 @@ def test_output_hook_error_fallback(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" + import think.muse - prompt_file = muse_dir / "broken_test.md" + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + prompt_file = tmp_path / "broken_test.md" prompt_file.write_text( '{\n "type": "generate",\n "title": "Broken",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"post": "broken_test"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nTest prompt' ) - hook_file = muse_dir / "broken_test.py" + hook_file = tmp_path / "broken_test.py" hook_file.write_text(""" def post_process(result, context): raise RuntimeError("Hook exploded!") """) - try: - # Mock the underlying generation function in think.models - import think.models - - monkeypatch.setattr( - think.models, - "generate_with_result", - lambda *a, **k: MOCK_RESULT, - ) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "broken_test", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } - - # Should not raise, should fall back gracefully - events = run_generator_with_config(mod, config, monkeypatch) - - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert finish_events[0]["result"] == MOCK_RESULT["text"] - - finally: - if hook_file.exists(): - hook_file.unlink() - if prompt_file.exists(): - prompt_file.unlink() + # Mock the underlying generation function in think.models + import think.models + + monkeypatch.setattr( + think.models, + "generate_with_result", + lambda *a, **k: MOCK_RESULT, + ) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + + config = { + "name": "broken_test", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } + + # Should not raise, should fall back gracefully + events = run_generator_with_config(mod, config, monkeypatch) + + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert finish_events[0]["result"] == MOCK_RESULT["text"] # ============================================================================= @@ -396,14 +381,16 @@ def test_pre_hook_invocation(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" + import think.muse - prompt_file = muse_dir / "prehooked_test.md" + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) + + prompt_file = tmp_path / "prehooked_test.md" prompt_file.write_text( '{\n "type": "generate",\n "title": "Prehooked",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "prehooked_test"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nOriginal prompt' ) - hook_file = muse_dir / "prehooked_test.py" + hook_file = tmp_path / "prehooked_test.py" hook_file.write_text(""" def pre_process(context): # Verify context has expected fields @@ -414,46 +401,39 @@ def pre_process(context): return {"prompt": context["prompt"] + " [pre-processed]"} """) - try: - # Track what generate_with_result receives - received_kwargs = {} - - def mock_generate(*args, **kwargs): - received_kwargs.update(kwargs) - received_kwargs["contents"] = args[0] if args else kwargs.get("contents") - return MOCK_RESULT + # Track what generate_with_result receives + received_kwargs = {} - import think.models + def mock_generate(*args, **kwargs): + received_kwargs.update(kwargs) + received_kwargs["contents"] = args[0] if args else kwargs.get("contents") + return MOCK_RESULT - monkeypatch.setattr(think.models, "generate_with_result", mock_generate) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + import think.models - config = { - "name": "prehooked_test", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } + monkeypatch.setattr(think.models, "generate_with_result", mock_generate) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - events = run_generator_with_config(mod, config, monkeypatch) + config = { + "name": "prehooked_test", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } - # Verify pre-hook modified the prompt - check in contents - contents = received_kwargs.get("contents", []) - # The prompt should contain [pre-processed] - prompt_found = any("[pre-processed]" in str(c) for c in contents) - assert prompt_found, f"Expected [pre-processed] in contents: {contents}" + events = run_generator_with_config(mod, config, monkeypatch) - # Verify generator still completed successfully - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 + # Verify pre-hook modified the prompt - check in contents + contents = received_kwargs.get("contents", []) + # The prompt should contain [pre-processed] + prompt_found = any("[pre-processed]" in str(c) for c in contents) + assert prompt_found, f"Expected [pre-processed] in contents: {contents}" - finally: - if hook_file.exists(): - hook_file.unlink() - if prompt_file.exists(): - prompt_file.unlink() + # Verify generator still completed successfully + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 def test_both_pre_and_post_hooks(tmp_path, monkeypatch): @@ -461,14 +441,16 @@ def test_both_pre_and_post_hooks(tmp_path, monkeypatch): mod = importlib.import_module("think.agents") copy_day(tmp_path) - muse_dir = Path(mod.__file__).resolve().parent.parent / "muse" + import think.muse + + monkeypatch.setattr(think.muse, "MUSE_DIR", tmp_path) - prompt_file = muse_dir / "both_hooks_test.md" + prompt_file = tmp_path / "both_hooks_test.md" prompt_file.write_text( '{\n "type": "generate",\n "title": "Both Hooks",\n "schedule": "daily",\n "priority": 10,\n "output": "md",\n "hook": {"pre": "both_hooks_test", "post": "both_hooks_test"},\n "instructions": {"system": "journal", "sources": {"audio": true, "screen": true}}\n}\n\nOriginal prompt' ) - hook_file = muse_dir / "both_hooks_test.py" + hook_file = tmp_path / "both_hooks_test.py" hook_file.write_text(""" def pre_process(context): return {"prompt": context["prompt"] + " [pre]"} @@ -477,42 +459,35 @@ def post_process(result, context): return result + "\\n\\n[post]" """) - try: - received_kwargs = {} - - def mock_generate(*args, **kwargs): - received_kwargs.update(kwargs) - received_kwargs["contents"] = args[0] if args else kwargs.get("contents") - return MOCK_RESULT - - import think.models - - monkeypatch.setattr(think.models, "generate_with_result", mock_generate) - monkeypatch.setenv("GOOGLE_API_KEY", "x") - monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) - - config = { - "name": "both_hooks_test", - "day": "20240101", - "output": "md", - "provider": "google", - "model": "gemini-2.0-flash", - } - - events = run_generator_with_config(mod, config, monkeypatch) - - # Verify pre-hook modified the prompt - check in contents - contents = received_kwargs.get("contents", []) - prompt_found = any("[pre]" in str(c) for c in contents) - assert prompt_found, f"Expected [pre] in contents: {contents}" - - # Verify post-hook modified the result - finish_events = [e for e in events if e["event"] == "finish"] - assert len(finish_events) == 1 - assert "[post]" in finish_events[0]["result"] - - finally: - if hook_file.exists(): - hook_file.unlink() - if prompt_file.exists(): - prompt_file.unlink() + received_kwargs = {} + + def mock_generate(*args, **kwargs): + received_kwargs.update(kwargs) + received_kwargs["contents"] = args[0] if args else kwargs.get("contents") + return MOCK_RESULT + + import think.models + + monkeypatch.setattr(think.models, "generate_with_result", mock_generate) + monkeypatch.setenv("GOOGLE_API_KEY", "x") + monkeypatch.setenv("JOURNAL_PATH", str(tmp_path)) + + config = { + "name": "both_hooks_test", + "day": "20240101", + "output": "md", + "provider": "google", + "model": "gemini-2.0-flash", + } + + events = run_generator_with_config(mod, config, monkeypatch) + + # Verify pre-hook modified the prompt - check in contents + contents = received_kwargs.get("contents", []) + prompt_found = any("[pre]" in str(c) for c in contents) + assert prompt_found, f"Expected [pre] in contents: {contents}" + + # Verify post-hook modified the result + finish_events = [e for e in events if e["event"] == "finish"] + assert len(finish_events) == 1 + assert "[post]" in finish_events[0]["result"]