@@ -36,14 +36,17 @@ func TestInstallVSCodeHooks_FreshInstall(t *testing.T) {
3636 tempDir := t .TempDir ()
3737
3838 ag := & CopilotCLIAgent {}
39- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
39+ count , err := ag .installVSCodeHooks (tempDir , false , false )
40+ require .NoError (t , err )
41+ require .Equal (t , 3 , count , "user-prompt-submitted + agent-stop + session-end" )
4042
4143 hooks := readVSCodeFile (t , tempDir )
4244
4345 // Only the two turn events are registered; the rest come from entire.json.
4446 require .Len (t , hooks , 2 , "only UserPromptSubmit and Stop should be registered" )
4547 require .Len (t , hooks [VSCodeEventUserPromptSubmit ], 1 )
46- require .Len (t , hooks [VSCodeEventStop ], 1 )
48+ // VS Code's single Stop event carries both agent-stop and session-end.
49+ require .Len (t , hooks [VSCodeEventStop ], 2 )
4750
4851 ups := hooks [VSCodeEventUserPromptSubmit ][0 ]
4952 require .Equal (t , "command" , ups .Type )
@@ -53,22 +56,37 @@ func TestInstallVSCodeHooks_FreshInstall(t *testing.T) {
5356 ups .Command ,
5457 "VS Code uses the command field, not bash" )
5558
56- require .Equal (t ,
57- agent .WrapProductionSilentHookCommand ("entire hooks copilot-cli agent-stop" ),
58- hooks [VSCodeEventStop ][0 ].Command )
59+ stopCommands := commandsOf (hooks [VSCodeEventStop ])
60+ require .Contains (t , stopCommands ,
61+ agent .WrapProductionSilentHookCommand ("entire hooks copilot-cli agent-stop" ))
62+ require .Contains (t , stopCommands ,
63+ agent .WrapProductionSilentHookCommand ("entire hooks copilot-cli session-end" ),
64+ "terminal VS Code Stop must route to session-end" )
65+ }
66+
67+ // commandsOf extracts the command strings from a slice of hook entries.
68+ func commandsOf (entries []VSCodeHookEntry ) []string {
69+ cmds := make ([]string , len (entries ))
70+ for i , e := range entries {
71+ cmds [i ] = e .Command
72+ }
73+ return cmds
5974}
6075
6176func TestInstallVSCodeHooks_Idempotent (t * testing.T ) {
6277 t .Parallel ()
6378 tempDir := t .TempDir ()
6479
6580 ag := & CopilotCLIAgent {}
66- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
67- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
81+ _ , err := ag .installVSCodeHooks (tempDir , false , false )
82+ require .NoError (t , err )
83+ count , err := ag .installVSCodeHooks (tempDir , false , false )
84+ require .NoError (t , err )
85+ require .Equal (t , 0 , count , "reinstall adds nothing" )
6886
6987 hooks := readVSCodeFile (t , tempDir )
7088 require .Len (t , hooks [VSCodeEventUserPromptSubmit ], 1 , "must not duplicate on reinstall" )
71- require .Len (t , hooks [VSCodeEventStop ], 1 )
89+ require .Len (t , hooks [VSCodeEventStop ], 2 , "agent-stop + session-end, no duplicates" )
7290}
7391
7492func TestInstallVSCodeHooks_PreservesUserHooks (t * testing.T ) {
@@ -92,12 +110,14 @@ func TestInstallVSCodeHooks_PreservesUserHooks(t *testing.T) {
92110 require .NoError (t , os .WriteFile (vsCodeHooksPath (tempDir ), []byte (seed ), 0o600 ))
93111
94112 ag := & CopilotCLIAgent {}
95- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
113+ _ , err := ag .installVSCodeHooks (tempDir , false , false )
114+ require .NoError (t , err )
96115
97116 hooks := readVSCodeFile (t , tempDir )
98- require .Len (t , hooks [VSCodeEventStop ], 2 , "user Stop hook retained alongside Entire's" )
117+ require .Len (t , hooks [VSCodeEventStop ], 3 , "user Stop hook retained alongside Entire's agent-stop + session-end " )
99118 require .Len (t , hooks [VSCodeEventPreToolUse ], 1 , "unknown event type preserved" )
100119 require .Equal (t , "echo user-pretool" , hooks [VSCodeEventPreToolUse ][0 ].Command )
120+ require .Contains (t , commandsOf (hooks [VSCodeEventStop ]), "echo user-stop" , "user Stop hook survives" )
101121
102122 // Unknown top-level field is preserved on round-trip.
103123 data , err := os .ReadFile (vsCodeHooksPath (tempDir ))
@@ -110,12 +130,13 @@ func TestUninstallVSCodeHooks_DeletesWhenEmpty(t *testing.T) {
110130 tempDir := t .TempDir ()
111131
112132 ag := & CopilotCLIAgent {}
113- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
133+ _ , err := ag .installVSCodeHooks (tempDir , false , false )
134+ require .NoError (t , err )
114135 require .True (t , ag .areVSCodeHooksInstalled (tempDir ))
115136
116137 require .NoError (t , ag .uninstallVSCodeHooks (tempDir ))
117138
118- _ , err : = os .Stat (vsCodeHooksPath (tempDir ))
139+ _ , err = os .Stat (vsCodeHooksPath (tempDir ))
119140 require .ErrorIs (t , err , os .ErrNotExist , "file removed when nothing user-owned remains" )
120141 require .False (t , ag .areVSCodeHooksInstalled (tempDir ))
121142}
@@ -125,7 +146,8 @@ func TestUninstallVSCodeHooks_KeepsUserHooks(t *testing.T) {
125146 tempDir := t .TempDir ()
126147
127148 ag := & CopilotCLIAgent {}
128- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
149+ _ , err := ag .installVSCodeHooks (tempDir , false , false )
150+ require .NoError (t , err )
129151
130152 // Add a user-owned hook to the Stop event.
131153 rawFile , rawHooks , err := readVSCodeHooksFile (vsCodeHooksPath (tempDir ))
@@ -158,12 +180,42 @@ func TestInstallVSCodeHooks_ForceReinstall(t *testing.T) {
158180 tempDir := t .TempDir ()
159181
160182 ag := & CopilotCLIAgent {}
161- require .NoError (t , ag .installVSCodeHooks (tempDir , false , false ))
162- require .NoError (t , ag .installVSCodeHooks (tempDir , false , true ))
183+ _ , err := ag .installVSCodeHooks (tempDir , false , false )
184+ require .NoError (t , err )
185+ _ , err = ag .installVSCodeHooks (tempDir , false , true )
186+ require .NoError (t , err )
163187
164188 hooks := readVSCodeFile (t , tempDir )
165189 require .Len (t , hooks [VSCodeEventUserPromptSubmit ], 1 )
166- require .Len (t , hooks [VSCodeEventStop ], 1 )
190+ require .Len (t , hooks [VSCodeEventStop ], 2 , "force reinstall keeps both agent-stop + session-end" )
191+ }
192+
193+ func TestUninstallVSCodeHooks_PreservesUserTopLevelFields (t * testing.T ) {
194+ t .Parallel ()
195+ tempDir := t .TempDir ()
196+
197+ // File with only Entire-managed hooks but a user-added top-level field.
198+ seed := `{
199+ "version": 1,
200+ "customField": "keep-me",
201+ "hooks": {
202+ "Stop": [
203+ {"type": "command", "command": "` + agent .WrapProductionSilentHookCommand ("entire hooks copilot-cli agent-stop" ) + `"}
204+ ]
205+ }
206+ }`
207+ require .NoError (t , os .MkdirAll (filepath .Join (tempDir , hooksDir ), 0o755 ))
208+ require .NoError (t , os .WriteFile (vsCodeHooksPath (tempDir ), []byte (seed ), 0o600 ))
209+
210+ ag := & CopilotCLIAgent {}
211+ require .NoError (t , ag .uninstallVSCodeHooks (tempDir ))
212+
213+ // File must survive (custom field) but Entire's hook is gone.
214+ require .FileExists (t , vsCodeHooksPath (tempDir ), "file kept because customField is user-owned" )
215+ data , err := os .ReadFile (vsCodeHooksPath (tempDir ))
216+ require .NoError (t , err )
217+ require .Contains (t , string (data ), "keep-me" )
218+ require .False (t , ag .areVSCodeHooksInstalled (tempDir ))
167219}
168220
169221func TestInstallHooks_AlsoInstallsVSCodeFile (t * testing.T ) {
0 commit comments