@@ -437,6 +437,7 @@ def _disable_codex(self, tmp_path, monkeypatch):
437437 monkeypatch .setattr ("dataclaw.parser._OPENCODE_PROJECT_INDEX" , {})
438438 monkeypatch .setattr ("dataclaw.parser.OPENCLAW_AGENTS_DIR" , tmp_path / "no-openclaw-agents" )
439439 monkeypatch .setattr ("dataclaw.parser._OPENCLAW_PROJECT_INDEX" , {})
440+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , tmp_path / "no-custom" )
440441
441442 def _write_opencode_db (self , db_path ):
442443 conn = sqlite3 .connect (db_path )
@@ -987,6 +988,7 @@ def _disable_codex(self, tmp_path, monkeypatch):
987988 monkeypatch .setattr ("dataclaw.parser._OPENCODE_PROJECT_INDEX" , {})
988989 monkeypatch .setattr ("dataclaw.parser.OPENCLAW_AGENTS_DIR" , tmp_path / "no-openclaw-agents" )
989990 monkeypatch .setattr ("dataclaw.parser._OPENCLAW_PROJECT_INDEX" , {})
991+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , tmp_path / "no-custom" )
990992
991993 def test_discover_includes_subagent_sessions (self , tmp_path , monkeypatch , mock_anonymizer ):
992994 self ._disable_codex (tmp_path , monkeypatch )
@@ -1585,6 +1587,7 @@ def _disable_others(self, tmp_path, monkeypatch):
15851587 monkeypatch .setattr ("dataclaw.parser.OPENCODE_DB_PATH" , tmp_path / "no-opencode.db" )
15861588 monkeypatch .setattr ("dataclaw.parser._OPENCODE_PROJECT_INDEX" , {})
15871589 monkeypatch .setattr ("dataclaw.parser._OPENCLAW_PROJECT_INDEX" , {})
1590+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , tmp_path / "no-custom" )
15881591
15891592 def test_discover_openclaw_projects (self , tmp_path , monkeypatch , mock_anonymizer ):
15901593 self ._disable_others (tmp_path , monkeypatch )
@@ -1657,3 +1660,117 @@ def test_multiple_agents_same_cwd(self, tmp_path, monkeypatch, mock_anonymizer):
16571660 projects = discover_projects ()
16581661 assert len (projects ) == 1
16591662 assert projects [0 ]["session_count" ] == 2
1663+
1664+
1665+ class TestDiscoverCustomProjects :
1666+ def _disable_others (self , tmp_path , monkeypatch ):
1667+ monkeypatch .setattr ("dataclaw.parser.PROJECTS_DIR" , tmp_path / "no-claude" )
1668+ monkeypatch .setattr ("dataclaw.parser.CODEX_SESSIONS_DIR" , tmp_path / "no-codex-sessions" )
1669+ monkeypatch .setattr ("dataclaw.parser.CODEX_ARCHIVED_DIR" , tmp_path / "no-codex-archived" )
1670+ monkeypatch .setattr ("dataclaw.parser._CODEX_PROJECT_INDEX" , {})
1671+ monkeypatch .setattr ("dataclaw.parser.GEMINI_DIR" , tmp_path / "no-gemini" )
1672+ monkeypatch .setattr ("dataclaw.parser.OPENCODE_DB_PATH" , tmp_path / "no-opencode.db" )
1673+ monkeypatch .setattr ("dataclaw.parser._OPENCODE_PROJECT_INDEX" , {})
1674+ monkeypatch .setattr ("dataclaw.parser.OPENCLAW_AGENTS_DIR" , tmp_path / "no-openclaw-agents" )
1675+ monkeypatch .setattr ("dataclaw.parser._OPENCLAW_PROJECT_INDEX" , {})
1676+
1677+ def _make_valid_session (self , session_id = "s1" , model = "gpt-4" , content = "hello" ):
1678+ return json .dumps ({
1679+ "session_id" : session_id ,
1680+ "model" : model ,
1681+ "messages" : [
1682+ {"role" : "user" , "content" : content },
1683+ {"role" : "assistant" , "content" : "hi there" },
1684+ ],
1685+ "stats" : {"user_messages" : 1 , "assistant_messages" : 1 , "tool_uses" : 0 ,
1686+ "input_tokens" : 10 , "output_tokens" : 5 },
1687+ })
1688+
1689+ def test_discover_custom_projects (self , tmp_path , monkeypatch , mock_anonymizer ):
1690+ self ._disable_others (tmp_path , monkeypatch )
1691+ custom_dir = tmp_path / "custom"
1692+ proj = custom_dir / "my-project"
1693+ proj .mkdir (parents = True )
1694+ (proj / "sessions.jsonl" ).write_text (
1695+ self ._make_valid_session ("s1" ) + "\n " + self ._make_valid_session ("s2" ) + "\n "
1696+ )
1697+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1698+ projects = discover_projects ()
1699+ assert len (projects ) == 1
1700+ assert projects [0 ]["display_name" ] == "custom:my-project"
1701+ assert projects [0 ]["session_count" ] == 2
1702+ assert projects [0 ]["source" ] == "custom"
1703+
1704+ def test_discover_skips_empty_dir (self , tmp_path , monkeypatch ):
1705+ self ._disable_others (tmp_path , monkeypatch )
1706+ custom_dir = tmp_path / "custom"
1707+ (custom_dir / "empty-project" ).mkdir (parents = True )
1708+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1709+ projects = discover_projects ()
1710+ assert len (projects ) == 0
1711+
1712+ def test_discover_missing_dir (self , tmp_path , monkeypatch ):
1713+ self ._disable_others (tmp_path , monkeypatch )
1714+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , tmp_path / "nonexistent" )
1715+ projects = discover_projects ()
1716+ assert len (projects ) == 0
1717+
1718+ def test_parse_valid_sessions (self , tmp_path , monkeypatch , mock_anonymizer ):
1719+ custom_dir = tmp_path / "custom"
1720+ proj = custom_dir / "test-proj"
1721+ proj .mkdir (parents = True )
1722+ (proj / "data.jsonl" ).write_text (
1723+ self ._make_valid_session ("s1" ) + "\n " + self ._make_valid_session ("s2" , model = "o1" ) + "\n "
1724+ )
1725+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1726+ sessions = parse_project_sessions ("test-proj" , mock_anonymizer , source = "custom" )
1727+ assert len (sessions ) == 2
1728+ assert sessions [0 ]["session_id" ] == "s1"
1729+ assert sessions [1 ]["model" ] == "o1"
1730+ assert sessions [0 ]["project" ] == "custom:test-proj"
1731+ assert sessions [0 ]["source" ] == "custom"
1732+
1733+ def test_parse_skips_missing_fields (self , tmp_path , monkeypatch , mock_anonymizer ):
1734+ custom_dir = tmp_path / "custom"
1735+ proj = custom_dir / "test-proj"
1736+ proj .mkdir (parents = True )
1737+ valid = self ._make_valid_session ("s1" )
1738+ no_model = json .dumps ({"session_id" : "s2" , "messages" : []})
1739+ no_messages = json .dumps ({"session_id" : "s3" , "model" : "m" })
1740+ no_session_id = json .dumps ({"model" : "m" , "messages" : []})
1741+ (proj / "data.jsonl" ).write_text (
1742+ "\n " .join ([valid , no_model , no_messages , no_session_id ]) + "\n "
1743+ )
1744+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1745+ sessions = parse_project_sessions ("test-proj" , mock_anonymizer , source = "custom" )
1746+ assert len (sessions ) == 1
1747+ assert sessions [0 ]["session_id" ] == "s1"
1748+
1749+ def test_parse_skips_invalid_json (self , tmp_path , monkeypatch , mock_anonymizer ):
1750+ custom_dir = tmp_path / "custom"
1751+ proj = custom_dir / "test-proj"
1752+ proj .mkdir (parents = True )
1753+ valid = self ._make_valid_session ("s1" )
1754+ (proj / "data.jsonl" ).write_text (valid + "\n " + "not-json\n " )
1755+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1756+ sessions = parse_project_sessions ("test-proj" , mock_anonymizer , source = "custom" )
1757+ assert len (sessions ) == 1
1758+
1759+ def test_parse_multiple_files (self , tmp_path , monkeypatch , mock_anonymizer ):
1760+ custom_dir = tmp_path / "custom"
1761+ proj = custom_dir / "test-proj"
1762+ proj .mkdir (parents = True )
1763+ (proj / "a.jsonl" ).write_text (self ._make_valid_session ("s1" ) + "\n " )
1764+ (proj / "b.jsonl" ).write_text (self ._make_valid_session ("s2" ) + "\n " )
1765+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1766+ sessions = parse_project_sessions ("test-proj" , mock_anonymizer , source = "custom" )
1767+ assert len (sessions ) == 2
1768+ ids = {s ["session_id" ] for s in sessions }
1769+ assert ids == {"s1" , "s2" }
1770+
1771+ def test_parse_nonexistent_project (self , tmp_path , monkeypatch , mock_anonymizer ):
1772+ custom_dir = tmp_path / "custom"
1773+ custom_dir .mkdir (parents = True )
1774+ monkeypatch .setattr ("dataclaw.parser.CUSTOM_DIR" , custom_dir )
1775+ sessions = parse_project_sessions ("nope" , mock_anonymizer , source = "custom" )
1776+ assert sessions == []
0 commit comments