@@ -4,16 +4,20 @@ import path from "node:path";
44import {
55 clearRuntimeAuthProfileStoreSnapshots ,
66 ensureAuthProfileStore ,
7+ upsertAuthProfile ,
78} from "openclaw/plugin-sdk/agent-runtime" ;
89import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api" ;
910import { afterEach , describe , expect , it , vi } from "vitest" ;
1011
11- const resolveCopilotApiTokenMock = vi . hoisted ( ( ) => vi . fn ( ) ) ;
12+ const mocks = vi . hoisted ( ( ) => ( {
13+ githubCopilotLoginCommand : vi . fn ( ) ,
14+ resolveCopilotApiToken : vi . fn ( ) ,
15+ } ) ) ;
1216
1317vi . mock ( "./register.runtime.js" , ( ) => ( {
1418 DEFAULT_COPILOT_API_BASE_URL : "https://api.githubcopilot.test" ,
15- resolveCopilotApiToken : resolveCopilotApiTokenMock ,
16- githubCopilotLoginCommand : vi . fn ( ) ,
19+ resolveCopilotApiToken : mocks . resolveCopilotApiToken ,
20+ githubCopilotLoginCommand : mocks . githubCopilotLoginCommand ,
1721 fetchCopilotUsage : vi . fn ( ) ,
1822} ) ) ;
1923
@@ -22,6 +26,7 @@ import plugin from "./index.js";
2226const tempDirs : string [ ] = [ ] ;
2327
2428afterEach ( async ( ) => {
29+ vi . clearAllMocks ( ) ;
2530 clearRuntimeAuthProfileStoreSnapshots ( ) ;
2631 await Promise . all ( tempDirs . splice ( 0 ) . map ( ( dir ) => fs . rm ( dir , { recursive : true , force : true } ) ) ) ;
2732} ) ;
@@ -98,11 +103,11 @@ describe("github-copilot plugin", () => {
98103 } as never ) ;
99104
100105 expect ( result ) . toBeNull ( ) ;
101- expect ( resolveCopilotApiTokenMock ) . not . toHaveBeenCalled ( ) ;
106+ expect ( mocks . resolveCopilotApiToken ) . not . toHaveBeenCalled ( ) ;
102107 } ) ;
103108
104109 it ( "uses live plugin config to re-enable discovery after startup disable" , async ( ) => {
105- resolveCopilotApiTokenMock . mockResolvedValueOnce ( {
110+ mocks . resolveCopilotApiToken . mockResolvedValueOnce ( {
106111 token : "copilot_api_token" ,
107112 baseUrl : "https://api.githubcopilot.live" ,
108113 } ) ;
@@ -125,7 +130,7 @@ describe("github-copilot plugin", () => {
125130 resolveProviderApiKey : ( ) => ( { apiKey : "gh_test_token" } ) ,
126131 } as never ) ;
127132
128- expect ( resolveCopilotApiTokenMock ) . toHaveBeenCalledWith ( {
133+ expect ( mocks . resolveCopilotApiToken ) . toHaveBeenCalledWith ( {
129134 githubToken : "gh_test_token" ,
130135 env : { GH_TOKEN : "gh_test_token" } ,
131136 } ) ;
@@ -137,6 +142,135 @@ describe("github-copilot plugin", () => {
137142 } ) ;
138143 } ) ;
139144
145+ it ( "offers to reuse an existing token profile during interactive onboarding" , async ( ) => {
146+ const provider = registerProviderWithPluginConfig ( { } ) ;
147+ const method = provider . auth [ 0 ] ;
148+ const agentDir = await createAgentDir ( ) ;
149+ await fs . writeFile (
150+ path . join ( agentDir , "auth-profiles.json" ) ,
151+ JSON . stringify ( {
152+ version : 1 ,
153+ profiles : {
154+ "github-copilot:github" : {
155+ type : "token" ,
156+ provider : "github-copilot" ,
157+ token : "existing-token" ,
158+ } ,
159+ } ,
160+ } ) ,
161+ ) ;
162+ const prompter = {
163+ confirm : vi . fn ( async ( ) => false ) ,
164+ note : vi . fn ( ) ,
165+ } ;
166+
167+ const result = await method . run ( {
168+ config : { } ,
169+ env : { } ,
170+ agentDir,
171+ workspaceDir : "/tmp/workspace" ,
172+ prompter,
173+ runtime : { log : vi . fn ( ) , error : vi . fn ( ) , exit : vi . fn ( ) } ,
174+ opts : { } ,
175+ secretInputMode : "plaintext" ,
176+ allowSecretRefPrompt : false ,
177+ isRemote : false ,
178+ openUrl : vi . fn ( ) ,
179+ oauth : { createVpsAwareHandlers : vi . fn ( ) } ,
180+ } as never ) ;
181+
182+ expect ( prompter . confirm ) . toHaveBeenCalledWith ( {
183+ message : "GitHub Copilot auth already exists. Re-run login?" ,
184+ initialValue : false ,
185+ } ) ;
186+ expect ( mocks . githubCopilotLoginCommand ) . not . toHaveBeenCalled ( ) ;
187+ expect ( result ) . toEqual ( {
188+ profiles : [
189+ {
190+ profileId : "github-copilot:github" ,
191+ credential : {
192+ type : "token" ,
193+ provider : "github-copilot" ,
194+ token : "existing-token" ,
195+ } ,
196+ } ,
197+ ] ,
198+ defaultModel : "github-copilot/claude-opus-4.7" ,
199+ } ) ;
200+ } ) ;
201+
202+ it ( "can refresh an existing token profile during interactive onboarding" , async ( ) => {
203+ const provider = registerProviderWithPluginConfig ( { } ) ;
204+ const method = provider . auth [ 0 ] ;
205+ const agentDir = await createAgentDir ( ) ;
206+ await fs . writeFile (
207+ path . join ( agentDir , "auth-profiles.json" ) ,
208+ JSON . stringify ( {
209+ version : 1 ,
210+ profiles : {
211+ "github-copilot:github" : {
212+ type : "token" ,
213+ provider : "github-copilot" ,
214+ token : "existing-token" ,
215+ } ,
216+ } ,
217+ } ) ,
218+ ) ;
219+ mocks . githubCopilotLoginCommand . mockImplementationOnce ( async ( opts : { agentDir ?: string } ) => {
220+ upsertAuthProfile ( {
221+ profileId : "github-copilot:github" ,
222+ credential : {
223+ type : "token" ,
224+ provider : "github-copilot" ,
225+ token : "refreshed-token" ,
226+ } ,
227+ agentDir : opts . agentDir ,
228+ } ) ;
229+ } ) ;
230+ const prompter = {
231+ confirm : vi . fn ( async ( ) => true ) ,
232+ note : vi . fn ( ) ,
233+ } ;
234+ const isTtyDescriptor = Object . getOwnPropertyDescriptor ( process . stdin , "isTTY" ) ;
235+ Object . defineProperty ( process . stdin , "isTTY" , {
236+ configurable : true ,
237+ value : true ,
238+ } ) ;
239+
240+ try {
241+ const result = await method . run ( {
242+ config : { } ,
243+ env : { } ,
244+ agentDir,
245+ workspaceDir : "/tmp/workspace" ,
246+ prompter,
247+ runtime : { log : vi . fn ( ) , error : vi . fn ( ) , exit : vi . fn ( ) } ,
248+ opts : { } ,
249+ secretInputMode : "plaintext" ,
250+ allowSecretRefPrompt : false ,
251+ isRemote : false ,
252+ openUrl : vi . fn ( ) ,
253+ oauth : { createVpsAwareHandlers : vi . fn ( ) } ,
254+ } as never ) ;
255+
256+ expect ( mocks . githubCopilotLoginCommand ) . toHaveBeenCalledWith (
257+ { yes : true , profileId : "github-copilot:github" , agentDir } ,
258+ expect . any ( Object ) ,
259+ ) ;
260+ expect ( result . profiles [ 0 ] ?. credential ) . toEqual ( {
261+ type : "token" ,
262+ provider : "github-copilot" ,
263+ token : "refreshed-token" ,
264+ } ) ;
265+ } finally {
266+ if ( isTtyDescriptor ) {
267+ Object . defineProperty ( process . stdin , "isTTY" , isTtyDescriptor ) ;
268+ } else {
269+ delete ( process . stdin as { isTTY ?: boolean } ) . isTTY ;
270+ }
271+ }
272+ } ) ;
273+
140274 it ( "stores GitHub Copilot token from non-interactive onboarding" , async ( ) => {
141275 const provider = registerProviderWithPluginConfig ( { } ) ;
142276 const method = provider . auth [ 0 ] ;
0 commit comments