22//!
33//! Tools: `worktree_list`, `worktree_start`, `worktree_status`, `worktree_ship`.
44//!
5- //! These are Phase 1 stubs — signatures and `todo!()` bodies.
6- //! Real implementations land in Phase 3 (issue #293).
7- //!
8- //! The `#[allow(dead_code)]` attributes are intentional: these handlers
9- //! will be called from the JSON-RPC dispatcher added in Phase 2 (#293).
5+ //! `worktree_list` and `worktree_status` are wired as read-only Phase 4
6+ //! handlers. Mutating operations remain stubs until later #293 phases.
107
8+ use crate :: config:: ParsecConfig ;
119use crate :: mcp:: McpContext ;
10+ use crate :: worktree:: WorktreeManager ;
11+ use anyhow:: { Context , Result } ;
12+ use serde_json:: json;
13+ use std:: path:: PathBuf ;
14+
15+ fn repo_path ( ctx : & McpContext , input : & serde_json:: Value ) -> PathBuf {
16+ input
17+ . get ( "repo" )
18+ . and_then ( serde_json:: Value :: as_str)
19+ . map ( PathBuf :: from)
20+ . unwrap_or_else ( || ctx. repo_path . clone ( ) )
21+ }
22+
23+ fn manager ( ctx : & McpContext , input : & serde_json:: Value ) -> Result < WorktreeManager > {
24+ let config = ParsecConfig :: load ( ) . context ( "failed to load parsec config" ) ?;
25+ WorktreeManager :: new ( & repo_path ( ctx, input) , & config)
26+ }
1227
1328/// `worktree_list` — list all parsec-managed worktrees.
1429///
1530/// # Errors
1631/// Returns an error if the git repository cannot be read.
17- #[ allow( dead_code) ]
18- pub fn list ( _ctx : & McpContext , _input : serde_json:: Value ) -> anyhow:: Result < serde_json:: Value > {
19- // Phase 3: call crate::worktree::manager to enumerate worktrees,
20- // optionally fetch PR/CI status via crate::github.
21- Err ( super :: not_implemented ( "worktree_list" ) )
32+ pub fn list ( ctx : & McpContext , input : serde_json:: Value ) -> Result < serde_json:: Value > {
33+ let manager = manager ( ctx, & input) ?;
34+ let worktrees = manager. list ( ) ?;
35+
36+ Ok ( json ! ( {
37+ "repo" : manager. repo_root( ) ,
38+ "count" : worktrees. len( ) ,
39+ "worktrees" : worktrees,
40+ "pr_overlay" : false ,
41+ "ci_overlay" : false ,
42+ } ) )
2243}
2344
2445/// `worktree_start` — create a new worktree for a ticket.
@@ -35,10 +56,23 @@ pub fn start(_ctx: &McpContext, _input: serde_json::Value) -> anyhow::Result<ser
3556///
3657/// # Errors
3758/// Returns an error if the ticket has no associated worktree.
38- #[ allow( dead_code) ]
39- pub fn status ( _ctx : & McpContext , _input : serde_json:: Value ) -> anyhow:: Result < serde_json:: Value > {
40- // Phase 3: combine git2 status + github PR/CI queries.
41- Err ( super :: not_implemented ( "worktree_status" ) )
59+ pub fn status ( ctx : & McpContext , input : serde_json:: Value ) -> Result < serde_json:: Value > {
60+ let ticket = input
61+ . get ( "ticket" )
62+ . and_then ( serde_json:: Value :: as_str)
63+ . filter ( |ticket| !ticket. is_empty ( ) )
64+ . context ( "worktree_status requires a non-empty ticket" ) ?;
65+ crate :: worktree:: validate_ticket_id ( ticket) ?;
66+
67+ let manager = manager ( ctx, & input) ?;
68+ let workspace = manager. get ( ticket) ?;
69+
70+ Ok ( json ! ( {
71+ "repo" : manager. repo_root( ) ,
72+ "workspace" : workspace,
73+ "pr_overlay" : null,
74+ "ci_overlay" : null,
75+ } ) )
4276}
4377
4478/// `worktree_ship` — push branch, open/update PR, optionally clean up.
@@ -51,3 +85,70 @@ pub fn ship(_ctx: &McpContext, _input: serde_json::Value) -> anyhow::Result<serd
5185 // Respect ctx.dry_run before any side effects.
5286 Err ( super :: not_implemented ( "worktree_ship" ) )
5387}
88+
89+ #[ cfg( test) ]
90+ mod tests {
91+ use super :: * ;
92+ use crate :: worktree:: { ParsecState , Workspace , WorkspaceStatus } ;
93+ use chrono:: TimeZone ;
94+
95+ fn fixture_repo ( ) -> ( tempfile:: TempDir , McpContext ) {
96+ let dir = tempfile:: tempdir ( ) . expect ( "temp repo" ) ;
97+ crate :: git:: run ( dir. path ( ) , & [ "init" ] ) . expect ( "git init" ) ;
98+
99+ let mut state = ParsecState :: default ( ) ;
100+ state. add_workspace ( Workspace {
101+ ticket : "ABC-123" . to_owned ( ) ,
102+ path : dir. path ( ) . join ( "../git-parsec.ABC-123" ) ,
103+ branch : "feat/ABC-123" . to_owned ( ) ,
104+ base_branch : "main" . to_owned ( ) ,
105+ created_at : chrono:: Utc . with_ymd_and_hms ( 2026 , 6 , 17 , 0 , 0 , 0 ) . unwrap ( ) ,
106+ ticket_title : Some ( "Wire MCP worktree status" . to_owned ( ) ) ,
107+ status : WorkspaceStatus :: Active ,
108+ parent_ticket : None ,
109+ } ) ;
110+ state. save ( dir. path ( ) ) . expect ( "state save" ) ;
111+
112+ let ctx = McpContext {
113+ repo_path : dir. path ( ) . to_path_buf ( ) ,
114+ github_token : None ,
115+ dry_run : false ,
116+ } ;
117+
118+ ( dir, ctx)
119+ }
120+
121+ #[ test]
122+ fn worktree_list_returns_registered_workspaces ( ) {
123+ let ( _dir, ctx) = fixture_repo ( ) ;
124+
125+ let payload = list ( & ctx, json ! ( { } ) ) . expect ( "worktree list" ) ;
126+
127+ assert_eq ! ( payload[ "count" ] , 1 ) ;
128+ assert_eq ! ( payload[ "worktrees" ] [ 0 ] [ "ticket" ] , "ABC-123" ) ;
129+ assert_eq ! ( payload[ "worktrees" ] [ 0 ] [ "branch" ] , "feat/ABC-123" ) ;
130+ assert_eq ! ( payload[ "pr_overlay" ] , false ) ;
131+ assert_eq ! ( payload[ "ci_overlay" ] , false ) ;
132+ }
133+
134+ #[ test]
135+ fn worktree_status_returns_matching_workspace ( ) {
136+ let ( _dir, ctx) = fixture_repo ( ) ;
137+
138+ let payload = status ( & ctx, json ! ( { "ticket" : "ABC-123" } ) ) . expect ( "worktree status payload" ) ;
139+
140+ assert_eq ! ( payload[ "workspace" ] [ "ticket" ] , "ABC-123" ) ;
141+ assert_eq ! ( payload[ "workspace" ] [ "base_branch" ] , "main" ) ;
142+ assert ! ( payload[ "pr_overlay" ] . is_null( ) ) ;
143+ assert ! ( payload[ "ci_overlay" ] . is_null( ) ) ;
144+ }
145+
146+ #[ test]
147+ fn worktree_status_requires_ticket ( ) {
148+ let ( _dir, ctx) = fixture_repo ( ) ;
149+
150+ let err = status ( & ctx, json ! ( { } ) ) . expect_err ( "missing ticket should fail" ) ;
151+
152+ assert ! ( err. to_string( ) . contains( "requires a non-empty ticket" ) ) ;
153+ }
154+ }
0 commit comments