@@ -767,6 +767,87 @@ def test_clear_log_available(self, client):
767767 r = client .post ("/clear_log" )
768768 assert r .status_code == 200
769769
770+ @pytest .fixture
771+ def _isolated_app (self , tmp_path , monkeypatch ):
772+ """Create an app with all dirs isolated under tmp_path."""
773+ import novelforge .config as cfg
774+ from novelforge import create_app , limiter
775+
776+ for attr , subdir in (
777+ ("NOVELS_DIR" , "novels" ),
778+ ("LOGS_DIR" , "logs" ),
779+ ("SESSION_FILE_DIR" , "sessions" ),
780+ ("EXPORT_DIR" , "exports" ),
781+ ):
782+ d = tmp_path / subdir
783+ d .mkdir ()
784+ monkeypatch .setattr (cfg , attr , str (d ))
785+
786+ flask_app = create_app (testing = True )
787+ flask_app .config ["SECRET_KEY" ] = "test-secret"
788+ flask_app .config ["WTF_CSRF_ENABLED" ] = False
789+ limiter .enabled = False
790+ return flask_app , tmp_path / "logs"
791+
792+ def test_llm_log_parses_single_line_entries (self , _isolated_app ):
793+ """One-JSON-per-line log format is parsed correctly."""
794+ flask_app , logs_dir = _isolated_app
795+ entry1 = {"type" : "request" , "action" : "test1" , "timestamp" : "2024-01-01 00:00:00" }
796+ entry2 = {"type" : "response" , "action" : "test2" , "timestamp" : "2024-01-01 00:00:01" }
797+ (logs_dir / "llm.log" ).write_text (
798+ json .dumps (entry1 ) + "\n " + json .dumps (entry2 ) + "\n " ,
799+ encoding = "utf-8" ,
800+ )
801+
802+ with flask_app .test_client () as c :
803+ r = c .get ("/llm_log" )
804+
805+ assert r .status_code == 200
806+ data = r .get_json ()
807+ assert len (data ["entries" ]) == 2
808+ assert data ["entries" ][0 ]["type" ] == "request"
809+ assert data ["entries" ][1 ]["type" ] == "response"
810+
811+ def test_llm_log_parses_entries_with_braces_in_strings (self , _isolated_app ):
812+ """Brace characters inside JSON string values do not break parsing."""
813+ flask_app , logs_dir = _isolated_app
814+ entry = {
815+ "type" : "request" ,
816+ "action" : "test" ,
817+ "timestamp" : "2024-01-01 00:00:00" ,
818+ "payload" : {"key" : "value with { brace } and another {brace}" },
819+ }
820+ (logs_dir / "llm.log" ).write_text (json .dumps (entry ) + "\n " , encoding = "utf-8" )
821+
822+ with flask_app .test_client () as c :
823+ r = c .get ("/llm_log" )
824+
825+ assert r .status_code == 200
826+ data = r .get_json ()
827+ assert len (data ["entries" ]) == 1
828+ assert data ["entries" ][0 ]["payload" ]["key" ] == "value with { brace } and another {brace}"
829+
830+ def test_llm_log_returns_last_ten_entries (self , _isolated_app ):
831+ """Only the last 10 entries are returned when the log has more."""
832+ flask_app , logs_dir = _isolated_app
833+ entries = [
834+ {"type" : "request" , "seq" : i , "timestamp" : f"2024-01-01 00:00:{ i :02d} " }
835+ for i in range (15 )
836+ ]
837+ (logs_dir / "llm.log" ).write_text (
838+ "\n " .join (json .dumps (e ) for e in entries ) + "\n " ,
839+ encoding = "utf-8" ,
840+ )
841+
842+ with flask_app .test_client () as c :
843+ r = c .get ("/llm_log" )
844+
845+ assert r .status_code == 200
846+ data = r .get_json ()
847+ assert len (data ["entries" ]) == 10
848+ assert data ["entries" ][0 ]["seq" ] == 5
849+ assert data ["entries" ][- 1 ]["seq" ] == 14
850+
770851
771852class TestNovelforgeDebugEnvVar :
772853 """Verify that NOVELFORGE_DEBUG controls debug mode in app.py entrypoint."""
0 commit comments