@@ -3,6 +3,7 @@ import { expect, test, type Browser, type BrowserContext, type Page } from "@pla
33import { spawn , type ChildProcess } from "node:child_process" ;
44import { createConnection } from "node:net" ;
55import { kill as killProcess } from "node:process" ;
6+ import type { WorkspaceToolContext } from "../src/lib/workspace/tool-context" ;
67
78const supabaseUrl = process . env . LOCAL_SUPABASE_URL ;
89const anonKey = process . env . LOCAL_SUPABASE_ANON_KEY ;
@@ -348,3 +349,142 @@ test("real local users collaborate, recover, review, restore, and revoke access"
348349 await Promise . allSettled ( ids . map ( ( id ) => admin . auth . admin . deleteUser ( id ) ) ) ;
349350 }
350351} ) ;
352+
353+ test ( "workspace evidence, team AI review, and practice survive a teammate reload" , async ( { browser } , testInfo ) => {
354+ test . skip ( testInfo . project . name === "mobile-chromium" , "The core mobile collaboration journey runs above" ) ;
355+ test . setTimeout ( 120_000 ) ;
356+ const admin = createClient ( supabaseUrl ! , serviceKey ! , { auth : { persistSession : false } } ) ;
357+ const password = `Local-${ crypto . randomUUID ( ) } !` ;
358+ const ids : string [ ] = [ ] ;
359+ const contexts : BrowserContext [ ] = [ ] ;
360+ let collaboration : ChildProcess | undefined ;
361+
362+ try {
363+ const sessions : Session [ ] = [ ] ;
364+ for ( const prefix of [ "practice-owner" , "practice-member" ] ) {
365+ const email = `${ prefix } -${ Date . now ( ) } -${ crypto . randomUUID ( ) } @workspace.test` ;
366+ const created = await admin . auth . admin . createUser ( { email, email_confirm : true , password } ) ;
367+ if ( created . error || ! created . data . user ) throw created . error ?? new Error ( "User creation failed" ) ;
368+ ids . push ( created . data . user . id ) ;
369+ const client = createClient ( supabaseUrl ! , anonKey ! , { auth : { persistSession : false } } ) ;
370+ const signedIn = await client . auth . signInWithPassword ( { email, password } ) ;
371+ if ( signedIn . error || ! signedIn . data . session ) throw signedIn . error ?? new Error ( "Sign in failed" ) ;
372+ sessions . push ( signedIn . data . session ) ;
373+ }
374+
375+ collaboration = await startCollaboration ( ) ;
376+ const owner = await authenticatedPage ( browser , sessions [ 0 ] ) ;
377+ const member = await authenticatedPage ( browser , sessions [ 1 ] ) ;
378+ contexts . push ( owner . context , member . context ) ;
379+ await owner . page . route ( "**/functions/v1/ai" , async ( route ) => {
380+ const body = route . request ( ) . postDataJSON ( ) as {
381+ input ?: { message ?: string } ;
382+ workspaceContext ?: WorkspaceToolContext ;
383+ } ;
384+ const workspaceContext = body . workspaceContext ;
385+ if ( workspaceContext ?. threadId && workspaceContext . threadRequest && body . input ?. message ) {
386+ const started = await admin . rpc ( "start_workspace_thread_generation" , {
387+ p_assistant_message_id : workspaceContext . threadRequest . assistantMessageId ,
388+ p_content : body . input . message ,
389+ p_request_id : workspaceContext . requestId ,
390+ p_thread_id : workspaceContext . threadId ,
391+ p_user_id : ids [ 0 ] ,
392+ p_user_message_id : workspaceContext . threadRequest . userMessageId ,
393+ p_workspace_id : workspaceContext . workspaceId ,
394+ } ) ;
395+ if ( started . error ) throw started . error ;
396+ }
397+ await route . fulfill ( {
398+ body : JSON . stringify ( { output : { response : "Phones should be limited with a clear enforcement mechanism." , followUpQuestions : [ ] , uncertainties : [ ] } } ) ,
399+ contentType : "application/json" ,
400+ status : 200 ,
401+ } ) ;
402+ } ) ;
403+
404+ await owner . page . goto ( "/app/workspaces" ) ;
405+ await owner . page . getByRole ( "textbox" , { name : "Motion" } ) . fill ( "Schools should limit phones" ) ;
406+ await owner . page . getByRole ( "button" , { name : "Create workspace" } ) . click ( ) ;
407+ await expect ( owner . page ) . toHaveURL ( / \/ a p p \/ w o r k s p a c e s \/ [ 0 - 9 a - f - ] + $ / , { timeout : 20_000 } ) ;
408+ const workspaceUrl = owner . page . url ( ) ;
409+ const workspaceId = workspaceUrl . split ( "/" ) . at ( - 1 ) ! ;
410+ const membership = await admin . from ( "workspace_members" ) . insert ( { workspace_id : workspaceId , user_id : ids [ 1 ] } ) ;
411+ if ( membership . error ) throw membership . error ;
412+
413+ await owner . page . getByRole ( "button" , { name : "R1" } ) . click ( ) ;
414+ await owner . page . locator ( ".ProseMirror" ) . fill ( "Phones interrupt learning" ) ;
415+ await expect ( owner . page . getByText ( "Saved" , { exact : true } ) ) . toBeVisible ( ) ;
416+
417+ await openPanel ( owner . page , "Evidence" ) ;
418+ await owner . page . getByLabel ( "Source title" ) . fill ( "Attention study" ) ;
419+ await owner . page . getByLabel ( "Quoted evidence" ) . fill ( "Notifications reduce sustained attention." ) ;
420+ await owner . page . getByRole ( "button" , { name : "Add evidence" } ) . click ( ) ;
421+ await expect ( owner . page . getByText ( "Attention study" , { exact : true } ) ) . toBeVisible ( ) ;
422+ await owner . page . getByRole ( "button" , { name : "Insert citation" } ) . click ( ) ;
423+ await expect ( owner . page . locator ( ".ProseMirror" ) ) . toContainText ( "Attention study" ) ;
424+
425+ await openPanel ( owner . page , "Team threads" ) ;
426+ await owner . page . getByLabel ( "Thread name" ) . fill ( "Both sides" ) ;
427+ await owner . page . getByRole ( "button" , { name : "Create" , exact : true } ) . click ( ) ;
428+ await owner . page . getByLabel ( "Team message" ) . fill ( "Compare both sides" ) ;
429+ await owner . page . getByRole ( "button" , { name : "Send" , exact : true } ) . click ( ) ;
430+ let pendingMessage : { request_id : string ; thread_id : string } | null = null ;
431+ await expect . poll ( async ( ) => {
432+ const result = await admin . from ( "workspace_thread_messages" )
433+ . select ( "request_id,thread_id" ) . eq ( "workspace_id" , workspaceId ) . eq ( "role" , "assistant" ) . eq ( "status" , "progress" ) . maybeSingle ( ) ;
434+ if ( result . error ) throw result . error ;
435+ pendingMessage = result . data ;
436+ return Boolean ( pendingMessage ) ;
437+ } ) . toBe ( true ) ;
438+ if ( ! pendingMessage ) throw new Error ( "Thread progress was not stored" ) ;
439+ const completed = await admin . rpc ( "complete_workspace_thread_generation" , {
440+ p_content : "Phones should be limited with a clear enforcement mechanism." ,
441+ p_request_id : pendingMessage . request_id ,
442+ p_status : "complete" ,
443+ p_thread_id : pendingMessage . thread_id ,
444+ p_user_id : ids [ 0 ] ,
445+ p_workspace_id : workspaceId ,
446+ } ) ;
447+ if ( completed . error ) throw completed . error ;
448+ await expect ( owner . page . getByText ( / c l e a r e n f o r c e m e n t m e c h a n i s m / ) ) . toBeVisible ( ) ;
449+
450+ await openPanel ( owner . page , "Practice" ) ;
451+ await owner . page . getByRole ( "button" , { name : "Suggest AI rewrite" } ) . click ( ) ;
452+ await openPanel ( owner . page , "Review" ) ;
453+ await expect ( owner . page . getByRole ( "button" , { name : "Reject" } ) ) . toHaveCount ( 1 ) ;
454+ await owner . page . getByRole ( "button" , { name : "Reject" } ) . click ( ) ;
455+ await expect ( owner . page . locator ( ".ProseMirror" ) ) . toContainText ( "Phones interrupt learning" ) ;
456+ await openPanel ( owner . page , "Practice" ) ;
457+ await owner . page . getByRole ( "button" , { name : "Suggest AI rewrite" } ) . click ( ) ;
458+ await openPanel ( owner . page , "Review" ) ;
459+ await expect ( owner . page . getByRole ( "button" , { name : "Accept" } ) ) . toHaveCount ( 1 ) ;
460+ await owner . page . getByRole ( "button" , { name : "Accept" } ) . click ( ) ;
461+ await expect ( owner . page . locator ( ".ProseMirror" ) ) . toContainText ( "clear enforcement mechanism" ) ;
462+
463+ await openPanel ( owner . page , "Practice" ) ;
464+ await owner . page . getByRole ( "button" , { name : "Crossfire" , exact : true } ) . click ( ) ;
465+ await expect ( owner . page ) . toHaveURL ( / \/ a p p \/ c r o s s f i r e $ / ) ;
466+ await expect ( owner . page . getByTestId ( "crossfire-resolution" ) ) . toHaveValue ( "Schools should limit phones" ) ;
467+ await expect ( owner . page . getByText ( / S h a r e d w o r k s p a c e s e s s i o n / ) ) . toBeVisible ( ) ;
468+ await expect . poll ( async ( ) => {
469+ const result = await admin . from ( "workspace_artifacts" ) . select ( "id" ) . eq ( "workspace_id" , workspaceId ) . eq ( "tool" , "crossfire" ) ;
470+ if ( result . error ) throw result . error ;
471+ return result . data . length ;
472+ } ) . toBe ( 1 ) ;
473+ await owner . page . getByRole ( "link" , { name : "Return to workspace" } ) . click ( ) ;
474+
475+ await member . page . goto ( workspaceUrl ) ;
476+ await member . page . getByRole ( "button" , { name : "R1" } ) . click ( ) ;
477+ await openPanel ( member . page , "Practice" ) ;
478+ await expect ( member . page . locator ( "summary" ) . filter ( { hasText : / C r o s s f i r e .* p r o g r e s s / i } ) ) . toBeVisible ( ) ;
479+ await member . page . reload ( ) ;
480+ await openPanel ( member . page , "Practice" ) ;
481+ await expect ( member . page . locator ( "summary" ) . filter ( { hasText : / C r o s s f i r e .* p r o g r e s s / i } ) ) . toBeVisible ( ) ;
482+
483+ await owner . page . goto ( "/app/ask" ) ;
484+ await expect ( owner . page . getByText ( / S h a r e d w o r k s p a c e s e s s i o n / ) ) . toHaveCount ( 0 ) ;
485+ } finally {
486+ await stop ( collaboration ) ;
487+ await Promise . allSettled ( contexts . map ( ( context ) => context . close ( ) ) ) ;
488+ await Promise . allSettled ( ids . map ( ( id ) => admin . auth . admin . deleteUser ( id ) ) ) ;
489+ }
490+ } ) ;
0 commit comments