@@ -27,6 +27,7 @@ const path = require('path');
2727
2828const ext = fs . readFileSync ( path . join ( __dirname , '..' , 'extension.js' ) , 'utf8' ) ;
2929const chatHtml = fs . readFileSync ( path . join ( __dirname , '..' , 'media' , 'chat.html' ) , 'utf8' ) ;
30+ const chatSessionsHtml = fs . readFileSync ( path . join ( __dirname , '..' , 'media' , 'sessionsView.html' ) , 'utf8' ) ;
3031const pkg = require ( '../package.json' ) ;
3132
3233let n = 0 ;
@@ -189,7 +190,11 @@ test('COMMAND: the chat TAB carries the actions the view title used to', () => {
189190 const onTab = ( pkg . contributes . menus [ 'editor/title' ] || [ ] )
190191 . filter ( ( m ) => m . when === "activeWebviewPanelId == 'levelcode.ai.chat'" ) ;
191192 const ids = onTab . map ( ( m ) => m . command ) ;
192- for ( const id of [ 'levelcode.ai.newChat' , 'levelcode.ai.addFileContext' , 'levelcode.ai.setApiKey' ] ) {
193+ // Sessions joined them: with the chat out of the right-hand bar, its container is one VS Code
194+ // hides by default, and `AI: Sessions` had no icon, no keybinding and no button anywhere — so the
195+ // only route to your own past conversations was knowing the palette entry existed.
196+ for ( const id of [ 'levelcode.ai.newChat' , 'levelcode.ai.addFileContext' , 'levelcode.ai.setApiKey' ,
197+ 'levelcode.ai.sessions' , 'levelcode.ai.memory' ] ) {
193198 assert . ok ( ids . includes ( id ) , id + ' lost its button when the sidebar view was removed' ) ;
194199 const cmd = pkg . contributes . commands . find ( ( c ) => c . command === id ) ;
195200 assert . ok ( cmd && cmd . icon , id + ' has no icon — an editor/title action with no icon renders as nothing' ) ;
@@ -199,6 +204,13 @@ test('COMMAND: the chat TAB carries the actions the view title used to', () => {
199204 m . command + ' must be in navigation, so VS Code can overflow it into … on a narrow tab' ) ;
200205 }
201206
207+ // The one thing still living in the right-hand container must stay reachable FROM the chat, or it
208+ // is reachable only by knowing it is there.
209+ assert . ok ( ids . includes ( 'levelcode.ai.sessions' ) ,
210+ 'no way from the conversation to the list of past conversations except the command palette' ) ;
211+ assert . ok ( ids . includes ( 'levelcode.ai.memory' ) ,
212+ 'no way from the conversation to what the project remembers except the command palette' ) ;
213+
202214 // And nothing may be scoped to the view that no longer exists — a stale `when` is a button that
203215 // never appears anywhere.
204216 const all = Object . values ( pkg . contributes . menus ) . flat ( ) ;
@@ -454,4 +466,41 @@ test('START: the docstring describes the values that actually exist', () => {
454466 assert . match ( doc , / ` e d i t o r ` [ \s \S ] * ` n o n e ` / , 'the docstring should name the two values that are actually supported' ) ;
455467} ) ;
456468
469+ test ( 'SESSIONS: each button lands on its own tab, and survives the view not existing yet' , ( ) => {
470+ // The panel's container is hidden by default, so the FIRST click of a session usually has to CREATE
471+ // the view — there is no webview to post `showTab` to at the moment the command runs. Posting once
472+ // and hoping is the bug this shape avoids: the tab is recorded first, posted immediately for a view
473+ // that is already up, and posted again from the view's own `listSessions` for one still loading.
474+ const body = fnBody ( ext , 'revealSessions' ) ;
475+ // Presence FIRST. An ordering assertion on indexOf alone passes when the thing is missing, because
476+ // -1 is less than every real index — so deleting the line entirely would have satisfied it.
477+ const recorded = body . indexOf ( 'pendingSessionsTab = tab' ) ;
478+ const revealed = body . indexOf ( 'levelcodeAi.sessions.focus' ) ;
479+ assert . ok ( recorded >= 0 , 'the requested tab is never recorded — the cold path has nothing to flush' ) ;
480+ assert . ok ( revealed >= 0 , 'nothing reveals the Sessions container' ) ;
481+ assert . ok ( recorded < revealed ,
482+ 'the tab must be recorded BEFORE the reveal — the view can resolve before the next statement runs' ) ;
483+ assert . match ( body , / i f \( s e s s i o n s W e b v i e w \) / , 'an already-open panel never gets told which tab to show' ) ;
484+ assert . match ( body , / p o s t M e s s a g e \( \{ t y p e : ' s h o w T a b ' , t a b \} \) / , 'the warm path does not post the tab' ) ;
485+
486+ // The cold path: the view announces itself with `listSessions`, which is where a tab requested
487+ // before it existed has to be flushed — and cleared, so a later plain reveal is not hijacked.
488+ // Anchored on the PROVIDER, not on the first `case 'listSessions'` in the file: the chat's own
489+ // /sessions modal handles a message by the same name, and an indexOf would read that one instead —
490+ // passing or failing on which handler happens to come first.
491+ const flush = fnBody ( ext , 'resolveWebviewView' ) ;
492+ assert . match ( flush , / p e n d i n g S e s s i o n s T a b / , 'a tab requested before the view loaded is never applied' ) ;
493+ assert . match ( flush , / p e n d i n g S e s s i o n s T a b = ' ' / , 'the pending tab is not cleared — the next reveal inherits it' ) ;
494+
495+ // The webview end of the contract, and the two commands that use it.
496+ assert . match ( chatSessionsHtml , / m \. t y p e = = = ' s h o w T a b ' / , 'the sessions view ignores showTab' ) ;
497+ assert . match ( ext , / r e g i s t e r C o m m a n d \( ' l e v e l c o d e \. a i \. s e s s i o n s ' , \( \) = > r e v e a l S e s s i o n s \( ' h i s t o r y ' \) \) / ,
498+ 'Sessions must name its tab, or the two buttons land in the same place' ) ;
499+ assert . match ( ext , / r e g i s t e r C o m m a n d \( ' l e v e l c o d e \. a i \. m e m o r y ' , \( \) = > r e v e a l S e s s i o n s \( ' m e m o r y ' \) \) / ,
500+ 'Memory must name its tab' ) ;
501+
502+ // Fire-and-forget reveal: a rejection here must not become an unhandled rejection in the host.
503+ assert . match ( body , / \. t h e n \( u n d e f i n e d , | \. c a t c h \( / , 'the reveal can reject unhandled' ) ;
504+ } ) ;
505+
457506console . log ( '\nchatSurface: ' + n + ' tests passed.' ) ;
0 commit comments