11import { expect , test } from "bun:test" ;
22import {
33 intelligenceApiOrigin ,
4+ learningSessionOrigin ,
45 type SetupBrowser ,
56 setupSelfHostedLearning ,
67} from "./self-hosted-learning" ;
@@ -14,6 +15,7 @@ type Response = { status: number; body: unknown };
1415
1516function fixture (
1617 override ?: ( url : string , method : string ) => Response | undefined ,
18+ sessionOrigin = apiUrl ,
1719) {
1820 const calls : {
1921 url : string ;
@@ -27,8 +29,8 @@ function fixture(
2729 navigations . push ( url ) ;
2830 } ,
2931 request : async ( url , options ) => {
30- expect ( new URL ( url ) . origin ) . toBe ( apiUrl ) ;
31- expect ( options . headers . Origin ) . toBe ( apiUrl ) ;
32+ expect ( new URL ( url ) . origin ) . toBe ( sessionOrigin ) ;
33+ expect ( options . headers . Origin ) . toBe ( sessionOrigin ) ;
3234 expect ( options . headers ) . not . toHaveProperty ( "Authorization" ) ;
3335 expect ( options . maxRedirects ) . toBe ( 0 ) ;
3436 calls . push ( { url, method : options . method , data : options . data } ) ;
@@ -255,3 +257,106 @@ test("API origins reject credentials, paths, and insecure non-loopback servers",
255257 expect ( ( ) => intelligenceApiOrigin ( value ) ) . toThrow ( ) ;
256258 }
257259} ) ;
260+
261+ const localApi = "http://localhost:61860" ;
262+ const localDashboard = "http://cpk.localhost:61861" ;
263+ const localStatus = ( api = localApi , dashboard = localDashboard ) => ( {
264+ status : "completed" ,
265+ stacks : [ { urls : { api, dashboard } } ] ,
266+ } ) ;
267+
268+ test ( "local setup signs in and provisions through the dashboard while preserving the runtime API" , async ( ) => {
269+ const f = fixture ( undefined , localDashboard ) ;
270+ const connection = await setupSelfHostedLearning ( {
271+ ...f . options ,
272+ apiUrl : localApi ,
273+ existingKey : "cpk-existing-runtime-key" ,
274+ runCli : async ( args ) => {
275+ expect ( args ) . toEqual ( [ "local" , "list" , "--json" ] ) ;
276+ return { exitCode : 0 , stdout : JSON . stringify ( localStatus ( ) ) } ;
277+ } ,
278+ } ) ;
279+ const login = new URL ( f . navigations [ 0 ] ) ;
280+ expect ( login . origin ) . toBe ( localDashboard ) ;
281+ expect ( login . pathname ) . toBe ( "/auth/signin" ) ;
282+ expect ( login . searchParams . get ( "callbackUrl" ) ) . toBe ( `${ localDashboard } /` ) ;
283+ expect ( connection ) . toEqual ( {
284+ apiUrl : localApi ,
285+ apiKey : "cpk-existing-runtime-key" ,
286+ learningContainerId : "openbot" ,
287+ } ) ;
288+ expect (
289+ f . calls . some (
290+ ( call ) =>
291+ call . url === `${ localDashboard } ${ path } ` && call . method === "POST" ,
292+ ) ,
293+ ) . toBe ( true ) ;
294+ expect ( f . closed ( ) ) . toBe ( 1 ) ;
295+ } ) ;
296+
297+ test . each ( [
298+ localStatus ( localApi , "https://unrelated.example" ) ,
299+ localStatus ( localApi , "http://cpk.localhost.evil.example:61861" ) ,
300+ localStatus ( localApi , "http://user:password@cpk.localhost:61861" ) ,
301+ localStatus ( localApi , `${ localDashboard } /unexpected` ) ,
302+ { status : "error" } ,
303+ ] ) (
304+ "invalid or mismatched local status fails before opening a browser: %j" ,
305+ async ( status ) => {
306+ const f = fixture ( ) ;
307+ await expect (
308+ setupSelfHostedLearning ( {
309+ ...f . options ,
310+ apiUrl : localApi ,
311+ runCli : async ( ) => ( { exitCode : 0 , stdout : JSON . stringify ( status ) } ) ,
312+ } ) ,
313+ ) . rejects . toThrow ( "Could not identify the local Intelligence dashboard" ) ;
314+ expect ( f . navigations ) . toHaveLength ( 0 ) ;
315+ expect ( f . calls ) . toHaveLength ( 0 ) ;
316+ } ,
317+ ) ;
318+
319+ test ( "remote setup does not run the local CLI" , async ( ) => {
320+ expect (
321+ await learningSessionOrigin ( apiUrl , async ( ) => {
322+ throw new Error ( "must not run" ) ;
323+ } ) ,
324+ ) . toBe ( apiUrl ) ;
325+ } ) ;
326+
327+ test . each ( [
328+ { exitCode : 1 , stdout : "private diagnostic" } ,
329+ { exitCode : 0 , stdout : "invalid JSON" } ,
330+ ] ) (
331+ "local discovery errors are actionable and do not disclose CLI output" ,
332+ async ( result ) => {
333+ await expect (
334+ learningSessionOrigin ( localApi , async ( ) => result ) ,
335+ ) . rejects . toThrow ( "Could not identify the local Intelligence dashboard" ) ;
336+ } ,
337+ ) ;
338+
339+ test ( "an unrelated local stack does not change a manually hosted deployment" , async ( ) => {
340+ expect (
341+ await learningSessionOrigin ( localApi , async ( ) => ( {
342+ exitCode : 0 ,
343+ stdout : JSON . stringify ( localStatus ( "http://localhost:9999" ) ) ,
344+ } ) ) ,
345+ ) . toBe ( localApi ) ;
346+ } ) ;
347+
348+ test ( "multiple stacks are matched by API origin, not selection or list order" , async ( ) => {
349+ expect (
350+ await learningSessionOrigin ( localApi , async ( ) => ( {
351+ exitCode : 0 ,
352+ stdout : JSON . stringify ( {
353+ status : "completed" ,
354+ stacks : [
355+ ...localStatus ( "http://localhost:9999" , "http://cpk.localhost:9998" )
356+ . stacks ,
357+ ...localStatus ( ) . stacks ,
358+ ] ,
359+ } ) ,
360+ } ) ) ,
361+ ) . toBe ( localDashboard ) ;
362+ } ) ;
0 commit comments