@@ -5,7 +5,7 @@ use std::path::PathBuf;
55use serde:: { Deserialize , Serialize } ;
66use skilllite_core:: skill:: discovery:: resolve_skills_dir_with_legacy_fallback;
77
8- use crate :: evolution_status:: resolve_workspace_root;
8+ use crate :: evolution_status:: { chat_root_for_workspace , resolve_workspace_root} ;
99use crate :: Result ;
1010
1111#[ derive( Debug , Clone , Serialize , Deserialize ) ]
@@ -68,8 +68,11 @@ fn truncate_utf8(s: &str, max: usize) -> String {
6868 format ! ( "{}…" , & s[ ..end] )
6969}
7070
71- pub fn query_backlog_desktop ( limit : usize ) -> Result < Vec < EvolutionBacklogRowSnapshot > > {
72- let chat_root = skilllite_core:: paths:: chat_root ( ) ;
71+ pub fn query_backlog_desktop (
72+ workspace : & str ,
73+ limit : usize ,
74+ ) -> Result < Vec < EvolutionBacklogRowSnapshot > > {
75+ let chat_root = chat_root_for_workspace ( workspace) ;
7376 let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) ?;
7477 let limit = limit. clamp ( 1 , 200 ) ;
7578 let mut stmt = conn
@@ -102,8 +105,11 @@ pub fn query_backlog_desktop(limit: usize) -> Result<Vec<EvolutionBacklogRowSnap
102105 . collect :: < Result < Vec < _ > > > ( )
103106}
104107
105- pub fn query_proposal_status ( proposal_id : & str ) -> Result < EvolutionProposalStatusSnapshot > {
106- let chat_root = skilllite_core:: paths:: chat_root ( ) ;
108+ pub fn query_proposal_status (
109+ workspace : & str ,
110+ proposal_id : & str ,
111+ ) -> Result < EvolutionProposalStatusSnapshot > {
112+ let chat_root = chat_root_for_workspace ( workspace) ;
107113 let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) ?;
108114 conn. query_row (
109115 "SELECT proposal_id, status, acceptance_status, updated_at, note
@@ -167,7 +173,7 @@ pub fn read_pending_skill_md(workspace: &str, skill_name: &str) -> Result<String
167173pub fn confirm_pending_skill ( workspace : & str , skill_name : & str ) -> Result < ( ) > {
168174 let skills_root = resolve_skills_root ( workspace) ?;
169175 skilllite_evolution:: skill_synth:: confirm_pending_skill ( & skills_root, skill_name) ?;
170- let chat_root = skilllite_core :: paths :: chat_root ( ) ;
176+ let chat_root = chat_root_for_workspace ( workspace ) ;
171177 if let Ok ( conn) = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) {
172178 let _ = skilllite_evolution:: log_evolution_event (
173179 & conn,
@@ -198,8 +204,8 @@ pub fn authorize_capability_evolution(
198204 outcome : & str ,
199205 summary : & str ,
200206) -> Result < AuthorizeCapabilitySnapshot > {
201- let _ = resolve_workspace_root ( workspace) ;
202- let chat_root = skilllite_core :: paths :: chat_root ( ) ;
207+ let workspace_root = resolve_workspace_root ( workspace) ;
208+ let chat_root = workspace_root . join ( "chat" ) ;
203209 let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) ?;
204210 let proposal_id =
205211 skilllite_evolution:: enqueue_user_capability_evolution ( & conn, tool_name, outcome, summary) ?;
@@ -223,7 +229,7 @@ pub fn log_manual_evolution_trigger(
223229 proposal_id : Option < & str > ,
224230 summary : & str ,
225231) -> Result < ( ) > {
226- let chat_root = skilllite_core :: paths :: chat_root ( ) ;
232+ let chat_root = chat_root_for_workspace ( workspace ) ;
227233 let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) ?;
228234 let clipped = clip_manual_trigger_summary ( summary) ;
229235 let _ = skilllite_evolution:: log_evolution_event (
@@ -240,6 +246,65 @@ pub fn log_manual_evolution_trigger(
240246#[ cfg( test) ]
241247mod tests {
242248 use super :: * ;
249+ use skilllite_core:: config:: env_keys:: paths as env_paths;
250+ use std:: sync:: Mutex ;
251+
252+ static ENV_LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
253+
254+ struct EnvRestore {
255+ key : & ' static str ,
256+ previous : Option < String > ,
257+ }
258+
259+ impl EnvRestore {
260+ fn set ( key : & ' static str , value : & str ) -> Self {
261+ let previous = std:: env:: var ( key) . ok ( ) ;
262+ skilllite_core:: config:: set_env_var ( key, value) ;
263+ Self { key, previous }
264+ }
265+ }
266+
267+ impl Drop for EnvRestore {
268+ fn drop ( & mut self ) {
269+ if let Some ( value) = & self . previous {
270+ skilllite_core:: config:: set_env_var ( self . key , value) ;
271+ } else {
272+ skilllite_core:: config:: remove_env_var ( self . key ) ;
273+ }
274+ }
275+ }
276+
277+ fn temp_workspace ( label : & str ) -> PathBuf {
278+ std:: env:: temp_dir ( ) . join ( format ! (
279+ "skilllite-evo-desktop-{label}-{}" ,
280+ uuid:: Uuid :: new_v4( )
281+ ) )
282+ }
283+
284+ fn seed_backlog_row ( workspace : & std:: path:: Path , proposal_id : & str , note : & str ) {
285+ let chat_root = workspace. join ( "chat" ) ;
286+ let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) . expect ( "open db" ) ;
287+ let dedupe_key = format ! ( "dedupe_{proposal_id}" ) ;
288+ conn. execute (
289+ "INSERT INTO evolution_backlog
290+ (proposal_id, source, dedupe_key, scope_json, risk_level, roi_score, expected_gain, effort, acceptance_criteria, status, acceptance_status, note)
291+ VALUES (?1, 'active', ?2, '{}', 'low', 0.5, 0.5, 1.0, '[]', 'queued', 'pending_validation', ?3)" ,
292+ [ proposal_id, dedupe_key. as_str ( ) , note] ,
293+ )
294+ . expect ( "insert backlog row" ) ;
295+ }
296+
297+ fn capability_rows ( workspace : & std:: path:: Path , tool_name : & str ) -> i64 {
298+ let chat_root = workspace. join ( "chat" ) ;
299+ let conn = skilllite_evolution:: feedback:: open_evolution_db ( & chat_root) . expect ( "open db" ) ;
300+ let dedupe_key = format ! ( "user_capability:{tool_name}:failure" ) ;
301+ conn. query_row (
302+ "SELECT COUNT(*) FROM evolution_backlog WHERE dedupe_key = ?1" ,
303+ [ dedupe_key. as_str ( ) ] ,
304+ |row| row. get ( 0 ) ,
305+ )
306+ . expect ( "count rows" )
307+ }
243308
244309 #[ test]
245310 fn manual_trigger_summary_clip_is_utf8_boundary_safe ( ) {
@@ -260,4 +325,74 @@ mod tests {
260325
261326 assert_eq ! ( clip_manual_trigger_summary( summary) , summary) ;
262327 }
328+
329+ #[ test]
330+ fn query_backlog_desktop_uses_workspace_argument_over_env ( ) {
331+ let _lock = ENV_LOCK . lock ( ) . expect ( "env lock" ) ;
332+ let env_workspace = temp_workspace ( "env" ) ;
333+ let target_workspace = temp_workspace ( "target" ) ;
334+ let _env_restore = EnvRestore :: set (
335+ env_paths:: SKILLLITE_WORKSPACE ,
336+ env_workspace. to_string_lossy ( ) . as_ref ( ) ,
337+ ) ;
338+ seed_backlog_row ( & env_workspace, "env_only" , "ENV_DB_ROW" ) ;
339+ seed_backlog_row ( & target_workspace, "target_only" , "TARGET_DB_ROW" ) ;
340+
341+ let rows =
342+ query_backlog_desktop ( target_workspace. to_string_lossy ( ) . as_ref ( ) , 10 ) . expect ( "query" ) ;
343+
344+ let ids: Vec < _ > = rows. into_iter ( ) . map ( |row| row. proposal_id ) . collect ( ) ;
345+ assert_eq ! ( ids, vec![ "target_only" ] ) ;
346+ let _ = std:: fs:: remove_dir_all ( env_workspace) ;
347+ let _ = std:: fs:: remove_dir_all ( target_workspace) ;
348+ }
349+
350+ #[ test]
351+ fn query_proposal_status_uses_workspace_argument_over_env ( ) {
352+ let _lock = ENV_LOCK . lock ( ) . expect ( "env lock" ) ;
353+ let env_workspace = temp_workspace ( "env" ) ;
354+ let target_workspace = temp_workspace ( "target" ) ;
355+ let _env_restore = EnvRestore :: set (
356+ env_paths:: SKILLLITE_WORKSPACE ,
357+ env_workspace. to_string_lossy ( ) . as_ref ( ) ,
358+ ) ;
359+ seed_backlog_row ( & env_workspace, "shared_id" , "ENV_DB_ROW" ) ;
360+ seed_backlog_row ( & target_workspace, "shared_id" , "TARGET_DB_ROW" ) ;
361+
362+ let row = query_proposal_status ( target_workspace. to_string_lossy ( ) . as_ref ( ) , "shared_id" )
363+ . expect ( "query" ) ;
364+
365+ assert_eq ! ( row. note. as_deref( ) , Some ( "TARGET_DB_ROW" ) ) ;
366+ let _ = std:: fs:: remove_dir_all ( env_workspace) ;
367+ let _ = std:: fs:: remove_dir_all ( target_workspace) ;
368+ }
369+
370+ #[ test]
371+ fn authorize_capability_uses_workspace_argument_over_env ( ) {
372+ let _lock = ENV_LOCK . lock ( ) . expect ( "env lock" ) ;
373+ let env_workspace = temp_workspace ( "env" ) ;
374+ let target_workspace = temp_workspace ( "target" ) ;
375+ let tool_name = "workspace_scope_regression" ;
376+ let _env_restore = EnvRestore :: set (
377+ env_paths:: SKILLLITE_WORKSPACE ,
378+ env_workspace. to_string_lossy ( ) . as_ref ( ) ,
379+ ) ;
380+ let _ = skilllite_evolution:: feedback:: open_evolution_db ( & env_workspace. join ( "chat" ) )
381+ . expect ( "open env db" ) ;
382+ let _ = skilllite_evolution:: feedback:: open_evolution_db ( & target_workspace. join ( "chat" ) )
383+ . expect ( "open target db" ) ;
384+
385+ authorize_capability_evolution (
386+ target_workspace. to_string_lossy ( ) . as_ref ( ) ,
387+ tool_name,
388+ "failure" ,
389+ "summary" ,
390+ )
391+ . expect ( "authorize" ) ;
392+
393+ assert_eq ! ( capability_rows( & env_workspace, tool_name) , 0 ) ;
394+ assert_eq ! ( capability_rows( & target_workspace, tool_name) , 1 ) ;
395+ let _ = std:: fs:: remove_dir_all ( env_workspace) ;
396+ let _ = std:: fs:: remove_dir_all ( target_workspace) ;
397+ }
263398}
0 commit comments