2026-05-13 18:41:44 -04:00
/* @jsxImportSource react */
import { render as renderInkTest } from 'ink-testing-library' ;
import { type ReactNode } from 'react' ;
import { describe , expect , it , vi } from 'vitest' ;
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
import { buildInitialState , buildPickerTree , type TreePickerNodeInput } from '../src/tree-picker-state.js' ;
2026-05-13 18:41:44 -04:00
import {
TreePickerApp ,
renderTreePickerTui ,
resolveTreePickerWidth ,
sanitizeTreePickerTuiError ,
treePickerCommandForInkInput ,
windowItems ,
windowOffset ,
type TreePickerChrome ,
type TreePickerInkInstance ,
type TreePickerInkRenderOptions ,
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
} from '../src/tree-picker-tui.js' ;
2026-05-13 18:41:44 -04:00
const IDS = {
engineering : '11111111-1111-1111-1111-111111111111' ,
architecture : '22222222-2222-2222-2222-222222222222' ,
marketing : '33333333-3333-3333-3333-333333333333' ,
finance : '44444444-4444-4444-4444-444444444444' ,
ops : '55555555-5555-5555-5555-555555555555' ,
sales : '66666666-6666-6666-6666-666666666666' ,
support : '77777777-7777-7777-7777-777777777777' ,
product : '88888888-8888-8888-8888-888888888888' ,
design : '99999999-9999-9999-9999-999999999999' ,
} ;
function pages ( ) : TreePickerNodeInput [ ] {
return [
{ id : IDS.engineering , title : 'Engineering Docs' , archived : false , parentId : null } ,
{ id : IDS.architecture , title : 'Architecture' , archived : false , parentId : IDS.engineering } ,
{ id : IDS.marketing , title : 'Marketing' , archived : false , parentId : null } ,
] ;
}
function manyPages ( ) : TreePickerNodeInput [ ] {
return [
{ id : IDS.engineering , title : 'Engineering Docs' , archived : false , parentId : null } ,
{ id : IDS.architecture , title : 'Architecture' , archived : false , parentId : IDS.engineering } ,
{ id : IDS.marketing , title : 'Marketing' , archived : false , parentId : null } ,
{ id : IDS.finance , title : 'Finance' , archived : false , parentId : null } ,
{ id : IDS.ops , title : 'Operations' , archived : false , parentId : null } ,
{ id : IDS.sales , title : 'Sales' , archived : false , parentId : null } ,
{ id : IDS.support , title : 'Support' , archived : false , parentId : null } ,
{ id : IDS.product , title : 'Product' , archived : false , parentId : null } ,
{ id : IDS.design , title : 'Design' , archived : false , parentId : null } ,
] ;
}
function state ( options : { requireConfirmOnSave? : boolean } = { } ) {
return buildInitialState ( {
tree : buildPickerTree ( pages ( ) ) ,
existingSelectedIds : [ ] ,
requireConfirmOnSave : options.requireConfirmOnSave ? ? false ,
} ) ;
}
function chrome ( overrides : Partial < TreePickerChrome > = { } ) : TreePickerChrome {
return {
title : 'Select items' ,
subtitleLines : [ 'Source: Test' ] ,
. . . overrides ,
} ;
}
async function waitForInkInput ( ) : Promise < void > {
await new Promise ( ( resolve ) = > setTimeout ( resolve , 10 ) ) ;
}
function fakeInkInstance ( ) : TreePickerInkInstance {
return {
rerender : vi.fn ( ) ,
unmount : vi.fn ( ) ,
waitUntilExit : vi.fn ( async ( ) = > undefined ) ,
} ;
}
function normalizeFrameWrap ( frame : string | undefined ) : string {
return frame ? . replace ( /\n/g , ' ' ) . replace ( /│ /g , '' ) . replace ( / +/g , ' ' ) ? ? '' ;
}
describe ( 'treePickerCommandForInkInput' , ( ) = > {
2026-05-24 19:29:37 +02:00
const browse = ( overrides : Partial < { search : { query : string } ; isNavigating : boolean ; pendingConfirm : null } > = { } ) = > ( {
search : { query : '' } ,
isNavigating : false ,
pendingConfirm : null ,
. . . overrides ,
} ) ;
const confirming = { . . . browse ( ) , pendingConfirm : 'save-confirm' as const } ;
it ( 'routes cursor and confirm keys when no query is typed' , ( ) = > {
expect ( treePickerCommandForInkInput ( '' , { downArrow : true } , browse ( ) ) ) . toBe ( 'cursor-down' ) ;
expect ( treePickerCommandForInkInput ( '' , { upArrow : true } , browse ( ) ) ) . toBe ( 'cursor-up' ) ;
expect ( treePickerCommandForInkInput ( '' , { rightArrow : true } , browse ( ) ) ) . toBe ( 'cursor-right' ) ;
expect ( treePickerCommandForInkInput ( '' , { leftArrow : true } , browse ( ) ) ) . toBe ( 'cursor-left' ) ;
expect ( treePickerCommandForInkInput ( '' , { return : true } , browse ( ) ) ) . toBe ( 'save-request' ) ;
expect ( treePickerCommandForInkInput ( '' , { escape : true } , browse ( ) ) ) . toBe ( 'quit' ) ;
expect ( treePickerCommandForInkInput ( 'c' , { ctrl : true } , browse ( ) ) ) . toBe ( 'quit' ) ;
} ) ;
it ( 'Tab toggles selection regardless of search/navigation state' , ( ) = > {
expect ( treePickerCommandForInkInput ( '' , { tab : true } , browse ( ) ) ) . toBe ( 'toggle-check' ) ;
expect ( treePickerCommandForInkInput ( '' , { tab : true } , browse ( { search : { query : 'foo' } , isNavigating : false } ) ) ) . toBe (
'toggle-check' ,
) ;
expect ( treePickerCommandForInkInput ( '' , { tab : true } , browse ( { isNavigating : true } ) ) ) . toBe ( 'toggle-check' ) ;
} ) ;
it ( 'Space toggles only when navigating; otherwise typed into the search query' , ( ) = > {
expect ( treePickerCommandForInkInput ( ' ' , { } , browse ( { isNavigating : true } ) ) ) . toBe ( 'toggle-check' ) ;
expect ( treePickerCommandForInkInput ( ' ' , { } , browse ( { isNavigating : false } ) ) ) . toEqual ( {
type : 'search-input' ,
value : ' ' ,
} ) ;
} ) ;
it ( 'typed printable chars feed the search query — including a, n, and slash' , ( ) = > {
expect ( treePickerCommandForInkInput ( 'a' , { } , browse ( ) ) ) . toEqual ( { type : 'search-input' , value : 'a' } ) ;
expect ( treePickerCommandForInkInput ( 'n' , { } , browse ( ) ) ) . toEqual ( { type : 'search-input' , value : 'n' } ) ;
expect ( treePickerCommandForInkInput ( '/' , { } , browse ( ) ) ) . toEqual ( { type : 'search-input' , value : '/' } ) ;
expect ( treePickerCommandForInkInput ( 'x' , { } , browse ( { search : { query : 'foo' } } ) ) ) . toEqual ( {
2026-05-13 18:41:44 -04:00
type : 'search-input' ,
value : 'x' ,
} ) ;
2026-05-24 19:29:37 +02:00
} ) ;
it ( 'Ctrl+A and Ctrl+N drive the bulk toggle helpers' , ( ) = > {
expect ( treePickerCommandForInkInput ( 'a' , { ctrl : true } , browse ( ) ) ) . toBe ( 'toggle-select-all-visible' ) ;
expect ( treePickerCommandForInkInput ( 'n' , { ctrl : true } , browse ( ) ) ) . toBe ( 'select-none' ) ;
} ) ;
it ( 'Backspace deletes from the query at any time; Esc clears query first then quits' , ( ) = > {
expect ( treePickerCommandForInkInput ( '' , { backspace : true } , browse ( { search : { query : 'x' } } ) ) ) . toBe (
2026-05-13 18:41:44 -04:00
'search-backspace' ,
) ;
2026-05-24 19:29:37 +02:00
expect ( treePickerCommandForInkInput ( '' , { delete : true } , browse ( { search : { query : 'x' } } ) ) ) . toBe (
'search-backspace' ,
2026-05-13 18:41:44 -04:00
) ;
2026-05-24 19:29:37 +02:00
expect ( treePickerCommandForInkInput ( '' , { escape : true } , browse ( { search : { query : 'x' } } ) ) ) . toBe ( 'search-clear' ) ;
expect ( treePickerCommandForInkInput ( '' , { escape : true } , browse ( ) ) ) . toBe ( 'quit' ) ;
} ) ;
2026-05-13 18:41:44 -04:00
2026-05-24 19:29:37 +02:00
it ( 'confirm prompts intercept y/n/Enter/Esc before search routing' , ( ) = > {
expect ( treePickerCommandForInkInput ( 'y' , { } , confirming ) ) . toBe ( 'save-confirm' ) ;
expect ( treePickerCommandForInkInput ( '' , { return : true } , confirming ) ) . toBe ( 'save-confirm' ) ;
expect ( treePickerCommandForInkInput ( 'n' , { } , confirming ) ) . toBe ( 'save-cancel' ) ;
expect ( treePickerCommandForInkInput ( '' , { escape : true } , confirming ) ) . toBe ( 'save-cancel' ) ;
2026-05-13 18:41:44 -04:00
} ) ;
} ) ;
describe ( 'window helpers' , ( ) = > {
it ( 'centers the selected row and returns the visible slice' , ( ) = > {
expect ( windowOffset ( 20 , 10 , 5 ) ) . toBe ( 8 ) ;
expect ( windowItems ( [ 'a' , 'b' , 'c' , 'd' , 'e' ] , 3 , 3 ) ) . toEqual ( { items : [ 'c' , 'd' , 'e' ] , offset : 2 } ) ;
} ) ;
it ( 'clamps picker width to the design rule' , ( ) = > {
expect ( resolveTreePickerWidth ( 200 ) ) . toBe ( 120 ) ;
expect ( resolveTreePickerWidth ( 100 ) ) . toBe ( 96 ) ;
expect ( resolveTreePickerWidth ( 50 ) ) . toBe ( 60 ) ;
expect ( resolveTreePickerWidth ( undefined ) ) . toBe ( 96 ) ;
} ) ;
} ) ;
describe ( 'TreePickerApp' , ( ) = > {
it ( 'renders chrome title, subtitle, warnings, help, and row glyphs' , ( ) = > {
const initialState = {
. . . state ( ) ,
preLoadWarnings : [ '1 stale stored selections - they will be removed if you save' ] ,
} ;
const { lastFrame } = renderInkTest (
< TreePickerApp
initialState = { initialState }
chrome = { chrome ( {
title : 'Select fancy widgets' ,
subtitleLines : [ 'Workspace: Design Workspace' ] ,
warningLines : [ '5000-page cap reached - some pages not shown' ] ,
} ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { vi . fn ( ) }
/ > ,
) ;
const frame = lastFrame ( ) ? ? '' ;
expect ( frame ) . toContain ( 'Select fancy widgets' ) ;
expect ( frame ) . toContain ( 'Workspace: Design Workspace' ) ;
expect ( frame ) . toContain ( '5000-page cap reached - some pages not shown' ) ;
expect ( frame ) . toContain ( '1 stale stored selections - they will be removed if you save' ) ;
expect ( frame ) . toContain ( '◻ Engineering Docs ▸ (1)' ) ;
expect ( frame ) . toContain ( '◻ Marketing' ) ;
expect ( normalizeFrameWrap ( frame ) ) . toContain (
2026-05-24 19:29:37 +02:00
'Up/Down to move, Right/Left to expand or collapse, Tab to select, Type to search, Enter to confirm, Escape to clear search or go back, Ctrl+C to exit.' ,
2026-05-13 18:41:44 -04:00
) ;
2026-05-24 19:29:37 +02:00
expect ( frame ) . toContain ( 'Search:' ) ;
2026-05-13 18:41:44 -04:00
} ) ;
it ( 'renders custom help text when supplied' , ( ) = > {
const { lastFrame } = renderInkTest (
< TreePickerApp
initialState = { state ( ) }
chrome = { chrome ( { helpText : 'Bespoke instructions here.' } ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { vi . fn ( ) }
/ > ,
) ;
expect ( lastFrame ( ) ? ? '' ) . toContain ( 'Bespoke instructions here.' ) ;
} ) ;
it ( 'renders checked parents and locked descendants with locked glyphs' , ( ) = > {
const initialState = {
. . . state ( ) ,
checked : new Set ( [ IDS . engineering ] ) ,
expanded : new Set ( [ IDS . engineering ] ) ,
} ;
const { lastFrame } = renderInkTest (
< TreePickerApp
initialState = { initialState }
chrome = { chrome ( ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { vi . fn ( ) }
/ > ,
) ;
const frame = lastFrame ( ) ? ? '' ;
expect ( frame ) . toContain ( '◼ Engineering Docs ▾' ) ;
expect ( frame ) . toContain ( ' ◼ Architecture' ) ;
} ) ;
2026-05-14 14:35:58 +02:00
it ( 'renders the partial glyph on a parent whose descendant is checked' , ( ) = > {
const partialPages : TreePickerNodeInput [ ] = [
{ id : IDS.engineering , title : 'Engineering Docs' , archived : false , parentId : null } ,
{ id : IDS.architecture , title : 'Architecture' , archived : false , parentId : IDS.engineering } ,
] ;
const initialState = buildInitialState ( {
tree : buildPickerTree ( partialPages ) ,
existingSelectedIds : [ IDS . architecture ] ,
} ) ;
const { lastFrame } = renderInkTest (
< TreePickerApp
initialState = { initialState }
chrome = { chrome ( ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { vi . fn ( ) }
/ > ,
) ;
const frame = lastFrame ( ) ? ? '' ;
expect ( frame ) . toContain ( '◧ Engineering Docs ▾' ) ;
expect ( frame ) . toContain ( ' ◼ Architecture' ) ;
expect ( frame ) . not . toContain ( '◻ Engineering Docs' ) ;
} ) ;
2026-05-13 18:41:44 -04:00
it ( 'supports keyboard selection, confirm-on-save, and save callback' , async ( ) = > {
const onExit = vi . fn ( ) ;
const { stdin , lastFrame } = renderInkTest (
< TreePickerApp
initialState = { state ( { requireConfirmOnSave : true } ) }
chrome = { chrome ( {
confirmSaveMessage : ( current ) = >
` Confirm: ${ current . checked . size } item ${ current . checked . size === 1 ? '' : 's' } ? Press Enter or Escape. ` ,
} ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { onExit }
/ > ,
) ;
2026-05-24 19:29:37 +02:00
stdin . write ( '\t' ) ;
2026-05-13 18:41:44 -04:00
await waitForInkInput ( ) ;
expect ( lastFrame ( ) ) . toContain ( '◼ Engineering Docs' ) ;
stdin . write ( '\r' ) ;
await waitForInkInput ( ) ;
expect ( normalizeFrameWrap ( lastFrame ( ) ) ) . toContain ( 'Confirm: 1 item? Press Enter or Escape.' ) ;
stdin . write ( 'y' ) ;
await waitForInkInput ( ) ;
expect ( onExit ) . toHaveBeenCalledWith ( { kind : 'save' , selectedIds : [ IDS . engineering ] } ) ;
} ) ;
it ( 'uses the chrome-supplied skip-empty message and quits on confirm' , async ( ) = > {
const onExit = vi . fn ( ) ;
const { stdin , lastFrame } = renderInkTest (
< TreePickerApp
initialState = { state ( ) }
chrome = { chrome ( { skipEmptyMessage : 'No selections. Skip or back?' } ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { onExit }
/ > ,
) ;
stdin . write ( '\r' ) ;
await waitForInkInput ( ) ;
expect ( normalizeFrameWrap ( lastFrame ( ) ) ) . toContain ( 'No selections. Skip or back?' ) ;
expect ( onExit ) . not . toHaveBeenCalled ( ) ;
stdin . write ( 'n' ) ;
await waitForInkInput ( ) ;
expect ( lastFrame ( ) ) . not . toContain ( 'No selections. Skip or back?' ) ;
expect ( onExit ) . not . toHaveBeenCalled ( ) ;
stdin . write ( '\r' ) ;
await waitForInkInput ( ) ;
expect ( lastFrame ( ) ) . toContain ( 'No selections. Skip or back?' ) ;
stdin . write ( '\r' ) ;
await waitForInkInput ( ) ;
expect ( onExit ) . toHaveBeenCalledWith ( { kind : 'quit' } ) ;
} ) ;
it ( 'renders row-window overflow indicators when the visible list is clipped' , async ( ) = > {
const onExit = vi . fn ( ) ;
const initialState = buildInitialState ( {
tree : buildPickerTree ( manyPages ( ) ) ,
existingSelectedIds : [ ] ,
} ) ;
initialState . expanded = new Set ( [ IDS . engineering ] ) ;
const { stdin , lastFrame } = renderInkTest (
< TreePickerApp
initialState = { initialState }
chrome = { chrome ( ) }
terminalRows = { 13 }
terminalWidth = { 100 }
onExit = { onExit }
/ > ,
) ;
expect ( lastFrame ( ) ) . toContain ( '↓ 4 more' ) ;
stdin . write ( ' [B' ) ;
stdin . write ( ' [B' ) ;
stdin . write ( ' [B' ) ;
stdin . write ( ' [B' ) ;
await waitForInkInput ( ) ;
const frame = lastFrame ( ) ? ? '' ;
expect ( frame ) . toContain ( '↑ ' ) ;
expect ( frame ) . toContain ( '↓ ' ) ;
expect ( onExit ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( 'quits without saving on Ctrl+C' , async ( ) = > {
const onExit = vi . fn ( ) ;
const { stdin } = renderInkTest (
< TreePickerApp
initialState = { state ( ) }
chrome = { chrome ( ) }
terminalRows = { 24 }
terminalWidth = { 100 }
onExit = { onExit }
/ > ,
) ;
stdin . write ( ' ' ) ;
await waitForInkInput ( ) ;
expect ( onExit ) . toHaveBeenCalledWith ( { kind : 'quit' } ) ;
} ) ;
} ) ;
describe ( 'renderTreePickerTui' , ( ) = > {
it ( 'returns the app result from the Ink runtime' , async ( ) = > {
const io = {
stdin : { isTTY : true , setRawMode : vi.fn ( ) } ,
stdout : { isTTY : true , columns : 100 , rows : 24 , write : vi.fn ( ) } ,
stderr : { write : vi.fn ( ) } ,
} ;
const renderInk = vi . fn ( ( _tree : ReactNode , _options : TreePickerInkRenderOptions ) = > fakeInkInstance ( ) ) ;
await expect (
renderTreePickerTui (
{ initialState : state ( ) , chrome : chrome ( ) } ,
io ,
{ renderInk } ,
) ,
) . resolves . toEqual ( { kind : 'quit' } ) ;
expect ( renderInk ) . toHaveBeenCalledOnce ( ) ;
} ) ;
it ( 'sanitizes render errors and uses the supplied scripted-mode hint' , async ( ) = > {
expect ( sanitizeTreePickerTuiError ( new Error ( 'token=secret https://api.example.com/v1/search' ) ) ) . toBe (
'[redacted] [redacted-url]' ,
) ;
} ) ;
it ( 'falls back to quit with the scripted-mode hint when Ink cannot initialize' , async ( ) = > {
let stderr = '' ;
const io = {
stdin : { isTTY : false , setRawMode : vi.fn ( ) } ,
stdout : { isTTY : false , columns : 100 , rows : 24 , write : vi.fn ( ) } ,
stderr : {
write ( chunk : string ) {
stderr += chunk ;
} ,
} ,
} ;
await expect (
renderTreePickerTui (
{ initialState : state ( ) , chrome : chrome ( ) } ,
io ,
{
renderInk : vi.fn ( ( ) = > {
throw new Error ( 'token=secret' ) ;
} ) ,
scriptedModeHint : 'Use --no-input --foo bar for scripted mode.' ,
} ,
) ,
) . resolves . toEqual ( { kind : 'quit' } ) ;
expect ( stderr ) . toContain ( 'Use --no-input --foo bar for scripted mode.' ) ;
expect ( stderr ) . not . toContain ( 'secret' ) ;
} ) ;
} ) ;