@@ -361,6 +361,27 @@ def test_recovers_when_concepts_section_missing(self, tmp_path):
361361 assert "[[concepts/attention]] — Focus" in text
362362 assert "[[summaries/my-doc]]" in text
363363
364+ def test_entities_inserted_before_explorations (self , tmp_path ):
365+ """#8: an old index.md predating ## Entities must get it inserted
366+ before ## Explorations, not appended after it (canonical order)."""
367+ wiki = tmp_path / "wiki"
368+ wiki .mkdir ()
369+ # Old order: no ## Entities section yet.
370+ (wiki / "index.md" ).write_text (
371+ "# Index\n \n ## Documents\n \n ## Concepts\n \n ## Explorations\n " ,
372+ encoding = "utf-8" ,
373+ )
374+ _update_index (
375+ wiki , "my-doc" , [],
376+ entity_names = ["anthropic" ],
377+ entity_meta = {"anthropic" : ("organization" , "AI lab." )},
378+ )
379+ text = (wiki / "index.md" ).read_text ()
380+ assert "## Entities" in text
381+ # Canonical order: Entities before Explorations.
382+ assert text .index ("## Entities" ) < text .index ("## Explorations" )
383+ assert "[[entities/anthropic]] (organization) — AI lab." in text
384+
364385
365386class TestReadWikiContext :
366387 def test_empty_wiki (self , tmp_path ):
@@ -561,6 +582,31 @@ def test_update_prepends_source_keeps_type(self, tmp_path):
561582 assert "v1." not in text
562583 assert "brief:" in text and "b2" in text
563584
585+ def test_update_rebuilds_frontmatter_when_no_closing_delim (self , tmp_path ):
586+ """#11: malformed existing file (opening --- but no closing ---) must
587+ not drop frontmatter; rebuild valid sources/type/brief on update."""
588+ entities = tmp_path / "entities"
589+ entities .mkdir (parents = True )
590+ # Opening delimiter, NO closing delimiter — find("---", 3) == -1.
591+ (entities / "anthropic.md" ).write_text (
592+ "---\n sources: [\" summaries/a.md\" ]\n type: organization\n "
593+ "# Anthropic (no closing fence)\n \n Old body." ,
594+ encoding = "utf-8" ,
595+ )
596+ _write_entity (
597+ tmp_path , "anthropic" , "# Anthropic\n \n v2 rewritten." ,
598+ "summaries/b.md" , is_update = True ,
599+ brief = "AI lab." , type_ = "organization" , aliases = None ,
600+ )
601+ text = (entities / "anthropic.md" ).read_text (encoding = "utf-8" )
602+ # Frontmatter rebuilt with a proper closing delimiter, not body-only.
603+ assert text .startswith ("---\n " )
604+ assert text .count ("---" ) == 2
605+ assert "sources:" in text and "summaries/b.md" in text
606+ assert "type:" in text and "organization" in text
607+ assert "brief:" in text and "AI lab." in text
608+ assert "v2 rewritten." in text
609+
564610
565611class TestBacklinkSummary :
566612 def test_adds_missing_concept_links (self , tmp_path ):
@@ -1051,6 +1097,33 @@ async def test_empty_plan_strips_v1_summary_ghosts(self, tmp_path):
10511097 assert "[[concepts/imaginary]]" not in text
10521098 assert "imaginary" in text # plain text preserved
10531099
1100+ @pytest .mark .asyncio
1101+ async def test_scalar_plan_handled_gracefully (self , tmp_path ):
1102+ """#10: a JSON scalar plan (valid JSON, not object/array) must not
1103+ crash with AttributeError; it takes the graceful empty-plan path —
1104+ v1 summary written, index updated, no concept/entity pages."""
1105+ wiki , source_path = self ._setup_kb (tmp_path )
1106+
1107+ summary_response = json .dumps ({
1108+ "brief" : "B" , "content" : "# Summary\n \n Plain body, no links." ,
1109+ })
1110+ # Plan call returns a bare JSON scalar (an integer).
1111+ scalar_plan_response = "42"
1112+
1113+ with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
1114+ mock_litellm .completion = MagicMock (
1115+ side_effect = _mock_completion ([summary_response , scalar_plan_response ])
1116+ )
1117+ # Must not raise (AttributeError) and must complete.
1118+ await compile_short_doc ("doc" , source_path , tmp_path , "gpt-4o-mini" )
1119+
1120+ # Summary still written, index updated with the document.
1121+ assert (wiki / "summaries" / "doc.md" ).exists ()
1122+ index_text = (wiki / "index.md" ).read_text ()
1123+ assert "[[summaries/doc]]" in index_text
1124+ # No concept pages produced from the unusable plan.
1125+ assert not list ((wiki / "concepts" ).glob ("*.md" ))
1126+
10541127
10551128class TestCacheControl :
10561129 """Verify cache_control breakpoints are emitted on the right messages
@@ -1346,6 +1419,47 @@ async def ordered_acompletion(*args, **kwargs):
13461419 assert "[[concepts/flash-attention]]" in index_text
13471420 assert "[[concepts/attention]]" in index_text
13481421
1422+ @pytest .mark .asyncio
1423+ async def test_empty_content_skips_page_no_json_body (self , tmp_path ):
1424+ """#9: when the page LLM returns parseable JSON with empty content
1425+ ({"content": ""}), the page is skipped (not written as raw JSON)."""
1426+ wiki = self ._setup_wiki (tmp_path )
1427+
1428+ plan_response = json .dumps ({
1429+ "create" : [{"name" : "ghost-concept" , "title" : "Ghost Concept" }],
1430+ "update" : [],
1431+ "related" : [],
1432+ })
1433+ # Parseable JSON, but empty content — old code fell back to raw JSON.
1434+ empty_content_response = json .dumps ({"brief" : "B" , "content" : "" })
1435+
1436+ system_msg = {"role" : "system" , "content" : "You are a wiki agent." }
1437+ doc_msg = {"role" : "user" , "content" : "Document content." }
1438+
1439+ with patch ("openkb.agent.compiler.litellm" ) as mock_litellm :
1440+ mock_litellm .completion = MagicMock (
1441+ side_effect = _mock_completion ([plan_response ])
1442+ )
1443+ mock_litellm .acompletion = AsyncMock (
1444+ side_effect = _mock_completion ([empty_content_response ])
1445+ )
1446+ await _compile_concepts (
1447+ wiki , tmp_path , "gpt-4o-mini" , system_msg , doc_msg ,
1448+ "Summary." , "test-doc" , 5 ,
1449+ )
1450+
1451+ # The concept page must NOT be written (generation raised + dropped).
1452+ page = wiki / "concepts" / "ghost-concept.md"
1453+ assert not page .exists ()
1454+ # And no concept index entry either.
1455+ index_text = (wiki / "index.md" ).read_text ()
1456+ assert "[[concepts/ghost-concept]]" not in index_text
1457+ # Definitely no raw JSON written anywhere as a body.
1458+ assert not any (
1459+ '"content":' in p .read_text ()
1460+ for p in (wiki / "concepts" ).glob ("*.md" )
1461+ )
1462+
13491463 @pytest .mark .asyncio
13501464 async def test_related_adds_link_no_llm (self , tmp_path ):
13511465 """Plan has only related items. No acompletion calls should be made."""
0 commit comments