import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { initKtxProject, loadKtxProject } from '../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createKtxCliScanConnector } from '../src/local-scan-connectors.js'; const bigQueryMock = vi.hoisted(() => ({ constructorInputs: [] as Array<{ connectionId: string; connection: unknown; }>, })); vi.mock('../src/connectors/bigquery/connector.js', () => ({ isKtxBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) => String(connection?.driver ?? '').toLowerCase() === 'bigquery', KtxBigQueryScanConnector: class { readonly id: string; readonly driver = 'bigquery'; constructor(options: { connectionId: string; connection: unknown }) { bigQueryMock.constructorInputs.push(options); this.id = `bigquery:${options.connectionId}`; } }, })); describe('createKtxCliScanConnector', () => { let tempDir: string; beforeEach(async () => { bigQueryMock.constructorInputs.length = 0; tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-scan-connector-')); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); it('creates a native sqlite connector from standalone config', async () => { await initKtxProject({ projectDir: tempDir }); await writeFile( join(tempDir, 'ktx.yaml'), [ 'connections:', ' warehouse:', ' driver: sqlite', ' path: warehouse.db', '', ].join('\n'), 'utf-8', ); const project = await loadKtxProject({ projectDir: tempDir }); const connector = await createKtxCliScanConnector(project, 'warehouse'); expect(connector.id).toBe('sqlite:warehouse'); expect(connector.driver).toBe('sqlite'); }); it('passes canonical BigQuery YAML scan limits through to the connector', async () => { await initKtxProject({ projectDir: tempDir }); await writeFile( join(tempDir, 'ktx.yaml'), [ 'connections:', ' warehouse:', ' driver: bigquery', ' dataset_id: analytics', ' max_bytes_billed: "987654321"', ' job_timeout_ms: 30000', '', ].join('\n'), 'utf-8', ); const project = await loadKtxProject({ projectDir: tempDir }); const connector = await createKtxCliScanConnector(project, 'warehouse'); expect(connector.id).toBe('bigquery:warehouse'); expect(connector.driver).toBe('bigquery'); expect(bigQueryMock.constructorInputs).toEqual([ expect.objectContaining({ connectionId: 'warehouse', connection: expect.objectContaining({ max_bytes_billed: '987654321', job_timeout_ms: 30000, }), }), ]); expect(bigQueryMock.constructorInputs[0]).not.toHaveProperty('maxBytesBilled'); }); it('rejects daemon-only fallback driver configs at config parse time', async () => { await initKtxProject({ projectDir: tempDir }); await writeFile( join(tempDir, 'ktx.yaml'), [ 'connections:', ' warehouse:', ' driver: duckdb', ' path: warehouse.duckdb', '', ].join('\n'), 'utf-8', ); await expect(loadKtxProject({ projectDir: tempDir })).rejects.toThrow( /connections\.warehouse\.driver:.*Invalid discriminator value/, ); }); it('rejects connection blocks with no driver field at config parse time', async () => { await initKtxProject({ projectDir: tempDir }); await writeFile( join(tempDir, 'ktx.yaml'), [ 'connections:', ' warehouse:', ' type: postgres', ' url: postgresql://example/db', '', ].join('\n'), 'utf-8', ); await expect(loadKtxProject({ projectDir: tempDir })).rejects.toThrow( /connections\.warehouse\.driver:.*Invalid discriminator value/, ); }); });