@@ -6,6 +6,7 @@ package app
66import (
77 "context"
88 "errors"
9+ "strings"
910 "testing"
1011 "time"
1112
@@ -52,6 +53,13 @@ func newExtractionModel(t *testing.T, steps map[extractionStep]stepStatus) *Mode
5253 return m
5354}
5455
56+ // hintIndex returns the byte index of a hint label in ANSI-stripped overlay
57+ // output. Keycap rendering inserts variable padding between key and label,
58+ // so this searches by label word alone.
59+ func hintIndex (stripped , label string ) int {
60+ return strings .Index (stripped , label )
61+ }
62+
5563func sendExtractionKey (m * Model , key string ) {
5664 var msg tea.KeyPressMsg
5765 switch key {
@@ -3542,3 +3550,142 @@ func TestExtractionLLMCancelFn_SetSynchronouslyOnRerun(t *testing.T) {
35423550 // Clean up by cancelling the timeout context so the test doesn't leak.
35433551 ex .cancelLLMTimeout ()
35443552}
3553+
3554+ func TestExploreMode_HintBarTrailingOrder (t * testing.T ) {
3555+ t .Parallel ()
3556+ m := newPreviewModel (t , []extract.Operation {
3557+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "A" }},
3558+ })
3559+ ex := m .ex .extraction
3560+
3561+ // Pipeline mode: x should come after a and before esc.
3562+ pipelineOut := m .buildExtractionPipelineOverlay (100 , 90 , "test" )
3563+ pipelinePlain := ansi .Strip (pipelineOut )
3564+
3565+ pipelineAcceptIdx := hintIndex (pipelinePlain , "accept" )
3566+ pipelineExploreIdx := hintIndex (pipelinePlain , "explore" )
3567+ pipelineEscIdx := hintIndex (pipelinePlain , "discard" )
3568+ require .Greater (t , pipelineAcceptIdx , 0 , "accept hint should appear" )
3569+ require .Greater (t , pipelineExploreIdx , 0 , "explore hint should appear" )
3570+ require .Greater (t , pipelineEscIdx , 0 , "discard hint should appear" )
3571+ assert .Less (t , pipelineAcceptIdx , pipelineExploreIdx ,
3572+ "accept should come before explore" )
3573+ assert .Less (t , pipelineExploreIdx , pipelineEscIdx ,
3574+ "explore should come before discard" )
3575+
3576+ // Enter explore mode: x should still come after a and before esc.
3577+ sendExtractionKey (m , "x" )
3578+ require .True (t , ex .exploring )
3579+
3580+ exploreOut := m .buildExtractionPipelineOverlay (100 , 90 , "test" )
3581+ explorePlain := ansi .Strip (exploreOut )
3582+
3583+ exploreAcceptIdx := hintIndex (explorePlain , "accept" )
3584+ exploreBackIdx := hintIndex (explorePlain , "back" )
3585+ exploreEscIdx := hintIndex (explorePlain , "discard" )
3586+ require .Greater (t , exploreAcceptIdx , 0 , "accept hint should appear" )
3587+ require .Greater (t , exploreBackIdx , 0 , "back hint should appear" )
3588+ require .Greater (t , exploreEscIdx , 0 , "discard hint should appear" )
3589+ assert .Less (t , exploreAcceptIdx , exploreBackIdx ,
3590+ "accept should come before back" )
3591+ assert .Less (t , exploreBackIdx , exploreEscIdx ,
3592+ "back should come before discard" )
3593+ }
3594+
3595+ func TestExploreMode_HintBarTrailingOrderMultiTab (t * testing.T ) {
3596+ t .Parallel ()
3597+ m := newPreviewModel (t , []extract.Operation {
3598+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "A" }},
3599+ {
3600+ Action : "create" ,
3601+ Table : data .TableQuotes ,
3602+ Data : map [string ]any {"total_cents" : float64 (100 )},
3603+ },
3604+ })
3605+ ex := m .ex .extraction
3606+
3607+ // Explore mode with multiple tabs: b/f tabs hint appears but
3608+ // a/x/esc must still be trailing three.
3609+ sendExtractionKey (m , "x" )
3610+ require .True (t , ex .exploring )
3611+ require .Greater (t , len (ex .previewGroups ), 1 , "need multiple tabs" )
3612+
3613+ out := m .buildExtractionPipelineOverlay (100 , 90 , "test" )
3614+ plain := ansi .Strip (out )
3615+
3616+ tabsIdx := hintIndex (plain , "tabs" )
3617+ acceptIdx := hintIndex (plain , "accept" )
3618+ backIdx := hintIndex (plain , "back" )
3619+ escIdx := hintIndex (plain , "discard" )
3620+ require .Greater (t , tabsIdx , 0 , "tabs hint should appear" )
3621+ require .Greater (t , acceptIdx , 0 )
3622+ require .Greater (t , backIdx , 0 )
3623+ require .Greater (t , escIdx , 0 )
3624+
3625+ assert .Less (t , tabsIdx , acceptIdx ,
3626+ "tabs should come before stable trailing group" )
3627+ assert .Less (t , acceptIdx , backIdx ,
3628+ "accept should come before back" )
3629+ assert .Less (t , backIdx , escIdx ,
3630+ "back should come before discard" )
3631+ }
3632+
3633+ func TestExploreMode_OverlayHeightStableAcrossToggle (t * testing.T ) {
3634+ t .Parallel ()
3635+ // Two tables with different row counts to trigger height change.
3636+ m := newPreviewModel (t , []extract.Operation {
3637+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "A" }},
3638+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "B" }},
3639+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "C" }},
3640+ {
3641+ Action : "create" ,
3642+ Table : data .TableQuotes ,
3643+ Data : map [string ]any {"total_cents" : float64 (100 )},
3644+ },
3645+ })
3646+ ex := m .ex .extraction
3647+
3648+ // Pipeline mode overlay.
3649+ pipelineOut := m .buildExtractionPipelineOverlay (100 , 90 , "test" )
3650+ pipelineLines := strings .Count (pipelineOut , "\n " )
3651+
3652+ // Enter explore mode and switch to tab with fewer rows.
3653+ sendExtractionKey (m , "x" )
3654+ require .True (t , ex .exploring )
3655+ sendExtractionKey (m , "f" ) // switch to quotes tab (1 row vs 3)
3656+ require .Equal (t , 1 , ex .previewTab )
3657+
3658+ exploreOut := m .buildExtractionPipelineOverlay (100 , 90 , "test" )
3659+ exploreLines := strings .Count (exploreOut , "\n " )
3660+
3661+ assert .Equal (t , pipelineLines , exploreLines ,
3662+ "overlay height should be identical regardless of active tab row count" )
3663+ }
3664+
3665+ func TestExploreMode_TabPersistsOnExit (t * testing.T ) {
3666+ t .Parallel ()
3667+ m := newPreviewModel (t , []extract.Operation {
3668+ {Action : "create" , Table : data .TableVendors , Data : map [string ]any {"name" : "A" }},
3669+ {
3670+ Action : "create" ,
3671+ Table : data .TableQuotes ,
3672+ Data : map [string ]any {"total_cents" : float64 (100 )},
3673+ },
3674+ })
3675+ ex := m .ex .extraction
3676+
3677+ // Enter explore, switch to tab 1, exit.
3678+ sendExtractionKey (m , "x" )
3679+ require .True (t , ex .exploring )
3680+ sendExtractionKey (m , "f" )
3681+ require .Equal (t , 1 , ex .previewTab )
3682+ sendExtractionKey (m , "x" ) // exit explore
3683+ require .False (t , ex .exploring )
3684+
3685+ // Pipeline mode should now show tab 1 (dimmed), not reset to 0.
3686+ out := m .renderOperationPreviewSection (80 , false )
3687+ plain := ansi .Strip (out )
3688+ // Tab 1 is Quotes -- its data should appear in rendered table.
3689+ assert .Contains (t , plain , "Total" ,
3690+ "pipeline mode should show last-explored tab content" )
3691+ }
0 commit comments