@@ -90,6 +90,45 @@ def test_maps_session_and_turn_fields(tmp_path) -> None:
9090 assert d .raw_path .endswith ("#s1" )
9191
9292
93+ def test_redacts_user_and_assistant_secrets_before_harvesting (tmp_path ) -> None :
94+ user_secret = "sk-abcdefghijklmnopqrstuvwxyz1234567890"
95+ assistant_secret = "super-secret-value-123456"
96+ path = _store (
97+ tmp_path ,
98+ [("s1" , r"C:\proj" , "repo" , "main" , "2026-01-01 10:00:00" , "2026-01-01 10:30:00" )],
99+ [
100+ (
101+ "s1" ,
102+ 0 ,
103+ f"Use Authorization: Bearer { user_secret } for this task" ,
104+ f"Configured api_key={ assistant_secret } " ,
105+ "2026-01-01 10:00:00" ,
106+ )
107+ ],
108+ )
109+
110+ [digest ] = harvest_copilot_cli (path , scope = "all" )
111+ harvested = "\n " .join (digest .user_prompts + digest .assistant_finals )
112+ assert user_secret not in harvested
113+ assert assistant_secret not in harvested
114+ assert "[REDACTED" in harvested
115+
116+
117+ def test_redacts_secrets_before_text_is_clipped (tmp_path ) -> None :
118+ secret = "sk-abcdefghijklmnopqrstuvwxyz1234567890"
119+ # The token begins just before the 4000-char boundary. Clipping first would
120+ # leave a secret fragment that no longer matches the redaction pattern.
121+ prompt = "x" * (4000 - 5 ) + secret
122+ path = _store (
123+ tmp_path ,
124+ [("s1" , r"C:\proj" , "repo" , "main" , "2026-01-01 10:00:00" , "2026-01-01 10:30:00" )],
125+ [("s1" , 0 , prompt , "done" , "2026-01-01 10:00:00" )],
126+ )
127+
128+ [digest ] = harvest_copilot_cli (path , scope = "all" )
129+ assert "sk-" not in digest .user_prompts [0 ]
130+
131+
93132def test_engine_self_calls_are_filtered (tmp_path ) -> None :
94133 # SkillOpt's own Copilot backend writes to this same store; harvesting them
95134 # would train the engine on its own output.
@@ -269,4 +308,3 @@ def _boom(_store_path):
269308
270309 monkeypatch .setattr ("skillopt_sleep.harvest_copilot_cli._connect" , _boom )
271310 assert harvest_copilot_cli (path , scope = "all" ) == []
272-
0 commit comments