diff --git a/docs-site/content/docs/cli-reference/ktx-setup.mdx b/docs-site/content/docs/cli-reference/ktx-setup.mdx index 17423534..2c19bd07 100644 --- a/docs-site/content/docs/cli-reference/ktx-setup.mdx +++ b/docs-site/content/docs/cli-reference/ktx-setup.mdx @@ -137,8 +137,10 @@ Enabling query history makes deep ingest readiness matter for later When query history is enabled for PostgreSQL, Snowflake, or BigQuery, `ktx setup` runs a non-blocking readiness probe after the connection test passes. A failed probe still writes setup changes, prints the warehouse-specific -grant or extension remediation, and leaves query-history ingest disabled until -you fix the prerequisite. +grant or extension remediation, and skips query-history processing until you +fix the prerequisite. If the later schema-context build also fails, interactive +setup offers **Disable query history and retry** so you can finish database +setup with `connections..context.queryHistory.enabled: false`. For BigQuery, the remediation tells you to grant `roles/bigquery.resourceViewer` on the BigQuery project, or grant a custom role that contains diff --git a/docs/code-design.md b/docs/code-design.md index c9f93c81..15d369bf 100644 --- a/docs/code-design.md +++ b/docs/code-design.md @@ -89,3 +89,41 @@ enough reason to fix it even when the local code "works." (`loadX` vs `loadHigherX`, `createY` vs `createDefaultY`, `xClient` vs `xService`), assume callers will pick the wrong one. Unify, or document inline why both must exist. + +## Dispatch and contract leaks across per-variant layers + +Layers with multiple per-variant implementations (warehouse drivers, +dialects, LLM providers, ingest adapters, historic-SQL probes) drift +toward parallel switches and informal contracts. The patterns below +look locally reasonable per file but multiply with the number of +variants times the number of consumers — every fix has to be applied +N times, and silent drift between variants is invisible until a user +hits it. + +- **MUST NOT**: Maintain two or more files that switch on the same + enum or string union to dispatch to per-variant behavior. Promote + the dispatch to a single registry table keyed by the union, exposed + through one resolution function. If you find yourself writing the + third such switch, the second one was already a bug. +- **MUST**: When every variant of an abstraction implements the same + method, the method belongs on the shared interface. An informal + contract that every implementation happens to satisfy is a leak + waiting to happen — callers will reach for the concrete class + instead of the contract, and the next variant added will silently + forget to implement it. +- **MUST**: When a layer has both a thin shared interface and rich + per-variant concrete classes, they must agree. Either widen the + interface so callers never need the concrete class, or make the + concrete class private (test-only `/** @internal */` JSDoc plus a + boundary check in `scripts/check-boundaries.mjs`). A class that is + public AND has methods the interface does not expose is the exact + configuration that produces leaks. + +The warehouse driver / dialect layer in +`packages/cli/src/connectors//` plus +`packages/cli/src/context/connections/{dialects,drivers}.ts` is the +canonical worked example: per-driver dialect classes carry +`/** @internal */`, `scripts/check-boundaries.mjs` enforces the import +boundary, and dispatch lives in the two registry files. Apply the +same shape to any other per-variant layer that grows beyond two +implementations. diff --git a/knip.json b/knip.json index 178ff87e..270c2310 100644 --- a/knip.json +++ b/knip.json @@ -14,8 +14,8 @@ "src/telemetry/schema-writer.ts!", "src/telemetry/index.ts!", "scripts/**/*.mjs", - "src/**/*.test-utils.ts", - "src/**/acceptance-fixtures.ts", + "test/**/*.test-utils.ts", + "test/**/acceptance-fixtures.ts", "src/context/scan/relationship-benchmarks.ts!", "src/context/scan/relationship-benchmark-report.ts!" ] diff --git a/packages/cli/package.json b/packages/cli/package.json index d10c1b2b..5e5a585a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,12 +32,12 @@ "build": "tsc -p tsconfig.json && node dist/telemetry/schema-writer.js src/telemetry/events.schema.json ../../python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json && node scripts/copy-runtime-assets.mjs && node ../../scripts/prepare-cli-bin.mjs", "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", "docs:commands": "pnpm run build && node dist/print-command-tree.js", - "smoke": "vitest run src/standalone-smoke.test.ts src/example-smoke.test.ts --testTimeout 30000", - "test": "vitest run --exclude src/standalone-smoke.test.ts --exclude src/example-smoke.test.ts --exclude src/setup-databases.test.ts --exclude src/scan.test.ts --exclude src/commands/connection-metabase-setup.test.ts --exclude src/setup-models.test.ts --exclude src/setup-sources.test.ts --exclude src/setup.test.ts --exclude src/connection.test.ts --exclude src/setup-embeddings.test.ts --exclude src/ingest.test.ts --exclude src/commands/connection-mapping.test.ts --exclude src/ingest-viz.test.ts --exclude src/demo.test.ts --exclude src/setup-project.test.ts --exclude src/sl.test.ts --exclude src/local-scan-connectors.test.ts --exclude src/commands/connection-notion.test.ts --exclude src/context/scan/local-scan.test.ts --exclude src/context/mcp/local-project-ports.test.ts --exclude src/context/ingest/local-stage-ingest.test.ts --exclude src/context/sl/pglite-sl-search-prototype.test.ts --exclude src/context/core/git.service.test.ts --exclude src/context/ingest/local-adapters.test.ts --exclude src/context/ingest/local-bundle-ingest.test.ts --exclude src/context/ingest/local-metabase-ingest.test.ts --exclude src/context/sl/local-sl.test.ts --exclude src/context/search/pglite-owner-process.test.ts --exclude src/context/scan/local-enrichment-artifacts.test.ts --exclude src/context/search/pglite-spike.test.ts --exclude src/context/wiki/local-knowledge.test.ts --exclude src/context/sl/local-query.test.ts --exclude src/context/scan/relationship-review-decisions.test.ts --exclude src/context/scan/relationship-profiling.test.ts", - "test:slow": "vitest run src/setup-databases.test.ts src/scan.test.ts src/commands/connection-metabase-setup.test.ts src/setup-models.test.ts src/setup-sources.test.ts src/setup.test.ts src/connection.test.ts src/setup-embeddings.test.ts src/ingest.test.ts src/commands/connection-mapping.test.ts src/ingest-viz.test.ts src/demo.test.ts src/setup-project.test.ts src/sl.test.ts src/local-scan-connectors.test.ts src/commands/connection-notion.test.ts src/context/scan/local-scan.test.ts src/context/mcp/local-project-ports.test.ts src/context/ingest/local-stage-ingest.test.ts src/context/sl/pglite-sl-search-prototype.test.ts src/context/core/git.service.test.ts src/context/ingest/local-adapters.test.ts src/context/ingest/local-bundle-ingest.test.ts src/context/ingest/local-metabase-ingest.test.ts src/context/sl/local-sl.test.ts src/context/search/pglite-owner-process.test.ts src/context/scan/local-enrichment-artifacts.test.ts src/context/search/pglite-spike.test.ts src/context/wiki/local-knowledge.test.ts src/context/sl/local-query.test.ts src/context/scan/relationship-review-decisions.test.ts src/context/scan/relationship-profiling.test.ts --testTimeout 30000", - "type-check": "tsc -p tsconfig.json --noEmit", + "smoke": "vitest run test/standalone-smoke.test.ts test/example-smoke.test.ts --testTimeout 30000", + "test": "vitest run --exclude test/standalone-smoke.test.ts --exclude test/example-smoke.test.ts --exclude test/setup-databases.test.ts --exclude test/scan.test.ts --exclude test/commands/connection-metabase-setup.test.ts --exclude test/setup-models.test.ts --exclude test/setup-sources.test.ts --exclude test/setup.test.ts --exclude test/connection.test.ts --exclude test/setup-embeddings.test.ts --exclude test/ingest.test.ts --exclude test/commands/connection-mapping.test.ts --exclude test/ingest-viz.test.ts --exclude test/demo.test.ts --exclude test/setup-project.test.ts --exclude test/sl.test.ts --exclude test/local-scan-connectors.test.ts --exclude test/commands/connection-notion.test.ts --exclude test/context/scan/local-scan.test.ts --exclude test/context/mcp/local-project-ports.test.ts --exclude test/context/ingest/local-stage-ingest.test.ts --exclude test/context/sl/pglite-sl-search-prototype.test.ts --exclude test/context/core/git.service.test.ts --exclude test/context/ingest/local-adapters.test.ts --exclude test/context/ingest/local-bundle-ingest.test.ts --exclude test/context/ingest/local-metabase-ingest.test.ts --exclude test/context/sl/local-sl.test.ts --exclude test/context/search/pglite-owner-process.test.ts --exclude test/context/scan/local-enrichment-artifacts.test.ts --exclude test/context/search/pglite-spike.test.ts --exclude test/context/wiki/local-knowledge.test.ts --exclude test/context/sl/local-query.test.ts --exclude test/context/scan/relationship-review-decisions.test.ts --exclude test/context/scan/relationship-profiling.test.ts", + "test:slow": "vitest run test/setup-databases.test.ts test/scan.test.ts test/commands/connection-metabase-setup.test.ts test/setup-models.test.ts test/setup-sources.test.ts test/setup.test.ts test/connection.test.ts test/setup-embeddings.test.ts test/ingest.test.ts test/commands/connection-mapping.test.ts test/ingest-viz.test.ts test/demo.test.ts test/setup-project.test.ts test/sl.test.ts test/local-scan-connectors.test.ts test/commands/connection-notion.test.ts test/context/scan/local-scan.test.ts test/context/mcp/local-project-ports.test.ts test/context/ingest/local-stage-ingest.test.ts test/context/sl/pglite-sl-search-prototype.test.ts test/context/core/git.service.test.ts test/context/ingest/local-adapters.test.ts test/context/ingest/local-bundle-ingest.test.ts test/context/ingest/local-metabase-ingest.test.ts test/context/sl/local-sl.test.ts test/context/search/pglite-owner-process.test.ts test/context/scan/local-enrichment-artifacts.test.ts test/context/search/pglite-spike.test.ts test/context/wiki/local-knowledge.test.ts test/context/sl/local-query.test.ts test/context/scan/relationship-review-decisions.test.ts test/context/scan/relationship-profiling.test.ts --testTimeout 30000", + "type-check": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", "relationships:benchmarks": "pnpm --silent run build && node ../../scripts/relationship-benchmark-report.mjs", - "relationships:benchmarks:test": "KTX_RUN_RELATIONSHIP_BENCHMARKS=1 vitest run src/context/scan/relationship-benchmarks.test.ts", + "relationships:benchmarks:test": "KTX_RUN_RELATIONSHIP_BENCHMARKS=1 vitest run test/context/scan/relationship-benchmarks.test.ts", "search:pglite-spike": "node ../../scripts/pglite-hybrid-search-spike.mjs", "search:pglite-owner-prototype": "node ../../scripts/pglite-owner-process-prototype.mjs", "search:pglite-sl-prototype": "node ../../scripts/pglite-sl-search-prototype.mjs" diff --git a/packages/cli/src/clack.ts b/packages/cli/src/clack.ts index 55d3e802..2ad51e6c 100644 --- a/packages/cli/src/clack.ts +++ b/packages/cli/src/clack.ts @@ -1,7 +1,30 @@ import { cancel, confirm, isCancel, log, spinner } from '@clack/prompts'; +import type { KtxCliIo } from './cli-runtime.js'; const ESC = String.fromCharCode(0x1b); +export interface RailBufferedSource { + stdoutText(): string; + stderrText(): string; +} + +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function writePrefixedLines(write: (chunk: string) => void, output: string): void { + for (const line of output.split(/\r?\n/)) { + if (line.length > 0) { + write(`│ ${line}\n`); + } + } +} + +export function flushPrefixedBufferedCommandOutput(io: KtxCliIo, buffered: RailBufferedSource): void { + writePrefixedLines((chunk) => io.stdout.write(chunk), buffered.stdoutText()); + writePrefixedLines((chunk) => io.stderr.write(chunk), buffered.stderrText()); +} + export interface KtxCliSpinner { start(message: string): void; message(message: string): void; diff --git a/packages/cli/src/connection.ts b/packages/cli/src/connection.ts index 335bfb47..abc501a6 100644 --- a/packages/cli/src/connection.ts +++ b/packages/cli/src/connection.ts @@ -6,6 +6,7 @@ import { type NotionBotInfo, NotionClient } from './context/ingest/adapters/noti import { createLocalLookerCredentialResolver } from './context/ingest/adapters/looker/local-looker.adapter.js'; import { metabaseRuntimeConfigFromLocalConnection } from './context/ingest/adapters/metabase/local-metabase.adapter.js'; import { testRepoConnection } from './context/ingest/repo-fetch.js'; +import { getDriverRegistration } from './context/connections/drivers.js'; import { parseNotionConnectionConfig, resolveNotionConnectionAuthToken } from './context/connections/notion-config.js'; import { resolveKtxConfigReference } from './context/core/config-reference.js'; import { type KtxLocalProject, loadKtxProject } from './context/project/project.js'; @@ -272,15 +273,7 @@ async function testConnectionByDriver( return { driver, detailKey: 'Repo', detailValue: result.repoUrl }; } - if ( - driver === 'sqlite' || - driver === 'postgres' || - driver === 'mysql' || - driver === 'clickhouse' || - driver === 'sqlserver' || - driver === 'bigquery' || - driver === 'snowflake' - ) { + if (getDriverRegistration(driver)) { const result = await testNativeConnection( project, connectionId, diff --git a/packages/cli/src/connectors/bigquery/connector.ts b/packages/cli/src/connectors/bigquery/connector.ts index 871f50f4..edebe284 100644 --- a/packages/cli/src/connectors/bigquery/connector.ts +++ b/packages/cli/src/connectors/bigquery/connector.ts @@ -1,5 +1,6 @@ import { BigQuery, type TableField } from '@google-cloud/bigquery'; import { normalizeBigQueryProjectId, normalizeBigQueryRegion } from '../../context/connections/bigquery-identifiers.js'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; @@ -26,7 +27,6 @@ import { import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; -import { KtxBigQueryDialect } from './dialect.js'; export interface KtxBigQueryConnectionConfig { driver?: string; @@ -235,6 +235,23 @@ function normalizeValue(value: unknown): unknown { return value; } +/** @internal */ +export function prepareBigQueryReadOnlyQuery( + sql: string, + params?: Record, +): { sql: string; params?: Record } { + if (!params) { + return { sql, params: undefined }; + } + let processedSql = sql; + const processedParams: Record = {}; + for (const [key, value] of Object.entries(params)) { + processedSql = processedSql.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); + processedParams[key] = value; + } + return { sql: processedSql, params: Object.keys(processedParams).length > 0 ? processedParams : undefined }; +} + export function isKtxBigQueryConnectionConfig( connection: KtxBigQueryConnectionConfig | undefined, ): connection is KtxBigQueryConnectionConfig { @@ -286,7 +303,7 @@ export class KtxBigQueryScanConnector implements KtxScanConnector { private readonly now: () => Date; private readonly maxBytesBilled?: number | string; private readonly queryTimeoutMs?: number; - private readonly dialect = new KtxBigQueryDialect(); + private readonly dialect = getDialectForDriver('bigquery'); private client: KtxBigQueryClient | null = null; constructor(options: KtxBigQueryScanConnectorOptions) { @@ -364,7 +381,7 @@ export class KtxBigQueryScanConnector implements KtxScanConnector { async executeReadOnly(input: KtxBigQueryReadOnlyQueryInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const limitedSql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows); - const prepared = this.dialect.prepareQuery(limitedSql, input.params); + const prepared = prepareBigQueryReadOnlyQuery(limitedSql, input.params); const result = await this.query(prepared.sql, prepared.params); return { ...result, rowCount: result.rows.length }; } @@ -411,7 +428,7 @@ export class KtxBigQueryScanConnector implements KtxScanConnector { return this.dialect.quoteIdentifier(identifier); } - async listDatasets(): Promise { + async listSchemas(): Promise { const [datasets] = await this.getClient().getDatasets(); return datasets.map((dataset) => dataset.id).filter((id): id is string => Boolean(id)); } @@ -437,6 +454,7 @@ export class KtxBigQueryScanConnector implements KtxScanConnector { params, ); return rows.map((row) => ({ + catalog: this.resolved.projectId, schema: row.table_schema, name: row.table_name, kind: diff --git a/packages/cli/src/connectors/bigquery/dialect.ts b/packages/cli/src/connectors/bigquery/dialect.ts index 02d904ed..0e2f883e 100644 --- a/packages/cli/src/connectors/bigquery/dialect.ts +++ b/packages/cli/src/connectors/bigquery/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type BigQueryTableNameRef = Pick & Partial>; -export class KtxBigQueryDialect { - readonly type = 'bigquery'; +/** @internal */ +export class KtxBigQueryDialect implements KtxDialect { + readonly type = 'bigquery' as const; private readonly typeMappings: Record = { TIMESTAMP: 'time', @@ -27,13 +36,19 @@ export class KtxBigQueryDialect { } formatTableName(table: BigQueryTableNameRef): string { - if (table.catalog && table.db) { - return `${this.quoteIdentifier(table.catalog)}.${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}`; - } - if (table.db) { - return `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}`; - } - return this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'three-part'); + } + + formatDisplayRef(table: BigQueryTableNameRef): string { + return formatDialectDisplayRef(table, 'three-part'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'three-part'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('three-part'); } mapDataType(nativeType: string): string { @@ -93,19 +108,6 @@ export class KtxBigQueryDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS STRING)) != '' ORDER BY RAND() LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - let processedSql = sql; - const processedParams: Record = {}; - for (const [key, value] of Object.entries(params)) { - processedSql = processedSql.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); - processedParams[key] = value; - } - return { sql: processedSql, params: Object.keys(processedParams).length > 0 ? processedParams : undefined }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -121,7 +123,11 @@ export class KtxBigQueryDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -132,6 +138,18 @@ export class KtxBigQueryDialect { return `APPROX_COUNT_DISTINCT(${column})`; } + textLengthExpression(columnSql: string): string { + return `LENGTH(CAST(${columnSql} AS STRING))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS STRING)`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT STRING_AGG(CAST(value AS STRING), '\\u001F') FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` WITH sampled AS ( @@ -172,36 +190,4 @@ export class KtxBigQueryDialect { FROM sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const bigQueryGranularity = granularity.toUpperCase(); - if (timezone) { - return `DATE_TRUNC(DATETIME(${column}, '${timezone}'), ${bigQueryGranularity})`; - } - return `DATE_TRUNC(${column}, ${bigQueryGranularity})`; - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const col = timezone ? `DATETIME(${column}, '${timezone}')` : column; - const [rawAmount, rawUnit] = interval.split(' '); - let diffUnit = rawUnit!.toUpperCase(); - let amount = Number(rawAmount); - let addUnit = diffUnit; - if (diffUnit === 'WEEK') { - diffUnit = 'DAY'; - amount = amount * 7; - addUnit = 'DAY'; - } - const originExpr = origin ? `TIMESTAMP '${origin}'` : `TIMESTAMP '1970-01-01'`; - return `TIMESTAMP_ADD(${originExpr}, INTERVAL CAST(FLOOR(TIMESTAMP_DIFF(${col}, ${originExpr}, ${diffUnit}) / ${amount}) * ${amount} AS INT64) ${addUnit})`; - } - - parseIntervalToSql(interval: string): string { - const [amount, unit] = interval.split(' '); - return `INTERVAL ${amount} ${unit!.toUpperCase()}`; - } } diff --git a/packages/cli/src/connectors/clickhouse/connector.ts b/packages/cli/src/connectors/clickhouse/connector.ts index a2ee568c..74ef7a77 100644 --- a/packages/cli/src/connectors/clickhouse/connector.ts +++ b/packages/cli/src/connectors/clickhouse/connector.ts @@ -1,4 +1,5 @@ import { createClient } from '@clickhouse/client'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { createKtxConnectorCapabilities, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaColumn, type KtxSchemaSnapshot, type KtxSchemaTable, type KtxTableRef, type KtxTableSampleInput, type KtxTableListEntry, type KtxTableSampleResult } from '../../context/scan/types.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; @@ -6,7 +7,6 @@ import { readFileSync } from 'node:fs'; import { Agent as HttpsAgent } from 'node:https'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; -import { KtxClickHouseDialect } from './dialect.js'; export interface KtxClickHouseConnectionConfig { driver?: string; @@ -198,6 +198,49 @@ function clickHouseTableKey(database: string, table: string): string { return `${database}.${table}`; } +function inferClickHouseQueryParamType(value: unknown): string { + if (value === null || value === undefined) { + return 'String'; + } + if (typeof value === 'boolean') { + return 'Bool'; + } + if (typeof value === 'number') { + return Number.isInteger(value) ? 'Int64' : 'Float64'; + } + if (value instanceof Date) { + return 'DateTime'; + } + return 'String'; +} + +/** @internal */ +export function prepareClickHouseReadOnlyQuery( + sql: string, + params?: Record, +): { sql: string; params?: Record } { + if (!params) { + return { sql, params: undefined }; + } + + let parameterizedQuery = sql; + const queryParams: Record = {}; + const sortedKeys = Object.keys(params).sort((a, b) => b.length - a.length); + + for (const key of sortedKeys) { + const placeholder = `:${key}`; + if (parameterizedQuery.includes(placeholder)) { + parameterizedQuery = parameterizedQuery.replace( + new RegExp(`:${key}\\b`, 'g'), + `{${key}:${inferClickHouseQueryParamType(params[key])}}`, + ); + queryParams[key] = params[key]; + } + } + + return { sql: parameterizedQuery, params: Object.keys(queryParams).length > 0 ? queryParams : undefined }; +} + export function isKtxClickHouseConnectionConfig( connection: KtxClickHouseConnectionConfig | undefined, ): connection is KtxClickHouseConnectionConfig { @@ -256,7 +299,7 @@ export class KtxClickHouseScanConnector implements KtxScanConnector { private readonly clientFactory: KtxClickHouseClientFactory; private readonly endpointResolver?: KtxClickHouseEndpointResolver; private readonly now: () => Date; - private readonly dialect = new KtxClickHouseDialect(); + private readonly dialect = getDialectForDriver('clickhouse'); private client: KtxClickHouseClient | null = null; private resolvedEndpoint: KtxClickHouseResolvedEndpoint | null = null; @@ -408,7 +451,7 @@ export class KtxClickHouseScanConnector implements KtxScanConnector { async executeReadOnly(input: KtxClickHouseReadOnlyQueryInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const limitedSql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows); - const prepared = this.dialect.prepareQuery(limitedSql, input.params); + const prepared = prepareClickHouseReadOnlyQuery(limitedSql, input.params); const result = await this.query(prepared.sql, prepared.params); return { ...result, rowCount: result.rows.length }; } @@ -488,6 +531,7 @@ export class KtxClickHouseScanConnector implements KtxScanConnector { { schemas: filterSchemas }, ); return rows.map((row) => ({ + catalog: null, schema: row.database, name: row.name, kind: row.engine === 'View' || row.engine === 'MaterializedView' ? ('view' as const) : ('table' as const), diff --git a/packages/cli/src/connectors/clickhouse/dialect.ts b/packages/cli/src/connectors/clickhouse/dialect.ts index 48452ea6..9e470cae 100644 --- a/packages/cli/src/connectors/clickhouse/dialect.ts +++ b/packages/cli/src/connectors/clickhouse/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type ClickHouseTableNameRef = Pick & Partial>; -export class KtxClickHouseDialect { - readonly type = 'clickhouse'; +/** @internal */ +export class KtxClickHouseDialect implements KtxDialect { + readonly type = 'clickhouse' as const; private readonly typeMappings: Record = { date: 'time', @@ -45,9 +54,19 @@ export class KtxClickHouseDialect { } formatTableName(table: ClickHouseTableNameRef): string { - return table.db - ? `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}` - : this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'ansi'); + } + + formatDisplayRef(table: ClickHouseTableNameRef): string { + return formatDialectDisplayRef(table, 'ansi'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'ansi'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('ansi'); } mapDataType(nativeType: string): string { @@ -97,29 +116,6 @@ export class KtxClickHouseDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND trim(toString(${quotedColumn})) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - - let parameterizedQuery = sql; - const queryParams: Record = {}; - const sortedKeys = Object.keys(params).sort((a, b) => b.length - a.length); - - for (const key of sortedKeys) { - const placeholder = `:${key}`; - if (parameterizedQuery.includes(placeholder)) { - parameterizedQuery = parameterizedQuery.replace( - new RegExp(`:${key}\\b`, 'g'), - `{${key}:${this.inferClickHouseType(params[key])}}`, - ); - queryParams[key] = params[key]; - } - } - - return { sql: parameterizedQuery, params: queryParams }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -132,7 +128,11 @@ export class KtxClickHouseDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -143,6 +143,18 @@ export class KtxClickHouseDialect { return `COUNT(DISTINCT ${column})`; } + textLengthExpression(columnSql: string): string { + return `length(toString(${columnSql}))`; + } + + castToText(columnSql: string): string { + return `toString(${columnSql})`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT arrayStringConcat(groupArray(toString(value)), '\\x1F') FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` SELECT COUNT(DISTINCT val) AS cardinality @@ -181,99 +193,9 @@ export class KtxClickHouseDialect { ) `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const tz = timezone ? `, '${timezone}'` : ''; - switch (granularity) { - case 'day': - return `toStartOfDay(${column}${tz})`; - case 'week': - return `toStartOfWeek(${column}, 1${tz})`; - case 'month': - return `toStartOfMonth(${column}${tz})`; - case 'quarter': - return `toStartOfQuarter(${column}${tz})`; - case 'year': - return `toStartOfYear(${column}${tz})`; - } - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const col = timezone ? `toTimezone(${column}, '${timezone}')` : column; - const [rawAmount, rawUnit] = interval.split(' '); - const amount = Number(rawAmount); - const unit = rawUnit!.toLowerCase(); - const originExpr = origin ? `toDateTime('${origin}')` : "toDateTime('1970-01-01')"; - const calendarUnit = this.toClickHouseDateDiffUnit(unit); - if (calendarUnit) { - return `dateAdd(${calendarUnit}, intDiv(dateDiff(${calendarUnit}, ${originExpr}, ${col}), ${amount}) * ${amount}, ${originExpr})`; - } - const seconds = this.intervalToSeconds(amount, unit); - return `addSeconds(${originExpr}, intDiv(toUInt64(dateDiff('second', ${originExpr}, ${col})), ${seconds}) * ${seconds})`; - } - - parseIntervalToSql(interval: string): string { - const [amount, unit] = interval.split(' '); - return `INTERVAL ${amount} ${unit!.toUpperCase()}`; - } - private unwrapClickHouseType(value: string, wrapper: string): string { const prefix = `${wrapper}(`; return value.startsWith(prefix) && value.endsWith(')') ? value.slice(prefix.length, -1) : value; } - private inferClickHouseType(value: unknown): string { - if (value === null || value === undefined) { - return 'String'; - } - if (typeof value === 'boolean') { - return 'Bool'; - } - if (typeof value === 'number') { - return Number.isInteger(value) ? 'Int64' : 'Float64'; - } - if (value instanceof Date) { - return 'DateTime'; - } - return 'String'; - } - - private toClickHouseDateDiffUnit(unit: string): string | null { - if (unit === 'month' || unit === 'months') { - return "'month'"; - } - if (unit === 'quarter' || unit === 'quarters') { - return "'quarter'"; - } - if (unit === 'year' || unit === 'years') { - return "'year'"; - } - return null; - } - - private intervalToSeconds(amount: number, unit: string): number { - switch (unit) { - case 'second': - case 'seconds': - return amount; - case 'minute': - case 'minutes': - return amount * 60; - case 'hour': - case 'hours': - return amount * 3600; - case 'day': - case 'days': - return amount * 86400; - case 'week': - case 'weeks': - return amount * 604800; - default: - return amount * 86400; - } - } } diff --git a/packages/cli/src/connectors/mysql/connector.ts b/packages/cli/src/connectors/mysql/connector.ts index 83c9712a..29dacc26 100644 --- a/packages/cli/src/connectors/mysql/connector.ts +++ b/packages/cli/src/connectors/mysql/connector.ts @@ -2,6 +2,7 @@ import mysql, { type FieldPacket, type Pool, type RowDataPacket } from 'mysql2/p import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { constraintDiscoveryWarning, @@ -30,7 +31,6 @@ import { type KtxTableSampleInput, type KtxTableSampleResult, } from '../../context/scan/types.js'; -import { KtxMysqlDialect } from './dialect.js'; export interface KtxMysqlConnectionConfig { driver?: string; @@ -303,6 +303,25 @@ function queryParams(params: Record | unknown[] | undefined): u return Array.isArray(params) ? params : Object.values(params); } +/** @internal */ +export function prepareMysqlReadOnlyQuery( + sql: string, + params?: Record, +): { sql: string; params?: unknown[] } { + if (!params) { + return { sql, params: undefined }; + } + const values: unknown[] = []; + const parameterizedQuery = sql.replace(/:([A-Za-z_][A-Za-z0-9_]*)\b/g, (placeholder, key: string) => { + if (!(key in params)) { + return placeholder; + } + values.push(params[key]); + return '?'; + }); + return { sql: parameterizedQuery, params: values }; +} + export function isKtxMysqlConnectionConfig( connection: KtxMysqlConnectionConfig | undefined, ): connection is KtxMysqlConnectionConfig { @@ -376,7 +395,7 @@ export class KtxMysqlScanConnector implements KtxScanConnector { private readonly poolFactory: KtxMysqlPoolFactory; private readonly endpointResolver?: KtxMysqlEndpointResolver; private readonly now: () => Date; - private readonly dialect = new KtxMysqlDialect(); + private readonly dialect = getDialectForDriver('mysql'); private pool: KtxMysqlPool | null = null; private resolvedEndpoint: KtxMysqlResolvedEndpoint | null = null; @@ -550,7 +569,7 @@ export class KtxMysqlScanConnector implements KtxScanConnector { const limitedSql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows); const prepared = Array.isArray(input.params) ? { sql: limitedSql, params: input.params } - : this.dialect.prepareQuery(limitedSql, input.params); + : prepareMysqlReadOnlyQuery(limitedSql, input.params); const result = await this.query(prepared.sql, prepared.params); return { ...result, rowCount: result.rows.length }; } @@ -625,6 +644,7 @@ export class KtxMysqlScanConnector implements KtxScanConnector { filterSchemas, ); return rows.map((row) => ({ + catalog: null, schema: row.TABLE_SCHEMA, name: row.TABLE_NAME, kind: row.TABLE_TYPE === 'VIEW' ? ('view' as const) : ('table' as const), diff --git a/packages/cli/src/connectors/mysql/dialect.ts b/packages/cli/src/connectors/mysql/dialect.ts index d61db36c..7f9cc725 100644 --- a/packages/cli/src/connectors/mysql/dialect.ts +++ b/packages/cli/src/connectors/mysql/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type MysqlTableNameRef = Pick & Partial>; -export class KtxMysqlDialect { - readonly type = 'mysql'; +/** @internal */ +export class KtxMysqlDialect implements KtxDialect { + readonly type = 'mysql' as const; private readonly typeMappings: Record = { datetime: 'time', @@ -41,9 +50,19 @@ export class KtxMysqlDialect { } formatTableName(table: MysqlTableNameRef): string { - return table.db - ? `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}` - : this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'ansi'); + } + + formatDisplayRef(table: MysqlTableNameRef): string { + return formatDialectDisplayRef(table, 'ansi'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'ansi'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('ansi'); } mapDataType(nativeType: string): string { @@ -91,21 +110,6 @@ export class KtxMysqlDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS CHAR)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - if (!params) { - return { sql, params: undefined }; - } - const values: unknown[] = []; - const parameterizedQuery = sql.replace(/:([A-Za-z_][A-Za-z0-9_]*)\b/g, (placeholder, key: string) => { - if (!(key in params)) { - return placeholder; - } - values.push(params[key]); - return '?'; - }); - return { sql: parameterizedQuery, params: values }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -118,7 +122,11 @@ export class KtxMysqlDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -129,6 +137,18 @@ export class KtxMysqlDialect { return `COUNT(DISTINCT ${column})`; } + textLengthExpression(columnSql: string): string { + return `CHAR_LENGTH(CAST(${columnSql} AS CHAR))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS CHAR)`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT GROUP_CONCAT(CAST(value AS CHAR) SEPARATOR CHAR(31)) FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` SELECT COUNT(DISTINCT val) AS cardinality @@ -167,36 +187,4 @@ export class KtxMysqlDialect { ) AS sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const col = timezone ? `CONVERT_TZ(${column}, '+00:00', '${timezone}')` : column; - switch (granularity) { - case 'day': - return `DATE(${col})`; - case 'week': - return `DATE(${col} - INTERVAL WEEKDAY(${col}) DAY)`; - case 'month': - return `DATE_FORMAT(${col}, '%Y-%m-01')`; - case 'quarter': - return `MAKEDATE(YEAR(${col}), 1) + INTERVAL (QUARTER(${col}) - 1) QUARTER`; - case 'year': - return `DATE_FORMAT(${col}, '%Y-01-01')`; - } - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const col = timezone ? `CONVERT_TZ(${column}, '+00:00', '${timezone}')` : column; - const [amount, unit] = interval.split(' '); - const originExpr = origin ? `'${origin}'` : `'1970-01-01'`; - return `DATE_ADD(${originExpr}, INTERVAL FLOOR(TIMESTAMPDIFF(${unit!.toUpperCase()}, ${originExpr}, ${col}) / ${amount}) * ${amount} ${unit!.toUpperCase()})`; - } - - parseIntervalToSql(interval: string): string { - const [amount, unit] = interval.split(' '); - return `INTERVAL ${amount} ${unit!.toUpperCase()}`; - } } diff --git a/packages/cli/src/connectors/postgres/connector.ts b/packages/cli/src/connectors/postgres/connector.ts index 1bab5e49..f206fa6a 100644 --- a/packages/cli/src/connectors/postgres/connector.ts +++ b/packages/cli/src/connectors/postgres/connector.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; @@ -26,7 +27,6 @@ import { type KtxTableSampleResult, } from '../../context/scan/types.js'; import { Pool } from 'pg'; -import { KtxPostgresDialect } from './dialect.js'; const PG_OID_TYPE_MAP: Record = { 16: 'boolean', @@ -219,6 +219,29 @@ function groupByTable(rows: T[]): Map, +): { sql: string; params?: unknown[] } { + if (!params) { + return { sql, params: undefined }; + } + const paramNames = Object.keys(params); + const values: unknown[] = new Array(paramNames.length); + const paramIndexMap = new Map(); + paramNames.forEach((name, index) => { + paramIndexMap.set(name, index + 1); + values[index] = params[name]; + }); + const sortedKeys = [...paramNames].sort((a, b) => b.length - a.length); + let parameterizedQuery = sql; + for (const name of sortedKeys) { + parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${name}\\b`, 'g'), `$${paramIndexMap.get(name)}`); + } + return { sql: parameterizedQuery, params: values }; +} + function primaryKeyMap(rows: PostgresPrimaryKeyRow[]): Map> { const grouped = new Map>(); for (const row of rows) { @@ -400,7 +423,7 @@ export class KtxPostgresScanConnector implements KtxScanConnector { private readonly poolFactory: KtxPostgresPoolFactory; private readonly endpointResolver?: KtxPostgresEndpointResolver; private readonly now: () => Date; - private readonly dialect = new KtxPostgresDialect(); + private readonly dialect = getDialectForDriver('postgres'); private pool: KtxPostgresPool | null = null; private lastIdlePoolError: Error | null = null; private resolvedEndpoint: KtxPostgresResolvedEndpoint | null = null; @@ -489,7 +512,7 @@ export class KtxPostgresScanConnector implements KtxScanConnector { const limitedSql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows); const prepared = Array.isArray(input.params) ? { sql: limitedSql, params: input.params } - : this.dialect.prepareQuery(limitedSql, input.params); + : preparePostgresReadOnlyQuery(limitedSql, input.params); const result = await this.query(prepared.sql, prepared.params); return { ...result, rowCount: result.rows.length }; } @@ -584,6 +607,7 @@ export class KtxPostgresScanConnector implements KtxScanConnector { [filterSchemas], ); return rows.map((row) => ({ + catalog: null, schema: row.schema_name, name: row.table_name, kind: row.table_kind === 'v' ? ('view' as const) : ('table' as const), diff --git a/packages/cli/src/connectors/postgres/dialect.ts b/packages/cli/src/connectors/postgres/dialect.ts index ea0590b8..49d5677d 100644 --- a/packages/cli/src/connectors/postgres/dialect.ts +++ b/packages/cli/src/connectors/postgres/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type PostgresTableNameRef = Pick & Partial>; -export class KtxPostgresDialect { - readonly type = 'postgresql'; +/** @internal */ +export class KtxPostgresDialect implements KtxDialect { + readonly type = 'postgres' as const; private readonly typeMappings: Record = { timestamp: 'time', @@ -45,9 +54,19 @@ export class KtxPostgresDialect { } formatTableName(table: PostgresTableNameRef): string { - return table.db - ? `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}` - : this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'ansi'); + } + + formatDisplayRef(table: PostgresTableNameRef): string { + return formatDialectDisplayRef(table, 'ansi'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'ansi'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('ansi'); } mapDataType(nativeType: string): string { @@ -92,25 +111,6 @@ export class KtxPostgresDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS TEXT)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - if (!params) { - return { sql, params: undefined }; - } - const paramNames = Object.keys(params); - const values: unknown[] = new Array(paramNames.length); - const paramIndexMap = new Map(); - paramNames.forEach((name, index) => { - paramIndexMap.set(name, index + 1); - values[index] = params[name]; - }); - const sortedKeys = [...paramNames].sort((a, b) => b.length - a.length); - let parameterizedQuery = sql; - for (const name of sortedKeys) { - parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${name}\\b`, 'g'), `$${paramIndexMap.get(name)}`); - } - return { sql: parameterizedQuery, params: values }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -126,7 +126,11 @@ export class KtxPostgresDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -137,6 +141,18 @@ export class KtxPostgresDialect { return `COUNT(DISTINCT ${column})`; } + textLengthExpression(columnSql: string): string { + return `LENGTH(CAST(${columnSql} AS TEXT))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS TEXT)`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT STRING_AGG(CAST(value AS TEXT), CHR(31)) FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` WITH sampled AS ( @@ -191,23 +207,4 @@ export class KtxPostgresDialect { FROM sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const col = timezone ? `(${column} AT TIME ZONE '${timezone.replace(/'/g, "''")}')` : column; - return `DATE_TRUNC('${granularity}', ${col})`; - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const col = timezone ? `(${column} AT TIME ZONE '${timezone.replace(/'/g, "''")}')` : column; - const originExpr = origin ? `TIMESTAMP '${origin.replace(/'/g, "''")}'` : "TIMESTAMP '1970-01-01'"; - return `${originExpr} + FLOOR(EXTRACT(EPOCH FROM (${col} - ${originExpr})) / EXTRACT(EPOCH FROM INTERVAL '${interval.replace(/'/g, "''")}')) * INTERVAL '${interval.replace(/'/g, "''")}'`; - } - - parseIntervalToSql(interval: string): string { - return `INTERVAL '${interval.replace(/'/g, "''")}'`; - } } diff --git a/packages/cli/src/connectors/snowflake/connector.ts b/packages/cli/src/connectors/snowflake/connector.ts index d8737559..86d7ebe7 100644 --- a/packages/cli/src/connectors/snowflake/connector.ts +++ b/packages/cli/src/connectors/snowflake/connector.ts @@ -2,6 +2,7 @@ import { createPrivateKey } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; @@ -27,7 +28,6 @@ import { } from '../../context/scan/types.js'; import snowflake from 'snowflake-sdk'; import type { Bind, Binds, Connection, ConnectionOptions } from 'snowflake-sdk'; -import { KtxSnowflakeDialect } from './dialect.js'; import { assertSafeSnowflakeIdentifier, quoteSnowflakeIdentifier } from './identifiers.js'; import { configureSnowflakeSdkLogger } from './sdk-logger.js'; @@ -229,6 +229,14 @@ function toSnowflakeBinds(params: unknown[] | undefined): Binds | undefined { return params?.map((value) => toSnowflakeBind(value)); } +/** @internal */ +export function prepareSnowflakeReadOnlyQuery( + sql: string, + params?: Record, +): { sql: string; params?: unknown[] } { + return { sql, params: params ? Object.values(params) : undefined }; +} + export function isKtxSnowflakeConnectionConfig( connection: KtxSnowflakeConnectionConfig | undefined, ): connection is KtxSnowflakeConnectionConfig { @@ -430,6 +438,7 @@ class SnowflakeSdkDriver implements KtxSnowflakeDriver { [this.resolved.database, ...(schemas ?? [])], ); return result.rows.map((row) => ({ + catalog: this.resolved.database, schema: String(row[0]), name: String(row[1]), kind: String(row[2]) === 'VIEW' ? ('view' as const) : ('table' as const), @@ -550,7 +559,7 @@ export class KtxSnowflakeScanConnector implements KtxScanConnector { private readonly resolved: KtxSnowflakeResolvedConnectionConfig; private readonly driverFactory: KtxSnowflakeDriverFactory; - private readonly dialect = new KtxSnowflakeDialect(); + private readonly dialect = getDialectForDriver('snowflake'); private readonly now: () => Date; private driverInstance: KtxSnowflakeDriver | null = null; @@ -635,7 +644,7 @@ export class KtxSnowflakeScanConnector implements KtxScanConnector { async executeReadOnly(input: KtxSnowflakeReadOnlyQueryInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const limitedSql = limitSqlForExecution(assertReadOnlySql(input.sql), input.maxRows); - const prepared = this.dialect.prepareQuery(limitedSql, input.params); + const prepared = prepareSnowflakeReadOnlyQuery(limitedSql, input.params); return this.getDriver().query(prepared.sql, prepared.params); } @@ -696,6 +705,7 @@ export class KtxSnowflakeScanConnector implements KtxScanConnector { [this.resolved.database, ...(schemas ?? [])], ); return result.rows.map((row) => ({ + catalog: this.resolved.database, schema: String(row[0]), name: String(row[1]), kind: String(row[2]) === 'VIEW' ? ('view' as const) : ('table' as const), diff --git a/packages/cli/src/connectors/snowflake/dialect.ts b/packages/cli/src/connectors/snowflake/dialect.ts index db508134..3fe04101 100644 --- a/packages/cli/src/connectors/snowflake/dialect.ts +++ b/packages/cli/src/connectors/snowflake/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type SnowflakeTableNameRef = Pick & Partial>; -export class KtxSnowflakeDialect { - readonly type = 'snowflake'; +/** @internal */ +export class KtxSnowflakeDialect implements KtxDialect { + readonly type = 'snowflake' as const; private readonly typeMappings: Record = { TIMESTAMP_NTZ: 'time', @@ -45,13 +54,19 @@ export class KtxSnowflakeDialect { } formatTableName(table: SnowflakeTableNameRef): string { - if (table.catalog && table.db) { - return `${this.quoteIdentifier(table.catalog)}.${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}`; - } - if (table.db) { - return `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}`; - } - return this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'three-part'); + } + + formatDisplayRef(table: SnowflakeTableNameRef): string { + return formatDialectDisplayRef(table, 'three-part'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'three-part'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('three-part'); } mapDataType(nativeType: string): string { @@ -96,10 +111,6 @@ export class KtxSnowflakeDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS STRING)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - return { sql, params: params ? Object.values(params) : undefined }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -115,7 +126,11 @@ export class KtxSnowflakeDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -126,6 +141,18 @@ export class KtxSnowflakeDialect { return `APPROX_COUNT_DISTINCT(${column})`; } + textLengthExpression(columnSql: string): string { + return `LENGTH(CAST(${columnSql} AS TEXT))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS VARCHAR)`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT LISTAGG(CAST(value AS VARCHAR), '\\x1f') FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` WITH sampled AS ( @@ -164,24 +191,4 @@ export class KtxSnowflakeDialect { FROM sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const target = timezone ? `CONVERT_TIMEZONE('UTC', '${timezone}', ${column})` : column; - return `DATE_TRUNC('${granularity}', ${target})`; - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const target = timezone ? `CONVERT_TIMEZONE('UTC', '${timezone}', ${column})` : column; - const [amount, unit] = interval.split(' '); - const originExpr = origin ? `'${origin}'::TIMESTAMP` : `'1970-01-01'::TIMESTAMP`; - return `DATEADD(${unit}, FLOOR(DATEDIFF(${unit}, ${originExpr}, ${target}) / ${amount}) * ${amount}, ${originExpr})`; - } - - parseIntervalToSql(interval: string): string { - return `INTERVAL '${interval}'`; - } } diff --git a/packages/cli/src/connectors/sqlite/connector.ts b/packages/cli/src/connectors/sqlite/connector.ts index 504e427d..e996bc25 100644 --- a/packages/cli/src/connectors/sqlite/connector.ts +++ b/packages/cli/src/connectors/sqlite/connector.ts @@ -3,11 +3,11 @@ import { existsSync, readFileSync, statSync } from 'node:fs'; import { homedir } from 'node:os'; import { isAbsolute, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { normalizeQueryRows } from '../../context/connections/query-executor.js'; -import { createKtxConnectorCapabilities, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaForeignKey, type KtxSchemaSnapshot, type KtxSchemaTable, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js'; +import { createKtxConnectorCapabilities, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaForeignKey, type KtxSchemaSnapshot, type KtxSchemaTable, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; -import { KtxSqliteDialect } from './dialect.js'; export interface KtxSqliteConnectionConfig { driver?: string; @@ -157,7 +157,7 @@ export class KtxSqliteScanConnector implements KtxScanConnector { private readonly connectionId: string; private readonly dbPath: string; private readonly now: () => Date; - private readonly dialect = new KtxSqliteDialect(); + private readonly dialect = getDialectForDriver('sqlite'); private db: Database.Database | null = null; constructor(options: KtxSqliteScanConnectorOptions) { @@ -209,6 +209,31 @@ export class KtxSqliteScanConnector implements KtxScanConnector { }; } + async listSchemas(): Promise { + return []; + } + + async listTables(_schemas?: string[]): Promise { + const rows = this.database() + .prepare( + ` + SELECT name, type + FROM sqlite_master + WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite_%' + ORDER BY name + `, + ) + .all() as SqliteMasterRow[]; + + return rows.map((row) => ({ + catalog: null, + schema: '', + name: row.name, + kind: row.type === 'view' ? ('view' as const) : ('table' as const), + })); + } + async sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const result = this.query(this.dialect.generateSampleQuery(this.qTableName(input.table), input.limit, input.columns)); diff --git a/packages/cli/src/connectors/sqlite/dialect.ts b/packages/cli/src/connectors/sqlite/dialect.ts index b5771b62..5472b674 100644 --- a/packages/cli/src/connectors/sqlite/dialect.ts +++ b/packages/cli/src/connectors/sqlite/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + limitOffsetClause, + parseDialectDisplayRef, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type SqliteTableNameRef = Pick & Partial>; -export class KtxSqliteDialect { - readonly type = 'sqlite'; +/** @internal */ +export class KtxSqliteDialect implements KtxDialect { + readonly type = 'sqlite' as const; private readonly typeMappings: Record = { DATETIME: 'time', @@ -29,7 +38,19 @@ export class KtxSqliteDialect { } formatTableName(table: SqliteTableNameRef): string { - return this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'sqlite'); + } + + formatDisplayRef(table: SqliteTableNameRef): string { + return formatDialectDisplayRef(table, 'sqlite'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'sqlite'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('sqlite'); } mapDataType(nativeType: string): string { @@ -76,10 +97,6 @@ export class KtxSqliteDialect { return `SELECT ${quoted} FROM ${tableName} WHERE ${quoted} IS NOT NULL AND TRIM(CAST(${quoted} AS TEXT)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown } { - return params ? { sql, params } : { sql }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -92,7 +109,11 @@ export class KtxSqliteDialect { } getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `LIMIT ${limit} OFFSET ${offset}` : `LIMIT ${limit}`; + return limitOffsetClause(limit, offset); + } + + getTopClause(_limit: number): string { + return ''; } getNullCountExpression(column: string): string { @@ -103,6 +124,18 @@ export class KtxSqliteDialect { return `COUNT(DISTINCT ${column})`; } + textLengthExpression(columnSql: string): string { + return `LENGTH(CAST(${columnSql} AS TEXT))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS TEXT)`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT GROUP_CONCAT(CAST(value AS TEXT), char(31)) FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` WITH sampled AS ( @@ -143,35 +176,4 @@ export class KtxSqliteDialect { FROM sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - _timezone?: string, - ): string { - switch (granularity) { - case 'day': - return `DATE(${column})`; - case 'week': - return `DATE(${column}, 'weekday 0', '-6 days')`; - case 'month': - return `DATE(${column}, 'start of month')`; - case 'quarter': - return `DATE(${column}, 'start of month', '-' || ((CAST(STRFTIME('%m', ${column}) AS INTEGER) - 1) % 3) || ' months')`; - case 'year': - return `DATE(${column}, 'start of year')`; - } - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, _timezone?: string): string { - const [amount, unit] = interval.split(' '); - const originExpr = origin ? `julianday('${origin}')` : `julianday('1970-01-01')`; - const unitDays = unit === 'day' ? 1 : unit === 'week' ? 7 : 30; - const intervalDays = Number(amount) * unitDays; - return `DATE(julianday('1970-01-01') + (CAST((julianday(${column}) - ${originExpr}) / ${intervalDays} AS INTEGER) * ${intervalDays}))`; - } - - parseIntervalToSql(interval: string): string { - return `'${interval}'`; - } } diff --git a/packages/cli/src/connectors/sqlserver/connector.ts b/packages/cli/src/connectors/sqlserver/connector.ts index 9895027f..0115781d 100644 --- a/packages/cli/src/connectors/sqlserver/connector.ts +++ b/packages/cli/src/connectors/sqlserver/connector.ts @@ -1,4 +1,5 @@ import { assertReadOnlySql } from '../../context/connections/read-only-sql.js'; +import { getDialectForDriver } from '../../context/connections/dialects.js'; import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; import { @@ -26,7 +27,6 @@ import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { resolve } from 'node:path'; import sql from 'mssql'; -import { KtxSqlServerDialect } from './dialect.js'; export interface KtxSqlServerConnectionConfig { driver?: string; @@ -158,6 +158,21 @@ function tableScopeSql( return { clause: `AND ${columnExpression} IN (${placeholders.join(', ')})`, params }; } +/** @internal */ +export function prepareSqlServerReadOnlyQuery( + sql: string, + params?: Record, +): { sql: string; params?: Record } { + if (!params) { + return { sql, params: undefined }; + } + let parameterizedQuery = sql; + for (const key of Object.keys(params)) { + parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); + } + return { sql: parameterizedQuery, params }; +} + class DefaultSqlServerPoolFactory implements KtxSqlServerPoolFactory { async createPool(config: KtxSqlServerPoolConfig): Promise { const pool = await new sql.ConnectionPool(config as sql.config).connect(); @@ -349,7 +364,7 @@ export class KtxSqlServerScanConnector implements KtxScanConnector { private readonly poolFactory: KtxSqlServerPoolFactory; private readonly endpointResolver?: KtxSqlServerEndpointResolver; private readonly now: () => Date; - private readonly dialect = new KtxSqlServerDialect(); + private readonly dialect = getDialectForDriver('sqlserver'); private pool: KtxSqlServerPool | null = null; private resolvedEndpoint: KtxSqlServerResolvedEndpoint | null = null; @@ -427,7 +442,7 @@ export class KtxSqlServerScanConnector implements KtxScanConnector { async executeReadOnly(input: KtxSqlServerReadOnlyQueryInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const limitedSql = limitSqlForSqlServerExecution(input.sql, input.maxRows); - const prepared = this.dialect.prepareQuery(limitedSql, input.params); + const prepared = prepareSqlServerReadOnlyQuery(limitedSql, input.params); const result = await this.query(prepared.sql, prepared.params); return { ...result, rowCount: result.rows.length }; } @@ -517,6 +532,7 @@ export class KtxSqlServerScanConnector implements KtxScanConnector { params, ); return rows.map((row) => ({ + catalog: this.poolConfig.database, schema: row.schema_name, name: row.table_name, kind: row.table_type === 'VIEW' ? ('view' as const) : ('table' as const), diff --git a/packages/cli/src/connectors/sqlserver/dialect.ts b/packages/cli/src/connectors/sqlserver/dialect.ts index 8444317d..6b1804f4 100644 --- a/packages/cli/src/connectors/sqlserver/dialect.ts +++ b/packages/cli/src/connectors/sqlserver/dialect.ts @@ -1,9 +1,18 @@ +import type { KtxDialect } from '../../context/connections/dialects.js'; +import { + columnDisplayPartCount, + formatDialectDisplayRef, + formatDialectTableName, + parseDialectDisplayRef, + safeSqlLimit, +} from '../../context/connections/dialect-helpers.js'; import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/types.js'; type SqlServerTableNameRef = Pick & Partial>; -export class KtxSqlServerDialect { - readonly type = 'sqlserver'; +/** @internal */ +export class KtxSqlServerDialect implements KtxDialect { + readonly type = 'sqlserver' as const; private readonly typeMappings: Record = { datetime: 'time', @@ -39,9 +48,19 @@ export class KtxSqlServerDialect { } formatTableName(table: SqlServerTableNameRef): string { - return table.db - ? `${this.quoteIdentifier(table.db)}.${this.quoteIdentifier(table.name)}` - : this.quoteIdentifier(table.name); + return formatDialectTableName(table, this.quoteIdentifier.bind(this), 'three-part'); + } + + formatDisplayRef(table: SqlServerTableNameRef): string { + return formatDialectDisplayRef(table, 'three-part'); + } + + parseDisplayRef(display: string): KtxTableRef | null { + return parseDialectDisplayRef(display, 'three-part'); + } + + columnDisplayTablePartCount(): 1 | 2 | 3 { + return columnDisplayPartCount('three-part'); } mapDataType(nativeType: string): string { @@ -86,17 +105,6 @@ export class KtxSqlServerDialect { return `SELECT TOP ${limit} ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND LTRIM(RTRIM(CAST(${quotedColumn} AS NVARCHAR(MAX)))) != ''`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - let parameterizedQuery = sql; - for (const key of Object.keys(params)) { - parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); - } - return { sql: parameterizedQuery, params }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -111,12 +119,12 @@ export class KtxSqlServerDialect { return `TABLESAMPLE (${samplePct * 100} PERCENT)`; } - getLimitOffsetClause(limit: number, offset?: number): string { - return offset !== undefined && offset > 0 ? `OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY` : ''; + getLimitOffsetClause(_limit: number, _offset?: number): string { + return ''; } getTopClause(limit: number): string { - return `TOP ${limit}`; + return `TOP (${safeSqlLimit(limit)})`; } getNullCountExpression(column: string): string { @@ -127,6 +135,18 @@ export class KtxSqlServerDialect { return `COUNT(DISTINCT ${column})`; } + textLengthExpression(columnSql: string): string { + return `LEN(CAST(${columnSql} AS NVARCHAR(MAX)))`; + } + + castToText(columnSql: string): string { + return `CAST(${columnSql} AS NVARCHAR(MAX))`; + } + + getSampleValueAggregation(innerSql: string): string { + return `(SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), CHAR(31)) FROM (${innerSql}) AS relationship_profile_values)`; + } + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string { return ` WITH sampled AS ( @@ -167,35 +187,4 @@ export class KtxSqlServerDialect { FROM sampled `; } - - getTimeTruncExpression( - column: string, - granularity: 'day' | 'week' | 'month' | 'quarter' | 'year', - timezone?: string, - ): string { - const col = timezone ? `${column} AT TIME ZONE 'UTC' AT TIME ZONE '${timezone}'` : column; - switch (granularity) { - case 'day': - return `CAST(${col} AS DATE)`; - case 'week': - return `DATEADD(WEEK, DATEDIFF(WEEK, 0, ${col}), 0)`; - case 'month': - return `DATEFROMPARTS(YEAR(${col}), MONTH(${col}), 1)`; - case 'quarter': - return `DATEFROMPARTS(YEAR(${col}), (DATEPART(QUARTER, ${col}) - 1) * 3 + 1, 1)`; - case 'year': - return `DATEFROMPARTS(YEAR(${col}), 1, 1)`; - } - } - - getCustomTimeTruncExpression(column: string, interval: string, origin?: string, timezone?: string): string { - const col = timezone ? `${column} AT TIME ZONE 'UTC' AT TIME ZONE '${timezone}'` : column; - const [amount, unit] = interval.split(' '); - const originExpr = origin ? `'${origin}'` : `'1970-01-01'`; - return `DATEADD(${unit}, (DATEDIFF(${unit}, ${originExpr}, ${col}) / ${amount}) * ${amount}, ${originExpr})`; - } - - parseIntervalToSql(interval: string): string { - return `'${interval}'`; - } } diff --git a/packages/cli/src/context/connections/dialect-helpers.ts b/packages/cli/src/context/connections/dialect-helpers.ts new file mode 100644 index 00000000..04ed569b --- /dev/null +++ b/packages/cli/src/context/connections/dialect-helpers.ts @@ -0,0 +1,87 @@ +import type { KtxTableRef } from '../scan/types.js'; + +export type KtxDialectIdentifierShape = 'ansi' | 'sqlite' | 'three-part'; + +export type KtxDialectTableRef = Pick & Partial>; + +export function safeSqlLimit(limit: number): number { + return Math.max(1, Math.floor(limit)); +} + +function safeSqlOffset(offset: number | undefined): number | null { + if (offset === undefined) { + return null; + } + const normalized = Math.floor(offset); + return normalized > 0 ? normalized : null; +} + +function cleanIdentifierPart(part: string): string { + return part.trim().replace(/^["'`\[]|["'`\]]$/g, ''); +} + +function splitDisplay(display: string): string[] { + return display.trim().split('.').map(cleanIdentifierPart).filter(Boolean); +} + +function tableParts(table: KtxDialectTableRef, shape: KtxDialectIdentifierShape): string[] { + if (shape === 'sqlite') { + return [table.name]; + } + return [table.catalog ?? null, table.db ?? null, table.name].filter((part): part is string => Boolean(part)); +} + +function acceptedDisplayPartCounts(shape: KtxDialectIdentifierShape): readonly number[] { + if (shape === 'sqlite') { + return [1]; + } + if (shape === 'three-part') { + return [3]; + } + return [2, 3]; +} + +export function formatDialectTableName( + table: KtxDialectTableRef, + quoteIdentifier: (identifier: string) => string, + shape: KtxDialectIdentifierShape, +): string { + return tableParts(table, shape).map(quoteIdentifier).join('.'); +} + +export function formatDialectDisplayRef(table: KtxDialectTableRef, shape: KtxDialectIdentifierShape): string { + return tableParts(table, shape).join('.'); +} + +export function parseDialectDisplayRef(display: string, shape: KtxDialectIdentifierShape): KtxTableRef | null { + const parts = splitDisplay(display); + if (!acceptedDisplayPartCounts(shape).includes(parts.length)) { + return null; + } + if (parts.length === 1) { + return { catalog: null, db: null, name: parts[0]! }; + } + if (parts.length === 2) { + return { catalog: null, db: parts[0]!, name: parts[1]! }; + } + if (parts.length === 3) { + return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! }; + } + return null; +} + +export function columnDisplayPartCount(shape: KtxDialectIdentifierShape): 1 | 2 | 3 { + if (shape === 'sqlite') { + return 1; + } + if (shape === 'three-part') { + return 3; + } + return 2; +} + +export function limitOffsetClause(limit: number, offset?: number): string { + const safeLimit = safeSqlLimit(limit); + const safeOffset = safeSqlOffset(offset); + return safeOffset === null ? `LIMIT ${safeLimit}` : `LIMIT ${safeLimit} OFFSET ${safeOffset}`; +} diff --git a/packages/cli/src/context/connections/dialects.test.ts b/packages/cli/src/context/connections/dialects.test.ts deleted file mode 100644 index d4f77997..00000000 --- a/packages/cli/src/context/connections/dialects.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getDialectForDriver } from './dialects.js'; - -describe('getDialectForDriver', () => { - it.each([ - ['postgres', '"public"."orders"'], - ['mysql', '`public`.`orders`'], - ['clickhouse', '`public`.`orders`'], - ['sqlite', '"orders"'], - ['snowflake', '"analytics"."public"."orders"'], - ['bigquery', '`analytics`.`public`.`orders`'], - ['sqlserver', '[analytics].[public].[orders]'], - ] as const)('formats table names for %s', (driver, expected) => { - const dialect = getDialectForDriver(driver); - expect( - dialect.formatTableName({ - catalog: driver === 'snowflake' || driver === 'bigquery' || driver === 'sqlserver' ? 'analytics' : null, - db: driver === 'sqlite' ? null : 'public', - name: 'orders', - }), - ).toBe(expected); - }); - - it('throws with a supported-driver list for unknown drivers', () => { - expect(() => getDialectForDriver('oracle')).toThrow( - 'Unsupported warehouse driver "oracle". Supported drivers: bigquery, clickhouse, mysql, postgres, sqlite, snowflake, sqlserver', - ); - }); - - it('rejects legacy driver aliases', () => { - expect(() => getDialectForDriver('postgresql')).toThrow('Unsupported warehouse driver "postgresql"'); - expect(() => getDialectForDriver('sqlite3')).toThrow('Unsupported warehouse driver "sqlite3"'); - }); -}); diff --git a/packages/cli/src/context/connections/dialects.ts b/packages/cli/src/context/connections/dialects.ts index 5c6cc27f..c7929cea 100644 --- a/packages/cli/src/context/connections/dialects.ts +++ b/packages/cli/src/context/connections/dialects.ts @@ -1,22 +1,40 @@ -import type { KtxSchemaDimensionType, KtxTableRef } from '../scan/types.js'; - -type SupportedDriver = - | 'postgres' - | 'mysql' - | 'sqlserver' - | 'snowflake' - | 'bigquery' - | 'clickhouse' - | 'sqlite'; +import { KtxBigQueryDialect } from '../../connectors/bigquery/dialect.js'; +import { KtxClickHouseDialect } from '../../connectors/clickhouse/dialect.js'; +import { KtxMysqlDialect } from '../../connectors/mysql/dialect.js'; +import { KtxPostgresDialect } from '../../connectors/postgres/dialect.js'; +import { KtxSqliteDialect } from '../../connectors/sqlite/dialect.js'; +import { KtxSnowflakeDialect } from '../../connectors/snowflake/dialect.js'; +import { KtxSqlServerDialect } from '../../connectors/sqlserver/dialect.js'; +import type { KtxConnectionDriver, KtxSchemaDimensionType, KtxTableRef } from '../scan/types.js'; +import type { KtxDialectTableRef } from './dialect-helpers.js'; export interface KtxDialect { - readonly type: SupportedDriver; + readonly type: KtxConnectionDriver; quoteIdentifier(identifier: string): string; - formatTableName(table: KtxTableRef): string; + formatTableName(table: KtxDialectTableRef): string; + formatDisplayRef(table: KtxDialectTableRef): string; + parseDisplayRef(display: string): KtxTableRef | null; + columnDisplayTablePartCount(): 1 | 2 | 3; + getLimitOffsetClause(limit: number, offset?: number): string; + getTopClause(limit: number): string; + getRandomSampleFilter(samplePct: number): string; + getTableSampleClause(samplePct: number): string; + generateSampleQuery(tableName: string, limit: number, columns?: string[]): string; + generateColumnSampleQuery(tableName: string, columnName: string, limit: number): string; + getSampleValueAggregation(innerSql: string): string; + generateCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string; + generateRandomizedCardinalitySampleQuery(tableName: string, columnName: string, sampleSize: number): string; + generateDistinctValuesQuery(tableName: string, columnName: string, limit: number): string; + generateColumnStatisticsQuery(schemaName: string, tableName: string): string | null; + getNullCountExpression(column: string): string; + getDistinctCountExpression(column: string): string; + textLengthExpression(columnSql: string): string; + castToText(columnSql: string): string; mapToDimensionType(nativeType: string): KtxSchemaDimensionType; + mapDataType(nativeType: string): string; } -const supportedDrivers: SupportedDriver[] = [ +const supportedDrivers: KtxConnectionDriver[] = [ 'bigquery', 'clickhouse', 'mysql', @@ -26,71 +44,21 @@ const supportedDrivers: SupportedDriver[] = [ 'sqlserver', ]; -function doubleQuoted(identifier: string): string { - return `"${identifier.replace(/"/g, '""')}"`; -} - -function backtickQuoted(identifier: string): string { - return `\`${identifier.replace(/`/g, '``')}\``; -} - -function bigQueryQuoted(identifier: string): string { - return `\`${identifier.replace(/`/g, '\\`')}\``; -} - -function bracketQuoted(identifier: string): string { - return `[${identifier.replace(/\]/g, ']]')}]`; -} - -function inferDimensionType(nativeType: string): KtxSchemaDimensionType { - const normalized = nativeType.toLowerCase().trim(); - if (normalized.includes('date') || normalized.includes('time')) { - return 'time'; - } - if ( - normalized.includes('int') || - normalized.includes('num') || - normalized.includes('dec') || - normalized.includes('float') || - normalized.includes('double') || - normalized.includes('real') - ) { - return 'number'; - } - if (normalized.includes('bool') || normalized === 'bit') { - return 'boolean'; - } - return 'string'; -} - -function formatWithParts(table: KtxTableRef, quote: (identifier: string) => string, sqlite = false): string { - const parts = sqlite ? [table.name] : [table.catalog, table.db, table.name].filter((part): part is string => !!part); - return parts.map(quote).join('.'); -} - -function createDialect(type: SupportedDriver, quote: (identifier: string) => string, sqlite = false): KtxDialect { - return { - type, - quoteIdentifier: quote, - formatTableName: (table) => formatWithParts(table, quote, sqlite), - mapToDimensionType: inferDimensionType, - }; -} - -const dialects: Record = { - postgres: createDialect('postgres', doubleQuoted), - mysql: createDialect('mysql', backtickQuoted), - clickhouse: createDialect('clickhouse', backtickQuoted), - sqlite: createDialect('sqlite', doubleQuoted, true), - snowflake: createDialect('snowflake', doubleQuoted), - bigquery: createDialect('bigquery', bigQueryQuoted), - sqlserver: createDialect('sqlserver', bracketQuoted), +const dialectFactories: Record KtxDialect> = { + bigquery: () => new KtxBigQueryDialect(), + clickhouse: () => new KtxClickHouseDialect(), + mysql: () => new KtxMysqlDialect(), + postgres: () => new KtxPostgresDialect(), + sqlite: () => new KtxSqliteDialect(), + snowflake: () => new KtxSnowflakeDialect(), + sqlserver: () => new KtxSqlServerDialect(), }; export function getDialectForDriver(driver: string): KtxDialect { const normalized = driver.toLowerCase().trim(); - if (normalized in dialects) { - return dialects[normalized as SupportedDriver]; + const factory = dialectFactories[normalized as KtxConnectionDriver]; + if (factory) { + return factory(); } throw new Error(`Unsupported warehouse driver "${driver}". Supported drivers: ${supportedDrivers.join(', ')}`); } diff --git a/packages/cli/src/context/connections/drivers.ts b/packages/cli/src/context/connections/drivers.ts new file mode 100644 index 00000000..1b87984b --- /dev/null +++ b/packages/cli/src/context/connections/drivers.ts @@ -0,0 +1,199 @@ +import type { KtxConnectionDriver, KtxScanConnector } from '../scan/types.js'; + +/** @internal */ +export type KtxScopeConfigKey = 'dataset_ids' | 'databases' | 'schemas' | 'schema_names'; + +/** @internal */ +export interface KtxDriverConnectorModule { + isConnectionConfig(connection: unknown): boolean; + createScanConnector(args: { + connectionId: string; + connection: unknown; + projectDir: string; + }): KtxScanConnector; +} + +export interface KtxDriverRegistration { + readonly driver: KtxConnectionDriver; + readonly scopeConfigKey: KtxScopeConfigKey | null; + readonly hasHistoricSqlReader: boolean; + readonly hasLocalQueryExecutor: boolean; + load(): Promise; +} + +function invalidConnectionConfig(driver: KtxConnectionDriver): Error { + return new Error(`Connection config does not match warehouse driver "${driver}".`); +} + +/** @internal */ +export const driverRegistrations: Record = { + bigquery: { + driver: 'bigquery', + scopeConfigKey: 'dataset_ids', + hasHistoricSqlReader: true, + hasLocalQueryExecutor: false, + load: async () => { + const m = await import('../../connectors/bigquery/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxBigQueryConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxBigQueryConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('bigquery'); + } + return new m.KtxBigQueryScanConnector({ connectionId, connection: typedConnection }); + }, + }; + }, + }, + clickhouse: { + driver: 'clickhouse', + scopeConfigKey: 'databases', + hasHistoricSqlReader: false, + hasLocalQueryExecutor: false, + load: async () => { + const m = await import('../../connectors/clickhouse/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxClickHouseConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxClickHouseConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('clickhouse'); + } + return new m.KtxClickHouseScanConnector({ connectionId, connection: typedConnection }); + }, + }; + }, + }, + mysql: { + driver: 'mysql', + scopeConfigKey: 'schemas', + hasHistoricSqlReader: false, + hasLocalQueryExecutor: false, + load: async () => { + const m = await import('../../connectors/mysql/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxMysqlConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxMysqlConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('mysql'); + } + return new m.KtxMysqlScanConnector({ connectionId, connection: typedConnection }); + }, + }; + }, + }, + postgres: { + driver: 'postgres', + scopeConfigKey: 'schemas', + hasHistoricSqlReader: true, + hasLocalQueryExecutor: true, + load: async () => { + const m = await import('../../connectors/postgres/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxPostgresConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxPostgresConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('postgres'); + } + return new m.KtxPostgresScanConnector({ connectionId, connection: typedConnection }); + }, + }; + }, + }, + sqlite: { + driver: 'sqlite', + scopeConfigKey: null, + hasHistoricSqlReader: false, + hasLocalQueryExecutor: true, + load: async () => { + const m = await import('../../connectors/sqlite/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxSqliteConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection, projectDir }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxSqliteConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('sqlite'); + } + return new m.KtxSqliteScanConnector({ connectionId, connection: typedConnection, projectDir }); + }, + }; + }, + }, + snowflake: { + driver: 'snowflake', + scopeConfigKey: 'schema_names', + hasHistoricSqlReader: true, + hasLocalQueryExecutor: false, + load: async () => { + const m = await import('../../connectors/snowflake/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxSnowflakeConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection, projectDir }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxSnowflakeConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('snowflake'); + } + return new m.KtxSnowflakeScanConnector({ connectionId, connection: typedConnection, projectDir }); + }, + }; + }, + }, + sqlserver: { + driver: 'sqlserver', + scopeConfigKey: 'schemas', + hasHistoricSqlReader: false, + hasLocalQueryExecutor: false, + load: async () => { + const m = await import('../../connectors/sqlserver/connector.js'); + return { + isConnectionConfig: (connection) => { + const typedConnection = connection as Parameters[0]; + return m.isKtxSqlServerConnectionConfig(typedConnection); + }, + createScanConnector: ({ connectionId, connection }) => { + const typedConnection = connection as Parameters[0]; + if (!m.isKtxSqlServerConnectionConfig(typedConnection)) { + throw invalidConnectionConfig('sqlserver'); + } + return new m.KtxSqlServerScanConnector({ connectionId, connection: typedConnection }); + }, + }; + }, + }, +}; + +const supportedDrivers = Object.keys(driverRegistrations).sort() as KtxConnectionDriver[]; + +function isRegisteredDriver(driver: string): driver is KtxConnectionDriver { + return Object.prototype.hasOwnProperty.call(driverRegistrations, driver); +} + +export function getDriverRegistration(driver: string): KtxDriverRegistration | undefined { + const normalized = driver.toLowerCase().trim(); + return isRegisteredDriver(normalized) ? driverRegistrations[normalized] : undefined; +} + +export function listSupportedDrivers(): KtxConnectionDriver[] { + return [...supportedDrivers]; +} diff --git a/packages/cli/src/context/connections/local-query-executor.ts b/packages/cli/src/context/connections/local-query-executor.ts index 72cefe2a..3a2e34c9 100644 --- a/packages/cli/src/context/connections/local-query-executor.ts +++ b/packages/cli/src/context/connections/local-query-executor.ts @@ -1,3 +1,4 @@ +import { driverRegistrations, getDriverRegistration } from './drivers.js'; import { createPostgresQueryExecutor } from './postgres-query-executor.js'; import type { KtxSqlQueryExecutionInput, @@ -5,6 +6,7 @@ import type { KtxSqlQueryExecutorPort, } from './query-executor.js'; import { createSqliteQueryExecutor } from './sqlite-query-executor.js'; +import type { KtxConnectionDriver } from '../scan/types.js'; export interface DefaultLocalQueryExecutorOptions { postgres?: KtxSqlQueryExecutorPort; @@ -15,20 +17,43 @@ function driverFor(input: KtxSqlQueryExecutionInput): string { return String(input.connection?.driver ?? '').toLowerCase(); } +function localExecutorMap( + options: DefaultLocalQueryExecutorOptions, +): Partial> { + const wiredExecutors: Partial> = { + postgres: options.postgres ?? createPostgresQueryExecutor(), + sqlite: options.sqlite ?? createSqliteQueryExecutor(), + }; + + const executors: Partial> = {}; + for (const registration of Object.values(driverRegistrations)) { + if (!registration.hasLocalQueryExecutor) continue; + const executor = wiredExecutors[registration.driver]; + if (executor) { + executors[registration.driver] = executor; + } + } + return executors; +} + export function createDefaultLocalQueryExecutor(options: DefaultLocalQueryExecutorOptions = {}): KtxSqlQueryExecutorPort { - const postgres = options.postgres ?? createPostgresQueryExecutor(); - const sqlite = options.sqlite ?? createSqliteQueryExecutor(); + const executors = localExecutorMap(options); return { async execute(input: KtxSqlQueryExecutionInput): Promise { const driver = driverFor(input); - if (driver === 'postgres') { - return postgres.execute(input); + const registration = getDriverRegistration(driver); + if (!registration?.hasLocalQueryExecutor) { + throw new Error(`No local query executor is configured for driver "${input.connection?.driver ?? 'unknown'}".`); } - if (driver === 'sqlite') { - return sqlite.execute(input); + + const executor = executors[registration.driver]; + if (!executor) { + throw new Error( + `Local query executor flag is enabled for driver "${registration.driver}", but no executor factory is wired.`, + ); } - throw new Error(`No local query executor is configured for driver "${input.connection?.driver ?? 'unknown'}".`); + return executor.execute(input); }, }; } diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts b/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts index c6b4c53b..dd95f87a 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts +++ b/packages/cli/src/context/ingest/adapters/historic-sql/connection-dialect.ts @@ -1,5 +1,9 @@ +import { getDriverRegistration } from '../../../connections/drivers.js'; +import type { KtxConnectionDriver } from '../../../scan/types.js'; import type { HistoricSqlDialect } from './types.js'; +const historicSqlDialects: readonly HistoricSqlDialect[] = ['postgres', 'bigquery', 'snowflake']; + function recordOrNull(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } @@ -10,6 +14,14 @@ function queryHistoryRecord(connection: unknown): Record | null return context ? recordOrNull(context.queryHistory) : null; } +function historicSqlDialectForDriver(driver: KtxConnectionDriver): HistoricSqlDialect { + const dialect = historicSqlDialects.find((candidate) => candidate === driver); + if (!dialect) { + throw new Error(`Driver "${driver}" is marked as historic-SQL capable but has no HistoricSqlDialect mapping.`); + } + return dialect; +} + export function isQueryHistoryEnabled(connection: unknown): boolean { return queryHistoryRecord(connection)?.enabled === true; } @@ -25,8 +37,6 @@ export function queryHistoryDialectForConnection(connection: unknown): HistoricS } const conn = recordOrNull(connection); const driver = String(conn?.driver ?? '').toLowerCase(); - if (driver === 'postgres') return 'postgres'; - if (driver === 'bigquery') return 'bigquery'; - if (driver === 'snowflake') return 'snowflake'; - return null; + const registration = getDriverRegistration(driver); + return registration?.hasHistoricSqlReader ? historicSqlDialectForDriver(registration.driver) : null; } diff --git a/packages/cli/src/context/scan/enabled-tables.ts b/packages/cli/src/context/scan/enabled-tables.ts index d4f1009c..96c94afd 100644 --- a/packages/cli/src/context/scan/enabled-tables.ts +++ b/packages/cli/src/context/scan/enabled-tables.ts @@ -27,12 +27,13 @@ export function resolveEnabledTables( function parseEnabledTableEntry(value: unknown): KtxTableRef | null { if (typeof value === 'string') { - return parseDottedEntry(value); + return parseDottedTableEntry(value); } return null; } -function parseDottedEntry(value: string): KtxTableRef | null { +/** @internal */ +export function parseDottedTableEntry(value: string): KtxTableRef | null { const trimmed = value.trim(); if (trimmed.length === 0) return null; const parts = trimmed.split('.'); diff --git a/packages/cli/src/context/scan/entity-details.ts b/packages/cli/src/context/scan/entity-details.ts index 37e766b6..731eea8f 100644 --- a/packages/cli/src/context/scan/entity-details.ts +++ b/packages/cli/src/context/scan/entity-details.ts @@ -1,7 +1,7 @@ import type { KtxLocalProject } from '../../context/project/project.js'; +import { getDialectForDriver, type KtxDialect } from '../connections/dialects.js'; import { readLocalScanStructuralSnapshot } from './local-structural-artifacts.js'; import type { - KtxConnectionDriver, KtxScanReport, KtxSchemaColumn, KtxSchemaSnapshot, @@ -88,59 +88,23 @@ function refsEqual(left: KtxTableRef, right: KtxTableRef): boolean { ); } -function cleanIdentifierPart(part: string): string { - return part.trim().replace(/^["'`\[]|["'`\]]$/g, ''); -} - -function splitDisplay(display: string): string[] { - return display - .trim() - .split('.') - .map(cleanIdentifierPart) - .filter(Boolean); -} - -function displayForTable(driver: KtxConnectionDriver, table: KtxTableRef): string { - if (driver === 'sqlite') { - return table.name; - } - return [table.catalog, table.db, table.name].filter((part): part is string => Boolean(part)).join('.'); -} - function tableRef(table: KtxSchemaTable): KtxTableRef { return { catalog: table.catalog, db: table.db, name: table.name }; } function candidateList( - driver: KtxConnectionDriver, + dialect: KtxDialect, tables: KtxSchemaTable[], ): Array<{ tableRef: KtxTableRef; display: string }> { return tables .map((table) => ({ tableRef: tableRef(table), - display: displayForTable(driver, table), + display: dialect.formatDisplayRef(table), })) .sort((left, right) => left.display.localeCompare(right.display)); } -function parseDisplayRef(driver: KtxConnectionDriver, display: string): KtxTableRef | null { - const parts = splitDisplay(display); - if (driver === 'sqlite') { - return parts.length === 1 ? { catalog: null, db: null, name: parts[0]! } : null; - } - if (driver === 'bigquery' || driver === 'snowflake' || driver === 'sqlserver') { - return parts.length === 3 ? { catalog: parts[0]!, db: parts[1]!, name: parts[2]! } : null; - } - if (parts.length === 2) { - return { catalog: null, db: parts[0]!, name: parts[1]! }; - } - if (parts.length === 3) { - return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! }; - } - return null; -} - -function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableInput): ResolveResult { +function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableInput, dialect: KtxDialect): ResolveResult { if (typeof input !== 'string') { const table = snapshot.tables.find((candidate) => refsEqual(candidate, input)) ?? null; return table @@ -149,13 +113,13 @@ function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableI table: null, error: { code: 'table_not_found', - message: `Table not found in latest scan: ${displayForTable(snapshot.driver, input)}`, - candidates: candidateList(snapshot.driver, snapshot.tables), + message: `Table not found in latest scan: ${dialect.formatDisplayRef(input)}`, + candidates: candidateList(dialect, snapshot.tables), }, }; } - const parsed = parseDisplayRef(snapshot.driver, input); + const parsed = dialect.parseDisplayRef(input); if (parsed) { const table = snapshot.tables.find((candidate) => refsEqual(candidate, parsed)) ?? null; return table @@ -165,7 +129,7 @@ function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableI error: { code: 'table_not_found', message: `Table not found in latest scan: ${input}`, - candidates: candidateList(snapshot.driver, snapshot.tables), + candidates: candidateList(dialect, snapshot.tables), }, }; } @@ -180,7 +144,7 @@ function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableI error: { code: 'ambiguous_table', message: `Table name "${input}" is ambiguous across schemas/catalogs; pass a structured table ref.`, - candidates: candidateList(snapshot.driver, byName), + candidates: candidateList(dialect, byName), }, }; } @@ -189,7 +153,7 @@ function resolveTable(snapshot: KtxSchemaSnapshot, input: KtxEntityDetailsTableI error: { code: 'table_not_found', message: `Table not found in latest scan: ${input}`, - candidates: candidateList(snapshot.driver, snapshot.tables), + candidates: candidateList(dialect, snapshot.tables), }, }; } @@ -261,9 +225,10 @@ export function createKtxEntityDetailsService(project: KtxLocalProject) { } const info = snapshotInfo(scan.report, scan.snapshot); + const dialect = getDialectForDriver(scan.snapshot.driver); const results: KtxEntityDetailsResponse['results'] = []; for (const entity of input.entities) { - const resolved = resolveTable(scan.snapshot, entity.table); + const resolved = resolveTable(scan.snapshot, entity.table, dialect); if (!resolved.table) { results.push({ ok: false, @@ -289,7 +254,7 @@ export function createKtxEntityDetailsService(project: KtxLocalProject) { snapshot: info, error: { code: 'column_not_found', - message: `Column(s) not found on ${displayForTable(scan.snapshot.driver, resolved.table)}: ${missing.join(', ')}`, + message: `Column(s) not found on ${dialect.formatDisplayRef(resolved.table)}: ${missing.join(', ')}`, candidates: resolved.table.columns.map((column) => column.name), }, }); @@ -300,7 +265,7 @@ export function createKtxEntityDetailsService(project: KtxLocalProject) { ok: true, connectionId: input.connectionId, tableRef: tableRef(resolved.table), - display: displayForTable(scan.snapshot.driver, resolved.table), + display: dialect.formatDisplayRef(resolved.table), kind: resolved.table.kind, comment: resolved.table.comment, estimatedRows: resolved.table.estimatedRows, diff --git a/packages/cli/src/context/scan/local-enrichment.ts b/packages/cli/src/context/scan/local-enrichment.ts index 545b2ad6..833cb5b1 100644 --- a/packages/cli/src/context/scan/local-enrichment.ts +++ b/packages/cli/src/context/scan/local-enrichment.ts @@ -1,5 +1,6 @@ import pLimit from 'p-limit'; import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js'; +import { getDialectForDriver } from '../connections/dialects.js'; import { buildDefaultKtxProjectConfig, type KtxScanRelationshipConfig } from '../project/config.js'; import { KtxDescriptionGenerator } from './description-generation.js'; import { buildKtxColumnEmbeddingText } from './embedding-text.js'; @@ -118,6 +119,18 @@ function targetMatchesForeignKey(table: KtxEnrichedTable, foreignKey: KtxSchemaF ); } +function assertConnectorDriverMatchesSnapshot(input: { + connector: KtxScanConnector; + snapshot: KtxSchemaSnapshot; + connectionId: string; +}): void { + if (input.connector.driver !== input.snapshot.driver) { + throw new Error( + `ktx scan connector driver "${input.connector.driver}" does not match snapshot driver "${input.snapshot.driver}" for connection "${input.connectionId}"`, + ); + } +} + function formalRelationshipsFromSnapshot( snapshot: KtxSchemaSnapshot, tables: readonly KtxEnrichedTable[], @@ -468,6 +481,12 @@ export async function runLocalScanEnrichment( )); await progress?.update(0.05, `Loaded schema snapshot with ${snapshot.tables.length} tables`); + assertConnectorDriverMatchesSnapshot({ + connector: input.connector, + snapshot, + connectionId: input.connectionId, + }); + const dialect = getDialectForDriver(snapshot.driver); const now = input.now ?? (() => new Date()); const state = completedKtxScanEnrichmentStateSummary(); const syncId = input.syncId ?? input.context.runId; @@ -575,7 +594,7 @@ export async function runLocalScanEnrichment( await relationshipProgress?.update(0, 'Detecting relationships'); const detection = await discoverKtxRelationships({ connectionId: input.connectionId, - driver: snapshot.driver, + dialect, connector: input.connector, schema, context: input.context, diff --git a/packages/cli/src/context/scan/relationship-benchmarks.ts b/packages/cli/src/context/scan/relationship-benchmarks.ts index f4367b5a..a07221a3 100644 --- a/packages/cli/src/context/scan/relationship-benchmarks.ts +++ b/packages/cli/src/context/scan/relationship-benchmarks.ts @@ -6,6 +6,7 @@ import { gunzipSync } from 'node:zlib'; import Database from 'better-sqlite3'; import YAML from 'yaml'; import { z } from 'zod'; +import { getDialectForDriver } from '../connections/dialects.js'; import type { KtxLlmRuntimePort } from '../llm/runtime-port.js'; import type { KtxEnrichedRelationship, KtxEnrichedSchema, KtxRelationshipType } from './enrichment-types.js'; import { snapshotToKtxEnrichedSchema } from './local-enrichment.js'; @@ -536,6 +537,7 @@ export function ktxRelationshipBenchmarkDetectorWithLlm( const formalLinks = formalMetadata.accepted.map((relationship) => relationshipToBenchmarkLink(relationship)); const acceptedKeys = new Set(formalLinks.map(fkKey)); const sqliteDataAvailable = Boolean(input.dataPath && input.snapshot.driver === 'sqlite'); + const dialect = getDialectForDriver(input.snapshot.driver); const profilingExecutor = sqliteDataAvailable && input.mode !== 'profiling_disabled' ? new KtxRelationshipBenchmarkSqliteExecutor(input.dataPath as string) @@ -550,7 +552,7 @@ export function ktxRelationshipBenchmarkDetectorWithLlm( }) : await profileKtxRelationshipSchema({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, schema: input.schema, executor: profilingExecutor, ctx: { runId: `relationship-benchmark:${input.fixtureId}:${input.mode}:profile` }, @@ -580,7 +582,7 @@ export function ktxRelationshipBenchmarkDetectorWithLlm( : Math.max(0, input.validationBudget - profiles.queryCount); const validatedBroadCandidates = await validateKtxRelationshipDiscoveryCandidates({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, candidates, profiles, executor: validationExecutor, @@ -597,7 +599,7 @@ export function ktxRelationshipBenchmarkDetectorWithLlm( input.mode !== 'validation_disabled' ? await discoverKtxCompositeRelationships({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, schema: input.schema, profiles, executor: validationExecutor, @@ -671,6 +673,7 @@ export function currentKtxRelationshipBenchmarkDetector(): KtxRelationshipBenchm const formalLinks = formalMetadata.accepted.map((relationship) => relationshipToBenchmarkLink(relationship)); const acceptedKeys = new Set(formalLinks.map(fkKey)); const sqliteDataAvailable = Boolean(input.dataPath && input.snapshot.driver === 'sqlite'); + const dialect = getDialectForDriver(input.snapshot.driver); const profilingExecutor = sqliteDataAvailable && input.mode !== 'profiling_disabled' ? new KtxRelationshipBenchmarkSqliteExecutor(input.dataPath as string) @@ -685,7 +688,7 @@ export function currentKtxRelationshipBenchmarkDetector(): KtxRelationshipBenchm }) : await profileKtxRelationshipSchema({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, schema: input.schema, executor: profilingExecutor, ctx: { runId: `relationship-benchmark:${input.fixtureId}:${input.mode}:profile` }, @@ -702,7 +705,7 @@ export function currentKtxRelationshipBenchmarkDetector(): KtxRelationshipBenchm : Math.max(0, input.validationBudget - profiles.queryCount); const validatedBroadCandidates = await validateKtxRelationshipDiscoveryCandidates({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, candidates: broadRelationshipCandidates, profiles, executor: validationExecutor, @@ -719,7 +722,7 @@ export function currentKtxRelationshipBenchmarkDetector(): KtxRelationshipBenchm input.mode !== 'validation_disabled' ? await discoverKtxCompositeRelationships({ connectionId: input.snapshot.connectionId, - driver: input.snapshot.driver, + dialect, schema: input.schema, profiles, executor: validationExecutor, diff --git a/packages/cli/src/context/scan/relationship-composite-candidates.ts b/packages/cli/src/context/scan/relationship-composite-candidates.ts index d8ee650d..28263a15 100644 --- a/packages/cli/src/context/scan/relationship-composite-candidates.ts +++ b/packages/cli/src/context/scan/relationship-composite-candidates.ts @@ -1,11 +1,10 @@ +import type { KtxDialect } from '../connections/dialects.js'; import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable, KtxRelationshipType } from './enrichment-types.js'; import { - formatKtxRelationshipTableRef, - quoteKtxRelationshipIdentifier, type KtxRelationshipProfileArtifact, type KtxRelationshipReadOnlyExecutor, } from './relationship-profiling.js'; -import type { KtxConnectionDriver, KtxQueryResult, KtxScanContext, KtxTableRef } from './types.js'; +import type { KtxQueryResult, KtxScanContext, KtxTableRef } from './types.js'; type KtxCompositeRelationshipStatus = 'accepted' | 'review' | 'rejected'; @@ -57,7 +56,7 @@ export interface KtxCompositeRelationshipCandidate { export interface DiscoverKtxCompositeRelationshipsInput { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; schema: KtxEnrichedSchema; profiles: KtxRelationshipProfileArtifact; executor: KtxRelationshipReadOnlyExecutor | null; @@ -224,28 +223,16 @@ function numberAt(result: KtxQueryResult, header: string): number { return 0; } -function topSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ` TOP (${Math.max(1, Math.floor(limit))})`; - } - return ''; +function sqlSuffix(fragment: string): string { + return fragment ? ` ${fragment}` : ''; } -function limitSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ''; - } - return ` LIMIT ${Math.max(1, Math.floor(limit))}`; +function aliasedTupleSelect(dialect: KtxDialect, columns: readonly string[]): string { + return columns.map((column, index) => `${dialect.quoteIdentifier(column)} AS c${index}`).join(', '); } -function aliasedTupleSelect(driver: KtxConnectionDriver, columns: readonly string[]): string { - return columns - .map((column, index) => `${quoteKtxRelationshipIdentifier(driver, column)} AS c${index}`) - .join(', '); -} - -function nonNullPredicate(driver: KtxConnectionDriver, columns: readonly string[]): string { - return columns.map((column) => `${quoteKtxRelationshipIdentifier(driver, column)} IS NOT NULL`).join(' AND '); +function nonNullPredicate(dialect: KtxDialect, columns: readonly string[]): string { + return columns.map((column) => `${dialect.quoteIdentifier(column)} IS NOT NULL`).join(' AND '); } function tupleEquality(columns: number): string { @@ -255,39 +242,39 @@ function tupleEquality(columns: number): string { } function buildTupleDistinctSql(input: { - driver: KtxConnectionDriver; + dialect: KtxDialect; table: KtxTableRef; columns: readonly string[]; }): string { - const tableSql = formatKtxRelationshipTableRef(input.driver, input.table); + const tableSql = input.dialect.formatTableName(input.table); return [ 'WITH tuple_values AS (', - `SELECT DISTINCT ${aliasedTupleSelect(input.driver, input.columns)} FROM ${tableSql}`, - `WHERE ${nonNullPredicate(input.driver, input.columns)}`, + `SELECT DISTINCT ${aliasedTupleSelect(input.dialect, input.columns)} FROM ${tableSql}`, + `WHERE ${nonNullPredicate(input.dialect, input.columns)}`, ')', 'SELECT COUNT(*) AS distinct_count FROM tuple_values', ].join(' '); } function buildCompositeCoverageSql(input: { - driver: KtxConnectionDriver; + dialect: KtxDialect; childTable: KtxTableRef; childColumns: readonly string[]; parentTable: KtxTableRef; parentColumns: readonly string[]; maxDistinctSourceValues: number; }): string { - const childTableSql = formatKtxRelationshipTableRef(input.driver, input.childTable); - const parentTableSql = formatKtxRelationshipTableRef(input.driver, input.parentTable); - const top = topSql(input.driver, input.maxDistinctSourceValues); - const limit = limitSql(input.driver, input.maxDistinctSourceValues); + const childTableSql = input.dialect.formatTableName(input.childTable); + const parentTableSql = input.dialect.formatTableName(input.parentTable); + const top = input.dialect.getTopClause(input.maxDistinctSourceValues); + const limit = sqlSuffix(input.dialect.getLimitOffsetClause(input.maxDistinctSourceValues)); return [ 'WITH child_values AS (', - `SELECT DISTINCT${top} ${aliasedTupleSelect(input.driver, input.childColumns)} FROM ${childTableSql}`, - `WHERE ${nonNullPredicate(input.driver, input.childColumns)}${limit}`, + `SELECT DISTINCT${top ? ` ${top}` : ''} ${aliasedTupleSelect(input.dialect, input.childColumns)} FROM ${childTableSql}`, + `WHERE ${nonNullPredicate(input.dialect, input.childColumns)}${limit}`, '), parent_values AS (', - `SELECT DISTINCT ${aliasedTupleSelect(input.driver, input.parentColumns)} FROM ${parentTableSql}`, - `WHERE ${nonNullPredicate(input.driver, input.parentColumns)}`, + `SELECT DISTINCT ${aliasedTupleSelect(input.dialect, input.parentColumns)} FROM ${parentTableSql}`, + `WHERE ${nonNullPredicate(input.dialect, input.parentColumns)}`, ')', 'SELECT', '(SELECT COUNT(*) FROM child_values) AS child_distinct,', @@ -335,7 +322,7 @@ function hasAcceptedSubset( async function detectCompositePrimaryKeys(input: { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; table: KtxEnrichedTable; profiles: KtxRelationshipProfileArtifact; executor: KtxRelationshipReadOnlyExecutor; @@ -379,7 +366,7 @@ async function detectCompositePrimaryKeys(input: { { connectionId: input.connectionId, sql: buildTupleDistinctSql({ - driver: input.driver, + dialect: input.dialect, table: input.table.ref, columns: columnNames, }), @@ -439,7 +426,7 @@ function compatibleTuple(sourceColumns: readonly KtxEnrichedColumn[], targetColu async function validateCompositeRelationship(input: { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; sourceTable: KtxEnrichedTable; sourceColumns: readonly KtxEnrichedColumn[]; targetKey: KtxCompositePrimaryKeyCandidate; @@ -454,7 +441,7 @@ async function validateCompositeRelationship(input: { { connectionId: input.connectionId, sql: buildCompositeCoverageSql({ - driver: input.driver, + dialect: input.dialect, childTable: input.sourceTable.ref, childColumns: input.sourceColumns.map((column) => column.name), parentTable: input.targetTable.ref, @@ -552,7 +539,7 @@ export async function discoverKtxCompositeRelationships( for (const table of tables) { const result = await detectCompositePrimaryKeys({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, table, profiles: input.profiles, executor: input.executor, @@ -595,7 +582,7 @@ export async function discoverKtxCompositeRelationships( const result = await validateCompositeRelationship({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, sourceTable, sourceColumns, targetKey, diff --git a/packages/cli/src/context/scan/relationship-discovery.ts b/packages/cli/src/context/scan/relationship-discovery.ts index 66a47395..fc755536 100644 --- a/packages/cli/src/context/scan/relationship-discovery.ts +++ b/packages/cli/src/context/scan/relationship-discovery.ts @@ -1,4 +1,5 @@ import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js'; +import type { KtxDialect } from '../connections/dialects.js'; import type { KtxScanRelationshipConfig } from '../project/config.js'; import type { KtxEnrichedRelationship, KtxEnrichedSchema, KtxRelationshipUpdate } from './enrichment-types.js'; import { @@ -24,7 +25,6 @@ import { } from './relationship-profiling.js'; import { validateKtxRelationshipDiscoveryCandidates } from './relationship-validation.js'; import type { - KtxConnectionDriver, KtxScanConnector, KtxScanContext, KtxScanEnrichmentSummary, @@ -34,7 +34,7 @@ import type { export interface DiscoverKtxRelationshipsInput { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; connector: KtxScanConnector; schema: KtxEnrichedSchema; context: KtxScanContext; @@ -122,7 +122,7 @@ function compositeSummary(relationships: readonly KtxCompositeRelationshipCandid async function detectCompositeRelationships(input: { connectionId: string; - driver: DiscoverKtxRelationshipsInput['driver']; + dialect: KtxDialect; schema: KtxEnrichedSchema; profile: KtxRelationshipProfileArtifact; executor: KtxRelationshipReadOnlyExecutor | null; @@ -135,7 +135,7 @@ async function detectCompositeRelationships(input: { try { const compositeDetection = await discoverKtxCompositeRelationships({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, schema: input.schema, profiles: input.profile, executor: input.executor, @@ -223,7 +223,7 @@ export async function discoverKtxRelationships( const profileCache = createKtxRelationshipProfileCache(); const profile = await profileKtxRelationshipSchema({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, schema: input.schema, executor, ctx: input.context, @@ -256,7 +256,7 @@ export async function discoverKtxRelationships( warnings.push(...llmProposalResult.warnings); const validated = await validateKtxRelationshipDiscoveryCandidates({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, candidates, profiles: profile, executor, @@ -282,7 +282,7 @@ export async function discoverKtxRelationships( }); const compositeRelationships = await detectCompositeRelationships({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, schema: input.schema, profile, executor, diff --git a/packages/cli/src/context/scan/relationship-profiling.ts b/packages/cli/src/context/scan/relationship-profiling.ts index 1824d263..0f22c21c 100644 --- a/packages/cli/src/context/scan/relationship-profiling.ts +++ b/packages/cli/src/context/scan/relationship-profiling.ts @@ -1,3 +1,4 @@ +import type { KtxDialect } from '../connections/dialects.js'; import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js'; import { mapWithConcurrency } from './relationship-validation.js'; import type { @@ -55,7 +56,7 @@ export interface KtxRelationshipProfileCache { export interface ProfileKtxRelationshipSchemaInput { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; schema: KtxEnrichedSchema; executor: KtxRelationshipReadOnlyExecutor | null; ctx: KtxScanContext; @@ -71,75 +72,6 @@ export function createKtxRelationshipProfileCache(): KtxRelationshipProfileCache const SAMPLE_VALUE_DELIMITER = '\u001f'; -type QuoteStyle = 'double' | 'backtick' | 'bracket'; - -function quoteStyle(driver: KtxConnectionDriver): QuoteStyle { - if (driver === 'mysql' || driver === 'clickhouse') { - return 'backtick'; - } - if (driver === 'sqlserver') { - return 'bracket'; - } - return 'double'; -} - -export function quoteKtxRelationshipIdentifier(driver: KtxConnectionDriver, identifier: string): string { - switch (quoteStyle(driver)) { - case 'backtick': - return `\`${identifier.replace(/`/g, '``')}\``; - case 'bracket': - return `[${identifier.replace(/\]/g, ']]')}]`; - case 'double': - return `"${identifier.replace(/"/g, '""')}"`; - } -} - -export function formatKtxRelationshipTableRef(driver: KtxConnectionDriver, table: KtxTableRef): string { - const parts = - driver === 'sqlite' - ? [table.name] - : [table.catalog, table.db, table.name].filter((value): value is string => Boolean(value)); - return parts.map((part) => quoteKtxRelationshipIdentifier(driver, part)).join('.'); -} - -function textLengthExpression(driver: KtxConnectionDriver, columnSql: string): string { - if (driver === 'mysql') { - return `CHAR_LENGTH(CAST(${columnSql} AS CHAR))`; - } - if (driver === 'sqlserver') { - return `LEN(CAST(${columnSql} AS NVARCHAR(MAX)))`; - } - if (driver === 'bigquery') { - return `LENGTH(CAST(${columnSql} AS STRING))`; - } - if (driver === 'clickhouse') { - return `length(toString(${columnSql}))`; - } - return `LENGTH(CAST(${columnSql} AS TEXT))`; -} - -function limitSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ''; - } - return ` LIMIT ${Math.max(1, Math.floor(limit))}`; -} - -function topSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ` TOP (${Math.max(1, Math.floor(limit))})`; - } - return ''; -} - -function sampledTableSql(driver: KtxConnectionDriver, tableSql: string, limit: number): string { - const safeLimit = Math.max(1, Math.floor(limit)); - if (driver === 'sqlserver') { - return `(SELECT TOP (${safeLimit}) * FROM ${tableSql}) AS relationship_profile_sample`; - } - return `(SELECT * FROM ${tableSql}${limitSql(driver, safeLimit)}) AS relationship_profile_sample`; -} - function firstRow(result: KtxQueryResult): unknown[] { return result.rows[0] ?? []; } @@ -191,7 +123,7 @@ function columnKey(table: KtxEnrichedTable, column: KtxEnrichedColumn): string { function tableProfileCacheKey(input: { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; ctx: KtxScanContext; table: KtxTableRef; sampleValuesPerColumn: number; @@ -200,7 +132,7 @@ function tableProfileCacheKey(input: { return [ input.ctx.runId, input.connectionId, - input.driver, + input.dialect.type, input.table.catalog ?? '', input.table.db ?? '', input.table.name, @@ -213,57 +145,47 @@ function sqlStringLiteral(value: string): string { return `'${value.replace(/'/g, "''")}'`; } -function sampleAggregateSql(driver: KtxConnectionDriver, innerSql: string): string { - if (driver === 'postgres') { - return `(SELECT STRING_AGG(CAST(value AS TEXT), CHR(31)) FROM (${innerSql}) AS relationship_profile_values)`; +function sqlSuffix(fragment: string): string { + return fragment ? ` ${fragment}` : ''; +} + +function sampledTableSql(dialect: KtxDialect, tableSql: string, limit: number): string { + const top = dialect.getTopClause(limit); + if (top) { + return `(SELECT ${top} * FROM ${tableSql}) AS relationship_profile_sample`; } - if (driver === 'bigquery') { - return `(SELECT STRING_AGG(CAST(value AS STRING), '\\u001F') FROM (${innerSql}) AS relationship_profile_values)`; - } - if (driver === 'mysql') { - return `(SELECT GROUP_CONCAT(CAST(value AS CHAR) SEPARATOR CHAR(31)) FROM (${innerSql}) AS relationship_profile_values)`; - } - if (driver === 'sqlserver') { - return `(SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), CHAR(31)) FROM (${innerSql}) AS relationship_profile_values)`; - } - if (driver === 'clickhouse') { - return `(SELECT arrayStringConcat(groupArray(toString(value)), '\\x1F') FROM (${innerSql}) AS relationship_profile_values)`; - } - if (driver === 'snowflake') { - return `(SELECT LISTAGG(CAST(value AS VARCHAR), '\\x1f') FROM (${innerSql}) AS relationship_profile_values)`; - } - return `(SELECT GROUP_CONCAT(CAST(value AS TEXT), char(31)) FROM (${innerSql}) AS relationship_profile_values)`; + return `(SELECT * FROM ${tableSql}${sqlSuffix(dialect.getLimitOffsetClause(limit))}) AS relationship_profile_sample`; } function sampleValuesSql(input: { - driver: KtxConnectionDriver; + dialect: KtxDialect; tableSql: string; columnSql: string; limit: number; }): string { + const top = input.dialect.getTopClause(input.limit); return [ - `SELECT${topSql(input.driver, input.limit)} ${input.columnSql} AS value`, + `SELECT${top ? ` ${top}` : ''} ${input.columnSql} AS value`, `FROM ${input.tableSql}`, `WHERE ${input.columnSql} IS NOT NULL`, `GROUP BY ${input.columnSql}`, `ORDER BY COUNT(*) DESC, ${input.columnSql} ASC`, - limitSql(input.driver, input.limit), + sqlSuffix(input.dialect.getLimitOffsetClause(input.limit)), ].join(' '); } function columnProfileSelectSql(input: { - connectionDriver: KtxConnectionDriver; + dialect: KtxDialect; tableSql: string; profileTableSql: string; column: KtxEnrichedColumn; sampleValuesPerColumn: number; }): string { - const columnSql = quoteKtxRelationshipIdentifier(input.connectionDriver, input.column.name); - const textLengthSql = textLengthExpression(input.connectionDriver, columnSql); - const samplesSql = sampleAggregateSql( - input.connectionDriver, + const columnSql = input.dialect.quoteIdentifier(input.column.name); + const textLengthSql = input.dialect.textLengthExpression(columnSql); + const samplesSql = input.dialect.getSampleValueAggregation( sampleValuesSql({ - driver: input.connectionDriver, + dialect: input.dialect, tableSql: input.profileTableSql, columnSql, limit: input.sampleValuesPerColumn, @@ -296,12 +218,12 @@ function splitSampleValues(value: unknown): string[] { async function queryCount(input: { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; table: KtxTableRef; executor: KtxRelationshipReadOnlyExecutor; ctx: KtxScanContext; }): Promise<{ rowCount: number; queryCount: number }> { - const tableSql = formatKtxRelationshipTableRef(input.driver, input.table); + const tableSql = input.dialect.formatTableName(input.table); const result = await input.executor.executeReadOnly( { connectionId: input.connectionId, sql: `SELECT COUNT(*) AS row_count FROM ${tableSql}`, maxRows: 1 }, input.ctx, @@ -311,7 +233,7 @@ async function queryCount(input: { async function queryTableProfile(input: { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; table: KtxEnrichedTable; executor: KtxRelationshipReadOnlyExecutor; ctx: KtxScanContext; @@ -325,7 +247,7 @@ async function queryTableProfile(input: { if (input.table.columns.length === 0) { const rowCount = await queryCount({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, table: input.table.ref, executor: input.executor, ctx: input.ctx, @@ -337,12 +259,12 @@ async function queryTableProfile(input: { }; } - const tableSql = formatKtxRelationshipTableRef(input.driver, input.table.ref); - const profileTableSql = sampledTableSql(input.driver, tableSql, input.profileSampleRows); + const tableSql = input.dialect.formatTableName(input.table.ref); + const profileTableSql = sampledTableSql(input.dialect, tableSql, input.profileSampleRows); const sql = input.table.columns .map((column) => columnProfileSelectSql({ - connectionDriver: input.driver, + dialect: input.dialect, tableSql, profileTableSql, column, @@ -401,7 +323,7 @@ export async function profileKtxRelationshipSchema( if (!input.executor) { return { connectionId: input.connectionId, - driver: input.driver, + driver: input.dialect.type, sqlAvailable: false, queryCount: 0, tables: [], @@ -425,7 +347,7 @@ export async function profileKtxRelationshipSchema( const profileSampleRows = input.profileSampleRows ?? 10000; const cacheKey = tableProfileCacheKey({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, ctx: input.ctx, table: table.ref, sampleValuesPerColumn, @@ -439,7 +361,7 @@ export async function profileKtxRelationshipSchema( try { const tableProfile = await queryTableProfile({ connectionId: input.connectionId, - driver: input.driver, + dialect: input.dialect, table, executor, ctx: input.ctx, @@ -481,7 +403,7 @@ export async function profileKtxRelationshipSchema( return { connectionId: input.connectionId, - driver: input.driver, + driver: input.dialect.type, sqlAvailable: true, queryCount: queryTotal, tables, diff --git a/packages/cli/src/context/scan/relationship-validation.ts b/packages/cli/src/context/scan/relationship-validation.ts index 685d1ea9..5fc0f3fb 100644 --- a/packages/cli/src/context/scan/relationship-validation.ts +++ b/packages/cli/src/context/scan/relationship-validation.ts @@ -1,13 +1,12 @@ +import type { KtxDialect } from '../connections/dialects.js'; import type { KtxRelationshipEndpoint } from './enrichment-types.js'; import { applyKtxRelationshipValidationBudget, type KtxRelationshipValidationBudget } from './relationship-budget.js'; import type { KtxRelationshipDiscoveryCandidate } from './relationship-candidates.js'; import { - formatKtxRelationshipTableRef, type KtxRelationshipProfileArtifact, type KtxRelationshipReadOnlyExecutor, - quoteKtxRelationshipIdentifier, } from './relationship-profiling.js'; -import type { KtxConnectionDriver, KtxQueryResult, KtxScanContext } from './types.js'; +import type { KtxQueryResult, KtxScanContext, KtxTableRef } from './types.js'; type KtxValidatedRelationshipStatus = 'accepted' | 'review' | 'rejected'; @@ -45,7 +44,7 @@ export interface KtxValidatedRelationshipDiscoveryCandidate export interface ValidateKtxRelationshipDiscoveryCandidatesInput { connectionId: string; - driver: KtxConnectionDriver; + dialect: KtxDialect; candidates: readonly KtxRelationshipDiscoveryCandidate[]; profiles: KtxRelationshipProfileArtifact; executor: KtxRelationshipReadOnlyExecutor | null; @@ -104,38 +103,28 @@ function numberAt(result: KtxQueryResult, header: string): number { return 0; } -function limitSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ''; - } - return ` LIMIT ${Math.max(1, Math.floor(limit))}`; -} - -function topSql(driver: KtxConnectionDriver, limit: number): string { - if (driver === 'sqlserver') { - return ` TOP (${Math.max(1, Math.floor(limit))})`; - } - return ''; +function sqlSuffix(fragment: string): string { + return fragment ? ` ${fragment}` : ''; } function buildCoverageSql(input: { - driver: KtxConnectionDriver; - childTable: string; + dialect: KtxDialect; + childTable: KtxTableRef; childColumn: string; - parentTable: string; + parentTable: KtxTableRef; parentColumn: string; maxDistinctSourceValues: number; }): string { - const childTable = formatKtxRelationshipTableRef(input.driver, { catalog: null, db: null, name: input.childTable }); - const parentTable = formatKtxRelationshipTableRef(input.driver, { catalog: null, db: null, name: input.parentTable }); - const childColumn = quoteKtxRelationshipIdentifier(input.driver, input.childColumn); - const parentColumn = quoteKtxRelationshipIdentifier(input.driver, input.parentColumn); - const limit = limitSql(input.driver, input.maxDistinctSourceValues); - const top = topSql(input.driver, input.maxDistinctSourceValues); + const childTable = input.dialect.formatTableName(input.childTable); + const parentTable = input.dialect.formatTableName(input.parentTable); + const childColumn = input.dialect.quoteIdentifier(input.childColumn); + const parentColumn = input.dialect.quoteIdentifier(input.parentColumn); + const limit = sqlSuffix(input.dialect.getLimitOffsetClause(input.maxDistinctSourceValues)); + const top = input.dialect.getTopClause(input.maxDistinctSourceValues); return [ 'WITH child_values AS (', - `SELECT DISTINCT${top} ${childColumn} AS value FROM ${childTable} WHERE ${childColumn} IS NOT NULL${limit}`, + `SELECT DISTINCT${top ? ` ${top}` : ''} ${childColumn} AS value FROM ${childTable} WHERE ${childColumn} IS NOT NULL${limit}`, '), parent_values AS (', `SELECT DISTINCT ${parentColumn} AS value FROM ${parentTable} WHERE ${parentColumn} IS NOT NULL`, ')', @@ -271,10 +260,10 @@ export async function validateKtxRelationshipDiscoveryCandidates( { connectionId: input.connectionId, sql: buildCoverageSql({ - driver: input.driver, - childTable: candidate.from.table.name, + dialect: input.dialect, + childTable: candidate.from.table, childColumn: sourceColumn, - parentTable: candidate.to.table.name, + parentTable: candidate.to.table, parentColumn: targetColumn, maxDistinctSourceValues: settings.maxDistinctSourceValues, }), diff --git a/packages/cli/src/context/scan/types.ts b/packages/cli/src/context/scan/types.ts index 5590b465..1d9e6d6a 100644 --- a/packages/cli/src/context/scan/types.ts +++ b/packages/cli/src/context/scan/types.ts @@ -297,6 +297,7 @@ export interface KtxQueryResult { } export interface KtxTableListEntry { + catalog: string | null; schema: string; name: string; kind: 'table' | 'view'; @@ -313,6 +314,8 @@ export interface KtxScanConnector { capabilities: KtxConnectorCapabilities; eventStreamDiscovery?: KtxEventStreamDiscoveryPort; introspect(input: KtxScanInput, ctx: KtxScanContext): Promise; + listSchemas(): Promise; + listTables(schemas?: string[]): Promise; testConnection?(): Promise; sampleColumn?(input: KtxColumnSampleInput, ctx: KtxScanContext): Promise; sampleTable?(input: KtxTableSampleInput, ctx: KtxScanContext): Promise; diff --git a/packages/cli/src/context/scan/warehouse-catalog.ts b/packages/cli/src/context/scan/warehouse-catalog.ts index b8e91492..f224432b 100644 --- a/packages/cli/src/context/scan/warehouse-catalog.ts +++ b/packages/cli/src/context/scan/warehouse-catalog.ts @@ -1,4 +1,4 @@ -import { getDialectForDriver } from '../../context/connections/dialects.js'; +import { getDialectForDriver, type KtxDialect } from '../connections/dialects.js'; import type { KtxFileStorePort } from '../../context/core/file-store.js'; import type { KtxConnectionDriver, @@ -128,46 +128,22 @@ function splitDisplay(display: string): string[] { .filter(Boolean); } -function formatDisplay(driver: CatalogDriver, table: KtxTableRef): string { - if (driver === 'sqlite') { - return table.name; - } - return [table.catalog, table.db, table.name].filter((part): part is string => Boolean(part)).join('.'); +function formatDisplay(dialect: KtxDialect, table: KtxTableRef): string { + return dialect.formatDisplayRef(table); } -function parseDisplay(driver: CatalogDriver, display: string): KtxTableRef | null { +function parseDisplay(dialect: KtxDialect, display: string): KtxTableRef | null { + const parsed = dialect.parseDisplayRef(display); + if (parsed) { + return parsed; + } const parts = splitDisplay(display); - if (driver === 'sqlite') { - return parts.length === 1 ? { catalog: null, db: null, name: parts[0]! } : null; - } - if (driver === 'bigquery' || driver === 'snowflake' || driver === 'sqlserver') { - if (parts.length !== 3) { - return null; - } - return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! }; - } - if (parts.length === 2) { - return { catalog: null, db: parts[0]!, name: parts[1]! }; - } - if (parts.length === 3) { - return { catalog: parts[0]!, db: parts[1]!, name: parts[2]! }; - } return parts.length === 1 ? { catalog: null, db: null, name: parts[0]! } : null; } -function expectedDisplayPartCount(driver: CatalogDriver): number { - if (driver === 'sqlite') { - return 1; - } - if (driver === 'bigquery' || driver === 'snowflake' || driver === 'sqlserver') { - return 3; - } - return 2; -} - -function parseColumnDisplay(driver: CatalogDriver, display: string): (KtxTableRef & { column: string }) | null { +function parseColumnDisplay(dialect: KtxDialect, display: string): (KtxTableRef & { column: string }) | null { const parts = splitDisplay(display); - const tablePartCount = expectedDisplayPartCount(driver); + const tablePartCount = dialect.columnDisplayTablePartCount(); if (parts.length !== tablePartCount + 1) { return null; } @@ -175,7 +151,7 @@ function parseColumnDisplay(driver: CatalogDriver, display: string): (KtxTableRe if (!column) { return null; } - const table = parseDisplay(driver, parts.slice(0, -1).join('.')); + const table = dialect.parseDisplayRef(parts.slice(0, -1).join('.')); return table ? { ...table, column } : null; } @@ -272,6 +248,7 @@ export class WarehouseCatalogService { if (!table) { return null; } + const dialect = getDialectForDriver(catalog.driver); const profileTables = catalog.profile?.tables ?? []; const profileTable = profileTables.find((candidate) => candidate.table && refsEqual(candidate.table, table)); const profileColumns = catalog.profile?.columns ?? {}; @@ -281,7 +258,7 @@ export class WarehouseCatalogService { catalog: table.catalog, db: table.db, name: table.name, - display: formatDisplay(catalog.driver, table), + display: formatDisplay(dialect, table), kind: table.kind, comment: table.comment, description: firstDescription(table.descriptions), @@ -321,16 +298,21 @@ export class WarehouseCatalogService { if (!catalog) { return { resolved: null, candidates: [], dialect: 'unknown' }; } - const dialect = getDialectForDriver(catalog.driver).type; - const parsed = parseDisplay(catalog.driver, display); + const dialect = getDialectForDriver(catalog.driver); + const parsed = parseDisplay(dialect, display); if (!parsed) { - return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect }; + return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect: dialect.type }; } - const table = catalog.tables.find((candidate) => refsEqual(candidate, parsed)); + const exactTable = catalog.tables.find((candidate) => refsEqual(candidate, parsed)); + const looseNameMatches = + parsed.catalog === null && parsed.db === null + ? catalog.tables.filter((candidate) => normalize(candidate.name) === normalize(parsed.name)) + : []; + const table = exactTable ?? (looseNameMatches.length === 1 ? looseNameMatches[0] : undefined); if (!table) { - return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect }; + return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect: dialect.type }; } - return { resolved: { catalog: table.catalog, db: table.db, name: table.name }, candidates: [], dialect }; + return { resolved: { catalog: table.catalog, db: table.db, name: table.name }, candidates: [], dialect: dialect.type }; } async resolveDisplayTarget(connectionId: string, display: string): Promise { @@ -339,20 +321,20 @@ export class WarehouseCatalogService { return { resolved: null, candidates: [], dialect: 'unknown' }; } - const dialect = getDialectForDriver(catalog.driver).type; + const dialect = getDialectForDriver(catalog.driver); const tableResolution = await this.resolveDisplay(connectionId, display); if (tableResolution.resolved) { return tableResolution; } - const parsedColumn = parseColumnDisplay(catalog.driver, display); + const parsedColumn = parseColumnDisplay(dialect, display); if (!parsedColumn) { - return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect }; + return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect: dialect.type }; } const table = catalog.tables.find((candidate) => refsEqual(candidate, parsedColumn)); if (!table) { - return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect }; + return { resolved: null, candidates: bestCandidates(catalog.tables, display), dialect: dialect.type }; } return { @@ -363,7 +345,7 @@ export class WarehouseCatalogService { column: parsedColumn.column, }, candidates: [], - dialect, + dialect: dialect.type, }; } @@ -372,6 +354,7 @@ export class WarehouseCatalogService { if (!catalog) { return []; } + const dialect = getDialectForDriver(catalog.driver); const hits: RawSchemaHit[] = []; for (const table of catalog.tables as TableWithDescriptions[]) { const tableMatch = matchedOnTable(table, query); @@ -380,7 +363,7 @@ export class WarehouseCatalogService { kind: 'table', connectionId, ref: { catalog: table.catalog, db: table.db, name: table.name }, - display: formatDisplay(catalog.driver, table), + display: formatDisplay(dialect, table), matchedOn: tableMatch, }); } @@ -393,7 +376,7 @@ export class WarehouseCatalogService { kind: 'column', connectionId, ref: { catalog: table.catalog, db: table.db, name: table.name, column: column.name }, - display: `${formatDisplay(catalog.driver, table)}.${column.name}`, + display: `${formatDisplay(dialect, table)}.${column.name}`, matchedOn: columnMatch, }); } diff --git a/packages/cli/src/database-tree-picker.ts b/packages/cli/src/database-tree-picker.ts index 6b357de6..6698a0d2 100644 --- a/packages/cli/src/database-tree-picker.ts +++ b/packages/cli/src/database-tree-picker.ts @@ -1,3 +1,4 @@ +import { parseDottedTableEntry } from './context/scan/enabled-tables.js'; import type { KtxTableListEntry } from './context/scan/types.js'; import type { KtxCliIo } from './cli-runtime.js'; import { profileMark } from './startup-profile.js'; @@ -73,7 +74,9 @@ export interface PickDatabaseScopeArgs { } function qualifiedTableId(entry: KtxTableListEntry): string { - return `${entry.schema}.${entry.name}`; + return entry.catalog !== null + ? `${entry.catalog}.${entry.schema}.${entry.name}` + : `${entry.schema}.${entry.name}`; } function tableTitle(entry: KtxTableListEntry): string { @@ -177,7 +180,8 @@ function schemasFromEnabledTables(enabledTables: readonly string[]): string[] { const seen = new Set(); const result: string[] = []; for (const qualified of enabledTables) { - const schema = qualified.split('.')[0] ?? ''; + const ref = parseDottedTableEntry(qualified); + const schema = ref?.db ?? ''; if (schema.length === 0 || seen.has(schema)) continue; seen.add(schema); result.push(schema); @@ -228,11 +232,14 @@ async function runStageTwoTreePicker(input: { ? initialSelectionForExisting(args.existing.enabledTables, byId) : initialSelectionFromDefaults(selectedSchemas, schemaIds); - const initialState = buildInitialState({ - tree, - existingSelectedIds: initialSelection, - skipEmptyAction: 'save-empty', - }); + const initialState = { + ...buildInitialState({ + tree, + existingSelectedIds: initialSelection, + skipEmptyAction: 'save-empty', + }), + expanded: new Set(schemaIds), + }; const schemaWordPlural = schemaCount === 1 ? args.schemaNoun : args.schemaNounPlural; const subtitleLines = [ diff --git a/packages/cli/src/error-message.ts b/packages/cli/src/error-message.ts new file mode 100644 index 00000000..8ec1355a --- /dev/null +++ b/packages/cli/src/error-message.ts @@ -0,0 +1,28 @@ +export function describeError(error: unknown): string { + if (!(error instanceof Error)) { + const text = String(error); + return text.length > 0 ? text : 'unknown error'; + } + const parts: string[] = []; + if (error.message.length > 0) { + parts.push(error.message); + } + const seen = new Set([error]); + let cause: unknown = error.cause; + while (cause && !seen.has(cause)) { + seen.add(cause); + if (cause instanceof Error) { + if (cause.message.length > 0) { + parts.push(cause.message); + } + cause = cause.cause; + } else { + const text = String(cause); + if (text.length > 0) { + parts.push(text); + } + break; + } + } + return parts.length > 0 ? parts.join(': ') : 'unknown error'; +} diff --git a/packages/cli/src/llm/embedding-health.ts b/packages/cli/src/llm/embedding-health.ts index 2c8ac53d..47d3f14f 100644 --- a/packages/cli/src/llm/embedding-health.ts +++ b/packages/cli/src/llm/embedding-health.ts @@ -1,3 +1,4 @@ +import { describeError } from '../error-message.js'; import { createKtxEmbeddingProvider, type KtxEmbeddingProviderDeps } from './embedding-provider.js'; import type { KtxEmbeddingConfig } from './types.js'; @@ -48,7 +49,6 @@ export async function runKtxEmbeddingHealthCheck( } return { ok: true }; } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { ok: false, message: redactHealthCheckMessage(message, config) }; + return { ok: false, message: redactHealthCheckMessage(describeError(error), config) }; } } diff --git a/packages/cli/src/local-scan-connectors.ts b/packages/cli/src/local-scan-connectors.ts index 31fc158e..4d98bc0c 100644 --- a/packages/cli/src/local-scan-connectors.ts +++ b/packages/cli/src/local-scan-connectors.ts @@ -1,7 +1,11 @@ +import { + getDriverRegistration, + listSupportedDrivers, +} from './context/connections/drivers.js'; import type { KtxLocalProject } from './context/project/project.js'; import type { KtxScanConnector } from './context/scan/types.js'; -const SUPPORTED_DRIVERS = 'sqlite, postgres, mysql, clickhouse, sqlserver, bigquery, snowflake'; +const SUPPORTED_DRIVERS = listSupportedDrivers().join(', '); export async function createKtxCliScanConnector( project: KtxLocalProject, @@ -17,58 +21,23 @@ export async function createKtxCliScanConnector( `Connection "${connectionId}" has no \`driver\` field in ktx.yaml. Supported drivers: ${SUPPORTED_DRIVERS}.`, ); } - if (driver === 'sqlite') { - const { KtxSqliteScanConnector, isKtxSqliteConnectionConfig } = await import('./connectors/sqlite/connector.js');; - if (!isKtxSqliteConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxSqliteScanConnector({ connectionId, connection, projectDir: project.projectDir }); + + const registration = getDriverRegistration(driver); + if (!registration) { + throw new Error( + `Connection "${connectionId}" uses driver "${driver}", which has no native standalone KTX scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`, + ); } - if (driver === 'postgres') { - const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('./connectors/postgres/connector.js');; - if (!isKtxPostgresConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxPostgresScanConnector({ connectionId, connection }); + + const connectorModule = await registration.load(); + if (!connectorModule.isConnectionConfig(connection)) { + throw invalidConnectionConfigError(connectionId, driver); } - if (driver === 'mysql') { - const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('./connectors/mysql/connector.js');; - if (!isKtxMysqlConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxMysqlScanConnector({ connectionId, connection }); - } - if (driver === 'clickhouse') { - const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('./connectors/clickhouse/connector.js');; - if (!isKtxClickHouseConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxClickHouseScanConnector({ connectionId, connection }); - } - if (driver === 'sqlserver') { - const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('./connectors/sqlserver/connector.js');; - if (!isKtxSqlServerConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxSqlServerScanConnector({ connectionId, connection }); - } - if (driver === 'bigquery') { - const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('./connectors/bigquery/connector.js');; - if (!isKtxBigQueryConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxBigQueryScanConnector({ connectionId, connection }); - } - if (driver === 'snowflake') { - const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('./connectors/snowflake/connector.js');; - if (!isKtxSnowflakeConnectionConfig(connection)) { - throw invalidConnectionConfigError(connectionId, driver); - } - return new KtxSnowflakeScanConnector({ connectionId, connection, projectDir: project.projectDir }); - } - throw new Error( - `Connection "${connectionId}" uses driver "${driver}", which has no native standalone KTX scan connector. Supported drivers: ${SUPPORTED_DRIVERS}.`, - ); + return connectorModule.createScanConnector({ + connectionId, + connection, + projectDir: project.projectDir, + }); } function invalidConnectionConfigError(connectionId: string, driver: string): Error { diff --git a/packages/cli/src/managed-local-embeddings.ts b/packages/cli/src/managed-local-embeddings.ts index b178be47..768648c1 100644 --- a/packages/cli/src/managed-local-embeddings.ts +++ b/packages/cli/src/managed-local-embeddings.ts @@ -1,5 +1,6 @@ import type { KtxEmbeddingConfig } from './llm/types.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { writePrefixedLines } from './clack.js'; import { ensureManagedPythonCommandRuntime, type KtxManagedPythonInstallPolicy, @@ -73,7 +74,7 @@ export async function ensureManagedLocalEmbeddingsDaemon( }); const verb = daemon.status === 'started' ? 'Started' : 'Using'; - options.io.stderr.write(`${verb} KTX daemon: ${daemon.baseUrl}\n`); + writePrefixedLines((chunk) => options.io.stderr.write(chunk), `${verb} KTX daemon: ${daemon.baseUrl}`); return { baseUrl: daemon.baseUrl, diff --git a/packages/cli/src/managed-python-daemon.ts b/packages/cli/src/managed-python-daemon.ts index 7bc92e14..4e56ca47 100644 --- a/packages/cli/src/managed-python-daemon.ts +++ b/packages/cli/src/managed-python-daemon.ts @@ -4,6 +4,7 @@ import { createServer } from 'node:net'; import { setTimeout as delay } from 'node:timers/promises'; import { promisify } from 'node:util'; import { z } from 'zod'; +import { describeError } from './error-message.js'; import { installManagedPythonRuntime, managedPythonDaemonLayout, @@ -16,6 +17,17 @@ import { } from './managed-python-runtime.js'; import { sanitizeChildProxyEnv } from './proxy-env.js'; +export class ManagedPythonDaemonStartError extends Error { + readonly detail: string; + readonly stderrLog: string; + constructor(detail: string, stderrLog: string) { + super(`KTX daemon failed to start: ${detail}. stderr: ${stderrLog}`); + this.name = 'ManagedPythonDaemonStartError'; + this.detail = detail; + this.stderrLog = stderrLog; + } +} + export interface ManagedPythonDaemonState { schemaVersion: 1; pid: number; @@ -237,7 +249,7 @@ async function healthOk(input: { } return { ok: true }; } catch (error) { - return { ok: false, detail: error instanceof Error ? error.message : String(error) }; + return { ok: false, detail: describeError(error) }; } } @@ -328,7 +340,7 @@ async function waitForHealth(input: { return; } lastDetail = finalHealth.detail; - throw new Error(`KTX daemon failed to start: ${lastDetail}. stderr: ${input.state.stderrLog}`); + throw new ManagedPythonDaemonStartError(lastDetail, input.state.stderrLog); } async function removeState(layout: ManagedPythonDaemonLayout): Promise { @@ -721,13 +733,21 @@ export async function startManagedPythonDaemon( stdoutLog: layout.daemonStdoutPath, stderrLog: layout.daemonStderrPath, }; - await waitForHealth({ - state, - cliVersion: options.cliVersion, - fetch: fetchImpl, - timeoutMs: options.startupTimeoutMs ?? 10_000, - pollIntervalMs: options.pollIntervalMs ?? 100, - }); + try { + await waitForHealth({ + state, + cliVersion: options.cliVersion, + fetch: fetchImpl, + timeoutMs: options.startupTimeoutMs ?? 30_000, + pollIntervalMs: options.pollIntervalMs ?? 100, + }); + } catch (error) { + if (processAlive(state.pid)) { + killProcess(state.pid); + } + await removeState(layout); + throw error; + } await writeState(layout.daemonStatePath, state); return { status: 'started', layout, state, baseUrl: baseUrl(state) }; } finally { diff --git a/packages/cli/src/managed-python-http.ts b/packages/cli/src/managed-python-http.ts index 0c9b24b3..728aa3ca 100644 --- a/packages/cli/src/managed-python-http.ts +++ b/packages/cli/src/managed-python-http.ts @@ -7,6 +7,7 @@ import type { LookerTableIdentifierParser } from './context/ingest/adapters/look import { createHttpSqlAnalysisPort, type KtxSqlAnalysisHttpJsonRunner } from './context/sql-analysis/http-sql-analysis-port.js'; import type { SqlAnalysisPort } from './context/sql-analysis/ports.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { writePrefixedLines } from './clack.js'; import { ensureManagedPythonCommandRuntime, type KtxManagedPythonInstallPolicy, @@ -137,7 +138,7 @@ export function createManagedPythonDaemonBaseUrlResolver( force: false, }); const verb = daemon.status === 'started' ? 'Started' : 'Using existing'; - options.io.stderr.write(`${verb} KTX daemon: ${daemon.baseUrl}\n`); + writePrefixedLines((chunk) => options.io.stderr.write(chunk), `${verb} KTX daemon: ${daemon.baseUrl}`); cachedBaseUrl = daemon.baseUrl; return cachedBaseUrl; }; diff --git a/packages/cli/src/setup-agents.ts b/packages/cli/src/setup-agents.ts index 99d510c5..a671ba4b 100644 --- a/packages/cli/src/setup-agents.ts +++ b/packages/cli/src/setup-agents.ts @@ -10,6 +10,7 @@ import { markKtxSetupStateStepComplete } from './context/project/setup-config.js import { serializeKtxProjectConfig } from './context/project/config.js'; import { strToU8, zipSync } from 'fflate'; import type { KtxCliIo } from './cli-runtime.js'; +import { errorMessage, writePrefixedLines } from './clack.js'; import { createKtxSetupPromptAdapter, createKtxSetupUiAdapter, @@ -1230,7 +1231,7 @@ export async function runKtxSetupAgentsStep( } return { status: 'ready', projectDir: args.projectDir, installs, nextActions }; } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error)); return { status: 'failed', projectDir: args.projectDir }; } } diff --git a/packages/cli/src/setup-context.ts b/packages/cli/src/setup-context.ts index aa519111..dc289278 100644 --- a/packages/cli/src/setup-context.ts +++ b/packages/cli/src/setup-context.ts @@ -5,6 +5,7 @@ import { type KtxLocalProject, loadKtxProject } from './context/project/project. import { markKtxSetupStateStepComplete, readKtxSetupState } from './context/project/setup-config.js'; import { serializeKtxProjectConfig } from './context/project/config.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { errorMessage, writePrefixedLines } from './clack.js'; import { buildPublicIngestPlan } from './public-ingest.js'; import { type KtxDatabaseContextDepth, @@ -745,7 +746,7 @@ export async function runKtxSetupContextStep( return await runBuild(args, io, deps, project, targets); } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error)); return { status: 'failed', projectDir: args.projectDir }; } } diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts index ec2f017f..0704ecd2 100644 --- a/packages/cli/src/setup-databases.ts +++ b/packages/cli/src/setup-databases.ts @@ -3,6 +3,7 @@ import { readFile, writeFile } from 'node:fs/promises'; import { delimiter, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; +import { getDriverRegistration } from './context/connections/drivers.js'; import { queryHistoryDialectForConnection } from './context/ingest/adapters/historic-sql/connection-dialect.js'; import type { HistoricSqlDialect } from './context/ingest/adapters/historic-sql/types.js'; import { @@ -15,6 +16,11 @@ import { loadKtxProject } from './context/project/project.js'; import { markKtxSetupStateStepComplete, setKtxSetupDatabaseConnectionIds } from './context/project/setup-config.js'; import type { KtxTableListEntry } from './context/scan/types.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { + errorMessage, + flushPrefixedBufferedCommandOutput, + writePrefixedLines, +} from './clack.js'; import { runKtxConnection } from './connection.js'; import { pickDatabaseScope as defaultPickDatabaseScope, @@ -112,13 +118,13 @@ export interface KtxSetupDatabasesDeps { } const DRIVER_OPTIONS: Array<{ value: KtxSetupDatabaseDriver; label: string }> = [ - { value: 'sqlite', label: 'SQLite' }, { value: 'postgres', label: 'PostgreSQL' }, + { value: 'bigquery', label: 'BigQuery' }, + { value: 'snowflake', label: 'Snowflake' }, { value: 'mysql', label: 'MySQL' }, { value: 'clickhouse', label: 'ClickHouse' }, { value: 'sqlserver', label: 'SQL Server' }, - { value: 'bigquery', label: 'BigQuery' }, - { value: 'snowflake', label: 'Snowflake' }, + { value: 'sqlite', label: 'SQLite' }, ]; const DRIVER_LABELS = Object.fromEntries(DRIVER_OPTIONS.map((option) => [option.value, option.label])) as Record< @@ -220,7 +226,7 @@ const SCOPE_DISCOVERY_SPECS: Partial; -type ConnectionSetupStatus = 'ready' | 'back' | 'failed'; +type ConnectionSetupStatus = 'ready' | 'back' | 'failed' | 'failed-query-history-unavailable'; const DRIVER_CONNECTION_DEFAULTS: Record = { postgres: { port: '5432' }, @@ -361,74 +367,18 @@ async function defaultListSchemas(projectDir: string, connectionId: string): Pro const project = await loadKtxProject({ projectDir }); const connection = project.config.connections[connectionId]; const driver = normalizeDriver(connection?.driver); + const registration = driver ? getDriverRegistration(driver) : undefined; + if (!registration) return []; - if (driver === 'postgres') { - const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('./connectors/postgres/connector.js');; - if (!isKtxPostgresConnectionConfig(connection)) return []; - const connector = new KtxPostgresScanConnector({ connectionId, connection }); - try { - return await connector.listSchemas(); - } finally { - await connector.cleanup(); - } + const connectorModule = await registration.load(); + if (!connectorModule.isConnectionConfig(connection)) return []; + + const connector = connectorModule.createScanConnector({ connectionId, connection, projectDir }); + try { + return await connector.listSchemas(); + } finally { + await connector.cleanup?.(); } - - if (driver === 'sqlserver') { - const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('./connectors/sqlserver/connector.js');; - if (!isKtxSqlServerConnectionConfig(connection)) return []; - const connector = new KtxSqlServerScanConnector({ connectionId, connection }); - try { - return await connector.listSchemas(); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'mysql') { - const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('./connectors/mysql/connector.js');; - if (!isKtxMysqlConnectionConfig(connection)) return []; - const connector = new KtxMysqlScanConnector({ connectionId, connection }); - try { - return await connector.listSchemas(); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'clickhouse') { - const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('./connectors/clickhouse/connector.js');; - if (!isKtxClickHouseConnectionConfig(connection)) return []; - const connector = new KtxClickHouseScanConnector({ connectionId, connection }); - try { - return await connector.listSchemas(); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'bigquery') { - const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('./connectors/bigquery/connector.js');; - if (!isKtxBigQueryConnectionConfig(connection)) return []; - const connector = new KtxBigQueryScanConnector({ connectionId, connection }); - try { - return await connector.listDatasets(); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'snowflake') { - const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('./connectors/snowflake/connector.js');; - if (!isKtxSnowflakeConnectionConfig(connection)) return []; - const connector = new KtxSnowflakeScanConnector({ connectionId, connection, projectDir }); - try { - return await connector.listSchemas(); - } finally { - await connector.cleanup(); - } - } - - return []; } function configuredSchemas(connection: KtxProjectConnectionConfig | undefined, driver: KtxSetupDatabaseDriver): string[] | undefined { @@ -448,74 +398,18 @@ async function defaultListTables( const connection = project.config.connections[connectionId]; const driver = normalizeDriver(connection?.driver); const schemas = schemasOverride ?? (driver ? configuredSchemas(connection, driver) : undefined); + const registration = driver ? getDriverRegistration(driver) : undefined; + if (!registration) return []; - if (driver === 'postgres') { - const { KtxPostgresScanConnector, isKtxPostgresConnectionConfig } = await import('./connectors/postgres/connector.js');; - if (!isKtxPostgresConnectionConfig(connection)) return []; - const connector = new KtxPostgresScanConnector({ connectionId, connection }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } + const connectorModule = await registration.load(); + if (!connectorModule.isConnectionConfig(connection)) return []; + + const connector = connectorModule.createScanConnector({ connectionId, connection, projectDir }); + try { + return await connector.listTables(schemas); + } finally { + await connector.cleanup?.(); } - - if (driver === 'mysql') { - const { KtxMysqlScanConnector, isKtxMysqlConnectionConfig } = await import('./connectors/mysql/connector.js');; - if (!isKtxMysqlConnectionConfig(connection)) return []; - const connector = new KtxMysqlScanConnector({ connectionId, connection }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'sqlserver') { - const { KtxSqlServerScanConnector, isKtxSqlServerConnectionConfig } = await import('./connectors/sqlserver/connector.js');; - if (!isKtxSqlServerConnectionConfig(connection)) return []; - const connector = new KtxSqlServerScanConnector({ connectionId, connection }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'bigquery') { - const { KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig } = await import('./connectors/bigquery/connector.js');; - if (!isKtxBigQueryConnectionConfig(connection)) return []; - const connector = new KtxBigQueryScanConnector({ connectionId, connection }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'snowflake') { - const { KtxSnowflakeScanConnector, isKtxSnowflakeConnectionConfig } = await import('./connectors/snowflake/connector.js');; - if (!isKtxSnowflakeConnectionConfig(connection)) return []; - const connector = new KtxSnowflakeScanConnector({ connectionId, connection, projectDir }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } - } - - if (driver === 'clickhouse') { - const { KtxClickHouseScanConnector, isKtxClickHouseConnectionConfig } = await import('./connectors/clickhouse/connector.js');; - if (!isKtxClickHouseConnectionConfig(connection)) return []; - const connector = new KtxClickHouseScanConnector({ connectionId, connection }); - try { - return await connector.listTables(schemas); - } finally { - await connector.cleanup(); - } - } - - return []; } function existingConnectionIdsByDriver( @@ -638,9 +532,9 @@ function scriptedScopeConfigForDriver( databaseSchemas: string[], ): Record { if (databaseSchemas.length === 0) return {}; - if (driver === 'bigquery') return { dataset_ids: databaseSchemas }; - if (driver === 'clickhouse') return { databases: databaseSchemas }; - return { schemas: databaseSchemas }; + const registration = getDriverRegistration(driver); + if (!registration?.scopeConfigKey) return {}; + return { [registration.scopeConfigKey]: databaseSchemas }; } function databaseNameFromLiteralUrl(url: string): string | undefined { @@ -1128,25 +1022,6 @@ function createBufferedCommandIo(): BufferedCommandIo { }; } -function flushBufferedCommandOutput(io: KtxCliIo, bufferedIo: BufferedCommandIo): void { - const stdout = bufferedIo.stdoutText(); - const stderr = bufferedIo.stderrText(); - if (stdout.length > 0) { - io.stdout.write(stdout); - } - if (stderr.length > 0) { - io.stderr.write(stderr); - } -} - -function writePrefixedLines(write: (chunk: string) => void, output: string): void { - for (const line of output.split(/\r?\n/)) { - if (line.length > 0) { - write(`│ ${line}\n`); - } - } -} - function envWithCurrentNodeFirst(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { return { ...env, @@ -1222,11 +1097,6 @@ async function defaultRebuildNativeSqlite(io: KtxCliIo): Promise { } } -function flushPrefixedBufferedCommandOutput(io: KtxCliIo, bufferedIo: BufferedCommandIo): void { - writePrefixedLines((chunk) => io.stdout.write(chunk), bufferedIo.stdoutText()); - writePrefixedLines((chunk) => io.stderr.write(chunk), bufferedIo.stderrText()); -} - function nativeSqliteAbiMismatchDetail(output: string): string | null { const mentionsBetterSqlite = /\bbetter-sqlite3\b|better_sqlite3/i.test(output); const mentionsAbiMismatch = /compiled against a different Node\.js version|NODE_MODULE_VERSION/i.test(output); @@ -1318,6 +1188,20 @@ async function writeConnectionConfig(input: { } } +async function disableConnectionQueryHistory(projectDir: string, connectionId: string): Promise { + const project = await loadKtxProject({ projectDir }); + const connection = project.config.connections[connectionId]; + if (!connection) { + return; + } + const existing = queryHistoryConfigRecord(connection) ?? historicSqlConfigRecord(connection) ?? {}; + await writeConnectionConfig({ + projectDir, + connectionId, + connection: withQueryHistoryConfig(connection, { ...existing, enabled: false }), + }); +} + async function createConnectionConfigRollback(projectDir: string, connectionId: string): Promise<() => Promise> { const project = await loadKtxProject({ projectDir }); const previousConnection = project.config.connections[connectionId]; @@ -1519,9 +1403,9 @@ async function maybeConfigureDatabaseScope(input: { input.connectionId, ); } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - input.io.stderr.write( - `Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`, + writePrefixedLines( + (chunk) => input.io.stderr.write(chunk), + `Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${errorMessage(error)}`, ); const typed = await promptCommaSeparatedScope({ prompts: input.prompts, @@ -1573,11 +1457,12 @@ async function maybeConfigureDatabaseScope(input: { input.io, ); } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - input.io.stderr.write( + const detail = errorMessage(error); + writePrefixedLines( + (chunk) => input.io.stderr.write(chunk), input.forcePrompt === true - ? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}\n` - : `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}\n`, + ? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}` + : `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}`, ); return input.forcePrompt === true ? 'failed' : 'ready'; } @@ -1665,19 +1550,19 @@ async function maybeRunHistoricSqlSetupProbe(input: { connectionId: string; io: KtxCliIo; deps: KtxSetupDatabasesDeps; -}): Promise { +}): Promise { const project = await loadKtxProject({ projectDir: input.projectDir }); const connection = project.config.connections[input.connectionId]; const queryHistory = queryHistoryConfigRecord(connection) ?? historicSqlConfigRecord(connection); if (queryHistory?.enabled !== true) { - return; + return true; } if (!connection) { - return; + return true; } const dialect = queryHistoryDialectForConnection(connection); if (!dialect) { - return; + return true; } input.io.stdout.write('│ Query history probe...\n'); @@ -1696,6 +1581,7 @@ async function maybeRunHistoricSqlSetupProbe(input: { if (!result.ok) { input.io.stdout.write('│ Setup written; query history will be skipped until fixed.\n'); } + return result.ok; } async function applyHistoricSqlConfigToExistingConnection(input: { @@ -1785,8 +1671,11 @@ async function validateAndScanConnection(input: { const testIo = createBufferedCommandIo(); const testCode = await testConnection(input.projectDir, input.connectionId, testIo); if (testCode !== 0) { - flushBufferedCommandOutput(input.io, testIo); - input.io.stderr.write(`Connection test failed for ${input.connectionId}.\n`); + flushPrefixedBufferedCommandOutput(input.io, testIo); + writePrefixedLines( + (chunk) => input.io.stderr.write(chunk), + `Connection test failed for ${input.connectionId}.`, + ); return 'failed'; } const testOutput = testIo.stdoutText(); @@ -1800,7 +1689,7 @@ async function validateAndScanConnection(input: { return scopeStatus; } - await maybeRunHistoricSqlSetupProbe({ + const queryHistoryAvailable = await maybeRunHistoricSqlSetupProbe({ projectDir: input.projectDir, connectionId: input.connectionId, io: input.io, @@ -1857,7 +1746,7 @@ async function validateAndScanConnection(input: { ); } if (scanCode !== 0) { - return 'failed'; + return queryHistoryAvailable ? 'failed' : 'failed-query-history-unavailable'; } } const scanOutput = scanIo.stdoutText(); @@ -1999,7 +1888,10 @@ async function runPrimarySourceFullEdit(input: { const existing = project.config.connections[input.connectionId]; const driver = normalizeDriver(existing?.driver); if (!existing || !driver) { - input.io.stderr.write(`Connection "${input.connectionId}" is not a configured database.\n`); + writePrefixedLines( + (chunk) => input.io.stderr.write(chunk), + `Connection "${input.connectionId}" is not a configured database.`, + ); return 'failed'; } @@ -2053,7 +1945,7 @@ async function runPrimarySourceFullEdit(input: { }); if (validated !== 'ready') { await rollback(); - return validated; + return validated === 'failed-query-history-unavailable' ? 'failed' : validated; } return 'ready'; } @@ -2188,7 +2080,7 @@ export async function runKtxSetupDatabasesStep( prompts, }); } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error)); return { status: 'failed', projectDir: args.projectDir }; } if (connectionChoice === 'back') { @@ -2332,14 +2224,18 @@ export async function runKtxSetupDatabasesStep( break; } if (args.inputMode === 'disabled') return { status: 'failed', projectDir: args.projectDir }; + const failureOptions = [ + { value: 'retry', label: 'Retry connection test' }, + { value: 're-enter', label: 'Re-enter connection details' }, + ...(setupStatus === 'failed-query-history-unavailable' + ? [{ value: 'disable-query-history', label: 'Disable query history and retry' }] + : []), + { value: 'skip', label: 'Skip this database' }, + { value: 'back', label: 'Back' }, + ]; const action = await prompts.select({ message: `Database setup failed for ${connectionChoice.connectionId}`, - options: [ - { value: 'retry', label: 'Retry connection test' }, - { value: 're-enter', label: 'Re-enter connection details' }, - { value: 'skip', label: 'Skip this database' }, - { value: 'back', label: 'Back' }, - ], + options: failureOptions, }); if (action === 'back') { if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir }; @@ -2359,6 +2255,16 @@ export async function runKtxSetupDatabasesStep( args, prompts, }); + } else if (action === 'disable-query-history') { + await disableConnectionQueryHistory(args.projectDir, connectionChoice.connectionId); + setupStatus = await validateAndScanConnection({ + projectDir: args.projectDir, + connectionId: connectionChoice.connectionId, + io, + deps, + args, + prompts, + }); } else if (action === 're-enter') { const connection = await buildConnectionConfig({ driver, diff --git a/packages/cli/src/setup-embeddings.ts b/packages/cli/src/setup-embeddings.ts index 0aedd264..8f49bcf1 100644 --- a/packages/cli/src/setup-embeddings.ts +++ b/packages/cli/src/setup-embeddings.ts @@ -6,12 +6,13 @@ import { markKtxSetupStateStepComplete, readKtxSetupState } from './context/proj import type { KtxEmbeddingConfig } from './llm/types.js'; import { type KtxEmbeddingHealthCheckResult, runKtxEmbeddingHealthCheck } from './llm/embedding-health.js'; import type { KtxCliIo } from './cli-runtime.js'; -import { createStaticCliSpinner, type KtxCliSpinner } from './clack.js'; +import { createStaticCliSpinner, errorMessage, writePrefixedLines, type KtxCliSpinner } from './clack.js'; import { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, type ManagedLocalEmbeddingsDaemon, } from './managed-local-embeddings.js'; +import { ManagedPythonDaemonStartError } from './managed-python-daemon.js'; import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js'; import { withTextInputNavigation } from './prompt-navigation.js'; import { envCredentialReference, writeProjectLocalSecretReference } from './setup-secrets.js'; @@ -419,7 +420,13 @@ export async function runKtxSetupEmbeddingsStep( io, }); } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + const write = (chunk: string) => io.stderr.write(chunk); + if (error instanceof ManagedPythonDaemonStartError) { + const tail = await readLocalEmbeddingDaemonStderrTail(error.stderrLog); + writePrefixedLines(write, localEmbeddingSetupMessage(error.detail, tail)); + } else { + writePrefixedLines(write, errorMessage(error)); + } return { status: 'failed', projectDir: args.projectDir }; } } diff --git a/packages/cli/src/setup-runtime.ts b/packages/cli/src/setup-runtime.ts index 25612065..19f09a53 100644 --- a/packages/cli/src/setup-runtime.ts +++ b/packages/cli/src/setup-runtime.ts @@ -1,6 +1,7 @@ import { loadKtxProject, type KtxLocalProject } from './context/project/project.js'; import { markKtxSetupStateStepComplete } from './context/project/setup-config.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { errorMessage, writePrefixedLines } from './clack.js'; import { ensureManagedLocalEmbeddingsDaemon, type ManagedLocalEmbeddingsDaemon, @@ -88,7 +89,7 @@ export async function runKtxSetupRuntimeStep( }); } } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error)); return { status: 'failed', projectDir: args.projectDir, requirements }; } diff --git a/packages/cli/src/setup-sources.ts b/packages/cli/src/setup-sources.ts index 410de812..dea1cd43 100644 --- a/packages/cli/src/setup-sources.ts +++ b/packages/cli/src/setup-sources.ts @@ -17,6 +17,7 @@ import { type KtxProjectConfig, type KtxProjectConnectionConfig, serializeKtxPro import { loadKtxProject } from './context/project/project.js'; import { markKtxSetupStateStepComplete } from './context/project/setup-config.js'; import type { KtxCliIo } from './cli-runtime.js'; +import { errorMessage, writePrefixedLines } from './clack.js'; import { pickNotionRootPages } from './notion-page-picker.js'; import { runKtxSourceMapping } from './source-mapping.js'; import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js'; @@ -1983,7 +1984,7 @@ export async function runKtxSetupSourcesStep( return { status: 'ready', projectDir: args.projectDir, connectionIds: readyConnectionIds }; } } catch (error) { - io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + writePrefixedLines((chunk) => io.stderr.write(chunk), errorMessage(error)); return { status: 'failed', projectDir: args.projectDir }; } } diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index 10637a3d..27c5004a 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -55,6 +55,10 @@ const emittedProjectSnapshots = new Set(); const MCP_SAMPLE_RATE = 0.1 as const; let mcpSampled: boolean | undefined; +function telemetryDebugEnabled(): boolean { + return process.env.KTX_TELEMETRY_DEBUG === '1'; +} + export function shouldEmitMcpTelemetry(): boolean { mcpSampled ??= Math.random() < MCP_SAMPLE_RATE; return mcpSampled; @@ -71,19 +75,21 @@ export async function emitTelemetryEvent(input: packageInfo?: KtxCliPackageInfo; projectDir?: string; }): Promise { + const debug = telemetryDebugEnabled(); const identity = await loadTelemetryIdentity({ stdoutIsTTY: input.io.stdout.isTTY === true, stderr: input.io.stderr, env: process.env, }); - if (!identity.enabled || !identity.installId) { + if ((!identity.enabled || !identity.installId) && !debug) { return; } const packageInfo = input.packageInfo ?? getKtxCliPackageInfo(); + const installId = identity.installId ?? 'debug'; - const projectId = input.projectDir ? computeTelemetryProjectId(identity.installId, input.projectDir) : undefined; + const projectId = input.projectDir ? computeTelemetryProjectId(installId, input.projectDir) : undefined; await trackTelemetryEvent({ event: buildTelemetryEvent( input.name, @@ -93,7 +99,7 @@ export async function emitTelemetryEvent(input: }), input.fields, ), - distinctId: identity.installId, + distinctId: installId, projectId, env: process.env, stderr: input.io.stderr, diff --git a/packages/cli/src/admin-reindex.test.ts b/packages/cli/test/admin-reindex.test.ts similarity index 96% rename from packages/cli/src/admin-reindex.test.ts rename to packages/cli/test/admin-reindex.test.ts index dace420f..bdfc5a09 100644 --- a/packages/cli/src/admin-reindex.test.ts +++ b/packages/cli/test/admin-reindex.test.ts @@ -1,9 +1,9 @@ import { createRequire } from 'node:module'; -import type { ReindexSummary } from './context/index-sync/types.js'; +import type { ReindexSummary } from '../src/context/index-sync/types.js'; import { describe, expect, it, vi } from 'vitest'; -import { renderReindexJson, renderReindexPlain, reindexHasErrors } from './admin-reindex.js'; -import { runKtxCli } from './index.js'; +import { renderReindexJson, renderReindexPlain, reindexHasErrors } from '../src/admin-reindex.js'; +import { runKtxCli } from '../src/index.js'; const cliVersion = (createRequire(import.meta.url)('@kaelio/ktx/package.json') as { version: string }) .version; diff --git a/packages/cli/src/admin.test.ts b/packages/cli/test/admin.test.ts similarity index 99% rename from packages/cli/src/admin.test.ts rename to packages/cli/test/admin.test.ts index 15f4179e..4f425e14 100644 --- a/packages/cli/src/admin.test.ts +++ b/packages/cli/test/admin.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { runKtxCli } from './index.js'; +import { runKtxCli } from '../src/index.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/cli-program-telemetry.test.ts b/packages/cli/test/cli-program-telemetry.test.ts similarity index 96% rename from packages/cli/src/cli-program-telemetry.test.ts rename to packages/cli/test/cli-program-telemetry.test.ts index db905442..8088e7f2 100644 --- a/packages/cli/src/cli-program-telemetry.test.ts +++ b/packages/cli/test/cli-program-telemetry.test.ts @@ -3,8 +3,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runCommanderKtxCli } from './cli-program.js'; -import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js'; +import { runCommanderKtxCli } from '../src/cli-program.js'; +import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from '../src/cli-runtime.js'; function makeIo(stdoutIsTTY = true): { io: KtxCliIo; stdout: () => string; stderr: () => string } { let stdout = ''; diff --git a/packages/cli/src/cli-program.test.ts b/packages/cli/test/cli-program.test.ts similarity index 93% rename from packages/cli/src/cli-program.test.ts rename to packages/cli/test/cli-program.test.ts index 009dfb8a..332645aa 100644 --- a/packages/cli/src/cli-program.test.ts +++ b/packages/cli/test/cli-program.test.ts @@ -1,7 +1,7 @@ import { Command, type CommandUnknownOpts } from '@commander-js/extra-typings'; import { describe, expect, it } from 'vitest'; -import { buildKtxProgram, collectCommandFlagsPresent } from './cli-program.js'; -import type { KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js'; +import { buildKtxProgram, collectCommandFlagsPresent } from '../src/cli-program.js'; +import type { KtxCliIo, KtxCliPackageInfo } from '../src/cli-runtime.js'; function stubIo(): KtxCliIo { return { diff --git a/packages/cli/src/command-tree.test.ts b/packages/cli/test/command-tree.test.ts similarity index 98% rename from packages/cli/src/command-tree.test.ts rename to packages/cli/test/command-tree.test.ts index 181fac77..2a9d4f87 100644 --- a/packages/cli/src/command-tree.test.ts +++ b/packages/cli/test/command-tree.test.ts @@ -1,6 +1,6 @@ import { Command } from '@commander-js/extra-typings'; import { describe, expect, it } from 'vitest'; -import { formatCommandTree, walkCommandTree } from './command-tree.js'; +import { formatCommandTree, walkCommandTree } from '../src/command-tree.js'; describe('walkCommandTree', () => { it('captures name, description, aliases, and nested children', () => { diff --git a/packages/cli/src/commands/mcp-commands.test.ts b/packages/cli/test/commands/mcp-commands.test.ts similarity index 97% rename from packages/cli/src/commands/mcp-commands.test.ts rename to packages/cli/test/commands/mcp-commands.test.ts index dfcd1946..cf9c0cd5 100644 --- a/packages/cli/src/commands/mcp-commands.test.ts +++ b/packages/cli/test/commands/mcp-commands.test.ts @@ -1,7 +1,7 @@ import { Command } from '@commander-js/extra-typings'; import { describe, expect, it, vi } from 'vitest'; -import type { KtxCliCommandContext } from '../cli-program.js'; -import { registerMcpCommands } from './mcp-commands.js'; +import type { KtxCliCommandContext } from '../../src/cli-program.js'; +import { registerMcpCommands } from '../../src/commands/mcp-commands.js'; function makeContext(overrides: Partial = {}): KtxCliCommandContext { let exitCode = 0; diff --git a/packages/cli/src/commands/sql-commands.test.ts b/packages/cli/test/commands/sql-commands.test.ts similarity index 95% rename from packages/cli/src/commands/sql-commands.test.ts rename to packages/cli/test/commands/sql-commands.test.ts index 4f2c0277..9db7e8bf 100644 --- a/packages/cli/src/commands/sql-commands.test.ts +++ b/packages/cli/test/commands/sql-commands.test.ts @@ -1,7 +1,7 @@ import { Command } from '@commander-js/extra-typings'; import { describe, expect, it, vi } from 'vitest'; -import type { KtxCliCommandContext } from '../cli-program.js'; -import { registerSqlCommands } from './sql-commands.js'; +import type { KtxCliCommandContext } from '../../src/cli-program.js'; +import { registerSqlCommands } from '../../src/commands/sql-commands.js'; function makeContext(overrides: Partial = {}): KtxCliCommandContext { let exitCode = 0; diff --git a/packages/cli/src/connection.test.ts b/packages/cli/test/connection.test.ts similarity index 97% rename from packages/cli/src/connection.test.ts rename to packages/cli/test/connection.test.ts index 7cfc5b93..59ead362 100644 --- a/packages/cli/src/connection.test.ts +++ b/packages/cli/test/connection.test.ts @@ -1,14 +1,14 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { LookerClient } from './context/ingest/adapters/looker/client.js'; -import type { MetabaseRuntimeClient } from './context/ingest/adapters/metabase/client-port.js'; -import type { NotionClient } from './context/ingest/adapters/notion/notion-client.js'; -import { initKtxProject } from './context/project/project.js'; -import { parseKtxProjectConfig, serializeKtxProjectConfig } from './context/project/config.js'; -import type { KtxConnectionDriver, KtxScanConnector } from './context/scan/types.js'; +import type { LookerClient } from '../src/context/ingest/adapters/looker/client.js'; +import type { MetabaseRuntimeClient } from '../src/context/ingest/adapters/metabase/client-port.js'; +import type { NotionClient } from '../src/context/ingest/adapters/notion/notion-client.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { parseKtxProjectConfig, serializeKtxProjectConfig } from '../src/context/project/config.js'; +import type { KtxConnectionDriver, KtxScanConnector } from '../src/context/scan/types.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxConnection } from './connection.js'; +import { runKtxConnection } from '../src/connection.js'; function stripAnsi(s: string): string { return s.replace(/\[[0-9;]*m/g, ''); @@ -59,6 +59,8 @@ function nativeConnector( introspect: vi.fn(async () => { throw new Error('introspect should not be called from connection test'); }), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), testConnection, cleanup, }; diff --git a/packages/cli/src/connectors/bigquery/connector.test.ts b/packages/cli/test/connectors/bigquery/connector.test.ts similarity index 93% rename from packages/cli/src/connectors/bigquery/connector.test.ts rename to packages/cli/test/connectors/bigquery/connector.test.ts index b9893ccf..11ad69d8 100644 --- a/packages/cli/src/connectors/bigquery/connector.test.ts +++ b/packages/cli/test/connectors/bigquery/connector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { bigQueryConnectionConfigFromConfig, isKtxBigQueryConnectionConfig, type KtxBigQueryClient, KtxBigQueryScanConnector, type KtxBigQueryClientFactory, type KtxBigQueryDataset, type KtxBigQueryQueryJob, type KtxBigQueryTableRef } from '../../connectors/bigquery/connector.js'; -import { createBigQueryLiveDatabaseIntrospection } from '../../connectors/bigquery/live-database-introspection.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { bigQueryConnectionConfigFromConfig, isKtxBigQueryConnectionConfig, type KtxBigQueryClient, KtxBigQueryScanConnector, type KtxBigQueryClientFactory, type KtxBigQueryDataset, type KtxBigQueryQueryJob, type KtxBigQueryTableRef, prepareBigQueryReadOnlyQuery } from '../../../src/connectors/bigquery/connector.js'; +import { createBigQueryLiveDatabaseIntrospection } from '../../../src/connectors/bigquery/live-database-introspection.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; function fakeClientFactory(options: { primaryKeyError?: Error } = {}): KtxBigQueryClientFactory { const queryResults = vi.fn(async (): ReturnType => [ @@ -98,6 +98,17 @@ const connection = { } as const; describe('KtxBigQueryScanConnector', () => { + it('prepares read-only SQL parameters with BigQuery named placeholders', () => { + expect(prepareBigQueryReadOnlyQuery('SELECT * FROM orders WHERE id = :id AND id_2 = :id_2', { id: 1, id_2: 2 })).toEqual({ + sql: 'SELECT * FROM orders WHERE id = @id AND id_2 = @id_2', + params: { id: 1, id_2: 2 }, + }); + expect(prepareBigQueryReadOnlyQuery('SELECT * FROM orders')).toEqual({ + sql: 'SELECT * FROM orders', + params: undefined, + }); + }); + it('resolves configuration safely', () => { expect(isKtxBigQueryConnectionConfig(connection)).toBe(true); expect(isKtxBigQueryConnectionConfig({ driver: 'mysql' })).toBe(false); @@ -256,7 +267,7 @@ describe('KtxBigQueryScanConnector', () => { ), ).resolves.toEqual({ values: ['open', 'paid'], cardinality: 2 }); await expect(connector.getTableRowCount('orders')).resolves.toBe(12); - await expect(connector.listDatasets()).resolves.toEqual(['analytics', 'staging']); + await expect(connector.listSchemas()).resolves.toEqual(['analytics', 'staging']); await expect( connector.columnStats( { connectionId: 'warehouse', table: { catalog: 'project-1', db: 'analytics', name: 'orders' }, column: 'status' }, @@ -366,9 +377,9 @@ describe('KtxBigQueryScanConnector', () => { }); await expect(connector.listTables(['analytics', 'mart'])).resolves.toEqual([ - { schema: 'analytics', name: 'orders', kind: 'table' }, - { schema: 'analytics', name: 'order_clone', kind: 'table' }, - { schema: 'mart', name: 'orders_mv', kind: 'view' }, + { catalog: 'project-1', schema: 'analytics', name: 'orders', kind: 'table' }, + { catalog: 'project-1', schema: 'analytics', name: 'order_clone', kind: 'table' }, + { catalog: 'project-1', schema: 'mart', name: 'orders_mv', kind: 'view' }, ]); expect(createQueryJob).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/connectors/bigquery/dialect.test.ts b/packages/cli/test/connectors/bigquery/dialect.test.ts similarity index 81% rename from packages/cli/src/connectors/bigquery/dialect.test.ts rename to packages/cli/test/connectors/bigquery/dialect.test.ts index d2033bd9..171617ce 100644 --- a/packages/cli/src/connectors/bigquery/dialect.test.ts +++ b/packages/cli/test/connectors/bigquery/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxBigQueryDialect } from './dialect.js'; +import { KtxBigQueryDialect } from '../../../src/connectors/bigquery/dialect.js'; describe('KtxBigQueryDialect', () => { const dialect = new KtxBigQueryDialect(); @@ -38,14 +38,6 @@ describe('KtxBigQueryDialect', () => { ); }); - it('rewrites colon parameters to BigQuery named parameters', () => { - expect(dialect.prepareQuery('SELECT * FROM orders WHERE id = :id AND id_2 = :id_2', { id: 1, id_2: 2 })).toEqual({ - sql: 'SELECT * FROM orders WHERE id = @id AND id_2 = @id_2', - params: { id: 1, id_2: 2 }, - }); - expect(dialect.prepareQuery('SELECT * FROM orders')).toEqual({ sql: 'SELECT * FROM orders', params: undefined }); - }); - it('keeps unsupported statistics explicit', () => { expect(dialect.generateColumnStatisticsQuery('analytics', 'orders')).toBeNull(); }); diff --git a/packages/cli/src/connectors/clickhouse/connector.test.ts b/packages/cli/test/connectors/clickhouse/connector.test.ts similarity index 88% rename from packages/cli/src/connectors/clickhouse/connector.test.ts rename to packages/cli/test/connectors/clickhouse/connector.test.ts index abc7cad5..aba3143f 100644 --- a/packages/cli/src/connectors/clickhouse/connector.test.ts +++ b/packages/cli/test/connectors/clickhouse/connector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { clickHouseClientConfigFromConfig, isKtxClickHouseConnectionConfig, KtxClickHouseScanConnector, type KtxClickHouseClientFactory } from '../../connectors/clickhouse/connector.js'; -import { createClickHouseLiveDatabaseIntrospection } from '../../connectors/clickhouse/live-database-introspection.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { clickHouseClientConfigFromConfig, isKtxClickHouseConnectionConfig, KtxClickHouseScanConnector, prepareClickHouseReadOnlyQuery, type KtxClickHouseClientFactory } from '../../../src/connectors/clickhouse/connector.js'; +import { createClickHouseLiveDatabaseIntrospection } from '../../../src/connectors/clickhouse/live-database-introspection.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; function result(payload: T) { return { @@ -15,8 +15,8 @@ function fakeClientFactory(): KtxClickHouseClientFactory { const query = vi.fn(async (input: { query: string; format: string; query_params?: Record }) => { if (input.query.includes('FROM system.tables')) { return result([ - { name: 'events', engine: 'MergeTree', comment: 'Event stream' }, - { name: 'event_summary', engine: 'View', comment: '' }, + { database: 'analytics', name: 'event_summary', engine: 'View', comment: '' }, + { database: 'analytics', name: 'events', engine: 'MergeTree', comment: 'Event stream' }, ]); } if (input.query.includes('FROM system.columns')) { @@ -136,6 +136,33 @@ function multiDatabaseClickHouseClientFactory(): KtxClickHouseClientFactory { } describe('KtxClickHouseScanConnector', () => { + it('prepares read-only SQL parameters with ClickHouse typed placeholders', () => { + expect( + prepareClickHouseReadOnlyQuery('select * from events where id = :id and event_name = :name', { + id: 10, + name: 'signup', + }), + ).toEqual({ + sql: 'select * from events where id = {id:Int64} and event_name = {name:String}', + params: { id: 10, name: 'signup' }, + }); + expect( + prepareClickHouseReadOnlyQuery('select * from events where enabled = :enabled and ratio = :ratio and created_at = :created_at', { + enabled: true, + ratio: 1.5, + created_at: new Date('2026-05-25T00:00:00.000Z'), + }), + ).toEqual({ + sql: 'select * from events where enabled = {enabled:Bool} and ratio = {ratio:Float64} and created_at = {created_at:DateTime}', + params: { + enabled: true, + ratio: 1.5, + created_at: new Date('2026-05-25T00:00:00.000Z'), + }, + }); + expect(prepareClickHouseReadOnlyQuery('select 1')).toEqual({ sql: 'select 1', params: undefined }); + }); + it('resolves ClickHouse connection configuration safely', () => { expect(isKtxClickHouseConnectionConfig({ driver: 'clickhouse', host: 'localhost', database: 'analytics' })).toBe( true, @@ -196,8 +223,8 @@ describe('KtxClickHouseScanConnector', () => { }, }); expect(snapshot.tables.map((table) => [table.name, table.kind, table.estimatedRows, table.comment])).toEqual([ - ['events', 'table', 2, 'Event stream'], ['event_summary', 'view', null, null], + ['events', 'table', 2, 'Event stream'], ]); expect(snapshot.tables.find((table) => table.name === 'events')?.columns[0]).toMatchObject({ name: 'id', @@ -344,6 +371,10 @@ describe('KtxClickHouseScanConnector', () => { await expect(connector.getTableRowCount('events')).resolves.toBe(2); await expect(connector.listSchemas()).resolves.toEqual(['analytics', 'warehouse']); + await expect(connector.listTables(['analytics'])).resolves.toEqual([ + { catalog: null, schema: 'analytics', name: 'event_summary', kind: 'view' }, + { catalog: null, schema: 'analytics', name: 'events', kind: 'table' }, + ]); await expect( connector.columnStats( { connectionId: 'warehouse', table: { catalog: null, db: 'analytics', name: 'events' }, column: 'event_name' }, diff --git a/packages/cli/src/connectors/clickhouse/dialect.test.ts b/packages/cli/test/connectors/clickhouse/dialect.test.ts similarity index 75% rename from packages/cli/src/connectors/clickhouse/dialect.test.ts rename to packages/cli/test/connectors/clickhouse/dialect.test.ts index 14a1032c..809b1304 100644 --- a/packages/cli/src/connectors/clickhouse/dialect.test.ts +++ b/packages/cli/test/connectors/clickhouse/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxClickHouseDialect } from './dialect.js'; +import { KtxClickHouseDialect } from '../../../src/connectors/clickhouse/dialect.js'; describe('KtxClickHouseDialect', () => { const dialect = new KtxClickHouseDialect(); @@ -23,7 +23,7 @@ describe('KtxClickHouseDialect', () => { expect(dialect.mapToDimensionType('')).toBe('string'); }); - it('builds sampling, distinct-value, pagination, and time SQL', () => { + it('builds sampling, distinct-value, and pagination SQL', () => { expect(dialect.generateSampleQuery('`analytics`.`events`', 25, ['id', 'event_name'])).toBe( 'SELECT `id`, `event_name` FROM `analytics`.`events` LIMIT 25', ); @@ -34,16 +34,6 @@ describe('KtxClickHouseDialect', () => { 'SELECT DISTINCT toString(`event_name`) AS val', ); expect(dialect.getLimitOffsetClause(10, 20)).toBe('LIMIT 10 OFFSET 20'); - expect(dialect.getTimeTruncExpression('created_at', 'week')).toBe('toStartOfWeek(created_at, 1)'); }); - it('prepares named parameters using ClickHouse typed placeholders', () => { - expect(dialect.prepareQuery('select * from events where id = :id and event_name = :name', { - id: 10, - name: 'signup', - })).toEqual({ - sql: 'select * from events where id = {id:Int64} and event_name = {name:String}', - params: { id: 10, name: 'signup' }, - }); - }); }); diff --git a/packages/cli/src/connectors/mysql/connector.test.ts b/packages/cli/test/connectors/mysql/connector.test.ts similarity index 92% rename from packages/cli/src/connectors/mysql/connector.test.ts rename to packages/cli/test/connectors/mysql/connector.test.ts index 6c69ea3d..c8334164 100644 --- a/packages/cli/src/connectors/mysql/connector.test.ts +++ b/packages/cli/test/connectors/mysql/connector.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; import type { FieldPacket, RowDataPacket } from 'mysql2/promise'; -import { createMysqlLiveDatabaseIntrospection } from '../../connectors/mysql/live-database-introspection.js'; -import { isKtxMysqlConnectionConfig, KtxMysqlScanConnector, mysqlConnectionPoolConfigFromConfig, type KtxMysqlConnectionConfig, type KtxMysqlPoolFactory } from '../../connectors/mysql/connector.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { createMysqlLiveDatabaseIntrospection } from '../../../src/connectors/mysql/live-database-introspection.js'; +import { isKtxMysqlConnectionConfig, KtxMysqlScanConnector, mysqlConnectionPoolConfigFromConfig, prepareMysqlReadOnlyQuery, type KtxMysqlConnectionConfig, type KtxMysqlPoolFactory } from '../../../src/connectors/mysql/connector.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; function mysqlResult(rows: Record[], fields: Array<{ name: string; type?: number }>): [RowDataPacket[], FieldPacket[]] { return [rows as RowDataPacket[], fields as FieldPacket[]]; @@ -13,9 +13,9 @@ function fakePoolFactory(): KtxMysqlPoolFactory { if (sql.includes('INFORMATION_SCHEMA.TABLES')) { return mysqlResult( [ - { TABLE_NAME: 'customers', TABLE_TYPE: 'BASE TABLE', TABLE_COMMENT: 'Customer table', TABLE_ROWS: 2 }, - { TABLE_NAME: 'orders', TABLE_TYPE: 'BASE TABLE', TABLE_COMMENT: 'InnoDB free: 1 kB; Order table', TABLE_ROWS: 2 }, - { TABLE_NAME: 'order_summary', TABLE_TYPE: 'VIEW', TABLE_COMMENT: '', TABLE_ROWS: null }, + { TABLE_SCHEMA: 'analytics', TABLE_NAME: 'customers', TABLE_TYPE: 'BASE TABLE', TABLE_COMMENT: 'Customer table', TABLE_ROWS: 2 }, + { TABLE_SCHEMA: 'analytics', TABLE_NAME: 'orders', TABLE_TYPE: 'BASE TABLE', TABLE_COMMENT: 'InnoDB free: 1 kB; Order table', TABLE_ROWS: 2 }, + { TABLE_SCHEMA: 'analytics', TABLE_NAME: 'order_summary', TABLE_TYPE: 'VIEW', TABLE_COMMENT: '', TABLE_ROWS: null }, ], [{ name: 'TABLE_NAME' }, { name: 'TABLE_TYPE' }, { name: 'TABLE_COMMENT' }, { name: 'TABLE_ROWS' }], ); @@ -173,6 +173,19 @@ function multiSchemaMysqlPoolFactory( } describe('KtxMysqlScanConnector', () => { + it('prepares read-only SQL parameters with MySQL positional placeholders', () => { + expect( + prepareMysqlReadOnlyQuery('select * from orders where id = :id and status = :status', { + status: 'paid', + id: 10, + }), + ).toEqual({ + sql: 'select * from orders where id = ? and status = ?', + params: [10, 'paid'], + }); + expect(prepareMysqlReadOnlyQuery('select 1')).toEqual({ sql: 'select 1', params: undefined }); + }); + it('resolves MySQL connection configuration safely', () => { expect(isKtxMysqlConnectionConfig({ driver: 'mysql', host: 'localhost', database: 'analytics' })).toBe(true); expect(isKtxMysqlConnectionConfig({ driver: 'postgres', host: 'localhost', database: 'analytics' })).toBe(false); @@ -497,6 +510,11 @@ describe('KtxMysqlScanConnector', () => { await expect(connector.getTableRowCount('orders')).resolves.toBe(2); await expect(connector.listSchemas()).resolves.toEqual(['analytics', 'warehouse']); + await expect(connector.listTables(['analytics'])).resolves.toEqual([ + { catalog: null, schema: 'analytics', name: 'customers', kind: 'table' }, + { catalog: null, schema: 'analytics', name: 'orders', kind: 'table' }, + { catalog: null, schema: 'analytics', name: 'order_summary', kind: 'view' }, + ]); await expect(connector.columnStats( { connectionId: 'warehouse', table: { catalog: null, db: 'analytics', name: 'orders' }, column: 'status' }, { runId: 'scan-run-1' }, diff --git a/packages/cli/src/connectors/mysql/dialect.test.ts b/packages/cli/test/connectors/mysql/dialect.test.ts similarity index 75% rename from packages/cli/src/connectors/mysql/dialect.test.ts rename to packages/cli/test/connectors/mysql/dialect.test.ts index cf15527b..a00d6188 100644 --- a/packages/cli/src/connectors/mysql/dialect.test.ts +++ b/packages/cli/test/connectors/mysql/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxMysqlDialect } from './dialect.js'; +import { KtxMysqlDialect } from '../../../src/connectors/mysql/dialect.js'; describe('KtxMysqlDialect', () => { const dialect = new KtxMysqlDialect(); @@ -23,7 +23,7 @@ describe('KtxMysqlDialect', () => { expect(dialect.mapToDimensionType('')).toBe('string'); }); - it('builds sampling, distinct-value, pagination, and time SQL', () => { + it('builds sampling, distinct-value, and pagination SQL', () => { expect(dialect.generateSampleQuery('`analytics`.`orders`', 25, ['id', 'status'])).toBe( 'SELECT `id`, `status` FROM `analytics`.`orders` LIMIT 25', ); @@ -34,16 +34,6 @@ describe('KtxMysqlDialect', () => { 'SELECT DISTINCT CAST(`status` AS CHAR) AS val', ); expect(dialect.getLimitOffsetClause(10, 20)).toBe('LIMIT 10 OFFSET 20'); - expect(dialect.getTimeTruncExpression('created_at', 'month')).toBe("DATE_FORMAT(created_at, '%Y-%m-01')"); }); - it('prepares named parameters in deterministic SQL placeholder order', () => { - expect(dialect.prepareQuery('select * from orders where id = :id and status = :status', { - status: 'paid', - id: 10, - })).toEqual({ - sql: 'select * from orders where id = ? and status = ?', - params: [10, 'paid'], - }); - }); }); diff --git a/packages/cli/src/connectors/postgres/connector.test.ts b/packages/cli/test/connectors/postgres/connector.test.ts similarity index 91% rename from packages/cli/src/connectors/postgres/connector.test.ts rename to packages/cli/test/connectors/postgres/connector.test.ts index d9fa45cf..e43e05a4 100644 --- a/packages/cli/src/connectors/postgres/connector.test.ts +++ b/packages/cli/test/connectors/postgres/connector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { createPostgresLiveDatabaseIntrospection } from '../../connectors/postgres/live-database-introspection.js'; -import { isKtxPostgresConnectionConfig, KtxPostgresScanConnector, postgresPoolConfigFromConfig, type KtxPostgresConnectionConfig, type KtxPostgresPoolFactory } from '../../connectors/postgres/connector.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { createPostgresLiveDatabaseIntrospection } from '../../../src/connectors/postgres/live-database-introspection.js'; +import { isKtxPostgresConnectionConfig, KtxPostgresScanConnector, postgresPoolConfigFromConfig, preparePostgresReadOnlyQuery, type KtxPostgresConnectionConfig, type KtxPostgresPoolFactory } from '../../../src/connectors/postgres/connector.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; interface FakeQueryResult { rows: Record[]; @@ -44,9 +44,9 @@ function metadataResults(): Map { 'FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n', { rows: [ - { table_name: 'customers', table_kind: 'r', row_count: '2', table_comment: 'Customers' }, - { table_name: 'orders', table_kind: 'r', row_count: '3', table_comment: null }, - { table_name: 'recent_orders', table_kind: 'v', row_count: '0', table_comment: 'Recent orders' }, + { schema_name: 'public', table_name: 'customers', table_kind: 'r', row_count: '2', table_comment: 'Customers' }, + { schema_name: 'public', table_name: 'orders', table_kind: 'r', row_count: '3', table_comment: null }, + { schema_name: 'public', table_name: 'recent_orders', table_kind: 'v', row_count: '0', table_comment: 'Recent orders' }, ], }, ], @@ -102,6 +102,28 @@ function metadataResults(): Map { } describe('KtxPostgresScanConnector', () => { + it('prepares read-only SQL parameters with PostgreSQL positional placeholders', () => { + expect( + preparePostgresReadOnlyQuery('select * from orders where id = :id and status = :status', { + id: 1, + status: 'paid', + }), + ).toEqual({ + sql: 'select * from orders where id = $1 and status = $2', + params: [1, 'paid'], + }); + expect( + preparePostgresReadOnlyQuery('select :Client_Name_10, :Client_Name_1', { + Client_Name_1: 'short', + Client_Name_10: 'long', + }), + ).toEqual({ + sql: 'select $2, $1', + params: ['short', 'long'], + }); + expect(preparePostgresReadOnlyQuery('select 1')).toEqual({ sql: 'select 1', params: undefined }); + }); + it('resolves configuration safely', () => { expect(isKtxPostgresConnectionConfig({ driver: 'postgres', url: 'env:DATABASE_URL' })).toBe(true); expect(isKtxPostgresConnectionConfig({ driver: 'postgresql', host: 'db', database: 'analytics' })).toBe(false); @@ -367,6 +389,11 @@ describe('KtxPostgresScanConnector', () => { }); await expect(connector.getTableRowCount({ db: 'public', name: 'orders' })).resolves.toBe(3); await expect(connector.listSchemas()).resolves.toEqual(['public']); + await expect(connector.listTables(['public'])).resolves.toEqual([ + { catalog: null, schema: 'public', name: 'customers', kind: 'table' }, + { catalog: null, schema: 'public', name: 'orders', kind: 'table' }, + { catalog: null, schema: 'public', name: 'recent_orders', kind: 'view' }, + ]); await expect(connector.testConnection()).resolves.toEqual({ success: true }); await expect( diff --git a/packages/cli/src/connectors/postgres/dialect.test.ts b/packages/cli/test/connectors/postgres/dialect.test.ts similarity index 64% rename from packages/cli/src/connectors/postgres/dialect.test.ts rename to packages/cli/test/connectors/postgres/dialect.test.ts index ffe85497..1a1d4768 100644 --- a/packages/cli/src/connectors/postgres/dialect.test.ts +++ b/packages/cli/test/connectors/postgres/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxPostgresDialect } from './dialect.js'; +import { KtxPostgresDialect } from '../../../src/connectors/postgres/dialect.js'; describe('KtxPostgresDialect', () => { const dialect = new KtxPostgresDialect(); @@ -18,7 +18,7 @@ describe('KtxPostgresDialect', () => { expect(dialect.mapToDimensionType('jsonb')).toBe('string'); }); - it('generates sample, distinct-value, statistics, and time SQL', () => { + it('generates sample, distinct-value, and statistics SQL', () => { expect(dialect.generateSampleQuery('"public"."orders"', 5, ['id', 'status'])).toBe( 'SELECT "id", "status" FROM "public"."orders" LIMIT 5', ); @@ -29,24 +29,6 @@ describe('KtxPostgresDialect', () => { 'SELECT DISTINCT "status"::text AS val', ); expect(dialect.generateColumnStatisticsQuery('public', 'orders')).toContain('FROM pg_stats s'); - expect(dialect.getTimeTruncExpression('"created_at"', 'month')).toBe('DATE_TRUNC(\'month\', "created_at")'); }); - it('prepares named parameters with PostgreSQL positional parameters', () => { - expect( - dialect.prepareQuery('select * from orders where id = :id and status = :status', { id: 1, status: 'paid' }), - ).toEqual({ - sql: 'select * from orders where id = $1 and status = $2', - params: [1, 'paid'], - }); - expect( - dialect.prepareQuery('select :Client_Name_10, :Client_Name_1', { - Client_Name_1: 'short', - Client_Name_10: 'long', - }), - ).toEqual({ - sql: 'select $2, $1', - params: ['short', 'long'], - }); - }); }); diff --git a/packages/cli/src/connectors/postgres/historic-sql-query-client.test.ts b/packages/cli/test/connectors/postgres/historic-sql-query-client.test.ts similarity index 90% rename from packages/cli/src/connectors/postgres/historic-sql-query-client.test.ts rename to packages/cli/test/connectors/postgres/historic-sql-query-client.test.ts index b9c9fd40..0ec03fe1 100644 --- a/packages/cli/src/connectors/postgres/historic-sql-query-client.test.ts +++ b/packages/cli/test/connectors/postgres/historic-sql-query-client.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { KtxPostgresHistoricSqlQueryClient } from './historic-sql-query-client.js'; -import type { KtxPostgresPoolConfig, KtxPostgresPoolFactory } from './connector.js'; +import { KtxPostgresHistoricSqlQueryClient } from '../../../src/connectors/postgres/historic-sql-query-client.js'; +import type { KtxPostgresPoolConfig, KtxPostgresPoolFactory } from '../../../src/connectors/postgres/connector.js'; describe('KtxPostgresHistoricSqlQueryClient', () => { it('executes parameterized read-only SQL through the native Postgres connector pool', async () => { diff --git a/packages/cli/src/connectors/snowflake/connector.test.ts b/packages/cli/test/connectors/snowflake/connector.test.ts similarity index 94% rename from packages/cli/src/connectors/snowflake/connector.test.ts rename to packages/cli/test/connectors/snowflake/connector.test.ts index 657dbaf1..1b00061b 100644 --- a/packages/cli/src/connectors/snowflake/connector.test.ts +++ b/packages/cli/test/connectors/snowflake/connector.test.ts @@ -7,9 +7,9 @@ vi.mock('snowflake-sdk', () => ({ createPool, })); -import { createSnowflakeLiveDatabaseIntrospection } from '../../connectors/snowflake/live-database-introspection.js'; -import { isKtxSnowflakeConnectionConfig, KtxSnowflakeScanConnector, snowflakeConnectionConfigFromConfig, type KtxSnowflakeConnectionConfig, type KtxSnowflakeDriver, type KtxSnowflakeDriverFactory } from '../../connectors/snowflake/connector.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { createSnowflakeLiveDatabaseIntrospection } from '../../../src/connectors/snowflake/live-database-introspection.js'; +import { isKtxSnowflakeConnectionConfig, KtxSnowflakeScanConnector, prepareSnowflakeReadOnlyQuery, snowflakeConnectionConfigFromConfig, type KtxSnowflakeConnectionConfig, type KtxSnowflakeDriver, type KtxSnowflakeDriverFactory } from '../../../src/connectors/snowflake/connector.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; function fakeDriverFactory(): KtxSnowflakeDriverFactory { const driver: KtxSnowflakeDriver = { @@ -64,8 +64,8 @@ function fakeDriverFactory(): KtxSnowflakeDriverFactory { ]), listSchemas: vi.fn(async () => ['PUBLIC', 'MART']), listTables: vi.fn(async () => [ - { schema: 'PUBLIC', name: 'ORDERS', kind: 'table' as const }, - { schema: 'PUBLIC', name: 'ORDER_SUMMARY', kind: 'view' as const }, + { catalog: 'ANALYTICS', schema: 'PUBLIC', name: 'ORDERS', kind: 'table' as const }, + { catalog: 'ANALYTICS', schema: 'PUBLIC', name: 'ORDER_SUMMARY', kind: 'view' as const }, ]), cleanup: vi.fn(async () => undefined), }; @@ -105,6 +105,17 @@ function installSnowflakePoolMock() { } describe('KtxSnowflakeScanConnector', () => { + it('prepares read-only SQL parameters with Snowflake bind arrays', () => { + expect(prepareSnowflakeReadOnlyQuery('SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', { id: 1, status: 'paid' })).toEqual({ + sql: 'SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', + params: [1, 'paid'], + }); + expect(prepareSnowflakeReadOnlyQuery('SELECT * FROM ORDERS')).toEqual({ + sql: 'SELECT * FROM ORDERS', + params: undefined, + }); + }); + it('resolves Snowflake connection configuration safely', () => { expect( isKtxSnowflakeConnectionConfig({ @@ -561,8 +572,8 @@ describe('KtxSnowflakeScanConnector', () => { }); await expect(connector.listTables(['MART', 'PUBLIC'])).resolves.toEqual([ - { schema: 'MART', name: 'ORDERS', kind: 'table' }, - { schema: 'PUBLIC', name: 'ORDER_SUMMARY', kind: 'view' }, + { catalog: 'ANALYTICS', schema: 'MART', name: 'ORDERS', kind: 'table' }, + { catalog: 'ANALYTICS', schema: 'PUBLIC', name: 'ORDER_SUMMARY', kind: 'view' }, ]); expect(queries).toHaveLength(1); diff --git a/packages/cli/src/connectors/snowflake/dialect.test.ts b/packages/cli/test/connectors/snowflake/dialect.test.ts similarity index 80% rename from packages/cli/src/connectors/snowflake/dialect.test.ts rename to packages/cli/test/connectors/snowflake/dialect.test.ts index 991a30b5..4f966ffb 100644 --- a/packages/cli/src/connectors/snowflake/dialect.test.ts +++ b/packages/cli/test/connectors/snowflake/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxSnowflakeDialect } from './dialect.js'; +import { KtxSnowflakeDialect } from '../../../src/connectors/snowflake/dialect.js'; describe('KtxSnowflakeDialect', () => { const dialect = new KtxSnowflakeDialect(); @@ -36,14 +36,6 @@ describe('KtxSnowflakeDialect', () => { ); }); - it('passes Snowflake positional parameters as bind arrays', () => { - expect(dialect.prepareQuery('SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', { id: 1, status: 'paid' })).toEqual({ - sql: 'SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', - params: [1, 'paid'], - }); - expect(dialect.prepareQuery('SELECT * FROM ORDERS')).toEqual({ sql: 'SELECT * FROM ORDERS', params: undefined }); - }); - it('keeps unsupported statistics explicit', () => { expect(dialect.generateColumnStatisticsQuery('PUBLIC', 'ORDERS')).toBeNull(); }); diff --git a/packages/cli/src/connectors/snowflake/identifiers.test.ts b/packages/cli/test/connectors/snowflake/identifiers.test.ts similarity index 93% rename from packages/cli/src/connectors/snowflake/identifiers.test.ts rename to packages/cli/test/connectors/snowflake/identifiers.test.ts index d2c3e448..0a3b4cb8 100644 --- a/packages/cli/src/connectors/snowflake/identifiers.test.ts +++ b/packages/cli/test/connectors/snowflake/identifiers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { assertSafeSnowflakeIdentifier, quoteSnowflakeIdentifier } from './identifiers.js'; +import { assertSafeSnowflakeIdentifier, quoteSnowflakeIdentifier } from '../../../src/connectors/snowflake/identifiers.js'; describe('Snowflake identifier guards', () => { it('quotes simple Snowflake identifiers', () => { diff --git a/packages/cli/src/connectors/snowflake/sdk-logger.test.ts b/packages/cli/test/connectors/snowflake/sdk-logger.test.ts similarity index 96% rename from packages/cli/src/connectors/snowflake/sdk-logger.test.ts rename to packages/cli/test/connectors/snowflake/sdk-logger.test.ts index 73bf0c76..1217b142 100644 --- a/packages/cli/src/connectors/snowflake/sdk-logger.test.ts +++ b/packages/cli/test/connectors/snowflake/sdk-logger.test.ts @@ -11,7 +11,7 @@ vi.mock('snowflake-sdk', () => ({ import { configureSnowflakeSdkLogger, resetSnowflakeSdkLoggerConfigurationForTests, -} from './sdk-logger.js'; +} from '../../../src/connectors/snowflake/sdk-logger.js'; describe('configureSnowflakeSdkLogger', () => { let projectDir: string; diff --git a/packages/cli/src/connectors/sqlite/connector.test.ts b/packages/cli/test/connectors/sqlite/connector.test.ts similarity index 91% rename from packages/cli/src/connectors/sqlite/connector.test.ts rename to packages/cli/test/connectors/sqlite/connector.test.ts index ecd283b7..27b00c57 100644 --- a/packages/cli/src/connectors/sqlite/connector.test.ts +++ b/packages/cli/test/connectors/sqlite/connector.test.ts @@ -4,9 +4,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createSqliteLiveDatabaseIntrospection } from '../../connectors/sqlite/live-database-introspection.js'; -import { isKtxSqliteConnectionConfig, KtxSqliteScanConnector, sqliteDatabasePathFromConfig } from '../../connectors/sqlite/connector.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { createSqliteLiveDatabaseIntrospection } from '../../../src/connectors/sqlite/live-database-introspection.js'; +import { isKtxSqliteConnectionConfig, KtxSqliteScanConnector, sqliteDatabasePathFromConfig } from '../../../src/connectors/sqlite/connector.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; describe('KtxSqliteScanConnector', () => { let tempDir: string; @@ -150,6 +150,20 @@ describe('KtxSqliteScanConnector', () => { ]); }); + it('lists schemaless tables and views for setup discovery', async () => { + const connector = new KtxSqliteScanConnector({ + connectionId: 'warehouse', + connection: { driver: 'sqlite', path: dbPath }, + }); + + await expect(connector.listSchemas()).resolves.toEqual([]); + await expect(connector.listTables(['ignored'])).resolves.toEqual([ + { catalog: null, schema: '', name: 'customers', kind: 'table' }, + { catalog: null, schema: '', name: 'orders', kind: 'table' }, + { catalog: null, schema: '', name: 'recent_orders', kind: 'view' }, + ]); + }); + it('runs samples, distinct values, statistics, and read-only SQL', async () => { const connector = new KtxSqliteScanConnector({ connectionId: 'warehouse', diff --git a/packages/cli/src/connectors/sqlite/dialect.test.ts b/packages/cli/test/connectors/sqlite/dialect.test.ts similarity index 95% rename from packages/cli/src/connectors/sqlite/dialect.test.ts rename to packages/cli/test/connectors/sqlite/dialect.test.ts index cefed21a..879d133c 100644 --- a/packages/cli/src/connectors/sqlite/dialect.test.ts +++ b/packages/cli/test/connectors/sqlite/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxSqliteDialect } from './dialect.js'; +import { KtxSqliteDialect } from '../../../src/connectors/sqlite/dialect.js'; describe('KtxSqliteDialect', () => { const dialect = new KtxSqliteDialect(); diff --git a/packages/cli/src/connectors/sqlserver/connector.test.ts b/packages/cli/test/connectors/sqlserver/connector.test.ts similarity index 89% rename from packages/cli/src/connectors/sqlserver/connector.test.ts rename to packages/cli/test/connectors/sqlserver/connector.test.ts index 4e84ff9a..b7318ab5 100644 --- a/packages/cli/src/connectors/sqlserver/connector.test.ts +++ b/packages/cli/test/connectors/sqlserver/connector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import { createSqlServerLiveDatabaseIntrospection } from '../../connectors/sqlserver/live-database-introspection.js'; -import { isKtxSqlServerConnectionConfig, KtxSqlServerScanConnector, sqlServerConnectionPoolConfigFromConfig, type KtxSqlServerConnectionConfig, type KtxSqlServerPoolFactory, type KtxSqlServerQueryResult } from '../../connectors/sqlserver/connector.js'; -import { tableRefSet } from '../../context/scan/table-ref.js'; +import { createSqlServerLiveDatabaseIntrospection } from '../../../src/connectors/sqlserver/live-database-introspection.js'; +import { isKtxSqlServerConnectionConfig, KtxSqlServerScanConnector, prepareSqlServerReadOnlyQuery, sqlServerConnectionPoolConfigFromConfig, type KtxSqlServerConnectionConfig, type KtxSqlServerPoolFactory, type KtxSqlServerQueryResult } from '../../../src/connectors/sqlserver/connector.js'; +import { tableRefSet } from '../../../src/context/scan/table-ref.js'; function recordset>( rows: T[], @@ -21,9 +21,9 @@ function fakePoolFactory(options: { primaryKeyError?: Error; foreignKeyError?: E if (sql.includes('INFORMATION_SCHEMA.TABLES')) { return result( [ - { table_name: 'customers', table_type: 'BASE TABLE' }, - { table_name: 'orders', table_type: 'BASE TABLE' }, - { table_name: 'order_summary', table_type: 'VIEW' }, + { schema_name: 'dbo', table_name: 'customers', table_type: 'BASE TABLE' }, + { schema_name: 'dbo', table_name: 'orders', table_type: 'BASE TABLE' }, + { schema_name: 'dbo', table_name: 'order_summary', table_type: 'VIEW' }, ], ['table_name', 'table_type'], ); @@ -100,13 +100,13 @@ function fakePoolFactory(options: { primaryKeyError?: Error; foreignKeyError?: E ['table_name', 'row_count'], ); } - if (sql.includes('SELECT TOP 1 [id], [status] FROM [dbo].[orders]')) { + if (sql.includes('SELECT TOP 1 [id], [status] FROM [analytics].[dbo].[orders]')) { return result([{ id: 10, status: 'paid' }], ['id', 'status']); } if (sql.includes('SELECT TOP 1 * FROM (select id, status from dbo.orders) AS ktx_query_result')) { return result([{ id: 10, status: 'paid' }], ['id', 'status']); } - if (sql.includes('SELECT TOP 5 [status] FROM [dbo].[orders]')) { + if (sql.includes('SELECT TOP 5 [status] FROM [analytics].[dbo].[orders]')) { return result([{ status: 'paid' }, { status: 'open' }], ['status']); } if (sql.includes('COUNT(DISTINCT val)')) { @@ -118,6 +118,16 @@ function fakePoolFactory(options: { primaryKeyError?: Error; foreignKeyError?: E if (sql.includes('SUM(p.rows) AS row_count') && sql.includes('t.name = @tableName')) { return result([{ row_count: 2 }], ['row_count']); } + if (sql.includes('FROM sys.objects o')) { + return result( + [ + { schema_name: 'dbo', table_name: 'customers', table_type: 'USER_TABLE' }, + { schema_name: 'dbo', table_name: 'order_summary', table_type: 'VIEW' }, + { schema_name: 'dbo', table_name: 'orders', table_type: 'USER_TABLE' }, + ], + ['schema_name', 'table_name', 'table_type'], + ); + } if (sql.includes('SELECT s.name AS schema_name')) { return result([{ schema_name: 'dbo' }, { schema_name: 'sales' }], ['schema_name']); } @@ -140,6 +150,19 @@ function fakePoolFactory(options: { primaryKeyError?: Error; foreignKeyError?: E } describe('KtxSqlServerScanConnector', () => { + it('prepares read-only SQL parameters with SQL Server named placeholders', () => { + expect( + prepareSqlServerReadOnlyQuery('select * from events where id = :id and name = :name', { + id: 10, + name: 'signup', + }), + ).toEqual({ + sql: 'select * from events where id = @id and name = @name', + params: { id: 10, name: 'signup' }, + }); + expect(prepareSqlServerReadOnlyQuery('select 1')).toEqual({ sql: 'select 1', params: undefined }); + }); + it('resolves SQL Server connection configuration safely', () => { expect( isKtxSqlServerConnectionConfig({ @@ -366,6 +389,11 @@ describe('KtxSqlServerScanConnector', () => { await expect(connector.getTableRowCount('orders')).resolves.toBe(2); await expect(connector.listSchemas()).resolves.toEqual(['dbo', 'sales']); + await expect(connector.listTables(['dbo'])).resolves.toEqual([ + { catalog: 'analytics', schema: 'dbo', name: 'customers', kind: 'table' }, + { catalog: 'analytics', schema: 'dbo', name: 'order_summary', kind: 'view' }, + { catalog: 'analytics', schema: 'dbo', name: 'orders', kind: 'table' }, + ]); await expect( connector.columnStats( { connectionId: 'warehouse', table: { catalog: 'analytics', db: 'dbo', name: 'orders' }, column: 'status' }, diff --git a/packages/cli/src/connectors/sqlserver/dialect.test.ts b/packages/cli/test/connectors/sqlserver/dialect.test.ts similarity index 64% rename from packages/cli/src/connectors/sqlserver/dialect.test.ts rename to packages/cli/test/connectors/sqlserver/dialect.test.ts index 5c855340..f019991c 100644 --- a/packages/cli/src/connectors/sqlserver/dialect.test.ts +++ b/packages/cli/test/connectors/sqlserver/dialect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { KtxSqlServerDialect } from './dialect.js'; +import { KtxSqlServerDialect } from '../../../src/connectors/sqlserver/dialect.js'; describe('KtxSqlServerDialect', () => { const dialect = new KtxSqlServerDialect(); @@ -7,7 +7,9 @@ describe('KtxSqlServerDialect', () => { it('quotes identifiers and formats schema-qualified table names', () => { expect(dialect.quoteIdentifier('events')).toBe('[events]'); expect(dialect.quoteIdentifier('odd]name')).toBe('[odd]]name]'); - expect(dialect.formatTableName({ catalog: 'warehouse', db: 'dbo', name: 'events' })).toBe('[dbo].[events]'); + expect(dialect.formatTableName({ catalog: 'warehouse', db: 'dbo', name: 'events' })).toBe( + '[warehouse].[dbo].[events]', + ); expect(dialect.formatTableName({ catalog: null, db: null, name: 'events' })).toBe('[events]'); }); @@ -20,7 +22,7 @@ describe('KtxSqlServerDialect', () => { expect(dialect.mapToDimensionType('')).toBe('string'); }); - it('builds sampling, distinct-value, pagination, and time SQL', () => { + it('builds sampling, distinct-value, and pagination SQL', () => { expect(dialect.generateSampleQuery('[dbo].[events]', 25, ['id', 'event_name'])).toBe( 'SELECT TOP 25 [id], [event_name] FROM [dbo].[events]', ); @@ -28,22 +30,8 @@ describe('KtxSqlServerDialect', () => { "SELECT TOP 10 [event_name] FROM [dbo].[events] WHERE [event_name] IS NOT NULL AND LTRIM(RTRIM(CAST([event_name] AS NVARCHAR(MAX)))) != ''", ); expect(dialect.generateDistinctValuesQuery('[dbo].[events]', '[event_name]', 5)).toContain('SELECT TOP 5 val'); - expect(dialect.getTopClause(10)).toBe('TOP 10'); - expect(dialect.getLimitOffsetClause(10, 20)).toBe('OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY'); - expect(dialect.getTimeTruncExpression('created_at', 'month')).toBe( - 'DATEFROMPARTS(YEAR(created_at), MONTH(created_at), 1)', - ); + expect(dialect.getTopClause(10)).toBe('TOP (10)'); + expect(dialect.getLimitOffsetClause(10, 20)).toBe(''); }); - it('prepares named parameters using SQL Server @ parameters', () => { - expect( - dialect.prepareQuery('select * from events where id = :id and name = :name', { - id: 10, - name: 'signup', - }), - ).toEqual({ - sql: 'select * from events where id = @id and name = @name', - params: { id: 10, name: 'signup' }, - }); - }); }); diff --git a/packages/cli/src/context-build-view.test.ts b/packages/cli/test/context-build-view.test.ts similarity index 99% rename from packages/cli/src/context-build-view.test.ts rename to packages/cli/test/context-build-view.test.ts index 089124f5..5936afa9 100644 --- a/packages/cli/src/context-build-view.test.ts +++ b/packages/cli/test/context-build-view.test.ts @@ -1,6 +1,6 @@ -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; import { describe, expect, it, vi } from 'vitest'; -import type { KtxPublicIngestProject, KtxPublicIngestTargetResult } from './public-ingest.js'; +import type { KtxPublicIngestProject, KtxPublicIngestTargetResult } from '../src/public-ingest.js'; import { type ContextBuildTargetState, extractProgressMessage, @@ -11,7 +11,7 @@ import { renderContextBuildView, runContextBuild, viewStateFromSourceProgress, -} from './context-build-view.js'; +} from '../src/context-build-view.js'; function makeIo(options: { isTTY?: boolean; columns?: number } = {}) { let stdout = ''; diff --git a/packages/cli/src/context/connections/bigquery-identifiers.test.ts b/packages/cli/test/context/connections/bigquery-identifiers.test.ts similarity index 92% rename from packages/cli/src/context/connections/bigquery-identifiers.test.ts rename to packages/cli/test/context/connections/bigquery-identifiers.test.ts index a1fd2e09..abc59164 100644 --- a/packages/cli/src/context/connections/bigquery-identifiers.test.ts +++ b/packages/cli/test/context/connections/bigquery-identifiers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeBigQueryProjectId, normalizeBigQueryRegion } from './bigquery-identifiers.js'; +import { normalizeBigQueryProjectId, normalizeBigQueryRegion } from '../../../src/context/connections/bigquery-identifiers.js'; describe('BigQuery identifier normalization', () => { it('normalizes project ids and regions for information schema paths', () => { diff --git a/packages/cli/test/context/connections/dialects.test.ts b/packages/cli/test/context/connections/dialects.test.ts new file mode 100644 index 00000000..0b72566e --- /dev/null +++ b/packages/cli/test/context/connections/dialects.test.ts @@ -0,0 +1,316 @@ +import { describe, expect, it } from 'vitest'; +import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; +import type { KtxConnectionDriver, KtxTableRef } from '../../../src/context/scan/types.js'; + +interface DialectFixture { + driver: KtxConnectionDriver; + table: KtxTableRef; + quoteInput: string; + quotedIdentifier: string; + formattedTable: string; + display: string; + invalidDisplay: string; + columnDisplayTablePartCount: 1 | 2 | 3; + limitClause: string; + topClause: string; + randomFilter: string; + tableSampleClause: string; + sampleQuery: string; + columnSampleContains: string; + nullCountExpression: string; + distinctCountExpression: string; + textLengthExpression: string; + castToText: string; + sampleValueAggregation: string; + cardinalityContains: string; + randomizedCardinalityContains: string; + distinctValuesContains: string; + statisticsContains: string | null; + dimensionInput: string; + dimensionType: 'time' | 'string' | 'number' | 'boolean'; + nativeTypeInput: string; + normalizedType: string; +} + +const innerSampleSql = 'SELECT status AS value FROM orders'; + +const fixtures: DialectFixture[] = [ + { + driver: 'postgres', + table: { catalog: null, db: 'public', name: 'orders' }, + quoteInput: 'order"items', + quotedIdentifier: '"order""items"', + formattedTable: '"public"."orders"', + display: 'public.orders', + invalidDisplay: 'orders', + columnDisplayTablePartCount: 2, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: 'RANDOM() < 0.25', + tableSampleClause: 'TABLESAMPLE SYSTEM (25)', + sampleQuery: 'SELECT "id", "status" FROM "public"."orders" LIMIT 5', + columnSampleContains: 'TRIM(CAST("status" AS TEXT)) != \'\'', + nullCountExpression: 'COUNT(*) FILTER (WHERE "status" IS NULL)', + distinctCountExpression: 'COUNT(DISTINCT "status")', + textLengthExpression: 'LENGTH(CAST("status" AS TEXT))', + castToText: 'CAST("status" AS TEXT)', + sampleValueAggregation: + '(SELECT STRING_AGG(CAST(value AS TEXT), CHR(31)) FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY RANDOM()', + distinctValuesContains: 'SELECT DISTINCT "status"::text AS val', + statisticsContains: 'FROM pg_stats s', + dimensionInput: 'timestamp with time zone', + dimensionType: 'time', + nativeTypeInput: 'numeric(12,2)', + normalizedType: 'numeric(12,2)', + }, + { + driver: 'mysql', + table: { catalog: null, db: 'analytics', name: 'orders' }, + quoteInput: 'order`items', + quotedIdentifier: '`order``items`', + formattedTable: '`analytics`.`orders`', + display: 'analytics.orders', + invalidDisplay: 'orders', + columnDisplayTablePartCount: 2, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: 'RAND() < 0.25', + tableSampleClause: '', + sampleQuery: 'SELECT `id`, `status` FROM `analytics`.`orders` LIMIT 5', + columnSampleContains: 'TRIM(CAST(`status` AS CHAR)) != \'\'', + nullCountExpression: 'SUM(CASE WHEN `status` IS NULL THEN 1 ELSE 0 END)', + distinctCountExpression: 'COUNT(DISTINCT `status`)', + textLengthExpression: 'CHAR_LENGTH(CAST(`status` AS CHAR))', + castToText: 'CAST(`status` AS CHAR)', + sampleValueAggregation: + '(SELECT GROUP_CONCAT(CAST(value AS CHAR) SEPARATOR CHAR(31)) FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY RAND()', + distinctValuesContains: 'SELECT DISTINCT CAST(`status` AS CHAR) AS val', + statisticsContains: null, + dimensionInput: 'tinyint(1)', + dimensionType: 'boolean', + nativeTypeInput: 'varchar(255)', + normalizedType: 'varchar(255)', + }, + { + driver: 'clickhouse', + table: { catalog: null, db: 'analytics', name: 'events' }, + quoteInput: 'order`items', + quotedIdentifier: '`order``items`', + formattedTable: '`analytics`.`events`', + display: 'analytics.events', + invalidDisplay: 'events', + columnDisplayTablePartCount: 2, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: 'rand() / 4294967295.0 < 0.25', + tableSampleClause: '', + sampleQuery: 'SELECT `id`, `status` FROM `analytics`.`events` LIMIT 5', + columnSampleContains: 'trim(toString(`status`)) != \'\'', + nullCountExpression: 'countIf(`status` IS NULL)', + distinctCountExpression: 'COUNT(DISTINCT `status`)', + textLengthExpression: 'length(toString(`status`))', + castToText: 'toString(`status`)', + sampleValueAggregation: + '(SELECT arrayStringConcat(groupArray(toString(value)), \'\\x1F\') FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY rand()', + distinctValuesContains: 'SELECT DISTINCT toString(`status`) AS val', + statisticsContains: null, + dimensionInput: 'Nullable(DateTime64(3))', + dimensionType: 'time', + nativeTypeInput: 'LowCardinality(String)', + normalizedType: 'LowCardinality(String)', + }, + { + driver: 'sqlite', + table: { catalog: null, db: null, name: 'orders' }, + quoteInput: 'order"items', + quotedIdentifier: '"order""items"', + formattedTable: '"orders"', + display: 'orders', + invalidDisplay: 'public.orders', + columnDisplayTablePartCount: 1, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: '(RANDOM() % 100) < 25', + tableSampleClause: '', + sampleQuery: 'SELECT "id", "status" FROM "orders" LIMIT 5', + columnSampleContains: 'TRIM(CAST("status" AS TEXT)) != \'\'', + nullCountExpression: 'SUM(CASE WHEN "status" IS NULL THEN 1 ELSE 0 END)', + distinctCountExpression: 'COUNT(DISTINCT "status")', + textLengthExpression: 'LENGTH(CAST("status" AS TEXT))', + castToText: 'CAST("status" AS TEXT)', + sampleValueAggregation: + '(SELECT GROUP_CONCAT(CAST(value AS TEXT), char(31)) FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY RANDOM()', + distinctValuesContains: 'SELECT DISTINCT CAST("status" AS TEXT) AS val', + statisticsContains: null, + dimensionInput: 'INTEGER', + dimensionType: 'number', + nativeTypeInput: 'VARCHAR(255)', + normalizedType: 'VARCHAR(255)', + }, + { + driver: 'snowflake', + table: { catalog: 'ANALYTICS', db: 'PUBLIC', name: 'ORDERS' }, + quoteInput: 'order"items', + quotedIdentifier: '"order""items"', + formattedTable: '"ANALYTICS"."PUBLIC"."ORDERS"', + display: 'ANALYTICS.PUBLIC.ORDERS', + invalidDisplay: 'PUBLIC.ORDERS', + columnDisplayTablePartCount: 3, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: 'UNIFORM(0::FLOAT, 1::FLOAT, RANDOM()) < 0.25', + tableSampleClause: 'SAMPLE (25)', + sampleQuery: 'SELECT "id", "status" FROM "ANALYTICS"."PUBLIC"."ORDERS" SAMPLE ROW (5 ROWS)', + columnSampleContains: 'TRIM(CAST("status" AS STRING)) != \'\'', + nullCountExpression: 'COUNT_IF("status" IS NULL)', + distinctCountExpression: 'APPROX_COUNT_DISTINCT("status")', + textLengthExpression: 'LENGTH(CAST("status" AS TEXT))', + castToText: 'CAST("status" AS VARCHAR)', + sampleValueAggregation: + '(SELECT LISTAGG(CAST(value AS VARCHAR), \'\\x1f\') FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'SAMPLE ROW (100 ROWS)', + distinctValuesContains: 'SELECT DISTINCT "status"::VARCHAR AS val', + statisticsContains: null, + dimensionInput: 'TIMESTAMP_NTZ', + dimensionType: 'time', + nativeTypeInput: 'NUMBER(38,0)', + normalizedType: 'NUMBER(38,0)', + }, + { + driver: 'bigquery', + table: { catalog: 'analytics-project', db: 'warehouse', name: 'orders' }, + quoteInput: 'order`items', + quotedIdentifier: '`order\\`items`', + formattedTable: '`analytics-project`.`warehouse`.`orders`', + display: 'analytics-project.warehouse.orders', + invalidDisplay: 'warehouse.orders', + columnDisplayTablePartCount: 3, + limitClause: 'LIMIT 25 OFFSET 5', + topClause: '', + randomFilter: 'RAND() < 0.25', + tableSampleClause: 'TABLESAMPLE SYSTEM (25 PERCENT)', + sampleQuery: 'SELECT `id`, `status` FROM `analytics-project`.`warehouse`.`orders` ORDER BY RAND() LIMIT 5', + columnSampleContains: 'TRIM(CAST(`status` AS STRING)) != \'\'', + nullCountExpression: 'COUNTIF(`status` IS NULL)', + distinctCountExpression: 'APPROX_COUNT_DISTINCT(`status`)', + textLengthExpression: 'LENGTH(CAST(`status` AS STRING))', + castToText: 'CAST(`status` AS STRING)', + sampleValueAggregation: + '(SELECT STRING_AGG(CAST(value AS STRING), \'\\u001F\') FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT APPROX_COUNT_DISTINCT(val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY RAND()', + distinctValuesContains: 'SELECT DISTINCT CAST(`status` AS STRING) AS val', + statisticsContains: null, + dimensionInput: 'INT64', + dimensionType: 'number', + nativeTypeInput: 'INT64', + normalizedType: 'BIGINT', + }, + { + driver: 'sqlserver', + table: { catalog: 'warehouse', db: 'dbo', name: 'events' }, + quoteInput: 'odd]name', + quotedIdentifier: '[odd]]name]', + formattedTable: '[warehouse].[dbo].[events]', + display: 'warehouse.dbo.events', + invalidDisplay: 'dbo.events', + columnDisplayTablePartCount: 3, + limitClause: '', + topClause: 'TOP (25)', + randomFilter: 'ABS(CHECKSUM(NEWID())) % 100 < 25', + tableSampleClause: 'TABLESAMPLE (25 PERCENT)', + sampleQuery: 'SELECT TOP 5 [id], [status] FROM [warehouse].[dbo].[events]', + columnSampleContains: 'LTRIM(RTRIM(CAST([status] AS NVARCHAR(MAX)))) != \'\'', + nullCountExpression: 'SUM(CASE WHEN [status] IS NULL THEN 1 ELSE 0 END)', + distinctCountExpression: 'COUNT(DISTINCT [status])', + textLengthExpression: 'LEN(CAST([status] AS NVARCHAR(MAX)))', + castToText: 'CAST([status] AS NVARCHAR(MAX))', + sampleValueAggregation: + '(SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), CHAR(31)) FROM (SELECT status AS value FROM orders) AS relationship_profile_values)', + cardinalityContains: 'SELECT COUNT(DISTINCT val) AS cardinality', + randomizedCardinalityContains: 'ORDER BY NEWID()', + distinctValuesContains: 'SELECT TOP 20 val', + statisticsContains: null, + dimensionInput: 'datetime2', + dimensionType: 'time', + nativeTypeInput: 'uniqueidentifier', + normalizedType: 'uniqueidentifier', + }, +]; + +describe('getDialectForDriver', () => { + it.each(fixtures)('returns a full KtxDialect for $driver', (fixture) => { + const dialect = getDialectForDriver(fixture.driver); + const column = dialect.quoteIdentifier('status'); + + expect(dialect.type).toBe(fixture.driver); + expect(dialect.quoteIdentifier(fixture.quoteInput)).toBe(fixture.quotedIdentifier); + expect(dialect.formatTableName(fixture.table)).toBe(fixture.formattedTable); + expect(dialect.formatDisplayRef(fixture.table)).toBe(fixture.display); + expect(dialect.parseDisplayRef(fixture.display)).toEqual(fixture.table); + expect(dialect.parseDisplayRef(fixture.invalidDisplay)).toBeNull(); + expect(dialect.columnDisplayTablePartCount()).toBe(fixture.columnDisplayTablePartCount); + expect(dialect.getLimitOffsetClause(25, 5)).toBe(fixture.limitClause); + expect(dialect.getTopClause(25)).toBe(fixture.topClause); + expect(dialect.getRandomSampleFilter(0.25)).toBe(fixture.randomFilter); + expect(dialect.getTableSampleClause(0.25)).toBe(fixture.tableSampleClause); + expect(dialect.generateSampleQuery(fixture.formattedTable, 5, ['id', 'status'])).toBe(fixture.sampleQuery); + expect(dialect.generateColumnSampleQuery(fixture.formattedTable, 'status', 10)).toContain( + fixture.columnSampleContains, + ); + expect(dialect.getNullCountExpression(column)).toBe(fixture.nullCountExpression); + expect(dialect.getDistinctCountExpression(column)).toBe(fixture.distinctCountExpression); + expect(dialect.textLengthExpression(column)).toBe(fixture.textLengthExpression); + expect(dialect.castToText(column)).toBe(fixture.castToText); + expect(dialect.getSampleValueAggregation(innerSampleSql)).toBe(fixture.sampleValueAggregation); + expect(dialect.generateCardinalitySampleQuery(fixture.formattedTable, column, 100)).toContain( + fixture.cardinalityContains, + ); + expect(dialect.generateRandomizedCardinalitySampleQuery(fixture.formattedTable, column, 100)).toContain( + fixture.randomizedCardinalityContains, + ); + expect(dialect.generateDistinctValuesQuery(fixture.formattedTable, column, 20)).toContain( + fixture.distinctValuesContains, + ); + const statistics = dialect.generateColumnStatisticsQuery(fixture.table.db ?? '', fixture.table.name); + if (fixture.statisticsContains) { + expect(statistics).toContain(fixture.statisticsContains); + } else { + expect(statistics).toBeNull(); + } + expect(dialect.mapToDimensionType(fixture.dimensionInput)).toBe(fixture.dimensionType); + expect(dialect.mapDataType(fixture.nativeTypeInput)).toBe(fixture.normalizedType); + }); + + it('accepts three-part ANSI display refs while keeping one-part names caller-owned', () => { + for (const driver of ['postgres', 'mysql', 'clickhouse'] as const) { + const dialect = getDialectForDriver(driver); + expect(dialect.parseDisplayRef('warehouse.public.orders')).toEqual({ + catalog: 'warehouse', + db: 'public', + name: 'orders', + }); + expect(dialect.parseDisplayRef('orders')).toBeNull(); + } + }); + + it('throws with a supported-driver list for unknown drivers', () => { + expect(() => getDialectForDriver('oracle')).toThrow( + 'Unsupported warehouse driver "oracle". Supported drivers: bigquery, clickhouse, mysql, postgres, sqlite, snowflake, sqlserver', + ); + }); + + it('rejects legacy driver aliases', () => { + expect(() => getDialectForDriver('postgresql')).toThrow('Unsupported warehouse driver "postgresql"'); + expect(() => getDialectForDriver('sqlite3')).toThrow('Unsupported warehouse driver "sqlite3"'); + }); +}); diff --git a/packages/cli/test/context/connections/drivers.test.ts b/packages/cli/test/context/connections/drivers.test.ts new file mode 100644 index 00000000..380b2265 --- /dev/null +++ b/packages/cli/test/context/connections/drivers.test.ts @@ -0,0 +1,145 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + driverRegistrations, + getDriverRegistration, + listSupportedDrivers, +} from '../../../src/context/connections/drivers.js'; +import type { + KtxDriverConnectorModule, + KtxScopeConfigKey, +} from '../../../src/context/connections/drivers.js'; +import type { KtxConnectionDriver } from '../../../src/context/scan/types.js'; + +type FixtureFactory = (projectDir: string) => Record; + +const connectionFixtures: Record = { + postgres: () => ({ + driver: 'postgres', + url: 'postgresql://reader:secret@localhost:5432/analytics', // pragma: allowlist secret + schemas: ['public'], + }), + sqlite: () => ({ driver: 'sqlite', path: 'warehouse.db' }), + mysql: () => ({ + driver: 'mysql', + host: 'localhost', + database: 'analytics', + username: 'reader', + password: 'secret', // pragma: allowlist secret + schemas: ['analytics'], + }), + clickhouse: () => ({ + driver: 'clickhouse', + url: 'http://localhost:8123', + database: 'analytics', + username: 'reader', + password: 'secret', // pragma: allowlist secret + }), + sqlserver: () => ({ + driver: 'sqlserver', + host: 'localhost', + database: 'analytics', + username: 'reader', + password: 'secret', // pragma: allowlist secret + schemas: ['dbo'], + }), + bigquery: () => ({ + driver: 'bigquery', + dataset_id: 'analytics', + credentials_json: JSON.stringify({ + project_id: 'project-1', + client_email: 'reader@example.test', + private_key: '-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----\n', // pragma: allowlist secret + }), + location: 'US', + }), + snowflake: () => ({ + driver: 'snowflake', + account: 'example-account', + username: 'reader', + password: 'secret', // pragma: allowlist secret + warehouse: 'COMPUTE_WH', + database: 'ANALYTICS', + schema: 'PUBLIC', + }), +}; + +const allowedScopeKeys = new Set(['dataset_ids', 'databases', 'schemas', 'schema_names']); +const historicSqlReaderDrivers = new Set(['postgres', 'bigquery', 'snowflake']); +const localExecutorDrivers = new Set(['postgres', 'sqlite']); + +function assertExportedRegistryBoundaryTypes(input: { + scopeConfigKey: KtxScopeConfigKey; + connectorModule: KtxDriverConnectorModule; +}): { + scopeConfigKey: KtxScopeConfigKey; + connectorModule: KtxDriverConnectorModule; +} { + return input; +} + +describe('driverRegistrations', () => { + let projectDir: string; + + beforeEach(async () => { + projectDir = await mkdtemp(join(tmpdir(), 'ktx-driver-registry-')); + }); + + afterEach(async () => { + await rm(projectDir, { recursive: true, force: true }); + }); + + it('lists every supported warehouse driver', () => { + const registryDrivers = Object.keys(driverRegistrations).sort(); + expect(listSupportedDrivers()).toEqual(registryDrivers); + expect(listSupportedDrivers()).toEqual([ + 'bigquery', + 'clickhouse', + 'mysql', + 'postgres', + 'snowflake', + 'sqlite', + 'sqlserver', + ]); + }); + + it('resolves registered drivers case-insensitively', () => { + expect(getDriverRegistration(' Postgres ')?.driver).toBe('postgres'); + expect(getDriverRegistration('unknown')).toBeUndefined(); + }); + + it.each(Object.values(driverRegistrations))('adapts $driver connector exports', async (registration) => { + const connectorModule = await registration.load(); + const connection = connectionFixtures[registration.driver](projectDir); + const exportedBoundary = assertExportedRegistryBoundaryTypes({ + scopeConfigKey: registration.scopeConfigKey ?? 'schemas', + connectorModule, + }); + expect(exportedBoundary.connectorModule.createScanConnector).toEqual(expect.any(Function)); + + expect(connectorModule.isConnectionConfig(connection)).toBe(true); + expect(connectorModule.isConnectionConfig({})).toBe(false); + + const connector = connectorModule.createScanConnector({ + connectionId: 'warehouse', + connection, + projectDir, + }); + + expect(connector.driver).toBe(registration.driver); + expect(connector.listSchemas).toEqual(expect.any(Function)); + expect(connector.listTables).toEqual(expect.any(Function)); + await connector.cleanup?.(); + + if (registration.driver === 'sqlite') { + expect(registration.scopeConfigKey).toBeNull(); + } else { + expect(registration.scopeConfigKey).not.toBeNull(); + expect(allowedScopeKeys.has(registration.scopeConfigKey ?? '')).toBe(true); + } + expect(registration.hasHistoricSqlReader).toBe(historicSqlReaderDrivers.has(registration.driver)); + expect(registration.hasLocalQueryExecutor).toBe(localExecutorDrivers.has(registration.driver)); + }); +}); diff --git a/packages/cli/src/context/connections/local-query-executor.test.ts b/packages/cli/test/context/connections/local-query-executor.test.ts similarity index 93% rename from packages/cli/src/context/connections/local-query-executor.test.ts rename to packages/cli/test/context/connections/local-query-executor.test.ts index d2f77975..ca700b04 100644 --- a/packages/cli/src/context/connections/local-query-executor.test.ts +++ b/packages/cli/test/context/connections/local-query-executor.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createDefaultLocalQueryExecutor } from './local-query-executor.js'; +import { createDefaultLocalQueryExecutor } from '../../../src/context/connections/local-query-executor.js'; describe('createDefaultLocalQueryExecutor', () => { it('dispatches postgres and sqlite drivers to their executors', async () => { diff --git a/packages/cli/src/context/connections/local-warehouse-descriptor.test.ts b/packages/cli/test/context/connections/local-warehouse-descriptor.test.ts similarity index 97% rename from packages/cli/src/context/connections/local-warehouse-descriptor.test.ts rename to packages/cli/test/context/connections/local-warehouse-descriptor.test.ts index 7e62f1dc..e0a285a9 100644 --- a/packages/cli/src/context/connections/local-warehouse-descriptor.test.ts +++ b/packages/cli/test/context/connections/local-warehouse-descriptor.test.ts @@ -3,7 +3,7 @@ import { localConnectionInfoFromConfig, localConnectionToWarehouseDescriptor, localConnectionTypeForConfig, -} from './local-warehouse-descriptor.js'; +} from '../../../src/context/connections/local-warehouse-descriptor.js'; describe('localConnectionToWarehouseDescriptor', () => { it('maps local Postgres URLs to canonical warehouse descriptors', () => { diff --git a/packages/cli/src/context/connections/notion-config.test.ts b/packages/cli/test/context/connections/notion-config.test.ts similarity index 98% rename from packages/cli/src/context/connections/notion-config.test.ts rename to packages/cli/test/context/connections/notion-config.test.ts index 6416bf99..4b1cde96 100644 --- a/packages/cli/src/context/connections/notion-config.test.ts +++ b/packages/cli/test/context/connections/notion-config.test.ts @@ -7,7 +7,7 @@ import { parseNotionConnectionConfig, redactNotionConnectionConfig, resolveNotionAuthToken, -} from './notion-config.js'; +} from '../../../src/context/connections/notion-config.js'; describe('standalone Notion connection config', () => { let tempDir: string; diff --git a/packages/cli/src/context/connections/postgres-query-executor.test.ts b/packages/cli/test/context/connections/postgres-query-executor.test.ts similarity index 96% rename from packages/cli/src/context/connections/postgres-query-executor.test.ts rename to packages/cli/test/context/connections/postgres-query-executor.test.ts index 6bb522cf..fe4ab15c 100644 --- a/packages/cli/src/context/connections/postgres-query-executor.test.ts +++ b/packages/cli/test/context/connections/postgres-query-executor.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createPostgresQueryExecutor } from './postgres-query-executor.js'; +import { createPostgresQueryExecutor } from '../../../src/context/connections/postgres-query-executor.js'; function makeClient() { const calls: unknown[] = []; diff --git a/packages/cli/src/context/connections/read-only-sql.test.ts b/packages/cli/test/context/connections/read-only-sql.test.ts similarity index 91% rename from packages/cli/src/context/connections/read-only-sql.test.ts rename to packages/cli/test/context/connections/read-only-sql.test.ts index 217bf23d..90affa04 100644 --- a/packages/cli/src/context/connections/read-only-sql.test.ts +++ b/packages/cli/test/context/connections/read-only-sql.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { assertReadOnlySql, limitSqlForExecution } from './read-only-sql.js'; +import { assertReadOnlySql, limitSqlForExecution } from '../../../src/context/connections/read-only-sql.js'; describe('assertReadOnlySql', () => { it('allows select and with queries', () => { diff --git a/packages/cli/src/context/connections/sqlite-query-executor.test.ts b/packages/cli/test/context/connections/sqlite-query-executor.test.ts similarity index 98% rename from packages/cli/src/context/connections/sqlite-query-executor.test.ts rename to packages/cli/test/context/connections/sqlite-query-executor.test.ts index facb5139..a9e61ba5 100644 --- a/packages/cli/src/context/connections/sqlite-query-executor.test.ts +++ b/packages/cli/test/context/connections/sqlite-query-executor.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import Database from 'better-sqlite3'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createSqliteQueryExecutor, sqliteDatabasePathFromConnection } from './sqlite-query-executor.js'; +import { createSqliteQueryExecutor, sqliteDatabasePathFromConnection } from '../../../src/context/connections/sqlite-query-executor.js'; describe('createSqliteQueryExecutor', () => { let tempDir: string; diff --git a/packages/cli/src/context/core/config-reference.test.ts b/packages/cli/test/context/core/config-reference.test.ts similarity index 96% rename from packages/cli/src/context/core/config-reference.test.ts rename to packages/cli/test/context/core/config-reference.test.ts index f12d0bd9..c4b7c848 100644 --- a/packages/cli/src/context/core/config-reference.test.ts +++ b/packages/cli/test/context/core/config-reference.test.ts @@ -2,7 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { resolveKtxConfigReference, resolveKtxHomePath } from './config-reference.js'; +import { resolveKtxConfigReference, resolveKtxHomePath } from '../../../src/context/core/config-reference.js'; describe('KTX config references', () => { it('resolves env references without returning empty values', () => { diff --git a/packages/cli/src/context/core/git.service.assert-worktree-clean.test.ts b/packages/cli/test/context/core/git.service.assert-worktree-clean.test.ts similarity index 92% rename from packages/cli/src/context/core/git.service.assert-worktree-clean.test.ts rename to packages/cli/test/context/core/git.service.assert-worktree-clean.test.ts index db7d7bd3..18ee0b74 100644 --- a/packages/cli/src/context/core/git.service.assert-worktree-clean.test.ts +++ b/packages/cli/test/context/core/git.service.assert-worktree-clean.test.ts @@ -3,9 +3,9 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { SimpleGit } from 'simple-git'; -import type { KtxCoreConfig } from './config.js'; -import { createSimpleGit } from './git-env.js'; -import { GitService } from './git.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { createSimpleGit } from '../../../src/context/core/git-env.js'; +import { GitService } from '../../../src/context/core/git.service.js'; describe('GitService.assertWorktreeClean', () => { let workdir: string; diff --git a/packages/cli/src/context/core/git.service.delete-directories.test.ts b/packages/cli/test/context/core/git.service.delete-directories.test.ts similarity index 92% rename from packages/cli/src/context/core/git.service.delete-directories.test.ts rename to packages/cli/test/context/core/git.service.delete-directories.test.ts index b6156349..1ccd6b95 100644 --- a/packages/cli/src/context/core/git.service.delete-directories.test.ts +++ b/packages/cli/test/context/core/git.service.delete-directories.test.ts @@ -3,9 +3,9 @@ import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { SimpleGit } from 'simple-git'; -import type { KtxCoreConfig } from './config.js'; -import { createSimpleGit } from './git-env.js'; -import { GitService } from './git.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { createSimpleGit } from '../../../src/context/core/git-env.js'; +import { GitService } from '../../../src/context/core/git.service.js'; describe('GitService.deleteDirectories', () => { let workdir: string; diff --git a/packages/cli/src/context/core/git.service.patch.test.ts b/packages/cli/test/context/core/git.service.patch.test.ts similarity index 96% rename from packages/cli/src/context/core/git.service.patch.test.ts rename to packages/cli/test/context/core/git.service.patch.test.ts index de1ccb9f..160c64aa 100644 --- a/packages/cli/src/context/core/git.service.patch.test.ts +++ b/packages/cli/test/context/core/git.service.patch.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { GitService } from './git.service.js'; +import { GitService } from '../../../src/context/core/git.service.js'; async function makeGit() { const homeDir = await mkdtemp(join(tmpdir(), 'ktx-git-patch-')); diff --git a/packages/cli/src/context/core/git.service.reset-hard.test.ts b/packages/cli/test/context/core/git.service.reset-hard.test.ts similarity index 90% rename from packages/cli/src/context/core/git.service.reset-hard.test.ts rename to packages/cli/test/context/core/git.service.reset-hard.test.ts index e688b8b3..e68a3dbe 100644 --- a/packages/cli/src/context/core/git.service.reset-hard.test.ts +++ b/packages/cli/test/context/core/git.service.reset-hard.test.ts @@ -3,9 +3,9 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { SimpleGit } from 'simple-git'; -import type { KtxCoreConfig } from './config.js'; -import { createSimpleGit } from './git-env.js'; -import { GitService } from './git.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { createSimpleGit } from '../../../src/context/core/git-env.js'; +import { GitService } from '../../../src/context/core/git.service.js'; describe('GitService.resetHardTo', () => { let workdir: string; diff --git a/packages/cli/src/context/core/git.service.test.ts b/packages/cli/test/context/core/git.service.test.ts similarity index 99% rename from packages/cli/src/context/core/git.service.test.ts rename to packages/cli/test/context/core/git.service.test.ts index e8a5aa73..8b19ed09 100644 --- a/packages/cli/src/context/core/git.service.test.ts +++ b/packages/cli/test/context/core/git.service.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KtxCoreConfig } from './config.js'; -import { GitService } from './git.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { GitService } from '../../../src/context/core/git.service.js'; // These tests drive a real git repo inside a temp directory — simple-git shells out to the // system `git` binary. They are fast enough to run as unit tests and catch real issues that diff --git a/packages/cli/src/context/core/session-worktree.service.test.ts b/packages/cli/test/context/core/session-worktree.service.test.ts similarity index 95% rename from packages/cli/src/context/core/session-worktree.service.test.ts rename to packages/cli/test/context/core/session-worktree.service.test.ts index 3cb66742..19f899e4 100644 --- a/packages/cli/src/context/core/session-worktree.service.test.ts +++ b/packages/cli/test/context/core/session-worktree.service.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, realpath, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { KtxCoreConfig } from './config.js'; -import { GitService } from './git.service.js'; -import { SessionWorktreeService, type WorktreeConfigPort } from './session-worktree.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { GitService } from '../../../src/context/core/git.service.js'; +import { SessionWorktreeService, type WorktreeConfigPort } from '../../../src/context/core/session-worktree.service.js'; interface TestWorktreeConfig extends WorktreeConfigPort { workdir?: string; diff --git a/packages/cli/src/context/daemon/semantic-layer-compute.test.ts b/packages/cli/test/context/daemon/semantic-layer-compute.test.ts similarity index 99% rename from packages/cli/src/context/daemon/semantic-layer-compute.test.ts rename to packages/cli/test/context/daemon/semantic-layer-compute.test.ts index dac37ef4..e6bbddbc 100644 --- a/packages/cli/src/context/daemon/semantic-layer-compute.test.ts +++ b/packages/cli/test/context/daemon/semantic-layer-compute.test.ts @@ -1,7 +1,7 @@ import { once } from 'node:events'; import { createServer } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; -import { createHttpSemanticLayerComputePort, createPythonSemanticLayerComputePort } from './semantic-layer-compute.js'; +import { createHttpSemanticLayerComputePort, createPythonSemanticLayerComputePort } from '../../../src/context/daemon/semantic-layer-compute.js'; const source = { name: 'orders', diff --git a/packages/cli/src/context/index-sync/reindex.test.ts b/packages/cli/test/context/index-sync/reindex.test.ts similarity index 95% rename from packages/cli/src/context/index-sync/reindex.test.ts rename to packages/cli/test/context/index-sync/reindex.test.ts index 90c6a178..fe7698ba 100644 --- a/packages/cli/src/context/index-sync/reindex.test.ts +++ b/packages/cli/test/context/index-sync/reindex.test.ts @@ -2,10 +2,10 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KtxEmbeddingPort } from '../../context/core/embedding.js'; -import { initKtxProject, loadKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { SqliteKnowledgeIndex } from '../wiki/sqlite-knowledge-index.js'; -import { reindexLocalIndexes } from './reindex.js'; +import type { KtxEmbeddingPort } from '../../../src/context/core/embedding.js'; +import { initKtxProject, loadKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { SqliteKnowledgeIndex } from '../../../src/context/wiki/sqlite-knowledge-index.js'; +import { reindexLocalIndexes } from '../../../src/context/index-sync/reindex.js'; class FakeEmbeddingPort implements KtxEmbeddingPort { readonly maxBatchSize = 8; diff --git a/packages/cli/src/context/ingest/action-identity.test.ts b/packages/cli/test/context/ingest/action-identity.test.ts similarity index 96% rename from packages/cli/src/context/ingest/action-identity.test.ts rename to packages/cli/test/context/ingest/action-identity.test.ts index 725a1d99..e4baaa70 100644 --- a/packages/cli/src/context/ingest/action-identity.test.ts +++ b/packages/cli/test/context/ingest/action-identity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { actionTargetConnectionId, memoryActionIdentity } from './action-identity.js'; +import { actionTargetConnectionId, memoryActionIdentity } from '../../../src/context/ingest/action-identity.js'; describe('memory action target identity', () => { it('keys SL actions by target connection and wiki actions by run connection', () => { diff --git a/packages/cli/src/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts b/packages/cli/test/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts rename to packages/cli/test/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts index f29cab06..8de52354 100644 --- a/packages/cli/src/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts +++ b/packages/cli/test/context/ingest/adapters/dbt-descriptions/parse-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseDbtSchemaFile, parseDbtSchemaFiles } from './parse-schema.js'; +import { parseDbtSchemaFile, parseDbtSchemaFiles } from '../../../../../src/context/ingest/adapters/dbt-descriptions/parse-schema.js'; describe('dbt descriptions schema parser', () => { it('resolves shared dbt vars and defaults before parsing schema YAML', () => { diff --git a/packages/cli/src/context/ingest/adapters/dbt/chunk.test.ts b/packages/cli/test/context/ingest/adapters/dbt/chunk.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/dbt/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/dbt/chunk.test.ts index 6eece2ac..ffda2887 100644 --- a/packages/cli/src/context/ingest/adapters/dbt/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/dbt/chunk.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { chunkDbtProject } from './chunk.js'; +import { chunkDbtProject } from '../../../../../src/context/ingest/adapters/dbt/chunk.js'; describe('chunkDbtProject', () => { const diffSet = (modified: string[]) => ({ added: [], modified, deleted: [], unchanged: [] }); diff --git a/packages/cli/src/context/ingest/adapters/dbt/dbt.adapter.test.ts b/packages/cli/test/context/ingest/adapters/dbt/dbt.adapter.test.ts similarity index 92% rename from packages/cli/src/context/ingest/adapters/dbt/dbt.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/dbt/dbt.adapter.test.ts index 2851318e..692e1e7f 100644 --- a/packages/cli/src/context/ingest/adapters/dbt/dbt.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/dbt/dbt.adapter.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { SourceAdapter } from '../../types.js'; -import { DbtSourceAdapter } from './dbt.adapter.js'; +import type { SourceAdapter } from '../../../../../src/context/ingest/types.js'; +import { DbtSourceAdapter } from '../../../../../src/context/ingest/adapters/dbt/dbt.adapter.js'; describe('DbtSourceAdapter', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/dbt/fetch.test.ts b/packages/cli/test/context/ingest/adapters/dbt/fetch.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/dbt/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/dbt/fetch.test.ts index eebff7c6..98ed1804 100644 --- a/packages/cli/src/context/ingest/adapters/dbt/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/dbt/fetch.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { fetchDbtRepo } from './fetch.js'; +import { fetchDbtRepo } from '../../../../../src/context/ingest/adapters/dbt/fetch.js'; describe('fetchDbtRepo', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/adapters/dbt/parse.test.ts b/packages/cli/test/context/ingest/adapters/dbt/parse.test.ts similarity index 73% rename from packages/cli/src/context/ingest/adapters/dbt/parse.test.ts rename to packages/cli/test/context/ingest/adapters/dbt/parse.test.ts index f373fd5b..33ff8b0e 100644 --- a/packages/cli/src/context/ingest/adapters/dbt/parse.test.ts +++ b/packages/cli/test/context/ingest/adapters/dbt/parse.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeDbtPath } from './parse.js'; +import { normalizeDbtPath } from '../../../../../src/context/ingest/adapters/dbt/parse.js'; describe('normalizeDbtPath', () => { it('normalizes Windows separators to POSIX separators', () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts index b9ee73b3..74393e4b 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/bigquery-query-history-reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { BigQueryHistoricSqlQueryHistoryReader } from './bigquery-query-history-reader.js'; -import { HistoricSqlGrantsMissingError } from './errors.js'; +import { BigQueryHistoricSqlQueryHistoryReader } from '../../../../../src/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js'; +import { HistoricSqlGrantsMissingError } from '../../../../../src/context/ingest/adapters/historic-sql/errors.js'; interface FakeQueryResult { headers: string[]; diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/buckets.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/buckets.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/historic-sql/buckets.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/buckets.test.ts index 78dc2859..253add1e 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/buckets.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/buckets.test.ts @@ -6,7 +6,7 @@ import { bucketFrequency, bucketP95Runtime, bucketRecency, -} from './buckets.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/buckets.js'; describe('historic-sql bucket helpers', () => { it('uses stable execution buckets', () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts index d8c0187f..a8e99c39 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/chunk-unified.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/chunk-unified.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { chunkHistoricSqlUnifiedStagedDir, describeHistoricSqlUnifiedScope } from './chunk-unified.js'; +import { chunkHistoricSqlUnifiedStagedDir, describeHistoricSqlUnifiedScope } from '../../../../../src/context/ingest/adapters/historic-sql/chunk-unified.js'; async function tempDir(): Promise { return mkdtemp(join(tmpdir(), 'historic-sql-unified-chunk-')); diff --git a/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts new file mode 100644 index 00000000..8dc2ec88 --- /dev/null +++ b/packages/cli/test/context/ingest/adapters/historic-sql/connection-dialect.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { queryHistoryDialectForConnection } from '../../../../../src/context/ingest/adapters/historic-sql/connection-dialect.js'; + +describe('queryHistoryDialectForConnection', () => { + it.each([ + ['postgres', 'postgres'], + ['bigquery', 'bigquery'], + ['snowflake', 'snowflake'], + ] as const)('returns %s when query history is enabled', (driver, dialect) => { + expect(queryHistoryDialectForConnection({ driver, context: { queryHistory: { enabled: true } } })).toBe(dialect); + }); + + it.each(['sqlite', 'mysql', 'clickhouse', 'sqlserver'] as const)( + 'returns null for %s because no historic-SQL reader is registered', + (driver) => { + expect(queryHistoryDialectForConnection({ driver, context: { queryHistory: { enabled: true } } })).toBeNull(); + }, + ); + + it('returns null when query history is disabled', () => { + expect(queryHistoryDialectForConnection({ driver: 'postgres', context: { queryHistory: { enabled: false } } })).toBeNull(); + }); +}); diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/detect.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/detect.test.ts similarity index 91% rename from packages/cli/src/context/ingest/adapters/historic-sql/detect.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/detect.test.ts index 9ad3cf39..4baef647 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/detect.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/detect.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { detectHistoricSqlStagedDir } from './detect.js'; -import { HISTORIC_SQL_SOURCE_KEY, stagedManifestSchema } from './types.js'; +import { detectHistoricSqlStagedDir } from '../../../../../src/context/ingest/adapters/historic-sql/detect.js'; +import { HISTORIC_SQL_SOURCE_KEY, stagedManifestSchema } from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; async function tempDir(): Promise { return mkdtemp(join(tmpdir(), 'historic-sql-detect-')); diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/evidence-tool.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/evidence-tool.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/historic-sql/evidence-tool.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/evidence-tool.test.ts index ae16d105..0185798b 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/evidence-tool.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/evidence-tool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; import { asSchema } from 'ai'; -import { createEmitHistoricSqlEvidenceTool } from './evidence-tool.js'; +import { createEmitHistoricSqlEvidenceTool } from '../../../../../src/context/ingest/adapters/historic-sql/evidence-tool.js'; describe('emit_historic_sql_evidence tool', () => { it('exposes an AI SDK v6 tool input schema with top-level object type', async () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/evidence.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/evidence.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/historic-sql/evidence.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/evidence.test.ts index 8858ed37..1f188a95 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/evidence.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/evidence.test.ts @@ -4,7 +4,7 @@ import { historicSqlEvidencePath, historicSqlPatternEvidenceSchema, historicSqlTableUsageEvidenceSchema, -} from './evidence.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/evidence.js'; describe('historic-sql evidence contracts', () => { it('validates table usage evidence emitted by table digest WorkUnits', () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts similarity index 89% rename from packages/cli/src/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts index 80df5c26..850be576 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/historic-sql.adapter.test.ts @@ -2,10 +2,10 @@ import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import type { SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js'; -import type { SourceAdapter } from '../../types.js'; -import { HistoricSqlSourceAdapter } from './historic-sql.adapter.js'; -import type { HistoricSqlReader } from './types.js'; +import type { SqlAnalysisPort } from '../../../../../src/context/sql-analysis/ports.js'; +import type { SourceAdapter } from '../../../../../src/context/ingest/types.js'; +import { HistoricSqlSourceAdapter } from '../../../../../src/context/ingest/adapters/historic-sql/historic-sql.adapter.js'; +import type { HistoricSqlReader } from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; async function tempDir(): Promise { return mkdtemp(join(tmpdir(), 'historic-sql-adapter-')); diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts similarity index 93% rename from packages/cli/src/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts index a443f995..1e626edd 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/local-ingest-acceptance.test.ts @@ -2,15 +2,15 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import YAML from 'yaml'; -import type { AgentRunnerPort, RunLoopParams } from '../../../../context/llm/runtime-port.js'; -import { initKtxProject, loadKtxProject, type KtxLocalProject } from '../../../../context/project/project.js'; -import type { SqlAnalysisBatchItem, SqlAnalysisBatchResult, SqlAnalysisDialect, SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js'; -import { searchLocalSlSources } from '../../../sl/local-sl.js'; -import { searchLocalKnowledgePages } from '../../../wiki/local-knowledge.js'; -import { runLocalIngest } from '../../local-ingest.js'; +import type { AgentRunnerPort, RunLoopParams } from '../../../../../src/context/llm/runtime-port.js'; +import { initKtxProject, loadKtxProject, type KtxLocalProject } from '../../../../../src/context/project/project.js'; +import type { SqlAnalysisBatchItem, SqlAnalysisBatchResult, SqlAnalysisDialect, SqlAnalysisPort } from '../../../../../src/context/sql-analysis/ports.js'; +import { searchLocalSlSources } from '../../../../../src/context/sl/local-sl.js'; +import { searchLocalKnowledgePages } from '../../../../../src/context/wiki/local-knowledge.js'; +import { runLocalIngest } from '../../../../../src/context/ingest/local-ingest.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HistoricSqlSourceAdapter } from './historic-sql.adapter.js'; -import type { AggregatedTemplate, HistoricSqlReader, HistoricSqlUnifiedPullConfig } from './types.js'; +import { HistoricSqlSourceAdapter } from '../../../../../src/context/ingest/adapters/historic-sql/historic-sql.adapter.js'; +import type { AggregatedTemplate, HistoricSqlReader, HistoricSqlUnifiedPullConfig } from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; class AcceptanceHistoricSqlReader implements HistoricSqlReader { async probe() { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts index d37ed193..9fae5e08 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/pattern-inputs.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/pattern-inputs.test.ts @@ -4,8 +4,8 @@ import { isHistoricSqlPatternInputShardPath, serializedStagedPatternsInputByteLength, splitHistoricSqlPatternInputs, -} from './pattern-inputs.js'; -import type { StagedPatternsInput } from './types.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/pattern-inputs.js'; +import type { StagedPatternsInput } from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; type PatternTemplate = StagedPatternsInput['templates'][number]; diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts index a91171cd..41baf331 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/postgres-pgss-reader.test.ts @@ -3,8 +3,8 @@ import { HistoricSqlExtensionMissingError, HistoricSqlGrantsMissingError, HistoricSqlVersionUnsupportedError, -} from './errors.js'; -import { PostgresPgssReader } from './postgres-pgss-reader.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/errors.js'; +import { PostgresPgssReader } from '../../../../../src/context/ingest/adapters/historic-sql/postgres-pgss-reader.js'; interface FakeQueryResult { headers: string[]; diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/projection.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/projection.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/historic-sql/projection.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/projection.test.ts index 28ddf5f8..722d7156 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/projection.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/projection.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import YAML from 'yaml'; import { describe, expect, it } from 'vitest'; -import { projectHistoricSqlEvidence } from './projection.js'; +import { projectHistoricSqlEvidence } from '../../../../../src/context/ingest/adapters/historic-sql/projection.js'; async function tempWorkdir(): Promise { return mkdtemp(join(tmpdir(), 'historic-sql-projection-')); diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/redaction.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/redaction.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/historic-sql/redaction.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/redaction.test.ts index d27015a6..262bc4ae 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/redaction.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/redaction.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { compileHistoricSqlRedactionPatterns, redactHistoricSqlText } from './redaction.js'; +import { compileHistoricSqlRedactionPatterns, redactHistoricSqlText } from '../../../../../src/context/ingest/adapters/historic-sql/redaction.js'; describe('historic-SQL redaction', () => { it('redacts regex matches and supports the (?i) case-insensitive prefix', () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/skill-schemas.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/skill-schemas.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/historic-sql/skill-schemas.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/skill-schemas.test.ts index b384c0c0..2a6ac805 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/skill-schemas.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/skill-schemas.test.ts @@ -4,7 +4,7 @@ import { patternOutputSchema, patternsArraySchema, tableUsageOutputSchema, -} from './skill-schemas.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/skill-schemas.js'; describe('historic-sql skill schemas', () => { it('accepts table usage output and preserves future keys', () => { diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts index b33183d7..7307fcdd 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/snowflake-query-history-reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { HistoricSqlGrantsMissingError } from './errors.js'; -import { SnowflakeHistoricSqlQueryHistoryReader } from './snowflake-query-history-reader.js'; +import { HistoricSqlGrantsMissingError } from '../../../../../src/context/ingest/adapters/historic-sql/errors.js'; +import { SnowflakeHistoricSqlQueryHistoryReader } from '../../../../../src/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js'; interface FakeQueryResult { headers: string[]; diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts index d49c3a1d..b930d695 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/stage-unified.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/stage-unified.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, readFile, readdir } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import type { SqlAnalysisPort } from '../../../../context/sql-analysis/ports.js'; -import { stageHistoricSqlAggregatedSnapshot } from './stage-unified.js'; -import type { AggregatedTemplate, HistoricSqlReader } from './types.js'; +import type { SqlAnalysisPort } from '../../../../../src/context/sql-analysis/ports.js'; +import { stageHistoricSqlAggregatedSnapshot } from '../../../../../src/context/ingest/adapters/historic-sql/stage-unified.js'; +import type { AggregatedTemplate, HistoricSqlReader } from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; async function tempDir(): Promise { return mkdtemp(join(tmpdir(), 'historic-sql-unified-stage-')); diff --git a/packages/cli/src/context/ingest/adapters/historic-sql/types.test.ts b/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/historic-sql/types.test.ts rename to packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts index 95b253a8..d9417521 100644 --- a/packages/cli/src/context/ingest/adapters/historic-sql/types.test.ts +++ b/packages/cli/test/context/ingest/adapters/historic-sql/types.test.ts @@ -5,7 +5,7 @@ import { stagedManifestSchema, stagedPatternsInputSchema, stagedTableInputSchema, -} from './types.js'; +} from '../../../../../src/context/ingest/adapters/historic-sql/types.js'; describe('historic-sql unified contracts', () => { it('parses minExecutions and service-account filters', () => { diff --git a/packages/cli/src/context/ingest/adapters/live-database/chunk.test.ts b/packages/cli/test/context/ingest/adapters/live-database/chunk.test.ts similarity index 92% rename from packages/cli/src/context/ingest/adapters/live-database/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/live-database/chunk.test.ts index 2e38be9a..d3c5207c 100644 --- a/packages/cli/src/context/ingest/adapters/live-database/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/live-database/chunk.test.ts @@ -2,9 +2,9 @@ import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import type { KtxSchemaSnapshot } from '../../../scan/types.js'; -import { chunkLiveDatabaseStagedDir } from './chunk.js'; -import { liveDatabaseTablePath, writeLiveDatabaseSnapshot } from './stage.js'; +import type { KtxSchemaSnapshot } from '../../../../../src/context/scan/types.js'; +import { chunkLiveDatabaseStagedDir } from '../../../../../src/context/ingest/adapters/live-database/chunk.js'; +import { liveDatabaseTablePath, writeLiveDatabaseSnapshot } from '../../../../../src/context/ingest/adapters/live-database/stage.js'; function snapshot(): KtxSchemaSnapshot { return { diff --git a/packages/cli/src/context/ingest/adapters/live-database/daemon-introspection.test.ts b/packages/cli/test/context/ingest/adapters/live-database/daemon-introspection.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/live-database/daemon-introspection.test.ts rename to packages/cli/test/context/ingest/adapters/live-database/daemon-introspection.test.ts index ca62ec05..5cc6affb 100644 --- a/packages/cli/src/context/ingest/adapters/live-database/daemon-introspection.test.ts +++ b/packages/cli/test/context/ingest/adapters/live-database/daemon-introspection.test.ts @@ -1,8 +1,8 @@ import { once } from 'node:events'; import { createServer } from 'node:http'; import { describe, expect, it, vi } from 'vitest'; -import { tableRefSet } from '../../../scan/table-ref.js'; -import { createDaemonLiveDatabaseIntrospection } from './daemon-introspection.js'; +import { tableRefSet } from '../../../../../src/context/scan/table-ref.js'; +import { createDaemonLiveDatabaseIntrospection } from '../../../../../src/context/ingest/adapters/live-database/daemon-introspection.js'; const daemonResponse = { connection_id: 'warehouse', diff --git a/packages/cli/src/context/ingest/adapters/live-database/live-database.adapter.test.ts b/packages/cli/test/context/ingest/adapters/live-database/live-database.adapter.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/live-database/live-database.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/live-database/live-database.adapter.test.ts index 6cd543e1..72c31446 100644 --- a/packages/cli/src/context/ingest/adapters/live-database/live-database.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/live-database/live-database.adapter.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readdir, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { tableRefSet, type KtxTableRefKey } from '../../../scan/table-ref.js'; -import { LiveDatabaseSourceAdapter } from './live-database.adapter.js'; +import { tableRefSet, type KtxTableRefKey } from '../../../../../src/context/scan/table-ref.js'; +import { LiveDatabaseSourceAdapter } from '../../../../../src/context/ingest/adapters/live-database/live-database.adapter.js'; describe('LiveDatabaseSourceAdapter', () => { it('fetches a schema snapshot through the introspection port', async () => { diff --git a/packages/cli/src/context/ingest/adapters/live-database/manifest.test.ts b/packages/cli/test/context/ingest/adapters/live-database/manifest.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/live-database/manifest.test.ts rename to packages/cli/test/context/ingest/adapters/live-database/manifest.test.ts index a97140a9..d32868ec 100644 --- a/packages/cli/src/context/ingest/adapters/live-database/manifest.test.ts +++ b/packages/cli/test/context/ingest/adapters/live-database/manifest.test.ts @@ -4,7 +4,7 @@ import { type LiveDatabaseManifestExistingDescriptions, type LiveDatabaseManifestJoinEntry, type LiveDatabaseManifestShard, -} from './manifest.js'; +} from '../../../../../src/context/ingest/adapters/live-database/manifest.js'; function shardObject(shards: Map): Record { return Object.fromEntries([...shards.entries()].sort(([a], [b]) => a.localeCompare(b))); diff --git a/packages/cli/src/context/ingest/adapters/live-database/stage.test.ts b/packages/cli/test/context/ingest/adapters/live-database/stage.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/live-database/stage.test.ts rename to packages/cli/test/context/ingest/adapters/live-database/stage.test.ts index 8fb675a2..b2382775 100644 --- a/packages/cli/src/context/ingest/adapters/live-database/stage.test.ts +++ b/packages/cli/test/context/ingest/adapters/live-database/stage.test.ts @@ -10,8 +10,8 @@ import { liveDatabaseTablePath, readLiveDatabaseTableFiles, writeLiveDatabaseSnapshot, -} from './stage.js'; -import type { KtxSchemaSnapshot } from '../../../scan/types.js'; +} from '../../../../../src/context/ingest/adapters/live-database/stage.js'; +import type { KtxSchemaSnapshot } from '../../../../../src/context/scan/types.js'; function snapshot(): KtxSchemaSnapshot { return { diff --git a/packages/cli/src/context/ingest/adapters/looker/chunk.test.ts b/packages/cli/test/context/ingest/adapters/looker/chunk.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/looker/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/looker/chunk.test.ts index 9d41d37a..f55a734b 100644 --- a/packages/cli/src/context/ingest/adapters/looker/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/chunk.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { chunkLookerStagedDir } from './chunk.js'; -import { writeLookerEvidenceDocuments } from './evidence-documents.js'; +import { chunkLookerStagedDir } from '../../../../../src/context/ingest/adapters/looker/chunk.js'; +import { writeLookerEvidenceDocuments } from '../../../../../src/context/ingest/adapters/looker/evidence-documents.js'; async function writeJson(stagedDir: string, relPath: string, value: unknown): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/looker/client-boundary.test.ts b/packages/cli/test/context/ingest/adapters/looker/client-boundary.test.ts similarity index 77% rename from packages/cli/src/context/ingest/adapters/looker/client-boundary.test.ts rename to packages/cli/test/context/ingest/adapters/looker/client-boundary.test.ts index 9172e23f..48cd4e4b 100644 --- a/packages/cli/src/context/ingest/adapters/looker/client-boundary.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/client-boundary.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; describe('LookerClient boundary', () => { it('does not import server or NestJS modules', async () => { - const source = await readFile(new URL('./client.ts', import.meta.url), 'utf-8'); + const source = await readFile(new URL('../../../../../src/context/ingest/adapters/looker/client.ts', import.meta.url), 'utf-8'); expect(source).not.toMatch(/@nestjs\/common/); expect(source).not.toMatch(/DataSourceClient/); diff --git a/packages/cli/src/context/ingest/adapters/looker/client.test.ts b/packages/cli/test/context/ingest/adapters/looker/client.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/looker/client.test.ts rename to packages/cli/test/context/ingest/adapters/looker/client.test.ts index 3b1822e0..e9aacb11 100644 --- a/packages/cli/src/context/ingest/adapters/looker/client.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/client.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { LookerClient, type LookerSdkPort } from './client.js'; +import { LookerClient, type LookerSdkPort } from '../../../../../src/context/ingest/adapters/looker/client.js'; const clientSecretParam = 'client_secret'; // pragma: allowlist secret diff --git a/packages/cli/src/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts b/packages/cli/test/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts similarity index 90% rename from packages/cli/src/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts rename to packages/cli/test/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts index 0da13d53..b8ae5b73 100644 --- a/packages/cli/src/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/daemon-table-identifier-parser.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createDaemonLookerTableIdentifierParser } from './daemon-table-identifier-parser.js'; +import { createDaemonLookerTableIdentifierParser } from '../../../../../src/context/ingest/adapters/looker/daemon-table-identifier-parser.js'; describe('createDaemonLookerTableIdentifierParser', () => { it('posts parse items to the daemon endpoint', async () => { diff --git a/packages/cli/src/context/ingest/adapters/looker/detect.test.ts b/packages/cli/test/context/ingest/adapters/looker/detect.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/looker/detect.test.ts rename to packages/cli/test/context/ingest/adapters/looker/detect.test.ts index 1490bcfa..08e8472b 100644 --- a/packages/cli/src/context/ingest/adapters/looker/detect.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/detect.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { detectLookerStagedDir } from './detect.js'; +import { detectLookerStagedDir } from '../../../../../src/context/ingest/adapters/looker/detect.js'; async function touch(stagedDir: string, relPath: string, body = '{}\n'): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/looker/evidence-documents.test.ts b/packages/cli/test/context/ingest/adapters/looker/evidence-documents.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/looker/evidence-documents.test.ts rename to packages/cli/test/context/ingest/adapters/looker/evidence-documents.test.ts index 6d4545ca..55da5fc9 100644 --- a/packages/cli/src/context/ingest/adapters/looker/evidence-documents.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/evidence-documents.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { getLookerTriageSignals, writeLookerEvidenceDocuments } from './evidence-documents.js'; +import { getLookerTriageSignals, writeLookerEvidenceDocuments } from '../../../../../src/context/ingest/adapters/looker/evidence-documents.js'; async function writeJson(root: string, relPath: string, value: unknown): Promise { const target = join(root, relPath); diff --git a/packages/cli/src/context/ingest/adapters/looker/factory.test.ts b/packages/cli/test/context/ingest/adapters/looker/factory.test.ts similarity index 86% rename from packages/cli/src/context/ingest/adapters/looker/factory.test.ts rename to packages/cli/test/context/ingest/adapters/looker/factory.test.ts index d68be942..ed5c5633 100644 --- a/packages/cli/src/context/ingest/adapters/looker/factory.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/factory.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it, vi } from 'vitest'; -import type { FetchContext } from '../../types.js'; -import type { LookerSdkPort } from './client.js'; +import type { FetchContext } from '../../../../../src/context/ingest/types.js'; +import type { LookerSdkPort } from '../../../../../src/context/ingest/adapters/looker/client.js'; import { DefaultLookerClientFactory, DefaultLookerConnectionClientFactory, type LookerCredentialResolver, -} from './factory.js'; -import type { LookerRuntimeClient } from './fetch.js'; -import type { LookerPullConfig } from './types.js'; +} from '../../../../../src/context/ingest/adapters/looker/factory.js'; +import type { LookerRuntimeClient } from '../../../../../src/context/ingest/adapters/looker/fetch.js'; +import type { LookerPullConfig } from '../../../../../src/context/ingest/adapters/looker/types.js'; function sdk(): LookerSdkPort { return { diff --git a/packages/cli/src/context/ingest/adapters/looker/fetch-report.test.ts b/packages/cli/test/context/ingest/adapters/looker/fetch-report.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/looker/fetch-report.test.ts rename to packages/cli/test/context/ingest/adapters/looker/fetch-report.test.ts index 157a6770..f9f5bcd3 100644 --- a/packages/cli/src/context/ingest/adapters/looker/fetch-report.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/fetch-report.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { readLookerFetchReport, writeLookerFetchReport } from './fetch-report.js'; +import { readLookerFetchReport, writeLookerFetchReport } from '../../../../../src/context/ingest/adapters/looker/fetch-report.js'; describe('Looker staged fetch report', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/looker/fetch.test.ts b/packages/cli/test/context/ingest/adapters/looker/fetch.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/looker/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/looker/fetch.test.ts index 2b18a3dd..95edb382 100644 --- a/packages/cli/src/context/ingest/adapters/looker/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/fetch.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { chunkLookerStagedDir } from './chunk.js'; -import { fetchLookerRuntimeBundle, type LookerRuntimeClient } from './fetch.js'; +import { chunkLookerStagedDir } from '../../../../../src/context/ingest/adapters/looker/chunk.js'; +import { fetchLookerRuntimeBundle, type LookerRuntimeClient } from '../../../../../src/context/ingest/adapters/looker/fetch.js'; const connectionId = '11111111-1111-4111-8111-111111111111'; diff --git a/packages/cli/src/context/ingest/adapters/looker/local-runtime-store.test.ts b/packages/cli/test/context/ingest/adapters/looker/local-runtime-store.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/looker/local-runtime-store.test.ts rename to packages/cli/test/context/ingest/adapters/looker/local-runtime-store.test.ts index 3f9bbdc5..ff7b1f5c 100644 --- a/packages/cli/src/context/ingest/adapters/looker/local-runtime-store.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/local-runtime-store.test.ts @@ -2,7 +2,7 @@ import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { LocalLookerRuntimeStore } from './local-runtime-store.js'; +import { LocalLookerRuntimeStore } from '../../../../../src/context/ingest/adapters/looker/local-runtime-store.js'; describe('LocalLookerRuntimeStore', () => { async function store() { diff --git a/packages/cli/src/context/ingest/adapters/looker/looker.adapter.test.ts b/packages/cli/test/context/ingest/adapters/looker/looker.adapter.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/looker/looker.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/looker/looker.adapter.test.ts index 64a35622..90623a37 100644 --- a/packages/cli/src/context/ingest/adapters/looker/looker.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/looker.adapter.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { LookerRuntimeClient } from './fetch.js'; -import { LookerSourceAdapter } from './looker.adapter.js'; +import type { LookerRuntimeClient } from '../../../../../src/context/ingest/adapters/looker/fetch.js'; +import { LookerSourceAdapter } from '../../../../../src/context/ingest/adapters/looker/looker.adapter.js'; const connectionId = '11111111-1111-4111-8111-111111111111'; diff --git a/packages/cli/src/context/ingest/adapters/looker/mapping.test.ts b/packages/cli/test/context/ingest/adapters/looker/mapping.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/looker/mapping.test.ts rename to packages/cli/test/context/ingest/adapters/looker/mapping.test.ts index c7b29b8a..0ac9c067 100644 --- a/packages/cli/src/context/ingest/adapters/looker/mapping.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/mapping.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { StagedExploreFile, StagedLookmlModelsFile } from './types.js'; +import type { StagedExploreFile, StagedLookmlModelsFile } from '../../../../../src/context/ingest/adapters/looker/types.js'; import { buildLookerPullConfigFromInputs, collectExploreParseItems, @@ -12,7 +12,7 @@ import { suggestKtxConnectionForLookerConnection, validateLookerMappings, validateLookerWarehouseTarget, -} from './mapping.js'; +} from '../../../../../src/context/ingest/adapters/looker/mapping.js'; const liveConnections = [ { diff --git a/packages/cli/src/context/ingest/adapters/looker/reconcile.test.ts b/packages/cli/test/context/ingest/adapters/looker/reconcile.test.ts similarity index 84% rename from packages/cli/src/context/ingest/adapters/looker/reconcile.test.ts rename to packages/cli/test/context/ingest/adapters/looker/reconcile.test.ts index 09e8685f..2ffcdaeb 100644 --- a/packages/cli/src/context/ingest/adapters/looker/reconcile.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/reconcile.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildLookerReconcileNotes } from './reconcile.js'; +import { buildLookerReconcileNotes } from '../../../../../src/context/ingest/adapters/looker/reconcile.js'; describe('buildLookerReconcileNotes', () => { it('instructs reconciliation to record subsumed provenance', () => { diff --git a/packages/cli/src/context/ingest/adapters/looker/scope.test.ts b/packages/cli/test/context/ingest/adapters/looker/scope.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/looker/scope.test.ts rename to packages/cli/test/context/ingest/adapters/looker/scope.test.ts index d7c2c56e..55592761 100644 --- a/packages/cli/src/context/ingest/adapters/looker/scope.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/scope.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { describeLookerScope, hashLookerScope, isPathInLookerScope } from './scope.js'; +import { describeLookerScope, hashLookerScope, isPathInLookerScope } from '../../../../../src/context/ingest/adapters/looker/scope.js'; async function writeJson(stagedDir: string, relPath: string, value: unknown): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/looker/target-connections.test.ts b/packages/cli/test/context/ingest/adapters/looker/target-connections.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/looker/target-connections.test.ts rename to packages/cli/test/context/ingest/adapters/looker/target-connections.test.ts index 10b2d892..5497914d 100644 --- a/packages/cli/src/context/ingest/adapters/looker/target-connections.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/target-connections.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { listLookerTargetConnectionIds } from './target-connections.js'; +import { listLookerTargetConnectionIds } from '../../../../../src/context/ingest/adapters/looker/target-connections.js'; describe('listLookerTargetConnectionIds', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts b/packages/cli/test/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts rename to packages/cli/test/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts index d4cd857b..2f8dc4a4 100644 --- a/packages/cli/src/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { ToolOutput } from '../../../../../context/tools/base-tool.js'; -import { buildLookerSlProposal, createLookerQueryToSlTool, type LookerSlProposal } from './looker-query-to-sl.tool.js'; +import type { ToolOutput } from '../../../../../../src/context/tools/base-tool.js'; +import { buildLookerSlProposal, createLookerQueryToSlTool, type LookerSlProposal } from '../../../../../../src/context/ingest/adapters/looker/tools/looker-query-to-sl.tool.js'; describe('buildLookerSlProposal', () => { it('suggests a measure and segment for an aggregated filtered Looker query', () => { diff --git a/packages/cli/src/context/ingest/adapters/looker/types.test.ts b/packages/cli/test/context/ingest/adapters/looker/types.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/looker/types.test.ts rename to packages/cli/test/context/ingest/adapters/looker/types.test.ts index 2a4c2b8c..113f9fe3 100644 --- a/packages/cli/src/context/ingest/adapters/looker/types.test.ts +++ b/packages/cli/test/context/ingest/adapters/looker/types.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parsedTargetTableSchema } from '../../parsed-target-table.js'; +import { parsedTargetTableSchema } from '../../../../../src/context/ingest/parsed-target-table.js'; import { lookerPullConfigSchema, parseLookerPullConfig, @@ -11,7 +11,7 @@ import { stagedLookerSignalsFileSchema, stagedLookFileSchema, stagedSyncConfigSchema, -} from './types.js'; +} from '../../../../../src/context/ingest/adapters/looker/types.js'; describe('Looker staged runtime schemas', () => { it('parses pull config and staged sync config', () => { diff --git a/packages/cli/src/context/ingest/adapters/lookml/chunk.test.ts b/packages/cli/test/context/ingest/adapters/lookml/chunk.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/lookml/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/chunk.test.ts index e9a8b5f3..5898a2a9 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/chunk.test.ts @@ -1,9 +1,9 @@ import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { chunkLookmlProject } from './chunk.js'; -import { type ParsedLookmlProject, parseLookmlStagedDir } from './parse.js'; +import { chunkLookmlProject } from '../../../../../src/context/ingest/adapters/lookml/chunk.js'; +import { type ParsedLookmlProject, parseLookmlStagedDir } from '../../../../../src/context/ingest/adapters/lookml/parse.js'; -const FIXTURE_ROOT = join(__dirname, '../../../../test/fixtures/lookml'); +const FIXTURE_ROOT = join(__dirname, '../../../../fixtures/lookml'); describe('chunkLookmlProject — first run', () => { it('single-model bundle → 1 WU with model + all views in rawFiles', async () => { diff --git a/packages/cli/src/context/ingest/adapters/lookml/detect.test.ts b/packages/cli/test/context/ingest/adapters/lookml/detect.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/lookml/detect.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/detect.test.ts index 040c1788..12640d07 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/detect.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/detect.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { detectLookmlStagedDir } from './detect.js'; +import { detectLookmlStagedDir } from '../../../../../src/context/ingest/adapters/lookml/detect.js'; describe('detectLookmlStagedDir', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/lookml/fetch-report.test.ts b/packages/cli/test/context/ingest/adapters/lookml/fetch-report.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/lookml/fetch-report.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/fetch-report.test.ts index ffeb52fb..4aa91e82 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/fetch-report.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/fetch-report.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { ParsedLookmlProject } from './parse.js'; +import type { ParsedLookmlProject } from '../../../../../src/context/ingest/adapters/lookml/parse.js'; import { LOOKML_FETCH_REPORT_FILE, LOOKML_MISMATCHED_MODELS_FILE, @@ -10,7 +10,7 @@ import { readLookmlFetchReport, readLookmlMismatchedModelNames, writeLookmlValidationArtifacts, -} from './fetch-report.js'; +} from '../../../../../src/context/ingest/adapters/lookml/fetch-report.js'; function project(models: ParsedLookmlProject['models']): ParsedLookmlProject { return { models, views: [], dashboards: [], allPaths: models.map((m) => m.path) }; diff --git a/packages/cli/src/context/ingest/adapters/lookml/fetch.test.ts b/packages/cli/test/context/ingest/adapters/lookml/fetch.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/lookml/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/fetch.test.ts index a0c293e7..05ee9bfc 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/fetch.test.ts @@ -3,10 +3,10 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { makeLocalGitRepo } from '../../../test/make-local-git-repo.js'; -import { fetchLookmlRepo } from './fetch.js'; -import type { LookmlPullConfig } from './pull-config.js'; +import { fetchLookmlRepo } from '../../../../../src/context/ingest/adapters/lookml/fetch.js'; +import type { LookmlPullConfig } from '../../../../../src/context/ingest/adapters/lookml/pull-config.js'; -const FIXTURE_ROOT = join(__dirname, '../../../../test/fixtures/lookml'); +const FIXTURE_ROOT = join(__dirname, '../../../../fixtures/lookml'); function pullConfig(overrides: Partial & Pick): LookmlPullConfig { return { diff --git a/packages/cli/src/context/ingest/adapters/lookml/graph.test.ts b/packages/cli/test/context/ingest/adapters/lookml/graph.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/lookml/graph.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/graph.test.ts index c1efd701..d0df93e3 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/graph.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/graph.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildLookmlGraph } from './graph.js'; -import type { ParsedLookmlProject } from './parse.js'; +import { buildLookmlGraph } from '../../../../../src/context/ingest/adapters/lookml/graph.js'; +import type { ParsedLookmlProject } from '../../../../../src/context/ingest/adapters/lookml/parse.js'; type LooseParsedLookmlProject = Omit, 'models' | 'views'> & { models?: Array & { connectionName?: string | null }>; diff --git a/packages/cli/src/context/ingest/adapters/lookml/lookml.adapter.test.ts b/packages/cli/test/context/ingest/adapters/lookml/lookml.adapter.test.ts similarity index 92% rename from packages/cli/src/context/ingest/adapters/lookml/lookml.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/lookml.adapter.test.ts index d22597b9..0d23cc95 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/lookml.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/lookml.adapter.test.ts @@ -3,8 +3,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { makeLocalGitRepo } from '../../../test/make-local-git-repo.js'; -import { LOOKML_FETCH_REPORT_FILE } from './fetch-report.js'; -import { LookmlSourceAdapter } from './lookml.adapter.js'; +import { LOOKML_FETCH_REPORT_FILE } from '../../../../../src/context/ingest/adapters/lookml/fetch-report.js'; +import { LookmlSourceAdapter } from '../../../../../src/context/ingest/adapters/lookml/lookml.adapter.js'; describe('LookmlSourceAdapter validation sidecars', () => { let tmpRoot: string; diff --git a/packages/cli/src/context/ingest/adapters/lookml/parse.test.ts b/packages/cli/test/context/ingest/adapters/lookml/parse.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/lookml/parse.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/parse.test.ts index 84ce5b5a..5372dc63 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/parse.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/parse.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { parseLookmlStagedDir } from './parse.js'; +import { parseLookmlStagedDir } from '../../../../../src/context/ingest/adapters/lookml/parse.js'; describe('parseLookmlStagedDir', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/lookml/pull-config.test.ts b/packages/cli/test/context/ingest/adapters/lookml/pull-config.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/lookml/pull-config.test.ts rename to packages/cli/test/context/ingest/adapters/lookml/pull-config.test.ts index 2edec99f..34cd9d8e 100644 --- a/packages/cli/src/context/ingest/adapters/lookml/pull-config.test.ts +++ b/packages/cli/test/context/ingest/adapters/lookml/pull-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseLookmlPullConfig, pullConfigFromIntegrationConfig } from './pull-config.js'; +import { parseLookmlPullConfig, pullConfigFromIntegrationConfig } from '../../../../../src/context/ingest/adapters/lookml/pull-config.js'; describe('lookml pull config', () => { it('parses a minimal valid config with defaulted branch', () => { diff --git a/packages/cli/src/context/ingest/adapters/metabase/card-references.test.ts b/packages/cli/test/context/ingest/adapters/metabase/card-references.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/metabase/card-references.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/card-references.test.ts index 8c179710..f619490f 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/card-references.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/card-references.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { CardReferenceCycleError, expandCardReferences } from './card-references.js'; +import { CardReferenceCycleError, expandCardReferences } from '../../../../../src/context/ingest/adapters/metabase/card-references.js'; describe('expandCardReferences', () => { const fetchCard = (id: number): Promise<{ native_query: string }> => { diff --git a/packages/cli/src/context/ingest/adapters/metabase/chunk.test.ts b/packages/cli/test/context/ingest/adapters/metabase/chunk.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/metabase/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/chunk.test.ts index 1991e147..333faf26 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/chunk.test.ts @@ -2,10 +2,10 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { chunkMetabaseStagedDir } from './chunk.js'; -import { stagedSyncConfigSchema } from './types.js'; +import { chunkMetabaseStagedDir } from '../../../../../src/context/ingest/adapters/metabase/chunk.js'; +import { stagedSyncConfigSchema } from '../../../../../src/context/ingest/adapters/metabase/types.js'; -const FIXTURES = resolve(__dirname, '../../../../test/fixtures/metabase'); +const FIXTURES = resolve(__dirname, '../../../../fixtures/metabase'); const SIMPLE = join(FIXTURES, 'simple'); const MULTI = join(FIXTURES, 'multi-collection'); const CARD_REF = join(FIXTURES, 'card-ref'); diff --git a/packages/cli/src/context/ingest/adapters/metabase/client-boundary.test.ts b/packages/cli/test/context/ingest/adapters/metabase/client-boundary.test.ts similarity index 92% rename from packages/cli/src/context/ingest/adapters/metabase/client-boundary.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/client-boundary.test.ts index 7df6691e..7e8a0e2e 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/client-boundary.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/client-boundary.test.ts @@ -1,9 +1,9 @@ import { readFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -const metabaseDir = dirname(fileURLToPath(import.meta.url)); +const metabaseDir = fileURLToPath(new URL('../../../../../src/context/ingest/adapters/metabase/', import.meta.url)); async function readMetabaseFile(name: string): Promise { return readFile(join(metabaseDir, name), 'utf-8'); diff --git a/packages/cli/src/context/ingest/adapters/metabase/client-port.test.ts b/packages/cli/test/context/ingest/adapters/metabase/client-port.test.ts similarity index 92% rename from packages/cli/src/context/ingest/adapters/metabase/client-port.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/client-port.test.ts index 8f775b56..3a9e2732 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/client-port.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/client-port.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { FetchContext } from '../../types.js'; +import type { FetchContext } from '../../../../../src/context/ingest/types.js'; import { IngestMetabaseClientFactory, type MetabaseCard, @@ -8,8 +8,8 @@ import { type MetabaseRuntimeClient, type MetabaseTemplateTag, type TestConnectionResult, -} from './client-port.js'; -import type { MetabasePullConfig } from './types.js'; +} from '../../../../../src/context/ingest/adapters/metabase/client-port.js'; +import type { MetabasePullConfig } from '../../../../../src/context/ingest/adapters/metabase/types.js'; function makeRuntimeClient(): MetabaseRuntimeClient { return { diff --git a/packages/cli/src/context/ingest/adapters/metabase/client.test.ts b/packages/cli/test/context/ingest/adapters/metabase/client.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/metabase/client.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/client.test.ts index 3d45a276..5ab2fd09 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/client.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/client.test.ts @@ -5,8 +5,8 @@ import { getDummyValueForWidgetType, MetabaseClient, stripOptionalClauses, -} from './client.js'; -import type { MetabaseCard, MetabaseTemplateTag } from './client-port.js'; +} from '../../../../../src/context/ingest/adapters/metabase/client.js'; +import type { MetabaseCard, MetabaseTemplateTag } from '../../../../../src/context/ingest/adapters/metabase/client-port.js'; const runtime = { apiUrl: 'https://metabase.example.test/api', diff --git a/packages/cli/src/context/ingest/adapters/metabase/detect.test.ts b/packages/cli/test/context/ingest/adapters/metabase/detect.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/metabase/detect.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/detect.test.ts index 816bbef6..44abe951 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/detect.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/detect.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { detectMetabaseStagedDir } from './detect.js'; +import { detectMetabaseStagedDir } from '../../../../../src/context/ingest/adapters/metabase/detect.js'; async function touch(stagedDir: string, relPath: string, body: string): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/metabase/fanout-planner.test.ts b/packages/cli/test/context/ingest/adapters/metabase/fanout-planner.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/metabase/fanout-planner.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/fanout-planner.test.ts index cb275472..d1e9e7e5 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/fanout-planner.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/fanout-planner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { planMetabaseFanoutChildren } from './fanout-planner.js'; +import { planMetabaseFanoutChildren } from '../../../../../src/context/ingest/adapters/metabase/fanout-planner.js'; describe('planMetabaseFanoutChildren', () => { it('builds ordered child plans for sync-enabled mapped Metabase databases', () => { diff --git a/packages/cli/src/context/ingest/adapters/metabase/fetch-scope.test.ts b/packages/cli/test/context/ingest/adapters/metabase/fetch-scope.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/metabase/fetch-scope.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/fetch-scope.test.ts index 9768c0c9..e2b1c6e7 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/fetch-scope.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/fetch-scope.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { computeFetchScope, type FetchScope, hashScope, isPathInMetabaseScope } from './fetch-scope.js'; -import type { StagedSyncConfig } from './types.js'; +import { computeFetchScope, type FetchScope, hashScope, isPathInMetabaseScope } from '../../../../../src/context/ingest/adapters/metabase/fetch-scope.js'; +import type { StagedSyncConfig } from '../../../../../src/context/ingest/adapters/metabase/types.js'; const BASE_CONFIG = { metabaseConnectionId: 'a1b2c3d4-e5f6-4789-9abc-def012345678', diff --git a/packages/cli/src/context/ingest/adapters/metabase/fetch.test.ts b/packages/cli/test/context/ingest/adapters/metabase/fetch.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/metabase/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/fetch.test.ts index 1f93765e..4e067663 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/fetch.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { FetchContext } from '../../types.js'; -import { fetchMetabaseBundle } from './fetch.js'; +import type { FetchContext } from '../../../../../src/context/ingest/types.js'; +import { fetchMetabaseBundle } from '../../../../../src/context/ingest/adapters/metabase/fetch.js'; const metabaseConnectionId = 'a1b2c3d4-e5f6-4789-9abc-def012345678'; const targetConnectionId = 'b2c3d4e5-f6a7-4890-abcd-ef0123456789'; diff --git a/packages/cli/src/context/ingest/adapters/metabase/local-metabase.adapter.test.ts b/packages/cli/test/context/ingest/adapters/metabase/local-metabase.adapter.test.ts similarity index 91% rename from packages/cli/src/context/ingest/adapters/metabase/local-metabase.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/local-metabase.adapter.test.ts index c20a65ac..1f860557 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/local-metabase.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/local-metabase.adapter.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KtxProjectConnectionConfig } from '../../../../context/project/config.js'; -import { metabaseRuntimeConfigFromLocalConnection } from './local-metabase.adapter.js'; +import type { KtxProjectConnectionConfig } from '../../../../../src/context/project/config.js'; +import { metabaseRuntimeConfigFromLocalConnection } from '../../../../../src/context/ingest/adapters/metabase/local-metabase.adapter.js'; describe('metabaseRuntimeConfigFromLocalConnection', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/adapters/metabase/local-source-state-store.test.ts b/packages/cli/test/context/ingest/adapters/metabase/local-source-state-store.test.ts similarity index 93% rename from packages/cli/src/context/ingest/adapters/metabase/local-source-state-store.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/local-source-state-store.test.ts index 0225f398..3f80f7af 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/local-source-state-store.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/local-source-state-store.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { buildDefaultKtxProjectConfig } from '../../../../context/project/config.js'; -import { connectionConfigSchema } from '../../../project/driver-schemas.js'; -import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from './local-source-state-store.js'; +import { buildDefaultKtxProjectConfig } from '../../../../../src/context/project/config.js'; +import { connectionConfigSchema } from '../../../../../src/context/project/driver-schemas.js'; +import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from '../../../../../src/context/ingest/adapters/metabase/local-source-state-store.js'; describe('Metabase YAML source state and discovery cache', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/adapters/metabase/mapping.test.ts b/packages/cli/test/context/ingest/adapters/metabase/mapping.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/metabase/mapping.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/mapping.test.ts index e347390c..b6f1f354 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/mapping.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/mapping.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { MetabaseRuntimeClient } from './client-port.js'; +import type { MetabaseRuntimeClient } from '../../../../../src/context/ingest/adapters/metabase/client-port.js'; import { METABASE_ENGINE_TO_CONNECTION_TYPE, computeMetabaseMappingDrift, @@ -9,7 +9,7 @@ import { refreshMetabaseMapping, validateMappingPhysicalMatch, validateMetabaseMappings, -} from './mapping.js'; +} from '../../../../../src/context/ingest/adapters/metabase/mapping.js'; describe('discoverMetabaseDatabases', () => { it('filters sample databases and extracts host plus database names from Metabase details', async () => { diff --git a/packages/cli/src/context/ingest/adapters/metabase/metabase.adapter.test.ts b/packages/cli/test/context/ingest/adapters/metabase/metabase.adapter.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/metabase/metabase.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/metabase.adapter.test.ts index a22c1f3b..a22e8b28 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/metabase.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/metabase.adapter.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { MetabaseSourceAdapter } from './metabase.adapter.js'; +import { MetabaseSourceAdapter } from '../../../../../src/context/ingest/adapters/metabase/metabase.adapter.js'; describe('MetabaseSourceAdapter', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/metabase/serialize-card.test.ts b/packages/cli/test/context/ingest/adapters/metabase/serialize-card.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/metabase/serialize-card.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/serialize-card.test.ts index ff10ce59..c743b74d 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/serialize-card.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/serialize-card.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { extractReferencedCardIds, serializeCard } from './serialize-card.js'; +import { extractReferencedCardIds, serializeCard } from '../../../../../src/context/ingest/adapters/metabase/serialize-card.js'; describe('extractReferencedCardIds', () => { it('pulls ids out of template tags with type=card', () => { diff --git a/packages/cli/src/context/ingest/adapters/metabase/types.test.ts b/packages/cli/test/context/ingest/adapters/metabase/types.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/metabase/types.test.ts rename to packages/cli/test/context/ingest/adapters/metabase/types.test.ts index 4a445d89..fd92090b 100644 --- a/packages/cli/src/context/ingest/adapters/metabase/types.test.ts +++ b/packages/cli/test/context/ingest/adapters/metabase/types.test.ts @@ -4,7 +4,7 @@ import { parseMetabasePullConfig, stagedCardFileSchema, stagedSyncConfigSchema, -} from './types.js'; +} from '../../../../../src/context/ingest/adapters/metabase/types.js'; describe('metabase adapter types', () => { it('parses a valid MetabasePullConfig', () => { diff --git a/packages/cli/src/context/ingest/adapters/metricflow/chunk.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/chunk.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/metricflow/chunk.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/chunk.test.ts index 88062fb3..b783087b 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/chunk.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/chunk.test.ts @@ -1,9 +1,9 @@ import { join, resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { chunkMetricFlowProject } from './chunk.js'; -import { parseMetricFlowStagedDir } from './parse.js'; +import { chunkMetricFlowProject } from '../../../../../src/context/ingest/adapters/metricflow/chunk.js'; +import { parseMetricFlowStagedDir } from '../../../../../src/context/ingest/adapters/metricflow/parse.js'; -const FIXTURES = resolve(__dirname, '../../../../test/fixtures/metricflow'); +const FIXTURES = resolve(__dirname, '../../../../fixtures/metricflow'); const SINGLE = join(FIXTURES, 'single-model'); const EXTENDS_CHAIN = join(FIXTURES, 'extends-chain'); const MULTI = join(FIXTURES, 'multi-component'); diff --git a/packages/cli/src/context/ingest/adapters/metricflow/deep-parse.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/deep-parse.test.ts similarity index 99% rename from packages/cli/src/context/ingest/adapters/metricflow/deep-parse.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/deep-parse.test.ts index 8896db68..e6747db1 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/deep-parse.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/deep-parse.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { parseMetricflowFiles, translateMetricflowJinjaFilter } from './deep-parse.js'; +import { parseMetricflowFiles, translateMetricflowJinjaFilter } from '../../../../../src/context/ingest/adapters/metricflow/deep-parse.js'; function yaml(strings: TemplateStringsArray, ...values: unknown[]): string { return String.raw(strings, ...values); diff --git a/packages/cli/src/context/ingest/adapters/metricflow/detect.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/detect.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/metricflow/detect.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/detect.test.ts index a8df434f..77923eb7 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/detect.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/detect.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { detectMetricFlowStagedDir } from './detect.js'; +import { detectMetricFlowStagedDir } from '../../../../../src/context/ingest/adapters/metricflow/detect.js'; async function touch(stagedDir: string, relPath: string, body = ''): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/metricflow/fetch.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/fetch.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/metricflow/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/fetch.test.ts index 70568be2..0fb67072 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/fetch.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { makeLocalGitRepo } from '../../../test/make-local-git-repo.js'; -import { fetchMetricflowRepo } from './fetch.js'; +import { fetchMetricflowRepo } from '../../../../../src/context/ingest/adapters/metricflow/fetch.js'; async function exists(path: string): Promise { try { diff --git a/packages/cli/src/context/ingest/adapters/metricflow/graph.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/graph.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/metricflow/graph.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/graph.test.ts index 93a3a6c6..1b252bc3 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/graph.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/graph.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildMetricFlowGraph } from './graph.js'; -import type { ParsedMetricFlowProject } from './parse.js'; +import { buildMetricFlowGraph } from '../../../../../src/context/ingest/adapters/metricflow/graph.js'; +import type { ParsedMetricFlowProject } from '../../../../../src/context/ingest/adapters/metricflow/parse.js'; function project(parts: Partial): ParsedMetricFlowProject { return { diff --git a/packages/cli/src/context/ingest/adapters/metricflow/import-semantic-models.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/import-semantic-models.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/metricflow/import-semantic-models.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/import-semantic-models.test.ts index d5a7e3c5..a247af55 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/import-semantic-models.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/import-semantic-models.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import type { MetricFlowParseResult } from './deep-parse.js'; -import { importMetricflowSemanticModels } from './import-semantic-models.js'; +import type { MetricFlowParseResult } from '../../../../../src/context/ingest/adapters/metricflow/deep-parse.js'; +import { importMetricflowSemanticModels } from '../../../../../src/context/ingest/adapters/metricflow/import-semantic-models.js'; const DBT_SYSTEM_EMAIL = ['system@kae', 'lio.dev'].join(''); diff --git a/packages/cli/src/context/ingest/adapters/metricflow/metricflow.adapter.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/metricflow.adapter.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/metricflow/metricflow.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/metricflow.adapter.test.ts index 232624a5..099a666f 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/metricflow.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/metricflow.adapter.test.ts @@ -3,10 +3,10 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { makeLocalGitRepo } from '../../../test/make-local-git-repo.js'; -import type { SourceAdapter } from '../../types.js'; -import type { MetricFlowParseResult } from './deep-parse.js'; -import { MetricflowSourceAdapter } from './metricflow.adapter.js'; -import { readMetricflowProjectionConfig, writeMetricflowProjectionConfig } from './projection-config.js'; +import type { SourceAdapter } from '../../../../../src/context/ingest/types.js'; +import type { MetricFlowParseResult } from '../../../../../src/context/ingest/adapters/metricflow/deep-parse.js'; +import { MetricflowSourceAdapter } from '../../../../../src/context/ingest/adapters/metricflow/metricflow.adapter.js'; +import { readMetricflowProjectionConfig, writeMetricflowProjectionConfig } from '../../../../../src/context/ingest/adapters/metricflow/projection-config.js'; function compileOnlyRequiredDepsCheck(): void { // @ts-expect-error MetricflowSourceAdapter requires an explicit cache home. diff --git a/packages/cli/src/context/ingest/adapters/metricflow/parse.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/parse.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/metricflow/parse.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/parse.test.ts index 72a94472..9c2e071f 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/parse.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/parse.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { parseMetricFlowStagedDir } from './parse.js'; +import { parseMetricFlowStagedDir } from '../../../../../src/context/ingest/adapters/metricflow/parse.js'; async function writeFixture(stagedDir: string, relPath: string, body: string): Promise { const abs = join(stagedDir, relPath); diff --git a/packages/cli/src/context/ingest/adapters/metricflow/pull-config.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/pull-config.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/metricflow/pull-config.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/pull-config.test.ts index 5137a4e6..a12cdccd 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/pull-config.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/pull-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseMetricflowPullConfig, pullConfigFromMetricflowIntegration } from './pull-config.js'; +import { parseMetricflowPullConfig, pullConfigFromMetricflowIntegration } from '../../../../../src/context/ingest/adapters/metricflow/pull-config.js'; describe('metricflow pull config', () => { it('applies defaults for optional git fields', () => { diff --git a/packages/cli/src/context/ingest/adapters/metricflow/semantic-models.test.ts b/packages/cli/test/context/ingest/adapters/metricflow/semantic-models.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/metricflow/semantic-models.test.ts rename to packages/cli/test/context/ingest/adapters/metricflow/semantic-models.test.ts index c22ac97d..0796ff0e 100644 --- a/packages/cli/src/context/ingest/adapters/metricflow/semantic-models.test.ts +++ b/packages/cli/test/context/ingest/adapters/metricflow/semantic-models.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { composeOverlay } from '../../../../context/sl/semantic-layer.service.js'; -import type { SemanticLayerSource } from '../../../../context/sl/types.js'; -import type { ParsedCrossModelMetric, ParsedMetricflowRelationship, ParsedSemanticModel } from './deep-parse.js'; +import { composeOverlay } from '../../../../../src/context/sl/semantic-layer.service.js'; +import type { SemanticLayerSource } from '../../../../../src/context/sl/types.js'; +import type { ParsedCrossModelMetric, ParsedMetricflowRelationship, ParsedSemanticModel } from '../../../../../src/context/ingest/adapters/metricflow/deep-parse.js'; import { buildMetricflowColumns, buildMetricflowJoinsForModel, @@ -13,7 +13,7 @@ import { resolveMetricflowSemanticModelSourceName, rewriteMetricflowManifestJoins, toKebabCaseMetricflowName, -} from './semantic-models.js'; +} from '../../../../../src/context/ingest/adapters/metricflow/semantic-models.js'; const ordersModel: ParsedSemanticModel = { name: 'orders', diff --git a/packages/cli/src/context/ingest/adapters/notion/cluster.test.ts b/packages/cli/test/context/ingest/adapters/notion/cluster.test.ts similarity index 94% rename from packages/cli/src/context/ingest/adapters/notion/cluster.test.ts rename to packages/cli/test/context/ingest/adapters/notion/cluster.test.ts index ad41b571..ca2dfc0e 100644 --- a/packages/cli/src/context/ingest/adapters/notion/cluster.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/cluster.test.ts @@ -2,9 +2,9 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, test } from 'vitest'; -import type { KtxEmbeddingPort } from '../../../core/embedding.js'; -import type { WorkUnit } from '../../types.js'; -import { clusterNotionWorkUnits, MIN_PAGES_TO_CLUSTER } from './cluster.js'; +import type { KtxEmbeddingPort } from '../../../../../src/context/core/embedding.js'; +import type { WorkUnit } from '../../../../../src/context/ingest/types.js'; +import { clusterNotionWorkUnits, MIN_PAGES_TO_CLUSTER } from '../../../../../src/context/ingest/adapters/notion/cluster.js'; function fakeEmbedding(text: string): number[] { const v = [0, 0, 0, 0]; diff --git a/packages/cli/src/context/ingest/adapters/notion/fetch.test.ts b/packages/cli/test/context/ingest/adapters/notion/fetch.test.ts similarity index 98% rename from packages/cli/src/context/ingest/adapters/notion/fetch.test.ts rename to packages/cli/test/context/ingest/adapters/notion/fetch.test.ts index b60170f7..10140074 100644 --- a/packages/cli/src/context/ingest/adapters/notion/fetch.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/fetch.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { fetchNotionSnapshot } from './fetch.js'; -import type { NotionApi } from './notion-client.js'; +import { fetchNotionSnapshot } from '../../../../../src/context/ingest/adapters/notion/fetch.js'; +import type { NotionApi } from '../../../../../src/context/ingest/adapters/notion/notion-client.js'; describe('fetchNotionSnapshot', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/adapters/notion/local-state-store.test.ts b/packages/cli/test/context/ingest/adapters/notion/local-state-store.test.ts similarity index 91% rename from packages/cli/src/context/ingest/adapters/notion/local-state-store.test.ts rename to packages/cli/test/context/ingest/adapters/notion/local-state-store.test.ts index 892ea6c1..da5d51c2 100644 --- a/packages/cli/src/context/ingest/adapters/notion/local-state-store.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/local-state-store.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { LocalNotionRuntimeStore } from './local-state-store.js'; +import { LocalNotionRuntimeStore } from '../../../../../src/context/ingest/adapters/notion/local-state-store.js'; describe('LocalNotionRuntimeStore', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/adapters/notion/normalize.test.ts b/packages/cli/test/context/ingest/adapters/notion/normalize.test.ts similarity index 96% rename from packages/cli/src/context/ingest/adapters/notion/normalize.test.ts rename to packages/cli/test/context/ingest/adapters/notion/normalize.test.ts index 3b90c4de..dcc4621e 100644 --- a/packages/cli/src/context/ingest/adapters/notion/normalize.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/normalize.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeNotionBlocksToMarkdown, normalizeNotionPageMetadata, propertyValueToText } from './normalize.js'; +import { normalizeNotionBlocksToMarkdown, normalizeNotionPageMetadata, propertyValueToText } from '../../../../../src/context/ingest/adapters/notion/normalize.js'; describe('Notion normalization', () => { it('converts common blocks into stable markdown', () => { diff --git a/packages/cli/src/context/ingest/adapters/notion/notion-client.test.ts b/packages/cli/test/context/ingest/adapters/notion/notion-client.test.ts similarity index 95% rename from packages/cli/src/context/ingest/adapters/notion/notion-client.test.ts rename to packages/cli/test/context/ingest/adapters/notion/notion-client.test.ts index fd3d54eb..bb3bdb0b 100644 --- a/packages/cli/src/context/ingest/adapters/notion/notion-client.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/notion-client.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { retryNotionRequest } from './notion-client.js'; +import { retryNotionRequest } from '../../../../../src/context/ingest/adapters/notion/notion-client.js'; describe('Notion client retry helper', () => { it('retries rate-limited requests and then returns the response', async () => { diff --git a/packages/cli/src/context/ingest/adapters/notion/notion.adapter.test.ts b/packages/cli/test/context/ingest/adapters/notion/notion.adapter.test.ts similarity index 97% rename from packages/cli/src/context/ingest/adapters/notion/notion.adapter.test.ts rename to packages/cli/test/context/ingest/adapters/notion/notion.adapter.test.ts index 0f500d5e..e02cf10b 100644 --- a/packages/cli/src/context/ingest/adapters/notion/notion.adapter.test.ts +++ b/packages/cli/test/context/ingest/adapters/notion/notion.adapter.test.ts @@ -2,9 +2,9 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DiffSetService } from '../../diff-set.service.js'; -import { NOTION_ORG_KNOWLEDGE_WARNING } from './chunk.js'; -import { NotionSourceAdapter } from './notion.adapter.js'; +import { DiffSetService } from '../../../../../src/context/ingest/diff-set.service.js'; +import { NOTION_ORG_KNOWLEDGE_WARNING } from '../../../../../src/context/ingest/adapters/notion/chunk.js'; +import { NotionSourceAdapter } from '../../../../../src/context/ingest/adapters/notion/notion.adapter.js'; describe('NotionSourceAdapter', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/artifact-gates.test.ts b/packages/cli/test/context/ingest/artifact-gates.test.ts similarity index 99% rename from packages/cli/src/context/ingest/artifact-gates.test.ts rename to packages/cli/test/context/ingest/artifact-gates.test.ts index cc786409..c93a24e5 100644 --- a/packages/cli/src/context/ingest/artifact-gates.test.ts +++ b/packages/cli/test/context/ingest/artifact-gates.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { validateFinalIngestArtifacts, validateProvenanceRawPaths } from './artifact-gates.js'; +import { validateFinalIngestArtifacts, validateProvenanceRawPaths } from '../../../src/context/ingest/artifact-gates.js'; function wikiServiceWithPages( pages: Record, diff --git a/packages/cli/src/context/ingest/canonical-pins.test.ts b/packages/cli/test/context/ingest/canonical-pins.test.ts similarity index 92% rename from packages/cli/src/context/ingest/canonical-pins.test.ts rename to packages/cli/test/context/ingest/canonical-pins.test.ts index dec62360..ae645b24 100644 --- a/packages/cli/src/context/ingest/canonical-pins.test.ts +++ b/packages/cli/test/context/ingest/canonical-pins.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildCanonicalPinsPromptBlock, type CanonicalPin, selectRelevantCanonicalPins } from './canonical-pins.js'; -import type { StageIndex } from './stages/stage-index.types.js'; +import { buildCanonicalPinsPromptBlock, type CanonicalPin, selectRelevantCanonicalPins } from '../../../src/context/ingest/canonical-pins.js'; +import type { StageIndex } from '../../../src/context/ingest/stages/stage-index.types.js'; function makeStageIndex(): StageIndex { return { diff --git a/packages/cli/src/context/ingest/clustering/kmeans.test.ts b/packages/cli/test/context/ingest/clustering/kmeans.test.ts similarity index 95% rename from packages/cli/src/context/ingest/clustering/kmeans.test.ts rename to packages/cli/test/context/ingest/clustering/kmeans.test.ts index 3cda76d1..a0346309 100644 --- a/packages/cli/src/context/ingest/clustering/kmeans.test.ts +++ b/packages/cli/test/context/ingest/clustering/kmeans.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { kmeans, pickK } from './kmeans.js'; +import { kmeans, pickK } from '../../../../src/context/ingest/clustering/kmeans.js'; describe('pickK', () => { test('uses ceil(N/8) heuristic clamped to [1, 10]', () => { diff --git a/packages/cli/src/context/ingest/context-candidates/candidate-dedup.service.test.ts b/packages/cli/test/context/ingest/context-candidates/candidate-dedup.service.test.ts similarity index 96% rename from packages/cli/src/context/ingest/context-candidates/candidate-dedup.service.test.ts rename to packages/cli/test/context/ingest/context-candidates/candidate-dedup.service.test.ts index a7b5520e..f69ab4db 100644 --- a/packages/cli/src/context/ingest/context-candidates/candidate-dedup.service.test.ts +++ b/packages/cli/test/context/ingest/context-candidates/candidate-dedup.service.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { ContextCandidateForDedup } from '../ports.js'; -import { CandidateDedupService } from './candidate-dedup.service.js'; -import type { ContextCandidateStorePort } from './store.js'; -import type { ContextCandidateEmbeddingPort } from './types.js'; +import type { ContextCandidateForDedup } from '../../../../src/context/ingest/ports.js'; +import { CandidateDedupService } from '../../../../src/context/ingest/context-candidates/candidate-dedup.service.js'; +import type { ContextCandidateStorePort } from '../../../../src/context/ingest/context-candidates/store.js'; +import type { ContextCandidateEmbeddingPort } from '../../../../src/context/ingest/context-candidates/types.js'; const vector = (...values: number[]): string => JSON.stringify(values); diff --git a/packages/cli/src/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts b/packages/cli/test/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts similarity index 94% rename from packages/cli/src/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts rename to packages/cli/test/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts index df452ca7..229b4a34 100644 --- a/packages/cli/src/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts +++ b/packages/cli/test/context/ingest/context-candidates/context-candidate-carryforward.service.test.ts @@ -1,8 +1,8 @@ import { createHash } from 'node:crypto'; import { describe, expect, it, vi } from 'vitest'; -import { ContextCandidateCarryforwardService } from './context-candidate-carryforward.service.js'; -import type { ContextCandidateStorePort } from './store.js'; -import type { BudgetExhaustedCandidateForCarryForward, CurrentRunEvidenceChunkForCarryForward } from './types.js'; +import { ContextCandidateCarryforwardService } from '../../../../src/context/ingest/context-candidates/context-candidate-carryforward.service.js'; +import type { ContextCandidateStorePort } from '../../../../src/context/ingest/context-candidates/store.js'; +import type { BudgetExhaustedCandidateForCarryForward, CurrentRunEvidenceChunkForCarryForward } from '../../../../src/context/ingest/context-candidates/types.js'; function candidate( overrides: Partial = {}, diff --git a/packages/cli/src/context/ingest/context-candidates/curator-pagination.service.test.ts b/packages/cli/test/context/ingest/context-candidates/curator-pagination.service.test.ts similarity index 96% rename from packages/cli/src/context/ingest/context-candidates/curator-pagination.service.test.ts rename to packages/cli/test/context/ingest/context-candidates/curator-pagination.service.test.ts index bf1876a3..7341f2e9 100644 --- a/packages/cli/src/context/ingest/context-candidates/curator-pagination.service.test.ts +++ b/packages/cli/test/context/ingest/context-candidates/curator-pagination.service.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ContextCandidateForDedup } from '../ports.js'; -import { type CuratorPaginationInput, CuratorPaginationService } from './curator-pagination.service.js'; -import type { ContextCandidateStorePort } from './store.js'; +import type { ContextCandidateForDedup } from '../../../../src/context/ingest/ports.js'; +import { type CuratorPaginationInput, CuratorPaginationService } from '../../../../src/context/ingest/context-candidates/curator-pagination.service.js'; +import type { ContextCandidateStorePort } from '../../../../src/context/ingest/context-candidates/store.js'; const candidate = (key: string, score: number): ContextCandidateForDedup => ({ id: `id-${key}`, diff --git a/packages/cli/src/context/ingest/context-candidates/embedding-text.test.ts b/packages/cli/test/context/ingest/context-candidates/embedding-text.test.ts similarity index 78% rename from packages/cli/src/context/ingest/context-candidates/embedding-text.test.ts rename to packages/cli/test/context/ingest/context-candidates/embedding-text.test.ts index e3e2e728..65857780 100644 --- a/packages/cli/src/context/ingest/context-candidates/embedding-text.test.ts +++ b/packages/cli/test/context/ingest/context-candidates/embedding-text.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildContextCandidateEmbeddingText } from './embedding-text.js'; +import { buildContextCandidateEmbeddingText } from '../../../../src/context/ingest/context-candidates/embedding-text.js'; describe('buildContextCandidateEmbeddingText', () => { it('matches the existing dedup embedding input format', () => { diff --git a/packages/cli/src/context/ingest/context-candidates/store.test.ts b/packages/cli/test/context/ingest/context-candidates/store.test.ts similarity index 89% rename from packages/cli/src/context/ingest/context-candidates/store.test.ts rename to packages/cli/test/context/ingest/context-candidates/store.test.ts index 1c2311ad..3dd3cbaf 100644 --- a/packages/cli/src/context/ingest/context-candidates/store.test.ts +++ b/packages/cli/test/context/ingest/context-candidates/store.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ContextCandidateForDedup } from '../ports.js'; -import type { ContextCandidateStorePort } from './store.js'; -import type { InsertContextCandidateInput } from './types.js'; +import type { ContextCandidateForDedup } from '../../../../src/context/ingest/ports.js'; +import type { ContextCandidateStorePort } from '../../../../src/context/ingest/context-candidates/store.js'; +import type { InsertContextCandidateInput } from '../../../../src/context/ingest/context-candidates/types.js'; const candidate: ContextCandidateForDedup = { id: 'candidate-1', diff --git a/packages/cli/src/context/ingest/context-evidence/context-evidence-index.service.test.ts b/packages/cli/test/context/ingest/context-evidence/context-evidence-index.service.test.ts similarity index 97% rename from packages/cli/src/context/ingest/context-evidence/context-evidence-index.service.test.ts rename to packages/cli/test/context/ingest/context-evidence/context-evidence-index.service.test.ts index d62c7b53..80caeec0 100644 --- a/packages/cli/src/context/ingest/context-evidence/context-evidence-index.service.test.ts +++ b/packages/cli/test/context/ingest/context-evidence/context-evidence-index.service.test.ts @@ -2,9 +2,9 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { ContextEvidenceIndexService } from './context-evidence-index.service.js'; -import type { ContextEvidenceIndexStorePort } from './store.js'; -import type { ContextEvidenceEmbeddingPort } from './types.js'; +import { ContextEvidenceIndexService } from '../../../../src/context/ingest/context-evidence/context-evidence-index.service.js'; +import type { ContextEvidenceIndexStorePort } from '../../../../src/context/ingest/context-evidence/store.js'; +import type { ContextEvidenceEmbeddingPort } from '../../../../src/context/ingest/context-evidence/types.js'; const vector384 = (first: number): number[] => [first, ...Array.from({ length: 383 }, () => 0)]; diff --git a/packages/cli/src/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts b/packages/cli/test/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts similarity index 98% rename from packages/cli/src/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts rename to packages/cli/test/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts index 6b575c00..6041c412 100644 --- a/packages/cli/src/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts +++ b/packages/cli/test/context/ingest/context-evidence/sqlite-context-evidence-store.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { InsertContextCandidateInput } from '../../../context/ingest/context-candidates/types.js'; -import type { JsonValue } from '../ports.js'; -import { SqliteContextEvidenceStore } from './sqlite-context-evidence-store.js'; +import type { InsertContextCandidateInput } from '../../../../src/context/ingest/context-candidates/types.js'; +import type { JsonValue } from '../../../../src/context/ingest/ports.js'; +import { SqliteContextEvidenceStore } from '../../../../src/context/ingest/context-evidence/sqlite-context-evidence-store.js'; describe('SqliteContextEvidenceStore', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/context-evidence/store.test.ts b/packages/cli/test/context/ingest/context-evidence/store.test.ts similarity index 92% rename from packages/cli/src/context/ingest/context-evidence/store.test.ts rename to packages/cli/test/context/ingest/context-evidence/store.test.ts index 9c2d281a..268aefec 100644 --- a/packages/cli/src/context/ingest/context-evidence/store.test.ts +++ b/packages/cli/test/context/ingest/context-evidence/store.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ContextEvidenceIndexStorePort } from './store.js'; -import type { ReplaceContextEvidenceChunk, UpsertContextEvidenceDocument } from './types.js'; +import type { ContextEvidenceIndexStorePort } from '../../../../src/context/ingest/context-evidence/store.js'; +import type { ReplaceContextEvidenceChunk, UpsertContextEvidenceDocument } from '../../../../src/context/ingest/context-evidence/types.js'; const documentInput: UpsertContextEvidenceDocument = { runId: 'run-1', diff --git a/packages/cli/src/context/ingest/dbt-shared/project-vars.test.ts b/packages/cli/test/context/ingest/dbt-shared/project-vars.test.ts similarity index 98% rename from packages/cli/src/context/ingest/dbt-shared/project-vars.test.ts rename to packages/cli/test/context/ingest/dbt-shared/project-vars.test.ts index 3b9ed6b3..c996e7aa 100644 --- a/packages/cli/src/context/ingest/dbt-shared/project-vars.test.ts +++ b/packages/cli/test/context/ingest/dbt-shared/project-vars.test.ts @@ -7,7 +7,7 @@ import { parseProjectName, parseProjectVars, resolveJinjaVariables, -} from './project-vars.js'; +} from '../../../../src/context/ingest/dbt-shared/project-vars.js'; function entries(map: Map): Record { return Object.fromEntries([...map.entries()].sort(([a], [b]) => a.localeCompare(b))); diff --git a/packages/cli/src/context/ingest/dbt-shared/schema-files.test.ts b/packages/cli/test/context/ingest/dbt-shared/schema-files.test.ts similarity index 93% rename from packages/cli/src/context/ingest/dbt-shared/schema-files.test.ts rename to packages/cli/test/context/ingest/dbt-shared/schema-files.test.ts index f55851f6..fd39c776 100644 --- a/packages/cli/src/context/ingest/dbt-shared/schema-files.test.ts +++ b/packages/cli/test/context/ingest/dbt-shared/schema-files.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { findDbtSchemaFiles, loadDbtSchemaFiles } from './schema-files.js'; +import { findDbtSchemaFiles, loadDbtSchemaFiles } from '../../../../src/context/ingest/dbt-shared/schema-files.js'; describe('dbt shared schema files', () => { let tmpRoot: string; diff --git a/packages/cli/src/context/ingest/diff-set.service.test.ts b/packages/cli/test/context/ingest/diff-set.service.test.ts similarity index 97% rename from packages/cli/src/context/ingest/diff-set.service.test.ts rename to packages/cli/test/context/ingest/diff-set.service.test.ts index 4eb3ceaa..9f198c54 100644 --- a/packages/cli/src/context/ingest/diff-set.service.test.ts +++ b/packages/cli/test/context/ingest/diff-set.service.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { computeDiffSetFromHashes, DiffSetService } from './diff-set.service.js'; +import { computeDiffSetFromHashes, DiffSetService } from '../../../src/context/ingest/diff-set.service.js'; function makeRepo(latest: Map) { return { diff --git a/packages/cli/src/context/ingest/final-gate-repair.test.ts b/packages/cli/test/context/ingest/final-gate-repair.test.ts similarity index 96% rename from packages/cli/src/context/ingest/final-gate-repair.test.ts rename to packages/cli/test/context/ingest/final-gate-repair.test.ts index 90ad707d..1a52442c 100644 --- a/packages/cli/src/context/ingest/final-gate-repair.test.ts +++ b/packages/cli/test/context/ingest/final-gate-repair.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { finalGateRepairPaths, repairFinalGateFailure } from './final-gate-repair.js'; -import { FileIngestTraceWriter } from './ingest-trace.js'; +import { finalGateRepairPaths, repairFinalGateFailure } from '../../../src/context/ingest/final-gate-repair.js'; +import { FileIngestTraceWriter } from '../../../src/context/ingest/ingest-trace.js'; async function makeHarness() { const root = await mkdtemp(join(tmpdir(), 'ktx-final-gate-repair-')); diff --git a/packages/cli/src/context/ingest/finalization-scope.test.ts b/packages/cli/test/context/ingest/finalization-scope.test.ts similarity index 98% rename from packages/cli/src/context/ingest/finalization-scope.test.ts rename to packages/cli/test/context/ingest/finalization-scope.test.ts index 28d0b863..02c535df 100644 --- a/packages/cli/src/context/ingest/finalization-scope.test.ts +++ b/packages/cli/test/context/ingest/finalization-scope.test.ts @@ -3,7 +3,7 @@ import { compareFinalizationDeclarations, deriveFinalizationTouchedSources, deriveFinalizationWikiPageKeys, -} from './finalization-scope.js'; +} from '../../../src/context/ingest/finalization-scope.js'; describe('deriveFinalizationWikiPageKeys', () => { it('maps changed global wiki markdown paths to page keys', () => { diff --git a/packages/cli/src/context/ingest/historic-sql-probes.test.ts b/packages/cli/test/context/ingest/historic-sql-probes.test.ts similarity index 96% rename from packages/cli/src/context/ingest/historic-sql-probes.test.ts rename to packages/cli/test/context/ingest/historic-sql-probes.test.ts index 275a84c7..542c4fde 100644 --- a/packages/cli/src/context/ingest/historic-sql-probes.test.ts +++ b/packages/cli/test/context/ingest/historic-sql-probes.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it, vi } from 'vitest'; -import type { HistoricSqlDialect } from './adapters/historic-sql/types.js'; +import type { HistoricSqlDialect } from '../../../src/context/ingest/adapters/historic-sql/types.js'; import { historicSqlProbeCatalogName, runHistoricSqlReadinessProbe, type HistoricSqlProbeRunner, type HistoricSqlProbeRunnerFactoryEntry, -} from './historic-sql-probes.js'; +} from '../../../src/context/ingest/historic-sql-probes.js'; function fakeRunner( dialect: HistoricSqlDialect, diff --git a/packages/cli/src/context/ingest/historic-sql-probes/bigquery-runner.test.ts b/packages/cli/test/context/ingest/historic-sql-probes/bigquery-runner.test.ts similarity index 93% rename from packages/cli/src/context/ingest/historic-sql-probes/bigquery-runner.test.ts rename to packages/cli/test/context/ingest/historic-sql-probes/bigquery-runner.test.ts index 7a2db117..d51aaf42 100644 --- a/packages/cli/src/context/ingest/historic-sql-probes/bigquery-runner.test.ts +++ b/packages/cli/test/context/ingest/historic-sql-probes/bigquery-runner.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { HistoricSqlGrantsMissingError } from '../adapters/historic-sql/errors.js'; -import { BigQueryJobsByProjectProbeRunner } from './bigquery-runner.js'; +import { HistoricSqlGrantsMissingError } from '../../../../src/context/ingest/adapters/historic-sql/errors.js'; +import { BigQueryJobsByProjectProbeRunner } from '../../../../src/context/ingest/historic-sql-probes/bigquery-runner.js'; describe('BigQueryJobsByProjectProbeRunner', () => { it('creates a region-scoped reader, runs it, and cleans up the connector', async () => { diff --git a/packages/cli/src/context/ingest/historic-sql-probes/postgres-runner.test.ts b/packages/cli/test/context/ingest/historic-sql-probes/postgres-runner.test.ts similarity index 94% rename from packages/cli/src/context/ingest/historic-sql-probes/postgres-runner.test.ts rename to packages/cli/test/context/ingest/historic-sql-probes/postgres-runner.test.ts index bcd6d187..c52443ee 100644 --- a/packages/cli/src/context/ingest/historic-sql-probes/postgres-runner.test.ts +++ b/packages/cli/test/context/ingest/historic-sql-probes/postgres-runner.test.ts @@ -3,8 +3,8 @@ import { HistoricSqlExtensionMissingError, HistoricSqlGrantsMissingError, HistoricSqlVersionUnsupportedError, -} from '../adapters/historic-sql/errors.js'; -import { PostgresPgssProbeRunner } from './postgres-runner.js'; +} from '../../../../src/context/ingest/adapters/historic-sql/errors.js'; +import { PostgresPgssProbeRunner } from '../../../../src/context/ingest/historic-sql-probes/postgres-runner.js'; describe('PostgresPgssProbeRunner', () => { it('runs the pg_stat_statements reader and cleans up the client', async () => { diff --git a/packages/cli/src/context/ingest/historic-sql-probes/snowflake-runner.test.ts b/packages/cli/test/context/ingest/historic-sql-probes/snowflake-runner.test.ts similarity index 91% rename from packages/cli/src/context/ingest/historic-sql-probes/snowflake-runner.test.ts rename to packages/cli/test/context/ingest/historic-sql-probes/snowflake-runner.test.ts index 2d6835bf..af3e73f3 100644 --- a/packages/cli/src/context/ingest/historic-sql-probes/snowflake-runner.test.ts +++ b/packages/cli/test/context/ingest/historic-sql-probes/snowflake-runner.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { HistoricSqlGrantsMissingError } from '../adapters/historic-sql/errors.js'; -import { SnowflakeAccountUsageProbeRunner } from './snowflake-runner.js'; +import { HistoricSqlGrantsMissingError } from '../../../../src/context/ingest/adapters/historic-sql/errors.js'; +import { SnowflakeAccountUsageProbeRunner } from '../../../../src/context/ingest/historic-sql-probes/snowflake-runner.js'; describe('SnowflakeAccountUsageProbeRunner', () => { it('runs the account usage reader and cleans up the client', async () => { diff --git a/packages/cli/src/context/ingest/ingest-bundle.runner.isolated-diff.test.ts b/packages/cli/test/context/ingest/ingest-bundle.runner.isolated-diff.test.ts similarity index 99% rename from packages/cli/src/context/ingest/ingest-bundle.runner.isolated-diff.test.ts rename to packages/cli/test/context/ingest/ingest-bundle.runner.isolated-diff.test.ts index 0201b881..bad40098 100644 --- a/packages/cli/src/context/ingest/ingest-bundle.runner.isolated-diff.test.ts +++ b/packages/cli/test/context/ingest/ingest-bundle.runner.isolated-diff.test.ts @@ -2,12 +2,12 @@ import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promis import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { GitService } from '../../context/core/git.service.js'; -import { SessionWorktreeService } from '../../context/core/session-worktree.service.js'; -import { LocalGitFileStore } from '../project/local-git-file-store.js'; -import { addTouchedSlSource } from '../../context/tools/touched-sl-sources.js'; -import { IngestBundleRunner } from './ingest-bundle.runner.js'; -import type { IngestBundleRunnerDeps } from './ports.js'; +import { GitService } from '../../../src/context/core/git.service.js'; +import { SessionWorktreeService } from '../../../src/context/core/session-worktree.service.js'; +import { LocalGitFileStore } from '../../../src/context/project/local-git-file-store.js'; +import { addTouchedSlSource } from '../../../src/context/tools/touched-sl-sources.js'; +import { IngestBundleRunner } from '../../../src/context/ingest/ingest-bundle.runner.js'; +import type { IngestBundleRunnerDeps } from '../../../src/context/ingest/ports.js'; async function makeRealGitRuntime() { const homeDir = await mkdtemp(join(tmpdir(), 'ktx-isolated-runner-')); diff --git a/packages/cli/src/context/ingest/ingest-bundle.runner.test.ts b/packages/cli/test/context/ingest/ingest-bundle.runner.test.ts similarity index 99% rename from packages/cli/src/context/ingest/ingest-bundle.runner.test.ts rename to packages/cli/test/context/ingest/ingest-bundle.runner.test.ts index 85f45049..447cd01e 100644 --- a/packages/cli/src/context/ingest/ingest-bundle.runner.test.ts +++ b/packages/cli/test/context/ingest/ingest-bundle.runner.test.ts @@ -2,11 +2,11 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { addTouchedSlSource } from '../../context/tools/touched-sl-sources.js'; -import { IngestBundleRunner } from './ingest-bundle.runner.js'; -import { createMemoryFlowLiveBuffer } from './memory-flow/live-buffer.js'; -import type { MemoryFlowReplayInput } from './memory-flow/types.js'; -import type { IngestBundleRunnerDeps } from './ports.js'; +import { addTouchedSlSource } from '../../../src/context/tools/touched-sl-sources.js'; +import { IngestBundleRunner } from '../../../src/context/ingest/ingest-bundle.runner.js'; +import { createMemoryFlowLiveBuffer } from '../../../src/context/ingest/memory-flow/live-buffer.js'; +import type { MemoryFlowReplayInput } from '../../../src/context/ingest/memory-flow/types.js'; +import type { IngestBundleRunnerDeps } from '../../../src/context/ingest/ports.js'; class TestJobContext { private currentProgress = 0; diff --git a/packages/cli/src/context/ingest/ingest-prompts.test.ts b/packages/cli/test/context/ingest/ingest-prompts.test.ts similarity index 79% rename from packages/cli/src/context/ingest/ingest-prompts.test.ts rename to packages/cli/test/context/ingest/ingest-prompts.test.ts index 8adcbfef..54617eb2 100644 --- a/packages/cli/src/context/ingest/ingest-prompts.test.ts +++ b/packages/cli/test/context/ingest/ingest-prompts.test.ts @@ -8,7 +8,7 @@ function forbiddenProductPattern() { describe('ingest prompt assets', () => { it('teaches WorkUnit agents to apply canonical pins before writing contested artifacts', async () => { const prompt = await readFile( - new URL('../../prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), + new URL('../../../src/prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), 'utf-8', ); @@ -20,7 +20,7 @@ describe('ingest prompt assets', () => { it('uses product-neutral KTX runtime wording', async () => { const prompt = await readFile( - new URL('../../prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), + new URL('../../../src/prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), 'utf-8', ); @@ -31,7 +31,7 @@ describe('ingest prompt assets', () => { it('uses shipped warehouse verification tools in the WorkUnit prompt', async () => { const prompt = await readFile( - new URL('../../prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), + new URL('../../../src/prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url), 'utf-8', ); @@ -42,7 +42,7 @@ describe('ingest prompt assets', () => { }); it('does not route historic-SQL through page-triage prompt examples', async () => { - const prompt = await readFile(new URL('../../prompts/skills/page_triage_classifier.md', import.meta.url), 'utf-8'); + const prompt = await readFile(new URL('../../../src/prompts/skills/page_triage_classifier.md', import.meta.url), 'utf-8'); expect(prompt).not.toContain(['historic_sql', 'template'].join('_')); expect(prompt).not.toContain('service_account_only=true AND below the frequency floor'); diff --git a/packages/cli/src/context/ingest/ingest-runtime-assets.test.ts b/packages/cli/test/context/ingest/ingest-runtime-assets.test.ts similarity index 92% rename from packages/cli/src/context/ingest/ingest-runtime-assets.test.ts rename to packages/cli/test/context/ingest/ingest-runtime-assets.test.ts index f6a46111..ad77e692 100644 --- a/packages/cli/src/context/ingest/ingest-runtime-assets.test.ts +++ b/packages/cli/test/context/ingest/ingest-runtime-assets.test.ts @@ -2,11 +2,11 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { PromptService } from '../../context/prompts/prompt.service.js'; -import { SkillsRegistryService } from '../../context/skills/skills-registry.service.js'; +import { PromptService } from '../../../src/context/prompts/prompt.service.js'; +import { SkillsRegistryService } from '../../../src/context/skills/skills-registry.service.js'; -const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url)); -const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url)); +const promptsDir = fileURLToPath(new URL('../../../src/prompts', import.meta.url)); +const skillsDir = fileURLToPath(new URL('../../../src/skills', import.meta.url)); const adapterSkillNames = [ 'live_database_ingest', diff --git a/packages/cli/src/context/ingest/ingest-trace.test.ts b/packages/cli/test/context/ingest/ingest-trace.test.ts similarity index 98% rename from packages/cli/src/context/ingest/ingest-trace.test.ts rename to packages/cli/test/context/ingest/ingest-trace.test.ts index 88b56a37..b10e2118 100644 --- a/packages/cli/src/context/ingest/ingest-trace.test.ts +++ b/packages/cli/test/context/ingest/ingest-trace.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { FileIngestTraceWriter, ingestTracePathForJob, traceTimed } from './ingest-trace.js'; +import { FileIngestTraceWriter, ingestTracePathForJob, traceTimed } from '../../../src/context/ingest/ingest-trace.js'; describe('FileIngestTraceWriter', () => { it('persists structured trace events as JSONL', async () => { diff --git a/packages/cli/src/context/ingest/isolated-diff/git-patch.test.ts b/packages/cli/test/context/ingest/isolated-diff/git-patch.test.ts similarity index 97% rename from packages/cli/src/context/ingest/isolated-diff/git-patch.test.ts rename to packages/cli/test/context/ingest/isolated-diff/git-patch.test.ts index 2a48ce9b..f9925ebb 100644 --- a/packages/cli/src/context/ingest/isolated-diff/git-patch.test.ts +++ b/packages/cli/test/context/ingest/isolated-diff/git-patch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { assertPatchAllowedForWorkUnit, parsePatchTouchedPaths, textArtifactRoots } from './git-patch.js'; +import { assertPatchAllowedForWorkUnit, parsePatchTouchedPaths, textArtifactRoots } from '../../../../src/context/ingest/isolated-diff/git-patch.js'; describe('isolated diff patch contract', () => { it('parses touched paths from no-rename git patches', () => { diff --git a/packages/cli/src/context/ingest/isolated-diff/patch-integrator.test.ts b/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts similarity index 98% rename from packages/cli/src/context/ingest/isolated-diff/patch-integrator.test.ts rename to packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts index e547e22e..1deabfe8 100644 --- a/packages/cli/src/context/ingest/isolated-diff/patch-integrator.test.ts +++ b/packages/cli/test/context/ingest/isolated-diff/patch-integrator.test.ts @@ -2,9 +2,9 @@ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { GitService } from '../../../context/core/git.service.js'; -import { FileIngestTraceWriter } from '../ingest-trace.js'; -import { integrateWorkUnitPatch } from './patch-integrator.js'; +import { GitService } from '../../../../src/context/core/git.service.js'; +import { FileIngestTraceWriter } from '../../../../src/context/ingest/ingest-trace.js'; +import { integrateWorkUnitPatch } from '../../../../src/context/ingest/isolated-diff/patch-integrator.js'; async function makeRepo() { const homeDir = await mkdtemp(join(tmpdir(), 'ktx-integrate-')); diff --git a/packages/cli/src/context/ingest/isolated-diff/textual-conflict-resolver.test.ts b/packages/cli/test/context/ingest/isolated-diff/textual-conflict-resolver.test.ts similarity index 94% rename from packages/cli/src/context/ingest/isolated-diff/textual-conflict-resolver.test.ts rename to packages/cli/test/context/ingest/isolated-diff/textual-conflict-resolver.test.ts index ae5b4e21..a03eb66d 100644 --- a/packages/cli/src/context/ingest/isolated-diff/textual-conflict-resolver.test.ts +++ b/packages/cli/test/context/ingest/isolated-diff/textual-conflict-resolver.test.ts @@ -2,8 +2,8 @@ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { FileIngestTraceWriter } from '../ingest-trace.js'; -import { resolveTextualConflict } from './textual-conflict-resolver.js'; +import { FileIngestTraceWriter } from '../../../../src/context/ingest/ingest-trace.js'; +import { resolveTextualConflict } from '../../../../src/context/ingest/isolated-diff/textual-conflict-resolver.js'; async function makeHarness() { const root = await mkdtemp(join(tmpdir(), 'ktx-textual-resolver-')); diff --git a/packages/cli/src/context/ingest/isolated-diff/work-unit-executor.test.ts b/packages/cli/test/context/ingest/isolated-diff/work-unit-executor.test.ts similarity index 95% rename from packages/cli/src/context/ingest/isolated-diff/work-unit-executor.test.ts rename to packages/cli/test/context/ingest/isolated-diff/work-unit-executor.test.ts index 5975dee8..cc06ba61 100644 --- a/packages/cli/src/context/ingest/isolated-diff/work-unit-executor.test.ts +++ b/packages/cli/test/context/ingest/isolated-diff/work-unit-executor.test.ts @@ -2,9 +2,9 @@ import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import { GitService } from '../../../context/core/git.service.js'; -import { FileIngestTraceWriter } from '../ingest-trace.js'; -import { runIsolatedWorkUnit } from './work-unit-executor.js'; +import { GitService } from '../../../../src/context/core/git.service.js'; +import { FileIngestTraceWriter } from '../../../../src/context/ingest/ingest-trace.js'; +import { runIsolatedWorkUnit } from '../../../../src/context/ingest/isolated-diff/work-unit-executor.js'; async function makeGit() { const homeDir = await mkdtemp(join(tmpdir(), 'ktx-isolated-wu-')); diff --git a/packages/cli/src/context/ingest/local-adapters.test.ts b/packages/cli/test/context/ingest/local-adapters.test.ts similarity index 97% rename from packages/cli/src/context/ingest/local-adapters.test.ts rename to packages/cli/test/context/ingest/local-adapters.test.ts index 373fc125..f70c4879 100644 --- a/packages/cli/src/context/ingest/local-adapters.test.ts +++ b/packages/cli/test/context/ingest/local-adapters.test.ts @@ -2,12 +2,12 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../context/project/project.js'; -import type { SqlAnalysisPort } from '../../context/sql-analysis/ports.js'; -import type { HistoricSqlReader } from './adapters/historic-sql/types.js'; -import { LocalLookerRuntimeStore } from './adapters/looker/local-runtime-store.js'; -import { LocalNotionRuntimeStore } from './adapters/notion/local-state-store.js'; -import { createDefaultLocalIngestAdapters, localPullConfigForAdapter } from './local-adapters.js'; +import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../../src/context/project/project.js'; +import type { SqlAnalysisPort } from '../../../src/context/sql-analysis/ports.js'; +import type { HistoricSqlReader } from '../../../src/context/ingest/adapters/historic-sql/types.js'; +import { LocalLookerRuntimeStore } from '../../../src/context/ingest/adapters/looker/local-runtime-store.js'; +import { LocalNotionRuntimeStore } from '../../../src/context/ingest/adapters/notion/local-state-store.js'; +import { createDefaultLocalIngestAdapters, localPullConfigForAdapter } from '../../../src/context/ingest/local-adapters.js'; describe('local ingest adapters', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/local-bundle-ingest.test.ts b/packages/cli/test/context/ingest/local-bundle-ingest.test.ts similarity index 97% rename from packages/cli/src/context/ingest/local-bundle-ingest.test.ts rename to packages/cli/test/context/ingest/local-bundle-ingest.test.ts index 4b4b834c..744af5be 100644 --- a/packages/cli/src/context/ingest/local-bundle-ingest.test.ts +++ b/packages/cli/test/context/ingest/local-bundle-ingest.test.ts @@ -3,22 +3,22 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import Database from 'better-sqlite3'; import YAML from 'yaml'; -import type { AgentRunnerPort, RunLoopParams } from '../../context/llm/runtime-port.js'; -import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../context/project/project.js'; +import type { AgentRunnerPort, RunLoopParams } from '../../../src/context/llm/runtime-port.js'; +import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../../src/context/project/project.js'; import { makeLocalGitRepo } from '../test/make-local-git-repo.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { FakeSourceAdapter } from './adapters/fake/fake.adapter.js'; -import { projectHistoricSqlEvidence } from './adapters/historic-sql/projection.js'; -import { LocalLookerRuntimeStore } from './adapters/looker/local-runtime-store.js'; -import { createDefaultLocalIngestAdapters, localPullConfigForAdapter } from './local-adapters.js'; -import { getLocalIngestStatus, runLocalIngest } from './local-ingest.js'; +import { FakeSourceAdapter } from '../../../src/context/ingest/adapters/fake/fake.adapter.js'; +import { projectHistoricSqlEvidence } from '../../../src/context/ingest/adapters/historic-sql/projection.js'; +import { LocalLookerRuntimeStore } from '../../../src/context/ingest/adapters/looker/local-runtime-store.js'; +import { createDefaultLocalIngestAdapters, localPullConfigForAdapter } from '../../../src/context/ingest/local-adapters.js'; +import { getLocalIngestStatus, runLocalIngest } from '../../../src/context/ingest/local-ingest.js'; import type { ChunkResult, DeterministicFinalizationContext, DiffSet, FinalizationResult, SourceAdapter, -} from './types.js'; +} from '../../../src/context/ingest/types.js'; class TestAgentRunner implements AgentRunnerPort { runLoop = vi.fn().mockResolvedValue({ stopReason: 'natural' as const }); diff --git a/packages/cli/src/context/ingest/local-bundle-runtime.test.ts b/packages/cli/test/context/ingest/local-bundle-runtime.test.ts similarity index 97% rename from packages/cli/src/context/ingest/local-bundle-runtime.test.ts rename to packages/cli/test/context/ingest/local-bundle-runtime.test.ts index 3c87c351..c17d6a6b 100644 --- a/packages/cli/src/context/ingest/local-bundle-runtime.test.ts +++ b/packages/cli/test/context/ingest/local-bundle-runtime.test.ts @@ -1,11 +1,11 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { AgentRunnerPort } from '../../context/llm/runtime-port.js'; -import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../context/project/project.js'; +import type { AgentRunnerPort } from '../../../src/context/llm/runtime-port.js'; +import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { FakeSourceAdapter } from './adapters/fake/fake.adapter.js'; -import { createLocalBundleIngestRuntime } from './local-bundle-runtime.js'; +import { FakeSourceAdapter } from '../../../src/context/ingest/adapters/fake/fake.adapter.js'; +import { createLocalBundleIngestRuntime } from '../../../src/context/ingest/local-bundle-runtime.js'; type RuntimeWithConnectionDeps = { deps: { diff --git a/packages/cli/src/context/ingest/local-embedding-provider.integration.test.ts b/packages/cli/test/context/ingest/local-embedding-provider.integration.test.ts similarity index 90% rename from packages/cli/src/context/ingest/local-embedding-provider.integration.test.ts rename to packages/cli/test/context/ingest/local-embedding-provider.integration.test.ts index 34114e88..f0214957 100644 --- a/packages/cli/src/context/ingest/local-embedding-provider.integration.test.ts +++ b/packages/cli/test/context/ingest/local-embedding-provider.integration.test.ts @@ -2,11 +2,11 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KtxEmbeddingPort } from '../core/embedding.js'; -import { CandidateDedupService } from './context-candidates/candidate-dedup.service.js'; -import { ContextEvidenceIndexService } from './context-evidence/context-evidence-index.service.js'; -import { SqliteContextEvidenceStore } from './context-evidence/sqlite-context-evidence-store.js'; -import type { DiffSet } from './types.js'; +import type { KtxEmbeddingPort } from '../../../src/context/core/embedding.js'; +import { CandidateDedupService } from '../../../src/context/ingest/context-candidates/candidate-dedup.service.js'; +import { ContextEvidenceIndexService } from '../../../src/context/ingest/context-evidence/context-evidence-index.service.js'; +import { SqliteContextEvidenceStore } from '../../../src/context/ingest/context-evidence/sqlite-context-evidence-store.js'; +import type { DiffSet } from '../../../src/context/ingest/types.js'; describe('local ingest embedding providers with SQLite ingest stores', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/local-mapping-reconcile.test.ts b/packages/cli/test/context/ingest/local-mapping-reconcile.test.ts similarity index 87% rename from packages/cli/src/context/ingest/local-mapping-reconcile.test.ts rename to packages/cli/test/context/ingest/local-mapping-reconcile.test.ts index 3eed9d53..8f7080c6 100644 --- a/packages/cli/src/context/ingest/local-mapping-reconcile.test.ts +++ b/packages/cli/test/context/ingest/local-mapping-reconcile.test.ts @@ -2,10 +2,10 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; -import { ktxLocalStateDbPath } from '../../context/project/local-state-db.js'; -import type { KtxLocalProject } from '../../context/project/project.js'; -import { LocalLookerRuntimeStore } from './adapters/looker/local-runtime-store.js'; -import { seedLocalMappingStateFromKtxYaml } from './local-mapping-reconcile.js'; +import { ktxLocalStateDbPath } from '../../../src/context/project/local-state-db.js'; +import type { KtxLocalProject } from '../../../src/context/project/project.js'; +import { LocalLookerRuntimeStore } from '../../../src/context/ingest/adapters/looker/local-runtime-store.js'; +import { seedLocalMappingStateFromKtxYaml } from '../../../src/context/ingest/local-mapping-reconcile.js'; describe('local mapping yaml reconciliation bridge', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/local-metabase-ingest.test.ts b/packages/cli/test/context/ingest/local-metabase-ingest.test.ts similarity index 95% rename from packages/cli/src/context/ingest/local-metabase-ingest.test.ts rename to packages/cli/test/context/ingest/local-metabase-ingest.test.ts index a6f5c4e0..06822aa2 100644 --- a/packages/cli/src/context/ingest/local-metabase-ingest.test.ts +++ b/packages/cli/test/context/ingest/local-metabase-ingest.test.ts @@ -1,12 +1,12 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { AgentRunnerPort, RunLoopParams } from '../../context/llm/runtime-port.js'; +import type { AgentRunnerPort, RunLoopParams } from '../../../src/context/llm/runtime-port.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { LocalMetabaseDiscoveryCache } from './adapters/metabase/local-source-state-store.js'; -import { getLocalIngestStatus, runLocalMetabaseIngest } from './local-ingest.js'; -import type { ChunkResult, FetchContext, SourceAdapter } from './types.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { LocalMetabaseDiscoveryCache } from '../../../src/context/ingest/adapters/metabase/local-source-state-store.js'; +import { getLocalIngestStatus, runLocalMetabaseIngest } from '../../../src/context/ingest/local-ingest.js'; +import type { ChunkResult, FetchContext, SourceAdapter } from '../../../src/context/ingest/types.js'; class TestAgentRunner implements AgentRunnerPort { runLoop = vi.fn(async (params: RunLoopParams) => { diff --git a/packages/cli/src/context/ingest/local-stage-ingest.test.ts b/packages/cli/test/context/ingest/local-stage-ingest.test.ts similarity index 97% rename from packages/cli/src/context/ingest/local-stage-ingest.test.ts rename to packages/cli/test/context/ingest/local-stage-ingest.test.ts index 3f0e617f..c57d18b4 100644 --- a/packages/cli/src/context/ingest/local-stage-ingest.test.ts +++ b/packages/cli/test/context/ingest/local-stage-ingest.test.ts @@ -2,16 +2,16 @@ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promise import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../context/project/project.js'; -import { FakeSourceAdapter } from './adapters/fake/fake.adapter.js'; -import { createDefaultLocalIngestAdapters } from './local-adapters.js'; +import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../../src/context/project/project.js'; +import { FakeSourceAdapter } from '../../../src/context/ingest/adapters/fake/fake.adapter.js'; +import { createDefaultLocalIngestAdapters } from '../../../src/context/ingest/local-adapters.js'; import { getLocalStageOnlyIngestStatus, runLocalStageOnlyIngest, -} from './local-stage-ingest.js'; -import { createMemoryFlowLiveBuffer } from './memory-flow/live-buffer.js'; -import type { MemoryFlowReplayInput } from './memory-flow/types.js'; -import type { SourceAdapter } from './types.js'; +} from '../../../src/context/ingest/local-stage-ingest.js'; +import { createMemoryFlowLiveBuffer } from '../../../src/context/ingest/memory-flow/live-buffer.js'; +import type { MemoryFlowReplayInput } from '../../../src/context/ingest/memory-flow/types.js'; +import type { SourceAdapter } from '../../../src/context/ingest/types.js'; async function writeWarehouseConfig(projectDir: string): Promise { await writeFile( diff --git a/packages/cli/src/context/ingest/memory-flow/acceptance-fixtures.ts b/packages/cli/test/context/ingest/memory-flow/acceptance-fixtures.ts similarity index 98% rename from packages/cli/src/context/ingest/memory-flow/acceptance-fixtures.ts rename to packages/cli/test/context/ingest/memory-flow/acceptance-fixtures.ts index 66f1afb8..1d57aaa8 100644 --- a/packages/cli/src/context/ingest/memory-flow/acceptance-fixtures.ts +++ b/packages/cli/test/context/ingest/memory-flow/acceptance-fixtures.ts @@ -1,4 +1,4 @@ -import type { MemoryFlowReplayInput } from './types.js'; +import type { MemoryFlowReplayInput } from '../../../../src/context/ingest/memory-flow/types.js'; function baseScenario(overrides: Partial = {}): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/context/ingest/memory-flow/acceptance.test.ts b/packages/cli/test/context/ingest/memory-flow/acceptance.test.ts similarity index 92% rename from packages/cli/src/context/ingest/memory-flow/acceptance.test.ts rename to packages/cli/test/context/ingest/memory-flow/acceptance.test.ts index 42bff8a0..04e7fa46 100644 --- a/packages/cli/src/context/ingest/memory-flow/acceptance.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/acceptance.test.ts @@ -6,8 +6,8 @@ import { successfulReplayScenario, validationRevertScenario, } from './acceptance-fixtures.js'; -import { renderMemoryFlowReplay } from './render.js'; -import { buildMemoryFlowViewModel } from './view-model.js'; +import { renderMemoryFlowReplay } from '../../../../src/context/ingest/memory-flow/render.js'; +import { buildMemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/view-model.js'; function renderScenario(input = successfulReplayScenario(), terminalWidth = 140): string { return renderMemoryFlowReplay(buildMemoryFlowViewModel(input), { terminalWidth }); diff --git a/packages/cli/src/context/ingest/memory-flow/events.test.ts b/packages/cli/test/context/ingest/memory-flow/events.test.ts similarity index 97% rename from packages/cli/src/context/ingest/memory-flow/events.test.ts rename to packages/cli/test/context/ingest/memory-flow/events.test.ts index be97342b..e29405a4 100644 --- a/packages/cli/src/context/ingest/memory-flow/events.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/events.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { LocalIngestRunRecord } from '../local-stage-ingest.js'; -import type { IngestReportSnapshot } from '../reports.js'; -import { ingestReportToMemoryFlowReplay, localIngestRunToMemoryFlowReplay } from './events.js'; +import type { LocalIngestRunRecord } from '../../../../src/context/ingest/local-stage-ingest.js'; +import type { IngestReportSnapshot } from '../../../../src/context/ingest/reports.js'; +import { ingestReportToMemoryFlowReplay, localIngestRunToMemoryFlowReplay } from '../../../../src/context/ingest/memory-flow/events.js'; function localRecord(): LocalIngestRunRecord { return { diff --git a/packages/cli/src/context/ingest/memory-flow/interaction.test.ts b/packages/cli/test/context/ingest/memory-flow/interaction.test.ts similarity index 98% rename from packages/cli/src/context/ingest/memory-flow/interaction.test.ts rename to packages/cli/test/context/ingest/memory-flow/interaction.test.ts index 290180df..012373f7 100644 --- a/packages/cli/src/context/ingest/memory-flow/interaction.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/interaction.test.ts @@ -8,8 +8,8 @@ import { selectedMemoryFlowColumn, selectedMemoryFlowDetails, visibleMemoryFlowChips, -} from './interaction.js'; -import type { MemoryFlowInteractionState, MemoryFlowViewModel } from './types.js'; +} from '../../../../src/context/ingest/memory-flow/interaction.js'; +import type { MemoryFlowInteractionState, MemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/types.js'; function view(): MemoryFlowViewModel { return { diff --git a/packages/cli/src/context/ingest/memory-flow/interactive-render.test.ts b/packages/cli/test/context/ingest/memory-flow/interactive-render.test.ts similarity index 95% rename from packages/cli/src/context/ingest/memory-flow/interactive-render.test.ts rename to packages/cli/test/context/ingest/memory-flow/interactive-render.test.ts index 6b703a2a..8c9a6a52 100644 --- a/packages/cli/src/context/ingest/memory-flow/interactive-render.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/interactive-render.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { createInitialMemoryFlowInteractionState, reduceMemoryFlowInteractionState } from './interaction.js'; -import { renderMemoryFlowInteractive } from './interactive-render.js'; -import type { MemoryFlowViewModel } from './types.js'; +import { createInitialMemoryFlowInteractionState, reduceMemoryFlowInteractionState } from '../../../../src/context/ingest/memory-flow/interaction.js'; +import { renderMemoryFlowInteractive } from '../../../../src/context/ingest/memory-flow/interactive-render.js'; +import type { MemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/types.js'; function view(): MemoryFlowViewModel { return { diff --git a/packages/cli/src/context/ingest/memory-flow/live-buffer.test.ts b/packages/cli/test/context/ingest/memory-flow/live-buffer.test.ts similarity index 95% rename from packages/cli/src/context/ingest/memory-flow/live-buffer.test.ts rename to packages/cli/test/context/ingest/memory-flow/live-buffer.test.ts index fc1962a3..a9c1210b 100644 --- a/packages/cli/src/context/ingest/memory-flow/live-buffer.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/live-buffer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { createMemoryFlowLiveBuffer, sanitizeMemoryFlowError } from './live-buffer.js'; -import type { MemoryFlowReplayInput } from './types.js'; +import { createMemoryFlowLiveBuffer, sanitizeMemoryFlowError } from '../../../../src/context/ingest/memory-flow/live-buffer.js'; +import type { MemoryFlowReplayInput } from '../../../../src/context/ingest/memory-flow/types.js'; function initialReplay(): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/context/ingest/memory-flow/render.test.ts b/packages/cli/test/context/ingest/memory-flow/render.test.ts similarity index 95% rename from packages/cli/src/context/ingest/memory-flow/render.test.ts rename to packages/cli/test/context/ingest/memory-flow/render.test.ts index 0053eefd..adcc89f0 100644 --- a/packages/cli/src/context/ingest/memory-flow/render.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/render.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { MemoryFlowViewModel } from './types.js'; -import { renderMemoryFlowReplay } from './render.js'; +import type { MemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/types.js'; +import { renderMemoryFlowReplay } from '../../../../src/context/ingest/memory-flow/render.js'; function view(): MemoryFlowViewModel { return { diff --git a/packages/cli/src/context/ingest/memory-flow/schema.test.ts b/packages/cli/test/context/ingest/memory-flow/schema.test.ts similarity index 97% rename from packages/cli/src/context/ingest/memory-flow/schema.test.ts rename to packages/cli/test/context/ingest/memory-flow/schema.test.ts index b8c70856..1aaeec4b 100644 --- a/packages/cli/src/context/ingest/memory-flow/schema.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/schema.test.ts @@ -3,8 +3,8 @@ import { memoryFlowReplayInputSchema, memoryFlowStreamEventSchema, parseMemoryFlowReplayInput, -} from './schema.js'; -import type { MemoryFlowReplayInput } from './types.js'; +} from '../../../../src/context/ingest/memory-flow/schema.js'; +import type { MemoryFlowReplayInput } from '../../../../src/context/ingest/memory-flow/types.js'; function snapshot(overrides: Partial = {}): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/context/ingest/memory-flow/summary.test.ts b/packages/cli/test/context/ingest/memory-flow/summary.test.ts similarity index 96% rename from packages/cli/src/context/ingest/memory-flow/summary.test.ts rename to packages/cli/test/context/ingest/memory-flow/summary.test.ts index a22ca1ff..967acf46 100644 --- a/packages/cli/src/context/ingest/memory-flow/summary.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/summary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { MemoryFlowReplayInput } from './types.js'; -import { formatMemoryFlowFinalSummary } from './summary.js'; +import type { MemoryFlowReplayInput } from '../../../../src/context/ingest/memory-flow/types.js'; +import { formatMemoryFlowFinalSummary } from '../../../../src/context/ingest/memory-flow/summary.js'; function input(overrides: Partial = {}): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/context/ingest/memory-flow/view-model.test.ts b/packages/cli/test/context/ingest/memory-flow/view-model.test.ts similarity index 98% rename from packages/cli/src/context/ingest/memory-flow/view-model.test.ts rename to packages/cli/test/context/ingest/memory-flow/view-model.test.ts index 4e6edae3..6bd64943 100644 --- a/packages/cli/src/context/ingest/memory-flow/view-model.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/view-model.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { MemoryFlowReplayInput } from './types.js'; -import { buildMemoryFlowViewModel } from './view-model.js'; +import type { MemoryFlowReplayInput } from '../../../../src/context/ingest/memory-flow/types.js'; +import { buildMemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/view-model.js'; function replayInput(): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/context/ingest/memory-flow/visuals.test.ts b/packages/cli/test/context/ingest/memory-flow/visuals.test.ts similarity index 94% rename from packages/cli/src/context/ingest/memory-flow/visuals.test.ts rename to packages/cli/test/context/ingest/memory-flow/visuals.test.ts index 7144c897..da271248 100644 --- a/packages/cli/src/context/ingest/memory-flow/visuals.test.ts +++ b/packages/cli/test/context/ingest/memory-flow/visuals.test.ts @@ -3,8 +3,8 @@ import { buildMemoryFlowVisualModel, memoryFlowStatusBadge, renderMemoryFlowConnectorLine, -} from './visuals.js'; -import type { MemoryFlowViewModel } from './types.js'; +} from '../../../../src/context/ingest/memory-flow/visuals.js'; +import type { MemoryFlowViewModel } from '../../../../src/context/ingest/memory-flow/types.js'; function viewWithStatuses(statuses: Array<'waiting' | 'active' | 'complete' | 'warning' | 'failed'>): MemoryFlowViewModel { const titles = ['SOURCE', 'CHUNKS', 'WORKUNITS', 'ACTIONS', 'GATES', 'SAVED']; diff --git a/packages/cli/src/context/ingest/page-triage/page-triage.service.test.ts b/packages/cli/test/context/ingest/page-triage/page-triage.service.test.ts similarity index 99% rename from packages/cli/src/context/ingest/page-triage/page-triage.service.test.ts rename to packages/cli/test/context/ingest/page-triage/page-triage.service.test.ts index 6432347d..33aa2979 100644 --- a/packages/cli/src/context/ingest/page-triage/page-triage.service.test.ts +++ b/packages/cli/test/context/ingest/page-triage/page-triage.service.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { PageTriageService } from './page-triage.service.js'; +import { PageTriageService } from '../../../../src/context/ingest/page-triage/page-triage.service.js'; describe('PageTriageService', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/raw-sources-paths.test.ts b/packages/cli/test/context/ingest/raw-sources-paths.test.ts similarity index 92% rename from packages/cli/src/context/ingest/raw-sources-paths.test.ts rename to packages/cli/test/context/ingest/raw-sources-paths.test.ts index dcc17ddc..045f479f 100644 --- a/packages/cli/src/context/ingest/raw-sources-paths.test.ts +++ b/packages/cli/test/context/ingest/raw-sources-paths.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildSyncId, provenanceMarker, rawSourcesDirForSync, rawSourcesRoot } from './raw-sources-paths.js'; +import { buildSyncId, provenanceMarker, rawSourcesDirForSync, rawSourcesRoot } from '../../../src/context/ingest/raw-sources-paths.js'; describe('raw-sources paths', () => { it('buildSyncId uses timestamp + jobId', () => { diff --git a/packages/cli/src/context/ingest/repo-fetch.test.ts b/packages/cli/test/context/ingest/repo-fetch.test.ts similarity index 95% rename from packages/cli/src/context/ingest/repo-fetch.test.ts rename to packages/cli/test/context/ingest/repo-fetch.test.ts index dcefd6ca..d9e66e33 100644 --- a/packages/cli/src/context/ingest/repo-fetch.test.ts +++ b/packages/cli/test/context/ingest/repo-fetch.test.ts @@ -4,10 +4,10 @@ import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { makeLocalGitRepo } from '../test/make-local-git-repo.js'; -const FIXTURE_ROOT = join(__dirname, '../../test/fixtures/lookml/single-model'); +const FIXTURE_ROOT = join(__dirname, '../../fixtures/lookml/single-model'); async function loadRepoFetch() { - return await import('./repo-fetch.js'); + return await import('../../../src/context/ingest/repo-fetch.js'); } describe('repo-fetch', () => { @@ -16,13 +16,13 @@ describe('repo-fetch', () => { beforeEach(async () => { tmpRoot = await mkdtemp(join(tmpdir(), 'repo-fetch-')); vi.resetModules(); - vi.doUnmock('./git-env.js'); + vi.doUnmock('../../../src/context/ingest/git-env.js'); }); afterEach(async () => { vi.restoreAllMocks(); vi.resetModules(); - vi.doUnmock('./git-env.js'); + vi.doUnmock('../../../src/context/ingest/git-env.js'); await rm(tmpRoot, { recursive: true, force: true }); }); @@ -98,7 +98,7 @@ describe('repo-fetch', () => { it('falls back to a fresh clone when the existing cache diverges locally', async () => { const { cloneOrPull } = await loadRepoFetch(); - const { createSimpleGit } = await import('./git-env.js'); + const { createSimpleGit } = await import('../../../src/context/ingest/git-env.js'); const repo = await makeLocalGitRepo(FIXTURE_ROOT, join(tmpRoot, 'origin')); const cacheDir = join(tmpRoot, 'cache', 'conn-diverged'); @@ -197,7 +197,7 @@ describe('repo-fetch', () => { clone: vi.fn(async () => undefined), }; - vi.doMock('./git-env.js', () => ({ + vi.doMock('../../../src/context/ingest/git-env.js', () => ({ createSimpleGit: vi.fn((baseDir?: string) => (baseDir ? worktreeGit : rootGit)), })); diff --git a/packages/cli/src/context/ingest/report-snapshot.test.ts b/packages/cli/test/context/ingest/report-snapshot.test.ts similarity index 99% rename from packages/cli/src/context/ingest/report-snapshot.test.ts rename to packages/cli/test/context/ingest/report-snapshot.test.ts index 028c222c..36f822e1 100644 --- a/packages/cli/src/context/ingest/report-snapshot.test.ts +++ b/packages/cli/test/context/ingest/report-snapshot.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseIngestReportSnapshot } from './report-snapshot.js'; +import { parseIngestReportSnapshot } from '../../../src/context/ingest/report-snapshot.js'; function validReportSnapshot() { return { diff --git a/packages/cli/src/context/ingest/semantic-layer-target-policy.test.ts b/packages/cli/test/context/ingest/semantic-layer-target-policy.test.ts similarity index 95% rename from packages/cli/src/context/ingest/semantic-layer-target-policy.test.ts rename to packages/cli/test/context/ingest/semantic-layer-target-policy.test.ts index 73d09dc0..5f0980f6 100644 --- a/packages/cli/src/context/ingest/semantic-layer-target-policy.test.ts +++ b/packages/cli/test/context/ingest/semantic-layer-target-policy.test.ts @@ -3,7 +3,7 @@ import { assertSemanticLayerTargetPathsAllowed, findDisallowedSemanticLayerTargetPaths, semanticLayerConnectionIdFromPath, -} from './semantic-layer-target-policy.js'; +} from '../../../src/context/ingest/semantic-layer-target-policy.js'; describe('semantic-layer target policy', () => { it('extracts connection ids from semantic-layer paths', () => { diff --git a/packages/cli/src/context/ingest/source-adapter-registry.test.ts b/packages/cli/test/context/ingest/source-adapter-registry.test.ts similarity index 87% rename from packages/cli/src/context/ingest/source-adapter-registry.test.ts rename to packages/cli/test/context/ingest/source-adapter-registry.test.ts index 9a74c597..abd34f51 100644 --- a/packages/cli/src/context/ingest/source-adapter-registry.test.ts +++ b/packages/cli/test/context/ingest/source-adapter-registry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { SourceAdapterRegistry } from './source-adapter-registry.js'; -import type { SourceAdapter } from './types.js'; +import { SourceAdapterRegistry } from '../../../src/context/ingest/source-adapter-registry.js'; +import type { SourceAdapter } from '../../../src/context/ingest/types.js'; const makeAdapter = (source: string): SourceAdapter => ({ source, diff --git a/packages/cli/src/context/ingest/sqlite-bundle-ingest-store.test.ts b/packages/cli/test/context/ingest/sqlite-bundle-ingest-store.test.ts similarity index 98% rename from packages/cli/src/context/ingest/sqlite-bundle-ingest-store.test.ts rename to packages/cli/test/context/ingest/sqlite-bundle-ingest-store.test.ts index 0cee47d0..9f6e9f9f 100644 --- a/packages/cli/src/context/ingest/sqlite-bundle-ingest-store.test.ts +++ b/packages/cli/test/context/ingest/sqlite-bundle-ingest-store.test.ts @@ -2,10 +2,10 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { DiffSetService } from './diff-set.service.js'; -import type { IngestDiffSummary, IngestTrigger } from '../../context/ingest/types.js'; -import type { IngestReportBody } from '../../context/ingest/reports.js'; -import { SqliteBundleIngestStore } from './sqlite-bundle-ingest-store.js'; +import { DiffSetService } from '../../../src/context/ingest/diff-set.service.js'; +import type { IngestDiffSummary, IngestTrigger } from '../../../src/context/ingest/types.js'; +import type { IngestReportBody } from '../../../src/context/ingest/reports.js'; +import { SqliteBundleIngestStore } from '../../../src/context/ingest/sqlite-bundle-ingest-store.js'; function idFactory(ids: string[]): () => string { let index = 0; diff --git a/packages/cli/src/context/ingest/sqlite-local-ingest-store.test.ts b/packages/cli/test/context/ingest/sqlite-local-ingest-store.test.ts similarity index 95% rename from packages/cli/src/context/ingest/sqlite-local-ingest-store.test.ts rename to packages/cli/test/context/ingest/sqlite-local-ingest-store.test.ts index 67fad006..3d38fe58 100644 --- a/packages/cli/src/context/ingest/sqlite-local-ingest-store.test.ts +++ b/packages/cli/test/context/ingest/sqlite-local-ingest-store.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SqliteLocalIngestStore } from './sqlite-local-ingest-store.js'; -import type { LocalIngestRunRecord } from './local-stage-ingest.js'; +import { SqliteLocalIngestStore } from '../../../src/context/ingest/sqlite-local-ingest-store.js'; +import type { LocalIngestRunRecord } from '../../../src/context/ingest/local-stage-ingest.js'; function runRecord(overrides: Partial = {}): LocalIngestRunRecord { return { diff --git a/packages/cli/src/context/ingest/stages/build-reconcile-context.context-candidates.test.ts b/packages/cli/test/context/ingest/stages/build-reconcile-context.context-candidates.test.ts similarity index 97% rename from packages/cli/src/context/ingest/stages/build-reconcile-context.context-candidates.test.ts rename to packages/cli/test/context/ingest/stages/build-reconcile-context.context-candidates.test.ts index 22427ddd..09a1c610 100644 --- a/packages/cli/src/context/ingest/stages/build-reconcile-context.context-candidates.test.ts +++ b/packages/cli/test/context/ingest/stages/build-reconcile-context.context-candidates.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildReconcileUserPrompt } from './build-reconcile-context.js'; +import { buildReconcileUserPrompt } from '../../../../src/context/ingest/stages/build-reconcile-context.js'; const emptyStageIndex = { jobId: 'job-1', diff --git a/packages/cli/src/context/ingest/stages/build-reconcile-context.test.ts b/packages/cli/test/context/ingest/stages/build-reconcile-context.test.ts similarity index 98% rename from packages/cli/src/context/ingest/stages/build-reconcile-context.test.ts rename to packages/cli/test/context/ingest/stages/build-reconcile-context.test.ts index 8de7611a..2aaeb061 100644 --- a/packages/cli/src/context/ingest/stages/build-reconcile-context.test.ts +++ b/packages/cli/test/context/ingest/stages/build-reconcile-context.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { buildReconcileSystemPrompt, buildReconcileToolSet, buildReconcileUserPrompt } from './build-reconcile-context.js'; +import { buildReconcileSystemPrompt, buildReconcileToolSet, buildReconcileUserPrompt } from '../../../../src/context/ingest/stages/build-reconcile-context.js'; describe('buildReconcileSystemPrompt', () => { it('appends canonical pins when relevant pins are supplied', () => { diff --git a/packages/cli/src/context/ingest/stages/build-wu-context.test.ts b/packages/cli/test/context/ingest/stages/build-wu-context.test.ts similarity index 99% rename from packages/cli/src/context/ingest/stages/build-wu-context.test.ts rename to packages/cli/test/context/ingest/stages/build-wu-context.test.ts index 81c0c923..cdbb22ba 100644 --- a/packages/cli/src/context/ingest/stages/build-wu-context.test.ts +++ b/packages/cli/test/context/ingest/stages/build-wu-context.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { buildWuSystemPrompt, buildWuToolSet, buildWuUserPrompt } from './build-wu-context.js'; +import { buildWuSystemPrompt, buildWuToolSet, buildWuUserPrompt } from '../../../../src/context/ingest/stages/build-wu-context.js'; describe('buildWuUserPrompt', () => { it('includes rawFiles, dependencyPaths, peerFileIndex, and priorProvenance when present', () => { diff --git a/packages/cli/src/context/ingest/stages/stage-1-stage-raw-files.test.ts b/packages/cli/test/context/ingest/stages/stage-1-stage-raw-files.test.ts similarity index 95% rename from packages/cli/src/context/ingest/stages/stage-1-stage-raw-files.test.ts rename to packages/cli/test/context/ingest/stages/stage-1-stage-raw-files.test.ts index 3cd5cde5..d943d2d0 100644 --- a/packages/cli/src/context/ingest/stages/stage-1-stage-raw-files.test.ts +++ b/packages/cli/test/context/ingest/stages/stage-1-stage-raw-files.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { stageRawFilesStage1 } from './stage-1-stage-raw-files.js'; +import { stageRawFilesStage1 } from '../../../../src/context/ingest/stages/stage-1-stage-raw-files.js'; describe('Stage 1 — stageRawFiles', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/stages/stage-3-work-units.test.ts b/packages/cli/test/context/ingest/stages/stage-3-work-units.test.ts similarity index 96% rename from packages/cli/src/context/ingest/stages/stage-3-work-units.test.ts rename to packages/cli/test/context/ingest/stages/stage-3-work-units.test.ts index fc39fd9b..6d6deccd 100644 --- a/packages/cli/src/context/ingest/stages/stage-3-work-units.test.ts +++ b/packages/cli/test/context/ingest/stages/stage-3-work-units.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { CaptureSession, MemoryAction } from '../../../context/memory/types.js'; -import { addTouchedSlSource, createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { WorkUnit } from '../types.js'; -import { executeWorkUnit, type WorkUnitExecutionDeps } from './stage-3-work-units.js'; +import type { CaptureSession, MemoryAction } from '../../../../src/context/memory/types.js'; +import { addTouchedSlSource, createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { WorkUnit } from '../../../../src/context/ingest/types.js'; +import { executeWorkUnit, type WorkUnitExecutionDeps } from '../../../../src/context/ingest/stages/stage-3-work-units.js'; const makeWu = (overrides: Partial = {}): WorkUnit => ({ unitKey: 'u1', diff --git a/packages/cli/src/context/ingest/stages/stage-4-reconciliation.test.ts b/packages/cli/test/context/ingest/stages/stage-4-reconciliation.test.ts similarity index 97% rename from packages/cli/src/context/ingest/stages/stage-4-reconciliation.test.ts rename to packages/cli/test/context/ingest/stages/stage-4-reconciliation.test.ts index 4244ab12..d66533a9 100644 --- a/packages/cli/src/context/ingest/stages/stage-4-reconciliation.test.ts +++ b/packages/cli/test/context/ingest/stages/stage-4-reconciliation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { runReconciliationStage4 } from './stage-4-reconciliation.js'; +import { runReconciliationStage4 } from '../../../../src/context/ingest/stages/stage-4-reconciliation.js'; describe('Stage 4 — runReconciliationStage4', () => { it('short-circuits when stage index is empty and eviction is empty', async () => { diff --git a/packages/cli/src/context/ingest/stages/validate-wu-sources.test.ts b/packages/cli/test/context/ingest/stages/validate-wu-sources.test.ts similarity index 93% rename from packages/cli/src/context/ingest/stages/validate-wu-sources.test.ts rename to packages/cli/test/context/ingest/stages/validate-wu-sources.test.ts index 668062e9..807a8b10 100644 --- a/packages/cli/src/context/ingest/stages/validate-wu-sources.test.ts +++ b/packages/cli/test/context/ingest/stages/validate-wu-sources.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { validateWuTouchedSources } from './validate-wu-sources.js'; +import { validateWuTouchedSources } from '../../../../src/context/ingest/stages/validate-wu-sources.js'; describe('validateWuTouchedSources', () => { it('validates each touched source against its own connection', async () => { diff --git a/packages/cli/src/context/ingest/tools/emit-reconciliation-records.tool.test.ts b/packages/cli/test/context/ingest/tools/emit-reconciliation-records.tool.test.ts similarity index 93% rename from packages/cli/src/context/ingest/tools/emit-reconciliation-records.tool.test.ts rename to packages/cli/test/context/ingest/tools/emit-reconciliation-records.tool.test.ts index 1cd77514..7801038b 100644 --- a/packages/cli/src/context/ingest/tools/emit-reconciliation-records.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/emit-reconciliation-records.tool.test.ts @@ -1,10 +1,10 @@ import type { Tool } from 'ai'; import { describe, expect, it } from 'vitest'; -import type { StageIndex } from '../stages/stage-index.types.js'; -import { createEmitArtifactResolutionTool } from './emit-artifact-resolution.tool.js'; -import { createEmitConflictResolutionTool } from './emit-conflict-resolution.tool.js'; -import { createEmitEvictionDecisionTool } from './emit-eviction-decision.tool.js'; -import { createEmitUnmappedFallbackTool } from './emit-unmapped-fallback.tool.js'; +import type { StageIndex } from '../../../../src/context/ingest/stages/stage-index.types.js'; +import { createEmitArtifactResolutionTool } from '../../../../src/context/ingest/tools/emit-artifact-resolution.tool.js'; +import { createEmitConflictResolutionTool } from '../../../../src/context/ingest/tools/emit-conflict-resolution.tool.js'; +import { createEmitEvictionDecisionTool } from '../../../../src/context/ingest/tools/emit-eviction-decision.tool.js'; +import { createEmitUnmappedFallbackTool } from '../../../../src/context/ingest/tools/emit-unmapped-fallback.tool.js'; function makeStageIndex(): StageIndex { return { diff --git a/packages/cli/src/context/ingest/tools/eviction-list.tool.test.ts b/packages/cli/test/context/ingest/tools/eviction-list.tool.test.ts similarity index 94% rename from packages/cli/src/context/ingest/tools/eviction-list.tool.test.ts rename to packages/cli/test/context/ingest/tools/eviction-list.tool.test.ts index 96fb7f65..3dcfd005 100644 --- a/packages/cli/src/context/ingest/tools/eviction-list.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/eviction-list.tool.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createEvictionListTool } from './eviction-list.tool.js'; +import { createEvictionListTool } from '../../../../src/context/ingest/tools/eviction-list.tool.js'; describe('eviction_list tool', () => { it('returns artifacts produced for each deleted raw path', async () => { diff --git a/packages/cli/src/context/ingest/tools/read-raw-file.tool.test.ts b/packages/cli/test/context/ingest/tools/read-raw-file.tool.test.ts similarity index 96% rename from packages/cli/src/context/ingest/tools/read-raw-file.tool.test.ts rename to packages/cli/test/context/ingest/tools/read-raw-file.tool.test.ts index db4aef42..041ea188 100644 --- a/packages/cli/src/context/ingest/tools/read-raw-file.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/read-raw-file.tool.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createReadRawFileTool } from './read-raw-file.tool.js'; +import { createReadRawFileTool } from '../../../../src/context/ingest/tools/read-raw-file.tool.js'; describe('read_raw_file tool', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/tools/read-raw-span.tool.test.ts b/packages/cli/test/context/ingest/tools/read-raw-span.tool.test.ts similarity index 95% rename from packages/cli/src/context/ingest/tools/read-raw-span.tool.test.ts rename to packages/cli/test/context/ingest/tools/read-raw-span.tool.test.ts index 30696046..cd4e8f2a 100644 --- a/packages/cli/src/context/ingest/tools/read-raw-span.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/read-raw-span.tool.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createReadRawSpanTool } from './read-raw-span.tool.js'; +import { createReadRawSpanTool } from '../../../../src/context/ingest/tools/read-raw-span.tool.js'; describe('read_raw_span tool', () => { let stagedDir: string; diff --git a/packages/cli/src/context/ingest/tools/stage-diff.tool.test.ts b/packages/cli/test/context/ingest/tools/stage-diff.tool.test.ts similarity index 97% rename from packages/cli/src/context/ingest/tools/stage-diff.tool.test.ts rename to packages/cli/test/context/ingest/tools/stage-diff.tool.test.ts index 0dae87ab..fd756099 100644 --- a/packages/cli/src/context/ingest/tools/stage-diff.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/stage-diff.tool.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createStageDiffTool } from './stage-diff.tool.js'; +import { createStageDiffTool } from '../../../../src/context/ingest/tools/stage-diff.tool.js'; describe('stage_diff tool', () => { const stageIndex = { diff --git a/packages/cli/src/context/ingest/tools/stage-list.tool.test.ts b/packages/cli/test/context/ingest/tools/stage-list.tool.test.ts similarity index 95% rename from packages/cli/src/context/ingest/tools/stage-list.tool.test.ts rename to packages/cli/test/context/ingest/tools/stage-list.tool.test.ts index d14acb2a..3ed975bc 100644 --- a/packages/cli/src/context/ingest/tools/stage-list.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/stage-list.tool.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createStageListTool } from './stage-list.tool.js'; +import { createStageListTool } from '../../../../src/context/ingest/tools/stage-list.tool.js'; describe('stage_list tool', () => { it('returns a compact summary of the stage index', async () => { diff --git a/packages/cli/src/context/ingest/tools/tool-transcript-summary.test.ts b/packages/cli/test/context/ingest/tools/tool-transcript-summary.test.ts similarity index 97% rename from packages/cli/src/context/ingest/tools/tool-transcript-summary.test.ts rename to packages/cli/test/context/ingest/tools/tool-transcript-summary.test.ts index 9e110789..ddc8d256 100644 --- a/packages/cli/src/context/ingest/tools/tool-transcript-summary.test.ts +++ b/packages/cli/test/context/ingest/tools/tool-transcript-summary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { ToolCallLogEntry } from './tool-call-logger.js'; -import { createMutableToolTranscriptSummary, recordToolTranscriptEntry } from './tool-transcript-summary.js'; +import type { ToolCallLogEntry } from '../../../../src/context/ingest/tools/tool-call-logger.js'; +import { createMutableToolTranscriptSummary, recordToolTranscriptEntry } from '../../../../src/context/ingest/tools/tool-transcript-summary.js'; function entry(overrides: Partial): ToolCallLogEntry { return { diff --git a/packages/cli/src/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts b/packages/cli/test/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts similarity index 94% rename from packages/cli/src/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts rename to packages/cli/test/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts index 7aebc101..09abbc6a 100644 --- a/packages/cli/src/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/warehouse-verification/discover-data.tool.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { WarehouseCatalogService } from '../../../scan/warehouse-catalog.js'; -import type { BaseTool, ToolContext } from '../../../../context/tools/base-tool.js'; -import { DiscoverDataTool } from './discover-data.tool.js'; +import type { WarehouseCatalogService } from '../../../../../src/context/scan/warehouse-catalog.js'; +import type { BaseTool, ToolContext } from '../../../../../src/context/tools/base-tool.js'; +import { DiscoverDataTool } from '../../../../../src/context/ingest/tools/warehouse-verification/discover-data.tool.js'; describe('DiscoverDataTool', () => { const wikiSearchTool = { call: vi.fn() } as unknown as BaseTool & { call: ReturnType }; diff --git a/packages/cli/src/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts b/packages/cli/test/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts similarity index 95% rename from packages/cli/src/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts rename to packages/cli/test/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts index fcef38df..0107951d 100644 --- a/packages/cli/src/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/warehouse-verification/entity-details.tool.test.ts @@ -2,10 +2,10 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../../../context/project/project.js'; -import { WarehouseCatalogService } from '../../../scan/warehouse-catalog.js'; -import type { ToolContext } from '../../../../context/tools/base-tool.js'; -import { EntityDetailsTool } from './entity-details.tool.js'; +import { initKtxProject, type KtxLocalProject } from '../../../../../src/context/project/project.js'; +import { WarehouseCatalogService } from '../../../../../src/context/scan/warehouse-catalog.js'; +import type { ToolContext } from '../../../../../src/context/tools/base-tool.js'; +import { EntityDetailsTool } from '../../../../../src/context/ingest/tools/warehouse-verification/entity-details.tool.js'; describe('EntityDetailsTool', () => { let tempDir: string; diff --git a/packages/cli/src/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts b/packages/cli/test/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts similarity index 89% rename from packages/cli/src/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts rename to packages/cli/test/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts index 4458471a..0fb87655 100644 --- a/packages/cli/src/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts +++ b/packages/cli/test/context/ingest/tools/warehouse-verification/sql-execution.tool.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { SlConnectionCatalogPort } from '../../../../context/sl/ports.js'; -import type { ToolContext } from '../../../../context/tools/base-tool.js'; -import { SqlExecutionTool } from './sql-execution.tool.js'; +import type { SlConnectionCatalogPort } from '../../../../../src/context/sl/ports.js'; +import type { ToolContext } from '../../../../../src/context/tools/base-tool.js'; +import { SqlExecutionTool } from '../../../../../src/context/ingest/tools/warehouse-verification/sql-execution.tool.js'; describe('SqlExecutionTool', () => { const connections = { diff --git a/packages/cli/src/context/ingest/wiki-body-refs.test.ts b/packages/cli/test/context/ingest/wiki-body-refs.test.ts similarity index 98% rename from packages/cli/src/context/ingest/wiki-body-refs.test.ts rename to packages/cli/test/context/ingest/wiki-body-refs.test.ts index 2af8935f..578dc600 100644 --- a/packages/cli/src/context/ingest/wiki-body-refs.test.ts +++ b/packages/cli/test/context/ingest/wiki-body-refs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { findInvalidWikiBodyRefs, parseWikiBodyRefs } from './wiki-body-refs.js'; +import { findInvalidWikiBodyRefs, parseWikiBodyRefs } from '../../../src/context/ingest/wiki-body-refs.js'; const sources = [ { diff --git a/packages/cli/src/context/ingest/wiki-sl-ref-repair.test.ts b/packages/cli/test/context/ingest/wiki-sl-ref-repair.test.ts similarity index 97% rename from packages/cli/src/context/ingest/wiki-sl-ref-repair.test.ts rename to packages/cli/test/context/ingest/wiki-sl-ref-repair.test.ts index bcf4a993..16f0acb5 100644 --- a/packages/cli/src/context/ingest/wiki-sl-ref-repair.test.ts +++ b/packages/cli/test/context/ingest/wiki-sl-ref-repair.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { repairWikiSlRefs } from './wiki-sl-ref-repair.js'; +import { repairWikiSlRefs } from '../../../src/context/ingest/wiki-sl-ref-repair.js'; describe('repairWikiSlRefs', () => { it('removes missing measure refs while keeping source, measure, segment, and manifest-backed refs', async () => { diff --git a/packages/cli/src/context/llm/ai-sdk-runtime.test.ts b/packages/cli/test/context/llm/ai-sdk-runtime.test.ts similarity index 98% rename from packages/cli/src/context/llm/ai-sdk-runtime.test.ts rename to packages/cli/test/context/llm/ai-sdk-runtime.test.ts index ba5e286d..5e5085ff 100644 --- a/packages/cli/src/context/llm/ai-sdk-runtime.test.ts +++ b/packages/cli/test/context/llm/ai-sdk-runtime.test.ts @@ -7,8 +7,8 @@ vi.mock('ai', () => ({ })); import { generateText } from 'ai'; -import { AiSdkKtxLlmRuntime } from './ai-sdk-runtime.js'; -import type { RunLoopStepInfo } from './runtime-port.js'; +import { AiSdkKtxLlmRuntime } from '../../../src/context/llm/ai-sdk-runtime.js'; +import type { RunLoopStepInfo } from '../../../src/context/llm/runtime-port.js'; describe('AiSdkKtxLlmRuntime.runAgentLoop', () => { let runtime: AiSdkKtxLlmRuntime; diff --git a/packages/cli/src/context/llm/claude-code-env.test.ts b/packages/cli/test/context/llm/claude-code-env.test.ts similarity index 92% rename from packages/cli/src/context/llm/claude-code-env.test.ts rename to packages/cli/test/context/llm/claude-code-env.test.ts index 19cbd1ff..9f015563 100644 --- a/packages/cli/src/context/llm/claude-code-env.test.ts +++ b/packages/cli/test/context/llm/claude-code-env.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { CLAUDE_CODE_PROVIDER_ENV_DENYLIST, createKtxClaudeCodeEnv } from './claude-code-env.js'; +import { CLAUDE_CODE_PROVIDER_ENV_DENYLIST, createKtxClaudeCodeEnv } from '../../../src/context/llm/claude-code-env.js'; describe('createKtxClaudeCodeEnv', () => { it('strips provider-routing credentials from the Claude Code child environment', () => { diff --git a/packages/cli/src/context/llm/claude-code-models.test.ts b/packages/cli/test/context/llm/claude-code-models.test.ts similarity index 85% rename from packages/cli/src/context/llm/claude-code-models.test.ts rename to packages/cli/test/context/llm/claude-code-models.test.ts index 482e6af8..34de9233 100644 --- a/packages/cli/src/context/llm/claude-code-models.test.ts +++ b/packages/cli/test/context/llm/claude-code-models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { resolveClaudeCodeModel } from './claude-code-models.js'; +import { resolveClaudeCodeModel } from '../../../src/context/llm/claude-code-models.js'; describe('resolveClaudeCodeModel', () => { it.each([ diff --git a/packages/cli/src/context/llm/claude-code-runtime.test.ts b/packages/cli/test/context/llm/claude-code-runtime.test.ts similarity index 99% rename from packages/cli/src/context/llm/claude-code-runtime.test.ts rename to packages/cli/test/context/llm/claude-code-runtime.test.ts index b1003b78..205c74e0 100644 --- a/packages/cli/src/context/llm/claude-code-runtime.test.ts +++ b/packages/cli/test/context/llm/claude-code-runtime.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { z } from 'zod'; import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk'; -import { ClaudeCodeKtxLlmRuntime, mapClaudeCodeStopReason, runClaudeCodeAuthProbe } from './claude-code-runtime.js'; +import { ClaudeCodeKtxLlmRuntime, mapClaudeCodeStopReason, runClaudeCodeAuthProbe } from '../../../src/context/llm/claude-code-runtime.js'; async function* stream(messages: SDKMessage[]): AsyncGenerator { for (const message of messages) { diff --git a/packages/cli/src/context/llm/debug-request-recorder.test.ts b/packages/cli/test/context/llm/debug-request-recorder.test.ts similarity index 98% rename from packages/cli/src/context/llm/debug-request-recorder.test.ts rename to packages/cli/test/context/llm/debug-request-recorder.test.ts index 4a00400f..e7a15b88 100644 --- a/packages/cli/src/context/llm/debug-request-recorder.test.ts +++ b/packages/cli/test/context/llm/debug-request-recorder.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { createJsonlKtxLlmDebugRequestRecorder, summarizeKtxLlmDebugRequest, -} from './debug-request-recorder.js'; +} from '../../../src/context/llm/debug-request-recorder.js'; describe('summarizeKtxLlmDebugRequest', () => { it('records providerOptions positions without message text or tool schemas', () => { diff --git a/packages/cli/src/context/llm/embedding-port.test.ts b/packages/cli/test/context/llm/embedding-port.test.ts similarity index 95% rename from packages/cli/src/context/llm/embedding-port.test.ts rename to packages/cli/test/context/llm/embedding-port.test.ts index 323e7c5b..6bde1f2a 100644 --- a/packages/cli/src/context/llm/embedding-port.test.ts +++ b/packages/cli/test/context/llm/embedding-port.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { KtxIngestEmbeddingPortAdapter, KtxScanEmbeddingPortAdapter } from './embedding-port.js'; +import { KtxIngestEmbeddingPortAdapter, KtxScanEmbeddingPortAdapter } from '../../../src/context/llm/embedding-port.js'; describe('KTX embedding port adapters', () => { it('adapts LLM modules embeddings to ingest embedding port shape', async () => { diff --git a/packages/cli/src/context/llm/local-config.test.ts b/packages/cli/test/context/llm/local-config.test.ts similarity index 98% rename from packages/cli/src/context/llm/local-config.test.ts rename to packages/cli/test/context/llm/local-config.test.ts index 930ee8a5..e153baaf 100644 --- a/packages/cli/src/context/llm/local-config.test.ts +++ b/packages/cli/test/context/llm/local-config.test.ts @@ -3,13 +3,13 @@ import { buildDefaultKtxProjectConfig, type KtxProjectEmbeddingConfig, type KtxProjectLlmConfig, -} from '../project/config.js'; +} from '../../../src/context/project/config.js'; import { createLocalKtxEmbeddingProviderFromConfig, createLocalKtxLlmProviderFromConfig, resolveLocalKtxEmbeddingConfig, resolveLocalKtxLlmConfig, -} from './local-config.js'; +} from '../../../src/context/llm/local-config.js'; describe('local KTX LLM config', () => { it('resolves env and file references into a KtxLlmConfig', () => { diff --git a/packages/cli/src/context/llm/runtime-local-config.test.ts b/packages/cli/test/context/llm/runtime-local-config.test.ts similarity index 93% rename from packages/cli/src/context/llm/runtime-local-config.test.ts rename to packages/cli/test/context/llm/runtime-local-config.test.ts index e5516ffa..9e432cec 100644 --- a/packages/cli/src/context/llm/runtime-local-config.test.ts +++ b/packages/cli/test/context/llm/runtime-local-config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createLocalKtxLlmProviderFromConfig, createLocalKtxLlmRuntimeFromConfig } from './local-config.js'; +import { createLocalKtxLlmProviderFromConfig, createLocalKtxLlmRuntimeFromConfig } from '../../../src/context/llm/local-config.js'; describe('local KTX LLM runtime config', () => { it('creates a Claude Code runtime for claude-code backend without creating an AI SDK provider', () => { diff --git a/packages/cli/src/context/llm/runtime-tools.test.ts b/packages/cli/test/context/llm/runtime-tools.test.ts similarity index 91% rename from packages/cli/src/context/llm/runtime-tools.test.ts rename to packages/cli/test/context/llm/runtime-tools.test.ts index c1276d7d..f3c1b7f8 100644 --- a/packages/cli/src/context/llm/runtime-tools.test.ts +++ b/packages/cli/test/context/llm/runtime-tools.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { z } from 'zod'; -import { createAiSdkToolSet, createClaudeSdkTools, normalizeKtxRuntimeToolOutput } from './runtime-tools.js'; -import type { KtxRuntimeToolDescriptor } from './runtime-port.js'; +import { createAiSdkToolSet, createClaudeSdkTools, normalizeKtxRuntimeToolOutput } from '../../../src/context/llm/runtime-tools.js'; +import type { KtxRuntimeToolDescriptor } from '../../../src/context/llm/runtime-port.js'; describe('runtime tool descriptors', () => { const descriptor: KtxRuntimeToolDescriptor<{ id: string }, { ok: boolean }> = { diff --git a/packages/cli/test/context/mcp/__snapshots__/mcp-tools-list.json b/packages/cli/test/context/mcp/__snapshots__/mcp-tools-list.json new file mode 100644 index 00000000..10cb0b77 --- /dev/null +++ b/packages/cli/test/context/mcp/__snapshots__/mcp-tools-list.json @@ -0,0 +1,1620 @@ +[ + { + "name": "connection_list", + "title": "Connection List", + "description": "List configured read-only data connections available to this KTX project. Use this before connection-scoped tools when the project may have multiple warehouses.", + "inputSchema": { + "type": "object", + "properties": {}, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "connections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "connectionType": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "connectionType" + ], + "additionalProperties": false + } + } + }, + "required": [ + "connections" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Connection List", + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "wiki_search", + "title": "Wiki Search", + "description": "Search KTX wiki pages for reusable business context. Example: wiki_search({ query: \"revenue recognition\", limit: 5 }).", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Natural-language wiki search query, e.g. \"revenue recognition policy\"." + }, + "limit": { + "default": 10, + "description": "Maximum wiki pages to return. Defaults to 10.", + "type": "integer", + "minimum": 1, + "maximum": 50 + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "path": { + "type": "string" + }, + "scope": { + "type": "string", + "enum": [ + "GLOBAL", + "USER" + ] + }, + "summary": { + "type": "string" + }, + "score": { + "type": "number" + }, + "matchReasons": { + "type": "array", + "items": { + "type": "string" + } + }, + "lanes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "lane": { + "type": "string" + }, + "status": { + "type": "string" + }, + "requestedCandidatePoolLimit": { + "type": "number" + }, + "effectiveCandidatePoolLimit": { + "type": "number" + }, + "returnedCandidateCount": { + "type": "number" + }, + "weight": { + "type": "number" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "lane", + "status", + "requestedCandidatePoolLimit", + "effectiveCandidatePoolLimit", + "returnedCandidateCount", + "weight" + ], + "additionalProperties": false + } + } + }, + "required": [ + "key", + "path", + "scope", + "summary", + "score" + ], + "additionalProperties": false + } + }, + "totalFound": { + "type": "number" + } + }, + "required": [ + "results", + "totalFound" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Wiki Search", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "wiki_read", + "title": "Wiki Read", + "description": "Read a KTX wiki page by key returned from wiki_search. Example: wiki_read({ key: \"global/revenue\" }).", + "inputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "Wiki page key returned by wiki_search, e.g. \"global/revenue\"." + } + }, + "required": [ + "key" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "content": { + "type": "string" + }, + "scope": { + "type": "string", + "enum": [ + "GLOBAL", + "USER" + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "slRefs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "key", + "summary", + "content", + "scope" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Wiki Read", + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "sl_read_source", + "title": "Semantic Layer Read Source", + "description": "Read a semantic-layer YAML source by connection id and source name. Example: sl_read_source({ connectionId: \"warehouse\", sourceName: \"orders\" }).", + "inputSchema": { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "minLength": 1, + "description": "Connection id that owns the semantic-layer source." + }, + "sourceName": { + "type": "string", + "minLength": 1, + "description": "Semantic-layer source name without \".yaml\", e.g. \"orders\"." + } + }, + "required": [ + "connectionId", + "sourceName" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "sourceName": { + "type": "string" + }, + "yaml": { + "type": "string" + } + }, + "required": [ + "sourceName", + "yaml" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Semantic Layer Read Source", + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "sl_query", + "title": "Semantic Layer Query", + "description": "Execute a semantic-layer query and return rows, headers, generated SQL, and plan details. Example: sl_query({ connectionId: \"warehouse\", measures: [\"orders.order_count\"], dimensions: [{ field: \"orders.created_at\", granularity: \"month\" }] }).", + "inputSchema": { + "type": "object", + "properties": { + "connectionId": { + "description": "Connection id to query. Omit only when the project has exactly one configured connection.", + "type": "string", + "minLength": 1 + }, + "measures": { + "minItems": 1, + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "description": "Semantic-layer measure key, e.g. \"orders.order_count\"." + }, + { + "type": "object", + "properties": { + "expr": { + "type": "string", + "minLength": 1, + "description": "Ad hoc aggregate expression, e.g. \"sum(orders.amount)\"." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Alias for the ad hoc measure, e.g. \"gross_revenue\"." + } + }, + "required": [ + "expr", + "name" + ] + } + ] + }, + "description": "Measures to select. Use semantic-layer keys when available." + }, + "dimensions": { + "default": [], + "description": "Dimensions to group by. Use {field, granularity?} entries.", + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "description": "Dimension to group by, e.g. \"orders.created_at\" or \"orders.status\"." + }, + "granularity": { + "description": "Time grain for time dimensions: day, week, month, quarter, or year.", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "field" + ] + } + }, + "filters": { + "default": [], + "description": "Semantic-layer filter expressions to apply.", + "type": "array", + "items": { + "type": "string", + "description": "Semantic-layer filter expression, e.g. \"orders.status = paid\"." + } + }, + "segments": { + "default": [], + "description": "Semantic-layer segment keys to apply.", + "type": "array", + "items": { + "type": "string", + "description": "Semantic-layer segment key to apply." + } + }, + "order_by": { + "default": [], + "description": "Sort clauses. Use {field, direction?} entries.", + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "description": "Field/measure/dimension id to order by, e.g. \"orders.created_at\", a dimension key like \"mart_nrr_quarterly.quarter_label\", or a measure alias." + }, + "direction": { + "default": "asc", + "description": "Sort direction: \"asc\" or \"desc\". Defaults to \"asc\".", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "required": [ + "field" + ] + } + }, + "limit": { + "default": 1000, + "description": "Maximum rows to return. Defaults to 1000.", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "include_empty": { + "default": true, + "description": "Whether to include empty dimension groups. Defaults to true.", + "type": "boolean" + } + }, + "required": [ + "measures" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "connectionId": { + "type": "string" + }, + "dialect": { + "type": "string" + }, + "sql": { + "type": "string" + }, + "headers": { + "type": "array", + "items": { + "type": "string" + } + }, + "rows": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "totalRows": { + "type": "number" + }, + "plan": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "sql", + "headers", + "rows", + "totalRows" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Semantic Layer Query", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "entity_details", + "title": "Entity Details", + "description": "Read table and column metadata from the latest live-database scan snapshot. Example: entity_details({ connectionId: \"warehouse\", entities: [{ table: { catalog: null, db: \"public\", name: \"orders\" }, columns: [\"id\"] }] }).", + "inputSchema": { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "minLength": 1, + "description": "Connection id whose latest scan snapshot should be read." + }, + "entities": { + "minItems": 1, + "maxItems": 20, + "type": "array", + "items": { + "type": "object", + "properties": { + "table": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "object", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Catalog/project/database. Use null when not applicable." + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Schema/database/dataset. Use null when not applicable." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Table name." + } + }, + "required": [ + "catalog", + "db", + "name" + ] + } + ], + "description": "Table display string or canonical object ref." + }, + "columns": { + "description": "Optional column filter.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "description": "Column name to inspect." + } + } + }, + "required": [ + "table" + ] + }, + "description": "Tables or columns to inspect. Maximum 20 entities." + } + }, + "required": [ + "connectionId", + "entities" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": true + }, + "connectionId": { + "type": "string" + }, + "tableRef": { + "type": "object", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "catalog", + "db", + "name" + ], + "additionalProperties": false + }, + "display": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "table", + "view", + "external", + "event_stream" + ] + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "estimatedRows": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "columns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "nativeType": { + "type": "string" + }, + "normalizedType": { + "type": "string" + }, + "dimensionType": { + "type": "string", + "enum": [ + "time", + "string", + "number", + "boolean" + ] + }, + "nullable": { + "type": "boolean" + }, + "primaryKey": { + "type": "boolean" + }, + "comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name", + "nativeType", + "normalizedType", + "dimensionType", + "nullable", + "primaryKey", + "comment" + ], + "additionalProperties": false + } + }, + "foreignKeys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fromColumn": { + "type": "string" + }, + "toCatalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "toDb": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "toTable": { + "type": "string" + }, + "toColumn": { + "type": "string" + }, + "constraintName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "fromColumn", + "toCatalog", + "toDb", + "toTable", + "toColumn", + "constraintName" + ], + "additionalProperties": false + } + }, + "snapshot": { + "type": "object", + "properties": { + "syncId": { + "type": "string" + }, + "extractedAt": { + "type": "string" + }, + "scanRunId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "syncId", + "extractedAt", + "scanRunId" + ], + "additionalProperties": false + } + }, + "required": [ + "ok", + "connectionId", + "tableRef", + "display", + "kind", + "comment", + "estimatedRows", + "columns", + "foreignKeys", + "snapshot" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": false + }, + "connectionId": { + "type": "string" + }, + "table": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "catalog", + "db", + "name" + ], + "additionalProperties": false + } + ] + }, + "snapshot": { + "type": "object", + "properties": { + "syncId": { + "type": "string" + }, + "extractedAt": { + "type": "string" + }, + "scanRunId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "syncId", + "extractedAt", + "scanRunId" + ], + "additionalProperties": false + }, + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "scan_missing", + "table_not_found", + "ambiguous_table", + "column_not_found" + ] + }, + "message": { + "type": "string" + }, + "candidates": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "tableRef": { + "type": "object", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "catalog", + "db", + "name" + ], + "additionalProperties": false + }, + "display": { + "type": "string" + } + }, + "required": [ + "tableRef", + "display" + ], + "additionalProperties": false + } + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "ok", + "connectionId", + "table", + "error" + ], + "additionalProperties": false + } + ] + } + } + }, + "required": [ + "results" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Entity Details", + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "dictionary_search", + "title": "Dictionary Search", + "description": "Search profile-sampled warehouse values to locate likely source columns for business values. Example: dictionary_search({ values: [\"Acme Corp\"], connectionId: \"warehouse\" }).", + "inputSchema": { + "type": "object", + "properties": { + "values": { + "minItems": 1, + "maxItems": 20, + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "description": "Business value to locate, e.g. \"Acme Corp\" or \"enterprise\"." + }, + "description": "Values to search for in sampled warehouse dictionaries." + }, + "connectionId": { + "description": "Optional connection id. Pass it when user intent pins a specific warehouse.", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "values" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "searched": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connectionId": { + "type": "string" + }, + "coverage": { + "type": "object", + "properties": { + "sampledRows": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "valuesPerColumn": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "profiledColumns": { + "type": "number" + }, + "syncId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "profiledAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "sampledRows", + "valuesPerColumn", + "profiledColumns", + "syncId", + "profiledAt" + ], + "additionalProperties": false + }, + "status": { + "type": "string", + "enum": [ + "ready", + "no_profile_artifact", + "no_candidate_columns" + ] + } + }, + "required": [ + "connectionId", + "coverage", + "status" + ], + "additionalProperties": false + } + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connectionId": { + "type": "string" + }, + "sourceName": { + "type": "string" + }, + "columnName": { + "type": "string" + }, + "matchedValue": { + "type": "string" + }, + "cardinality": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "connectionId", + "sourceName", + "columnName", + "matchedValue", + "cardinality" + ], + "additionalProperties": false + } + }, + "misses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connectionId": { + "type": "string" + }, + "reason": { + "type": "string", + "enum": [ + "no_profile_artifact", + "no_candidate_columns", + "value_not_in_sample" + ] + } + }, + "required": [ + "connectionId", + "reason" + ], + "additionalProperties": false + } + } + }, + "required": [ + "value", + "matches", + "misses" + ], + "additionalProperties": false + } + } + }, + "required": [ + "searched", + "results" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Dictionary Search", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "discover_data", + "title": "Discover Data", + "description": "Search across KTX wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: \"monthly orders by customer\", connectionId: \"warehouse\", kinds: [\"sl_source\", \"table\"] }).", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Natural-language discovery query, e.g. \"monthly orders by customer\"." + }, + "connectionId": { + "description": "Optional connection id. Pass it when user intent pins a specific warehouse.", + "type": "string", + "minLength": 1 + }, + "kinds": { + "description": "Optional kind filter.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "wiki", + "sl_source", + "sl_measure", + "sl_dimension", + "table", + "column" + ], + "description": "Reference kind to include." + } + }, + "limit": { + "description": "Maximum refs to return. Defaults to 15.", + "default": 15, + "type": "integer", + "minimum": 1, + "maximum": 50 + } + }, + "required": [ + "query" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "refs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "wiki", + "sl_source", + "sl_measure", + "sl_dimension", + "table", + "column" + ] + }, + "id": { + "type": "string" + }, + "score": { + "type": "number" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "snippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "matchedOn": { + "type": "string", + "enum": [ + "name", + "display", + "description", + "comment", + "expr", + "sample_value", + "body" + ] + }, + "connectionId": { + "type": "string" + }, + "tableRef": { + "type": "object", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "db": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "catalog", + "db", + "name" + ], + "additionalProperties": false + }, + "columnName": { + "type": "string" + } + }, + "required": [ + "kind", + "id", + "score", + "summary", + "snippet", + "matchedOn" + ], + "additionalProperties": false + } + } + }, + "required": [ + "refs" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Discover Data", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "sql_execution", + "title": "SQL Execution", + "description": "Execute one parser-validated read-only SQL query against a configured KTX connection. Example: sql_execution({ connectionId: \"warehouse\", sql: \"select count(*) from public.orders\", maxRows: 100 }).", + "inputSchema": { + "type": "object", + "properties": { + "connectionId": { + "type": "string", + "minLength": 1, + "description": "Connection id to execute against. Required for raw SQL." + }, + "sql": { + "type": "string", + "minLength": 1, + "description": "Parser-validated read-only SQL, e.g. \"select count(*) from public.orders\"." + }, + "maxRows": { + "description": "Maximum rows to return. Defaults to 1000.", + "default": 1000, + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "connectionId", + "sql" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "headers": { + "type": "array", + "items": { + "type": "string" + } + }, + "headerTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "rows": { + "type": "array", + "items": { + "type": "array", + "items": {} + } + }, + "rowCount": { + "type": "number" + } + }, + "required": [ + "headers", + "rows", + "rowCount" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "SQL Execution", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "memory_ingest", + "title": "Memory Ingest", + "description": "Ingest free-form markdown knowledge into durable KTX memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: \"warehouse\", content: \"ARR is reported in cents in this warehouse.\" }).", + "inputSchema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "minLength": 1, + "description": "Free-form markdown to ingest. Include the knowledge itself plus any context (source, the user question, why this came up) that the memory agent should consider when triaging into wiki/SL." + }, + "connectionId": { + "description": "Scope this memory to a specific connection. Required when the knowledge is warehouse-specific, including measure definitions, schema gotchas, or anything tied to a particular warehouse. Omit only for global wiki knowledge.", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "content" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "runId": { + "type": "string" + } + }, + "required": [ + "runId" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Memory Ingest", + "destructiveHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + }, + { + "name": "memory_ingest_status", + "title": "Memory Ingest Status", + "description": "Read the current or final status for a memory ingest run. Example: memory_ingest_status({ runId: \"memory-run-1\" }).", + "inputSchema": { + "type": "object", + "properties": { + "runId": { + "type": "string", + "minLength": 1, + "description": "The memory ingest run id returned by memory_ingest." + } + }, + "required": [ + "runId" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "outputSchema": { + "type": "object", + "properties": { + "runId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "done", + "error" + ] + }, + "stage": { + "type": "string" + }, + "done": { + "type": "boolean" + }, + "captured": { + "type": "object", + "properties": { + "wiki": { + "type": "array", + "items": { + "type": "string" + } + }, + "sl": { + "type": "array", + "items": { + "type": "string" + } + }, + "xrefs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "wiki", + "sl", + "xrefs" + ], + "additionalProperties": false + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "commitHash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "skillsLoaded": { + "type": "array", + "items": { + "type": "string" + } + }, + "signalDetected": { + "type": "boolean" + } + }, + "required": [ + "runId", + "status", + "stage", + "done", + "captured", + "error", + "commitHash", + "skillsLoaded", + "signalDetected" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false + }, + "annotations": { + "title": "Memory Ingest Status", + "readOnlyHint": true, + "openWorldHint": false + }, + "execution": { + "taskSupport": "forbidden" + } + } +] diff --git a/packages/cli/src/context/mcp/local-project-ports.test.ts b/packages/cli/test/context/mcp/local-project-ports.test.ts similarity index 98% rename from packages/cli/src/context/mcp/local-project-ports.test.ts rename to packages/cli/test/context/mcp/local-project-ports.test.ts index aa06b47e..afe85b4c 100644 --- a/packages/cli/src/context/mcp/local-project-ports.test.ts +++ b/packages/cli/test/context/mcp/local-project-ports.test.ts @@ -2,10 +2,10 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initKtxProject } from '../../context/project/project.js'; -import { createKtxConnectorCapabilities, type KtxQueryResult, type KtxScanConnector, type KtxSchemaSnapshot } from '../../context/scan/types.js'; -import { writeLocalSlSource } from '../../context/sl/local-sl.js'; -import { createLocalProjectMcpContextPorts } from './local-project-ports.js'; +import { initKtxProject } from '../../../src/context/project/project.js'; +import { createKtxConnectorCapabilities, type KtxQueryResult, type KtxScanConnector, type KtxSchemaSnapshot } from '../../../src/context/scan/types.js'; +import { writeLocalSlSource } from '../../../src/context/sl/local-sl.js'; +import { createLocalProjectMcpContextPorts } from '../../../src/context/mcp/local-project-ports.js'; describe('createLocalProjectMcpContextPorts', () => { let tempDir: string; @@ -56,6 +56,8 @@ describe('createLocalProjectMcpContextPorts', () => { driver: snapshot.driver, capabilities: createKtxConnectorCapabilities({ readOnlySql: queryResult !== undefined }), introspect: vi.fn(async () => snapshot), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), executeReadOnly: queryResult === undefined ? undefined : vi.fn(async () => queryResult), cleanup: vi.fn(async () => {}), }; diff --git a/packages/cli/src/context/mcp/server.test.ts b/packages/cli/test/context/mcp/server.test.ts similarity index 98% rename from packages/cli/src/context/mcp/server.test.ts rename to packages/cli/test/context/mcp/server.test.ts index e6666364..f6d3e37e 100644 --- a/packages/cli/src/context/mcp/server.test.ts +++ b/packages/cli/test/context/mcp/server.test.ts @@ -4,12 +4,12 @@ import { join } from 'node:path'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { createLocalProjectMemoryIngest } from '../../context/memory/local-memory.js'; -import { detectCaptureSignals } from '../../context/memory/capture-signals.js'; -import type { MemoryAgentInput } from '../../context/memory/types.js'; -import { initKtxProject } from '../../context/project/project.js'; -import { jsonToolResult } from './context-tools.js'; -import { createDefaultKtxMcpServer, createKtxMcpServer } from './server.js'; +import { createLocalProjectMemoryIngest } from '../../../src/context/memory/local-memory.js'; +import { detectCaptureSignals } from '../../../src/context/memory/capture-signals.js'; +import type { MemoryAgentInput } from '../../../src/context/memory/types.js'; +import { initKtxProject } from '../../../src/context/project/project.js'; +import { jsonToolResult } from '../../../src/context/mcp/context-tools.js'; +import { createDefaultKtxMcpServer, createKtxMcpServer } from '../../../src/context/mcp/server.js'; import type { KtxDiscoverDataMcpPort, KtxDictionarySearchMcpPort, @@ -21,7 +21,7 @@ import type { KtxSqlExecutionMcpPort, KtxSqlExecutionResponse, MemoryIngestPort, -} from './types.js'; +} from '../../../src/context/mcp/types.js'; type RegisteredTool = { name: string; diff --git a/packages/cli/src/context/memory/local-memory.test.ts b/packages/cli/test/context/memory/local-memory.test.ts similarity index 96% rename from packages/cli/src/context/memory/local-memory.test.ts rename to packages/cli/test/context/memory/local-memory.test.ts index 1a7240c9..23a93cc4 100644 --- a/packages/cli/src/context/memory/local-memory.test.ts +++ b/packages/cli/test/context/memory/local-memory.test.ts @@ -2,9 +2,9 @@ import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { initKtxProject } from '../../context/project/project.js'; -import { createLocalProjectMemoryIngest } from './local-memory.js'; -import { LocalMemoryRunStore } from './local-memory-runs.js'; +import { initKtxProject } from '../../../src/context/project/project.js'; +import { createLocalProjectMemoryIngest } from '../../../src/context/memory/local-memory.js'; +import { LocalMemoryRunStore } from '../../../src/context/memory/local-memory-runs.js'; vi.mock('ai', () => ({ generateText: vi.fn().mockResolvedValue({ text: '', toolCalls: [] }), diff --git a/packages/cli/src/context/memory/memory-agent.service.ingest.test.ts b/packages/cli/test/context/memory/memory-agent.service.ingest.test.ts similarity index 98% rename from packages/cli/src/context/memory/memory-agent.service.ingest.test.ts rename to packages/cli/test/context/memory/memory-agent.service.ingest.test.ts index 1c13bdd2..acb1c2f8 100644 --- a/packages/cli/src/context/memory/memory-agent.service.ingest.test.ts +++ b/packages/cli/test/context/memory/memory-agent.service.ingest.test.ts @@ -13,8 +13,8 @@ vi.mock('ai', () => ({ // Imported AFTER vi.mock so the mocked module is used. import { generateText } from 'ai'; -import { SYSTEM_GIT_AUTHOR } from '../../context/tools/authors.js'; -import { MemoryAgentService } from './memory-agent.service.js'; +import { SYSTEM_GIT_AUTHOR } from '../../../src/context/tools/authors.js'; +import { MemoryAgentService } from '../../../src/context/memory/memory-agent.service.js'; interface BuiltMocks { appSettings: any; diff --git a/packages/cli/src/context/memory/memory-agent.service.test.ts b/packages/cli/test/context/memory/memory-agent.service.test.ts similarity index 98% rename from packages/cli/src/context/memory/memory-agent.service.test.ts rename to packages/cli/test/context/memory/memory-agent.service.test.ts index cea83674..ba91444e 100644 --- a/packages/cli/src/context/memory/memory-agent.service.test.ts +++ b/packages/cli/test/context/memory/memory-agent.service.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import { validateSingleSource } from '../../context/sl/tools/sl-warehouse-validation.js'; -import { createTouchedSlSources, hasTouchedSlSource } from '../../context/tools/touched-sl-sources.js'; -import { detectCaptureSignals, isWorthAnalyzing } from './capture-signals.js'; -import { MemoryAgentService } from './memory-agent.service.js'; +import { validateSingleSource } from '../../../src/context/sl/tools/sl-warehouse-validation.js'; +import { createTouchedSlSources, hasTouchedSlSource } from '../../../src/context/tools/touched-sl-sources.js'; +import { detectCaptureSignals, isWorthAnalyzing } from '../../../src/context/memory/capture-signals.js'; +import { MemoryAgentService } from '../../../src/context/memory/memory-agent.service.js'; const passthroughValidator = { validateSingleSource: (d: unknown, c: string, n: string) => validateSingleSource(d as never, c, n), diff --git a/packages/cli/src/context/memory/memory-runs.test.ts b/packages/cli/test/context/memory/memory-runs.test.ts similarity index 95% rename from packages/cli/src/context/memory/memory-runs.test.ts rename to packages/cli/test/context/memory/memory-runs.test.ts index e4d0d4ba..b515750e 100644 --- a/packages/cli/src/context/memory/memory-runs.test.ts +++ b/packages/cli/test/context/memory/memory-runs.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { MemoryAgentInput, MemoryAgentResult } from '../../context/memory/types.js'; -import type { MemoryAgentService } from '../../context/memory/memory-agent.service.js'; -import { MemoryIngestService, type MemoryRunStorePort } from './memory-runs.js'; +import type { MemoryAgentInput, MemoryAgentResult } from '../../../src/context/memory/types.js'; +import type { MemoryAgentService } from '../../../src/context/memory/memory-agent.service.js'; +import { MemoryIngestService, type MemoryRunStorePort } from '../../../src/context/memory/memory-runs.js'; class InMemoryRunStore implements MemoryRunStorePort { readonly rows = new Map< diff --git a/packages/cli/src/context/memory/memory-runtime-assets.test.ts b/packages/cli/test/context/memory/memory-runtime-assets.test.ts similarity index 94% rename from packages/cli/src/context/memory/memory-runtime-assets.test.ts rename to packages/cli/test/context/memory/memory-runtime-assets.test.ts index 55d9047c..ab6ff324 100644 --- a/packages/cli/src/context/memory/memory-runtime-assets.test.ts +++ b/packages/cli/test/context/memory/memory-runtime-assets.test.ts @@ -2,13 +2,13 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import { PromptService } from '../../context/prompts/prompt.service.js'; -import { SkillsRegistryService } from '../../context/skills/skills-registry.service.js'; -import { DEFAULT_SKILL_NAMES, promptNameFor } from '../../context/memory/capture-signals.js'; -import type { MemoryAgentSourceType } from '../../context/memory/types.js'; +import { PromptService } from '../../../src/context/prompts/prompt.service.js'; +import { SkillsRegistryService } from '../../../src/context/skills/skills-registry.service.js'; +import { DEFAULT_SKILL_NAMES, promptNameFor } from '../../../src/context/memory/capture-signals.js'; +import type { MemoryAgentSourceType } from '../../../src/context/memory/types.js'; -const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url)); -const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url)); +const promptsDir = fileURLToPath(new URL('../../../src/prompts', import.meta.url)); +const skillsDir = fileURLToPath(new URL('../../../src/skills', import.meta.url)); const memorySourceTypes: MemoryAgentSourceType[] = ['research', 'external_ingest', 'backfill']; const expectedSkillHeadings: Record = { wiki_capture: '# Wiki Capture', diff --git a/packages/cli/src/context/project/config.test.ts b/packages/cli/test/context/project/config.test.ts similarity index 99% rename from packages/cli/src/context/project/config.test.ts rename to packages/cli/test/context/project/config.test.ts index 28e00f74..b2ea498c 100644 --- a/packages/cli/src/context/project/config.test.ts +++ b/packages/cli/test/context/project/config.test.ts @@ -5,7 +5,7 @@ import { parseKtxProjectConfig, serializeKtxProjectConfig, validateKtxProjectConfig, -} from './config.js'; +} from '../../../src/context/project/config.js'; describe('KTX project config', () => { it.each(['status', 'replay', 'run', 'watch'])('accepts former ingest subcommand name "%s" as a connection id', (connectionId) => { diff --git a/packages/cli/src/context/project/driver-schemas.test.ts b/packages/cli/test/context/project/driver-schemas.test.ts similarity index 98% rename from packages/cli/src/context/project/driver-schemas.test.ts rename to packages/cli/test/context/project/driver-schemas.test.ts index 1c5b1276..c83a27a1 100644 --- a/packages/cli/src/context/project/driver-schemas.test.ts +++ b/packages/cli/test/context/project/driver-schemas.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { connectionConfigSchema } from './driver-schemas.js'; +import { connectionConfigSchema } from '../../../src/context/project/driver-schemas.js'; describe('connectionConfigSchema (driver discriminated union)', () => { it.each([ diff --git a/packages/cli/src/context/project/local-git-file-store.test.ts b/packages/cli/test/context/project/local-git-file-store.test.ts similarity index 94% rename from packages/cli/src/context/project/local-git-file-store.test.ts rename to packages/cli/test/context/project/local-git-file-store.test.ts index 1bee3c1e..9a7a6948 100644 --- a/packages/cli/src/context/project/local-git-file-store.test.ts +++ b/packages/cli/test/context/project/local-git-file-store.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { GitService } from '../../context/core/git.service.js'; -import type { KtxCoreConfig } from '../../context/core/config.js'; -import { LocalGitFileStore } from './local-git-file-store.js'; +import { GitService } from '../../../src/context/core/git.service.js'; +import type { KtxCoreConfig } from '../../../src/context/core/config.js'; +import { LocalGitFileStore } from '../../../src/context/project/local-git-file-store.js'; describe('LocalGitFileStore', () => { let tempDir: string; diff --git a/packages/cli/src/context/project/mappings-yaml-schema.test.ts b/packages/cli/test/context/project/mappings-yaml-schema.test.ts similarity index 98% rename from packages/cli/src/context/project/mappings-yaml-schema.test.ts rename to packages/cli/test/context/project/mappings-yaml-schema.test.ts index f7001a70..917cd808 100644 --- a/packages/cli/src/context/project/mappings-yaml-schema.test.ts +++ b/packages/cli/test/context/project/mappings-yaml-schema.test.ts @@ -7,7 +7,7 @@ import { parseLookmlMappingBootstrap, parseLookerMappingBootstrap, parseMetabaseMappingBootstrap, -} from './mappings-yaml-schema.js'; +} from '../../../src/context/project/mappings-yaml-schema.js'; describe('ktx.yaml mapping bootstrap schema', () => { it('parses Metabase mapping intent with CLI syncMode default ALL', () => { diff --git a/packages/cli/src/context/project/project.test.ts b/packages/cli/test/context/project/project.test.ts similarity index 96% rename from packages/cli/src/context/project/project.test.ts rename to packages/cli/test/context/project/project.test.ts index 21e27d6a..668fa264 100644 --- a/packages/cli/src/context/project/project.test.ts +++ b/packages/cli/test/context/project/project.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, loadKtxProject } from './project.js'; +import { initKtxProject, loadKtxProject } from '../../../src/context/project/project.js'; describe('KTX local project runtime', () => { let tempDir: string; diff --git a/packages/cli/src/context/project/setup-config.test.ts b/packages/cli/test/context/project/setup-config.test.ts similarity index 93% rename from packages/cli/src/context/project/setup-config.test.ts rename to packages/cli/test/context/project/setup-config.test.ts index 88c5376e..948d9d54 100644 --- a/packages/cli/src/context/project/setup-config.test.ts +++ b/packages/cli/test/context/project/setup-config.test.ts @@ -2,13 +2,13 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { buildDefaultKtxProjectConfig } from './config.js'; +import { buildDefaultKtxProjectConfig } from '../../../src/context/project/config.js'; import { markKtxSetupStateStepComplete, mergeKtxSetupGitignoreEntries, readKtxSetupState, setKtxSetupDatabaseConnectionIds, -} from './setup-config.js'; +} from '../../../src/context/project/setup-config.js'; describe('KTX setup config helpers', () => { let tempDir: string; diff --git a/packages/cli/src/context/prompts/prompt.service.test.ts b/packages/cli/test/context/prompts/prompt.service.test.ts similarity index 95% rename from packages/cli/src/context/prompts/prompt.service.test.ts rename to packages/cli/test/context/prompts/prompt.service.test.ts index 046b777b..df2e407c 100644 --- a/packages/cli/src/context/prompts/prompt.service.test.ts +++ b/packages/cli/test/context/prompts/prompt.service.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { PromptService } from './prompt.service.js'; +import { PromptService } from '../../../src/context/prompts/prompt.service.js'; describe('PromptService', () => { let dir: string; diff --git a/packages/cli/src/context/scan/constraint-discovery.test.ts b/packages/cli/test/context/scan/constraint-discovery.test.ts similarity index 97% rename from packages/cli/src/context/scan/constraint-discovery.test.ts rename to packages/cli/test/context/scan/constraint-discovery.test.ts index 78620204..0a06f1f1 100644 --- a/packages/cli/src/context/scan/constraint-discovery.test.ts +++ b/packages/cli/test/context/scan/constraint-discovery.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { constraintDiscoveryWarning, tryConstraintQuery } from './constraint-discovery.js'; +import { constraintDiscoveryWarning, tryConstraintQuery } from '../../../src/context/scan/constraint-discovery.js'; describe('tryConstraintQuery', () => { it('returns the query value when the query succeeds', async () => { diff --git a/packages/cli/src/context/scan/credentials.test.ts b/packages/cli/test/context/scan/credentials.test.ts similarity index 96% rename from packages/cli/src/context/scan/credentials.test.ts rename to packages/cli/test/context/scan/credentials.test.ts index 891c58a9..62ee2952 100644 --- a/packages/cli/src/context/scan/credentials.test.ts +++ b/packages/cli/test/context/scan/credentials.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from 'vitest'; -import { REDACTED_KTX_CREDENTIAL_VALUE } from '../core/redaction.js'; +import { REDACTED_KTX_CREDENTIAL_VALUE } from '../../../src/context/core/redaction.js'; import { redactKtxCredentialEnvelope, redactKtxCredentialValue, redactKtxScanMetadata, redactKtxScanReport, redactKtxScanWarning, -} from './credentials.js'; -import type { KtxCredentialEnvelope, KtxScanReport, KtxScanWarning } from './types.js'; +} from '../../../src/context/scan/credentials.js'; +import type { KtxCredentialEnvelope, KtxScanReport, KtxScanWarning } from '../../../src/context/scan/types.js'; describe('KTX scan credential redaction', () => { it('keeps credential references inspectable', () => { diff --git a/packages/cli/src/context/scan/data-dictionary.test.ts b/packages/cli/test/context/scan/data-dictionary.test.ts similarity index 99% rename from packages/cli/src/context/scan/data-dictionary.test.ts rename to packages/cli/test/context/scan/data-dictionary.test.ts index b8b39376..daf20559 100644 --- a/packages/cli/src/context/scan/data-dictionary.test.ts +++ b/packages/cli/test/context/scan/data-dictionary.test.ts @@ -3,7 +3,7 @@ import { defaultKtxDataDictionarySettings, isKtxDataDictionaryCandidate, shouldKtxSampleColumnForDictionary, -} from './data-dictionary.js'; +} from '../../../src/context/scan/data-dictionary.js'; const defaultPatterns = defaultKtxDataDictionarySettings.excludePatterns; diff --git a/packages/cli/src/context/scan/description-generation.test.ts b/packages/cli/test/context/scan/description-generation.test.ts similarity index 99% rename from packages/cli/src/context/scan/description-generation.test.ts rename to packages/cli/test/context/scan/description-generation.test.ts index bc7b1e25..811752e5 100644 --- a/packages/cli/src/context/scan/description-generation.test.ts +++ b/packages/cli/test/context/scan/description-generation.test.ts @@ -12,8 +12,8 @@ import { buildKtxTableDescriptionPrompt, type KtxDescriptionCachePort, KtxDescriptionGenerator, -} from './description-generation.js'; -import { createKtxConnectorCapabilities, type KtxScanConnector } from './types.js'; +} from '../../../src/context/scan/description-generation.js'; +import { createKtxConnectorCapabilities, type KtxScanConnector } from '../../../src/context/scan/types.js'; function createCache(initial: Record = {}): KtxDescriptionCachePort { const data = new Map(Object.entries(initial)); @@ -72,6 +72,8 @@ function createConnector(): KtxScanConnector { introspect: vi.fn(async () => { throw new Error('introspection is not used by description generation'); }), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), sampleColumn: vi.fn(async () => ({ values: ['paid', 'refunded', null], nullCount: 1, diff --git a/packages/cli/src/context/scan/embedding-text.test.ts b/packages/cli/test/context/scan/embedding-text.test.ts similarity index 94% rename from packages/cli/src/context/scan/embedding-text.test.ts rename to packages/cli/test/context/scan/embedding-text.test.ts index ee019bce..523d1d5c 100644 --- a/packages/cli/src/context/scan/embedding-text.test.ts +++ b/packages/cli/test/context/scan/embedding-text.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildKtxColumnEmbeddingText } from './embedding-text.js'; +import { buildKtxColumnEmbeddingText } from '../../../src/context/scan/embedding-text.js'; describe('KTX scan embedding text', () => { it('builds column embedding text with table, description, FK, and sample-value context', () => { diff --git a/packages/cli/src/context/scan/enrichment-state.test.ts b/packages/cli/test/context/scan/enrichment-state.test.ts similarity index 95% rename from packages/cli/src/context/scan/enrichment-state.test.ts rename to packages/cli/test/context/scan/enrichment-state.test.ts index 4ae597c6..24b4bae3 100644 --- a/packages/cli/src/context/scan/enrichment-state.test.ts +++ b/packages/cli/test/context/scan/enrichment-state.test.ts @@ -6,9 +6,9 @@ import { completedKtxScanEnrichmentStateSummary, computeKtxScanEnrichmentInputHash, summarizeKtxScanEnrichmentState, -} from './enrichment-state.js'; -import { SqliteLocalScanEnrichmentStateStore } from './sqlite-local-enrichment-state-store.js'; -import type { KtxSchemaSnapshot } from './types.js'; +} from '../../../src/context/scan/enrichment-state.js'; +import { SqliteLocalScanEnrichmentStateStore } from '../../../src/context/scan/sqlite-local-enrichment-state-store.js'; +import type { KtxSchemaSnapshot } from '../../../src/context/scan/types.js'; const snapshot: KtxSchemaSnapshot = { connectionId: 'warehouse', diff --git a/packages/cli/src/context/scan/enrichment-summary.test.ts b/packages/cli/test/context/scan/enrichment-summary.test.ts similarity index 96% rename from packages/cli/src/context/scan/enrichment-summary.test.ts rename to packages/cli/test/context/scan/enrichment-summary.test.ts index f320046b..783e18cd 100644 --- a/packages/cli/src/context/scan/enrichment-summary.test.ts +++ b/packages/cli/test/context/scan/enrichment-summary.test.ts @@ -3,7 +3,7 @@ import { failedKtxScanEnrichmentSummary, ktxScanErrorMessage, skippedKtxScanEnrichmentSummary, -} from './enrichment-summary.js'; +} from '../../../src/context/scan/enrichment-summary.js'; describe('KTX scan enrichment summaries', () => { it('keeps structural scans skipped when no enrichment was requested', () => { diff --git a/packages/cli/src/context/scan/enrichment-types.test.ts b/packages/cli/test/context/scan/enrichment-types.test.ts similarity index 99% rename from packages/cli/src/context/scan/enrichment-types.test.ts rename to packages/cli/test/context/scan/enrichment-types.test.ts index 3f7828dc..72e7b247 100644 --- a/packages/cli/src/context/scan/enrichment-types.test.ts +++ b/packages/cli/test/context/scan/enrichment-types.test.ts @@ -9,7 +9,7 @@ import type { KtxRelationshipUpdate, KtxScanMetadataStore, KtxStructuralSyncPlan, -} from './enrichment-types.js'; +} from '../../../src/context/scan/enrichment-types.js'; describe('KTX scan enrichment contracts', () => { it('models an enriched schema with reusable table, column, and relationship metadata', () => { diff --git a/packages/cli/src/context/scan/entity-details.test.ts b/packages/cli/test/context/scan/entity-details.test.ts similarity index 91% rename from packages/cli/src/context/scan/entity-details.test.ts rename to packages/cli/test/context/scan/entity-details.test.ts index ddccef87..ea8d01b7 100644 --- a/packages/cli/src/context/scan/entity-details.test.ts +++ b/packages/cli/test/context/scan/entity-details.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { createKtxEntityDetailsService } from './entity-details.js'; -import type { KtxConnectionDriver, KtxScanReport, KtxSchemaTable } from './types.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { createKtxEntityDetailsService } from '../../../src/context/scan/entity-details.js'; +import type { KtxConnectionDriver, KtxScanReport, KtxSchemaTable } from '../../../src/context/scan/types.js'; describe('createKtxEntityDetailsService', () => { let tempDir: string; @@ -201,6 +201,22 @@ describe('createKtxEntityDetailsService', () => { }); }); + it('resolves quoted qualified display strings through the dialect parser', async () => { + await seedScan({ syncId: 'sync-1', runId: 'scan-1' }); + const service = createKtxEntityDetailsService(project); + + const result = await service.read({ + connectionId: 'warehouse', + entities: [{ table: '"public"."orders"' }], + }); + + expect(result.results[0]).toMatchObject({ + ok: true, + display: 'public.orders', + tableRef: { catalog: null, db: 'public', name: 'orders' }, + }); + }); + it('filters requested columns while keeping full-table foreign keys', async () => { await seedScan({ syncId: 'sync-1', runId: 'scan-1' }); const service = createKtxEntityDetailsService(project); diff --git a/packages/cli/src/context/scan/local-enrichment-artifacts.test.ts b/packages/cli/test/context/scan/local-enrichment-artifacts.test.ts similarity index 98% rename from packages/cli/src/context/scan/local-enrichment-artifacts.test.ts rename to packages/cli/test/context/scan/local-enrichment-artifacts.test.ts index 8a49fc78..638bafb2 100644 --- a/packages/cli/src/context/scan/local-enrichment-artifacts.test.ts +++ b/packages/cli/test/context/scan/local-enrichment-artifacts.test.ts @@ -3,10 +3,10 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import YAML from 'yaml'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import type { KtxLocalScanEnrichmentResult } from './local-enrichment.js'; -import { writeLocalScanEnrichmentArtifacts, writeLocalScanManifestShards } from './local-enrichment-artifacts.js'; -import type { KtxSchemaSnapshot } from './types.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import type { KtxLocalScanEnrichmentResult } from '../../../src/context/scan/local-enrichment.js'; +import { writeLocalScanEnrichmentArtifacts, writeLocalScanManifestShards } from '../../../src/context/scan/local-enrichment-artifacts.js'; +import type { KtxSchemaSnapshot } from '../../../src/context/scan/types.js'; const snapshot: KtxSchemaSnapshot = { connectionId: 'warehouse', diff --git a/packages/cli/src/context/scan/local-enrichment.test.ts b/packages/cli/test/context/scan/local-enrichment.test.ts similarity index 96% rename from packages/cli/src/context/scan/local-enrichment.test.ts rename to packages/cli/test/context/scan/local-enrichment.test.ts index 9647c8b9..9704d071 100644 --- a/packages/cli/src/context/scan/local-enrichment.test.ts +++ b/packages/cli/test/context/scan/local-enrichment.test.ts @@ -1,17 +1,17 @@ import Database from 'better-sqlite3'; import { describe, expect, it, vi } from 'vitest'; -import { buildDefaultKtxProjectConfig } from '../project/config.js'; +import { buildDefaultKtxProjectConfig } from '../../../src/context/project/config.js'; import type { KtxScanEnrichmentCompletedStage, KtxScanEnrichmentFailedStage, KtxScanEnrichmentStageLookup, KtxScanEnrichmentStateStore, -} from './enrichment-state.js'; +} from '../../../src/context/scan/enrichment-state.js'; import { createDeterministicLocalScanEnrichmentProviders, runLocalScanEnrichment, snapshotToKtxEnrichedSchema, -} from './local-enrichment.js'; +} from '../../../src/context/scan/local-enrichment.js'; import { createKtxConnectorCapabilities, type KtxQueryResult, @@ -20,7 +20,7 @@ import { type KtxScanConnector, type KtxScanContext, type KtxSchemaSnapshot, -} from './types.js'; +} from '../../../src/context/scan/types.js'; function fakeScanEmbedding(options: { dimensions: number; maxBatchSize?: number }): KtxEmbeddingPort { return { @@ -104,6 +104,8 @@ function connector(): KtxScanConnector { columnStats: true, }), introspect: vi.fn(async () => snapshot), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), sampleTable: vi.fn(async () => ({ headers: ['id', 'customer_id'], rows: [[1, 10]], @@ -331,6 +333,27 @@ describe('local scan enrichment', () => { expect(scanConnector.introspect).toHaveBeenCalledTimes(1); }); + it('fails when connector driver and snapshot driver differ', async () => { + const mismatchedConnector: KtxScanConnector = { + ...connector(), + driver: 'mysql', + }; + + await expect( + runLocalScanEnrichment({ + connectionId: 'warehouse', + mode: 'relationships', + detectRelationships: true, + connector: mismatchedConnector, + snapshot, + context: { runId: 'scan-run-driver-mismatch' }, + providers: null, + }), + ).rejects.toThrow( + 'ktx scan connector driver "mysql" does not match snapshot driver "postgres" for connection "warehouse"', + ); + }); + it('runs deterministic relationship detection for relationship scans', async () => { const result = await runLocalScanEnrichment({ connectionId: 'warehouse', diff --git a/packages/cli/src/context/scan/local-scan.test.ts b/packages/cli/test/context/scan/local-scan.test.ts similarity index 98% rename from packages/cli/src/context/scan/local-scan.test.ts rename to packages/cli/test/context/scan/local-scan.test.ts index f3c1353d..931fd6b0 100644 --- a/packages/cli/src/context/scan/local-scan.test.ts +++ b/packages/cli/test/context/scan/local-scan.test.ts @@ -3,18 +3,23 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import YAML from 'yaml'; -import type { SourceAdapter } from '../../context/ingest/types.js'; -import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js'; -import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../context/project/project.js'; -import { resolveEnabledTables } from './enabled-tables.js'; -import { getLocalScanReport, getLocalScanStatus, runLocalScan } from './local-scan.js'; -import { tableRefKey, tableRefSet, type KtxTableRefKey } from './table-ref.js'; +import type { SourceAdapter } from '../../../src/context/ingest/types.js'; +import type { KtxLlmRuntimePort } from '../../../src/context/llm/runtime-port.js'; +import { initKtxProject, type KtxLocalProject, loadKtxProject } from '../../../src/context/project/project.js'; +import { resolveEnabledTables } from '../../../src/context/scan/enabled-tables.js'; +import { getLocalScanReport, getLocalScanStatus, runLocalScan } from '../../../src/context/scan/local-scan.js'; +import { tableRefKey, tableRefSet, type KtxTableRefKey } from '../../../src/context/scan/table-ref.js'; import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanConnector, KtxSchemaSnapshot, -} from './types.js'; +} from '../../../src/context/scan/types.js'; + +const connectorScopeListing = { + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), +}; function relationshipSqlResult( input: KtxReadOnlyQueryInput, @@ -254,6 +259,7 @@ function nativeScanConnector(options: { cleanup?: () => Promise } = {}): K formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, introspect: vi.fn(async () => nativeScanSnapshot()), sampleTable: vi.fn(async () => ({ headers: ['id'], rows: [[1]], totalRows: 1 })), sampleColumn: vi.fn(async () => ({ values: ['1'], nullCount: 0, distinctCount: 1 })), @@ -656,6 +662,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -741,6 +748,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -930,6 +938,14 @@ describe('local scan', () => { }; } + async listSchemas(): Promise { + return []; + } + + async listTables() { + return []; + } + async executeReadOnly(input: KtxReadOnlyQueryInput): Promise { return relationshipSqlResult(input); } @@ -972,6 +988,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1073,6 +1090,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1200,6 +1218,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1340,6 +1359,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1455,6 +1475,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1550,6 +1571,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1666,6 +1688,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', diff --git a/packages/cli/src/context/scan/local-structural-artifacts.test.ts b/packages/cli/test/context/scan/local-structural-artifacts.test.ts similarity index 97% rename from packages/cli/src/context/scan/local-structural-artifacts.test.ts rename to packages/cli/test/context/scan/local-structural-artifacts.test.ts index f5c5869c..519bb26d 100644 --- a/packages/cli/src/context/scan/local-structural-artifacts.test.ts +++ b/packages/cli/test/context/scan/local-structural-artifacts.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { readLocalScanStructuralSnapshot } from './local-structural-artifacts.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { readLocalScanStructuralSnapshot } from '../../../src/context/scan/local-structural-artifacts.js'; describe('readLocalScanStructuralSnapshot', () => { let tempDir: string; diff --git a/packages/cli/src/context/scan/relationship-benchmark-report.test.ts b/packages/cli/test/context/scan/relationship-benchmark-report.test.ts similarity index 99% rename from packages/cli/src/context/scan/relationship-benchmark-report.test.ts rename to packages/cli/test/context/scan/relationship-benchmark-report.test.ts index 8941eec1..0837ed3c 100644 --- a/packages/cli/src/context/scan/relationship-benchmark-report.test.ts +++ b/packages/cli/test/context/scan/relationship-benchmark-report.test.ts @@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest'; import { buildKtxRelationshipBenchmarkReport, formatKtxRelationshipBenchmarkReportMarkdown, -} from './relationship-benchmark-report.js'; +} from '../../../src/context/scan/relationship-benchmark-report.js'; import type { KtxRelationshipBenchmarkCaseResult, KtxRelationshipBenchmarkFixture, KtxRelationshipBenchmarkSuiteResult, -} from './relationship-benchmarks.js'; +} from '../../../src/context/scan/relationship-benchmarks.js'; type CaseResultOverrides = Omit, 'metrics'> & { metrics?: Partial; diff --git a/packages/cli/src/context/scan/relationship-benchmarks.test.ts b/packages/cli/test/context/scan/relationship-benchmarks.test.ts similarity index 96% rename from packages/cli/src/context/scan/relationship-benchmarks.test.ts rename to packages/cli/test/context/scan/relationship-benchmarks.test.ts index aff025aa..daaa2142 100644 --- a/packages/cli/src/context/scan/relationship-benchmarks.test.ts +++ b/packages/cli/test/context/scan/relationship-benchmarks.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; import type { KtxRelationshipBenchmarkExpectedLinks, KtxRelationshipBenchmarkFixture, -} from './relationship-benchmarks.js'; +} from '../../../src/context/scan/relationship-benchmarks.js'; import { currentKtxRelationshipBenchmarkDetector, loadKtxRelationshipBenchmarkFixture, @@ -13,8 +13,8 @@ import { maskKtxRelationshipBenchmarkSnapshot, runKtxRelationshipBenchmarkCase, runKtxRelationshipBenchmarkSuite, -} from './relationship-benchmarks.js'; -import type { KtxSchemaSnapshot } from './types.js'; +} from '../../../src/context/scan/relationship-benchmarks.js'; +import type { KtxSchemaSnapshot } from '../../../src/context/scan/types.js'; const EXPECTED_LINKS: KtxRelationshipBenchmarkExpectedLinks = { expectedPks: [ @@ -140,7 +140,7 @@ function snapshot(): KtxSchemaSnapshot { describe('relationship benchmarks', () => { it('keeps the current benchmark detector on the relationship-discovery path only', async () => { - const source = await readFile(new URL('relationship-benchmarks.ts', import.meta.url), 'utf-8'); + const source = await readFile(new URL('../../../src/context/scan/relationship-benchmarks.ts', import.meta.url), 'utf-8'); expect(source).not.toMatch(/KtxRelationshipDetector/); expect(source).not.toMatch(/relationship-detection\.js/); @@ -261,7 +261,7 @@ describe('relationship benchmarks', () => { }); it('loads the composite-key fixture and accepts composite ground truth as headline evidence', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'composite_keys_no_declared_constraints'), ); @@ -586,7 +586,7 @@ describe('relationship benchmarks', () => { }); it('loads every checked-in relationship benchmark fixture with explicit provenance', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixtureDirs = (await readdir(fixtureRoot, { withFileTypes: true })) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) @@ -601,7 +601,7 @@ describe('relationship benchmarks', () => { }); it('loads May 8 evidence-fusion adversarial fixtures as reported synthetic evidence', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixtures = await loadKtxRelationshipBenchmarkFixtures(fixtureRoot.pathname); const byId = new Map(fixtures.map((fixture) => [fixture.id, fixture])); const adversarialIds = [ @@ -634,7 +634,7 @@ describe('relationship benchmarks', () => { }); it('loads the May 8 scale stress fixture with bounded benchmark validation', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'scale_stress_no_declared_constraints'), ); @@ -651,7 +651,7 @@ describe('relationship benchmarks', () => { }); adHocRelationshipBenchmarkIt('runs the scale stress fixture inside the benchmark validation budget', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'scale_stress_no_declared_constraints'), ); @@ -846,7 +846,7 @@ describe('relationship benchmarks', () => { }); it('loads the packaged B2B demo fixtures and records the current relationship-discovery baseline', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const declared = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'demo_b2b_declared_metadata'), ); @@ -927,7 +927,7 @@ describe('relationship benchmarks', () => { }); it('loads the public Chinook benchmark fixture with declared metadata', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'chinook_with_declared_metadata'), ); @@ -945,7 +945,7 @@ describe('relationship benchmarks', () => { }); it('loads the public Northwind benchmark fixture with declared metadata', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'northwind_with_declared_metadata'), ); @@ -961,7 +961,7 @@ describe('relationship benchmarks', () => { }); it('loads the public Sakila benchmark fixture with declared metadata', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'sakila_with_declared_metadata'), ); @@ -977,7 +977,7 @@ describe('relationship benchmarks', () => { }); it('loads the public AdventureWorksLT benchmark fixture with declared metadata', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'adventureworkslt_with_declared_metadata'), ); @@ -1037,7 +1037,7 @@ describe('relationship benchmarks', () => { }); it('loads the full AdventureWorks OLTP benchmark fixture with declared metadata', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'adventureworks_oltp_with_declared_metadata'), ); @@ -1097,7 +1097,7 @@ describe('relationship benchmarks', () => { }); it('loads the row-bearing natural-key fixture and counts it as headline evidence', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const naturalKeys = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'natural_keys_no_declared_constraints'), ); @@ -1131,7 +1131,7 @@ describe('relationship benchmarks', () => { }); it('accepts plan-code suffix relationships only when validation is available', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'plan_code_no_declared_constraints'), ); @@ -1192,7 +1192,7 @@ describe('relationship benchmarks', () => { }); it('uses embedding fixtures for semantic alias relationship benchmark cases', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'semantic_embedding_aliases_no_declared_constraints'), ); @@ -1223,7 +1223,7 @@ describe('relationship benchmarks', () => { }); it('loads the Orbit-style product fixture as curated relationship-discovery benchmark evidence', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'orbit_style_product_no_declared_constraints'), ); diff --git a/packages/cli/src/context/scan/relationship-budget.test.ts b/packages/cli/test/context/scan/relationship-budget.test.ts similarity index 97% rename from packages/cli/src/context/scan/relationship-budget.test.ts rename to packages/cli/test/context/scan/relationship-budget.test.ts index 479e5b23..b3c6edcf 100644 --- a/packages/cli/src/context/scan/relationship-budget.test.ts +++ b/packages/cli/test/context/scan/relationship-budget.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { applyKtxRelationshipValidationBudget, defaultKtxRelationshipValidationBudget } from './relationship-budget.js'; +import { applyKtxRelationshipValidationBudget, defaultKtxRelationshipValidationBudget } from '../../../src/context/scan/relationship-budget.js'; interface Candidate { id: string; diff --git a/packages/cli/src/context/scan/relationship-candidates.test.ts b/packages/cli/test/context/scan/relationship-candidates.test.ts similarity index 98% rename from packages/cli/src/context/scan/relationship-candidates.test.ts rename to packages/cli/test/context/scan/relationship-candidates.test.ts index 795d7791..cfe5ce2c 100644 --- a/packages/cli/src/context/scan/relationship-candidates.test.ts +++ b/packages/cli/test/context/scan/relationship-candidates.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest'; -import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js'; -import { normalizeKtxRelationshipName } from './relationship-name-similarity.js'; +import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from '../../../src/context/scan/enrichment-types.js'; +import { normalizeKtxRelationshipName } from '../../../src/context/scan/relationship-name-similarity.js'; import { generateKtxRelationshipDiscoveryCandidates, inferKtxRelationshipTargetPks, mergeKtxRelationshipDiscoveryCandidates, -} from './relationship-candidates.js'; -import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js'; +} from '../../../src/context/scan/relationship-candidates.js'; +import type { KtxRelationshipProfileArtifact } from '../../../src/context/scan/relationship-profiling.js'; function column( tableId: string, diff --git a/packages/cli/src/context/scan/relationship-composite-candidates.test.ts b/packages/cli/test/context/scan/relationship-composite-candidates.test.ts similarity index 80% rename from packages/cli/src/context/scan/relationship-composite-candidates.test.ts rename to packages/cli/test/context/scan/relationship-composite-candidates.test.ts index abf495e1..e0a9ca6c 100644 --- a/packages/cli/src/context/scan/relationship-composite-candidates.test.ts +++ b/packages/cli/test/context/scan/relationship-composite-candidates.test.ts @@ -1,11 +1,12 @@ import Database from 'better-sqlite3'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import { snapshotToKtxEnrichedSchema } from './local-enrichment.js'; -import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot } from './relationship-benchmarks.js'; -import { discoverKtxCompositeRelationships } from './relationship-composite-candidates.js'; -import { profileKtxRelationshipSchema, type KtxRelationshipReadOnlyExecutor } from './relationship-profiling.js'; -import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from './types.js'; +import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; +import { snapshotToKtxEnrichedSchema } from '../../../src/context/scan/local-enrichment.js'; +import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot } from '../../../src/context/scan/relationship-benchmarks.js'; +import { discoverKtxCompositeRelationships } from '../../../src/context/scan/relationship-composite-candidates.js'; +import { profileKtxRelationshipSchema, type KtxRelationshipReadOnlyExecutor } from '../../../src/context/scan/relationship-profiling.js'; +import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from '../../../src/context/scan/types.js'; class TestSqliteExecutor implements KtxRelationshipReadOnlyExecutor { private readonly db: Database.Database; @@ -32,7 +33,7 @@ class TestSqliteExecutor implements KtxRelationshipReadOnlyExecutor { describe('composite relationship discovery detector', () => { it('infers composite primary keys and validates composite foreign keys from row evidence', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'composite_keys_no_declared_constraints'), ); @@ -41,7 +42,7 @@ describe('composite relationship discovery detector', () => { const executor = new TestSqliteExecutor(fixture.dataPath ?? ''); const profiles = await profileKtxRelationshipSchema({ connectionId: snapshot.connectionId, - driver: snapshot.driver, + dialect: getDialectForDriver(snapshot.driver), schema, executor, ctx: { runId: 'test:composite-profile' }, @@ -49,7 +50,7 @@ describe('composite relationship discovery detector', () => { const result = await discoverKtxCompositeRelationships({ connectionId: snapshot.connectionId, - driver: snapshot.driver, + dialect: getDialectForDriver(snapshot.driver), schema, profiles, executor, diff --git a/packages/cli/src/context/scan/relationship-diagnostics.test.ts b/packages/cli/test/context/scan/relationship-diagnostics.test.ts similarity index 98% rename from packages/cli/src/context/scan/relationship-diagnostics.test.ts rename to packages/cli/test/context/scan/relationship-diagnostics.test.ts index 7c1dbb76..647ac931 100644 --- a/packages/cli/src/context/scan/relationship-diagnostics.test.ts +++ b/packages/cli/test/context/scan/relationship-diagnostics.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; -import type { KtxEnrichedRelationship, KtxRelationshipEndpoint } from './enrichment-types.js'; -import type { KtxResolvedRelationshipDiscoveryCandidate } from './relationship-graph-resolver.js'; +import type { KtxEnrichedRelationship, KtxRelationshipEndpoint } from '../../../src/context/scan/enrichment-types.js'; +import type { KtxResolvedRelationshipDiscoveryCandidate } from '../../../src/context/scan/relationship-graph-resolver.js'; import { buildKtxRelationshipArtifacts, buildKtxRelationshipDiagnostics, emptyKtxRelationshipProfileArtifact, -} from './relationship-diagnostics.js'; +} from '../../../src/context/scan/relationship-diagnostics.js'; function endpoint(table: string, column: string): KtxRelationshipEndpoint { return { diff --git a/packages/cli/src/context/scan/relationship-discovery.test.ts b/packages/cli/test/context/scan/relationship-discovery.test.ts similarity index 94% rename from packages/cli/src/context/scan/relationship-discovery.test.ts rename to packages/cli/test/context/scan/relationship-discovery.test.ts index 400fae62..55341645 100644 --- a/packages/cli/src/context/scan/relationship-discovery.test.ts +++ b/packages/cli/test/context/scan/relationship-discovery.test.ts @@ -1,15 +1,16 @@ import Database from 'better-sqlite3'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js'; -import { buildDefaultKtxProjectConfig } from '../project/config.js'; -import { snapshotToKtxEnrichedSchema } from './local-enrichment.js'; +import type { KtxLlmRuntimePort } from '../../../src/context/llm/runtime-port.js'; +import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; +import { buildDefaultKtxProjectConfig } from '../../../src/context/project/config.js'; +import { snapshotToKtxEnrichedSchema } from '../../../src/context/scan/local-enrichment.js'; import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot, -} from './relationship-benchmarks.js'; -import { discoverKtxRelationships } from './relationship-discovery.js'; -import { createKtxConnectorCapabilities } from './types.js'; -import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanConnector, KtxScanContext, KtxSchemaSnapshot } from './types.js'; +} from '../../../src/context/scan/relationship-benchmarks.js'; +import { discoverKtxRelationships } from '../../../src/context/scan/relationship-discovery.js'; +import { createKtxConnectorCapabilities } from '../../../src/context/scan/types.js'; +import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanConnector, KtxScanContext, KtxSchemaSnapshot } from '../../../src/context/scan/types.js'; class InMemorySqliteExecutor { readonly db = new Database(':memory:'); @@ -212,6 +213,8 @@ function connector(executor: InMemorySqliteExecutor | null): KtxScanConnector { columnSampling: false, }), introspect: async () => snapshot(), + listSchemas: async () => [], + listTables: async () => [], executeReadOnly: executor ? executor.executeReadOnly.bind(executor) : undefined, }; } @@ -308,7 +311,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: connector(executor), schema: snapshotToKtxEnrichedSchema(snapshot()), context: { runId: 'relationship-run-1' }, @@ -347,7 +350,7 @@ describe('production relationship discovery', () => { const schema = naturalKeySnapshot(); const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: { ...connector(executor), introspect: async () => schema, @@ -397,7 +400,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: { ...connector(executor), introspect: async () => sourceSnapshot, @@ -430,7 +433,7 @@ describe('production relationship discovery', () => { it('keeps candidates review-only when read-only SQL is unavailable', async () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: connector(null), schema: snapshotToKtxEnrichedSchema(snapshot()), context: { runId: 'relationship-run-no-sql' }, @@ -456,7 +459,7 @@ describe('production relationship discovery', () => { const sourceSnapshot = declaredForeignKeySnapshot(); const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: connector(null), schema: snapshotToKtxEnrichedSchema(sourceSnapshot), context: { runId: 'formal-metadata-no-sql' }, @@ -503,7 +506,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: connector(executor), schema: snapshotToKtxEnrichedSchema(llmOnlyRelationshipSnapshot()), context: { runId: 'llm-relationship-orchestrator' }, @@ -543,7 +546,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: connector(executor), schema: snapshotToKtxEnrichedSchema(snapshot()), context: { runId: 'configured-thresholds' }, @@ -604,7 +607,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), connector: { ...connector(executor), introspect: async () => richSnapshot, @@ -628,7 +631,7 @@ describe('production relationship discovery', () => { it('accepts SQL-validated composite relationships in production relationship-discovery detection', async () => { const fixtureRoot = new URL( - '../../test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints', + '../../fixtures/relationship-benchmarks/composite_keys_no_declared_constraints', import.meta.url, ); const fixture = await loadKtxRelationshipBenchmarkFixture(fixtureRoot.pathname); @@ -644,6 +647,8 @@ describe('production relationship discovery', () => { columnSampling: false, }), introspect: async () => maskedSnapshot, + listSchemas: async () => [], + listTables: async () => [], executeReadOnly: async (input) => { const rows = database.prepare(input.sql).all() as Record[]; const headers = Object.keys(rows[0] ?? {}); @@ -658,7 +663,7 @@ describe('production relationship discovery', () => { const result = await discoverKtxRelationships({ connectionId: maskedSnapshot.connectionId, - driver: maskedSnapshot.driver, + dialect: getDialectForDriver(maskedSnapshot.driver), connector: testConnector, schema: snapshotToKtxEnrichedSchema(maskedSnapshot, new Map()), context: { runId: 'test:production-composite' }, diff --git a/packages/cli/src/context/scan/relationship-formal-metadata.test.ts b/packages/cli/test/context/scan/relationship-formal-metadata.test.ts similarity index 96% rename from packages/cli/src/context/scan/relationship-formal-metadata.test.ts rename to packages/cli/test/context/scan/relationship-formal-metadata.test.ts index 8e4a57ff..3fd94a42 100644 --- a/packages/cli/src/context/scan/relationship-formal-metadata.test.ts +++ b/packages/cli/test/context/scan/relationship-formal-metadata.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { KtxEnrichedRelationship, KtxEnrichedSchema } from './enrichment-types.js'; -import { collectKtxFormalMetadataRelationships } from './relationship-formal-metadata.js'; +import type { KtxEnrichedRelationship, KtxEnrichedSchema } from '../../../src/context/scan/enrichment-types.js'; +import { collectKtxFormalMetadataRelationships } from '../../../src/context/scan/relationship-formal-metadata.js'; function schema(relationships: KtxEnrichedRelationship[]): KtxEnrichedSchema { return { diff --git a/packages/cli/src/context/scan/relationship-graph-resolver.test.ts b/packages/cli/test/context/scan/relationship-graph-resolver.test.ts similarity index 98% rename from packages/cli/src/context/scan/relationship-graph-resolver.test.ts rename to packages/cli/test/context/scan/relationship-graph-resolver.test.ts index 945e8257..643d7956 100644 --- a/packages/cli/src/context/scan/relationship-graph-resolver.test.ts +++ b/packages/cli/test/context/scan/relationship-graph-resolver.test.ts @@ -4,10 +4,10 @@ import type { KtxEnrichedSchema, KtxEnrichedTable, KtxRelationshipEndpoint, -} from './enrichment-types.js'; -import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js'; -import type { KtxValidatedRelationshipDiscoveryCandidate } from './relationship-validation.js'; -import { resolveKtxRelationshipGraph } from './relationship-graph-resolver.js'; +} from '../../../src/context/scan/enrichment-types.js'; +import type { KtxRelationshipProfileArtifact } from '../../../src/context/scan/relationship-profiling.js'; +import type { KtxValidatedRelationshipDiscoveryCandidate } from '../../../src/context/scan/relationship-validation.js'; +import { resolveKtxRelationshipGraph } from '../../../src/context/scan/relationship-graph-resolver.js'; function column(tableId: string, name: string, overrides: Partial = {}): KtxEnrichedColumn { const tableRef = overrides.tableRef ?? { catalog: null, db: null, name: tableId }; diff --git a/packages/cli/src/context/scan/relationship-llm-proposal.test.ts b/packages/cli/test/context/scan/relationship-llm-proposal.test.ts similarity index 94% rename from packages/cli/src/context/scan/relationship-llm-proposal.test.ts rename to packages/cli/test/context/scan/relationship-llm-proposal.test.ts index 46e22dcb..0713a1c6 100644 --- a/packages/cli/src/context/scan/relationship-llm-proposal.test.ts +++ b/packages/cli/test/context/scan/relationship-llm-proposal.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js'; -import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js'; -import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js'; -import { proposeKtxRelationshipCandidatesWithLlm } from './relationship-llm-proposal.js'; +import type { KtxLlmRuntimePort } from '../../../src/context/llm/runtime-port.js'; +import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from '../../../src/context/scan/enrichment-types.js'; +import type { KtxRelationshipProfileArtifact } from '../../../src/context/scan/relationship-profiling.js'; +import { proposeKtxRelationshipCandidatesWithLlm } from '../../../src/context/scan/relationship-llm-proposal.js'; function llmRuntime(output?: unknown): KtxLlmRuntimePort { return { diff --git a/packages/cli/src/context/scan/relationship-locality.test.ts b/packages/cli/test/context/scan/relationship-locality.test.ts similarity index 96% rename from packages/cli/src/context/scan/relationship-locality.test.ts rename to packages/cli/test/context/scan/relationship-locality.test.ts index 85dd4350..1a7f09fd 100644 --- a/packages/cli/src/context/scan/relationship-locality.test.ts +++ b/packages/cli/test/context/scan/relationship-locality.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { KtxEnrichedColumn, KtxEnrichedTable } from './enrichment-types.js'; -import { localCandidateTables } from './relationship-locality.js'; +import type { KtxEnrichedColumn, KtxEnrichedTable } from '../../../src/context/scan/enrichment-types.js'; +import { localCandidateTables } from '../../../src/context/scan/relationship-locality.js'; function column( tableId: string, diff --git a/packages/cli/src/context/scan/relationship-name-similarity.test.ts b/packages/cli/test/context/scan/relationship-name-similarity.test.ts similarity index 97% rename from packages/cli/src/context/scan/relationship-name-similarity.test.ts rename to packages/cli/test/context/scan/relationship-name-similarity.test.ts index 34730c81..0f8f437c 100644 --- a/packages/cli/src/context/scan/relationship-name-similarity.test.ts +++ b/packages/cli/test/context/scan/relationship-name-similarity.test.ts @@ -5,7 +5,7 @@ import { singularizeKtxRelationshipToken, tokenSimilarity, tokenizeKtxRelationshipName, -} from './relationship-name-similarity.js'; +} from '../../../src/context/scan/relationship-name-similarity.js'; describe('relationship name similarity', () => { it('tokenizes common warehouse naming styles', () => { diff --git a/packages/cli/src/context/scan/relationship-profiling.test.ts b/packages/cli/test/context/scan/relationship-profiling.test.ts similarity index 90% rename from packages/cli/src/context/scan/relationship-profiling.test.ts rename to packages/cli/test/context/scan/relationship-profiling.test.ts index 76151d23..7983d958 100644 --- a/packages/cli/src/context/scan/relationship-profiling.test.ts +++ b/packages/cli/test/context/scan/relationship-profiling.test.ts @@ -2,16 +2,12 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import Database from 'better-sqlite3'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js'; -import { snapshotToKtxEnrichedSchema } from './local-enrichment.js'; -import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot } from './relationship-benchmarks.js'; -import { - createKtxRelationshipProfileCache, - formatKtxRelationshipTableRef, - profileKtxRelationshipSchema, - quoteKtxRelationshipIdentifier, -} from './relationship-profiling.js'; -import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from './types.js'; +import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; +import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from '../../../src/context/scan/enrichment-types.js'; +import { snapshotToKtxEnrichedSchema } from '../../../src/context/scan/local-enrichment.js'; +import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot } from '../../../src/context/scan/relationship-benchmarks.js'; +import { createKtxRelationshipProfileCache, profileKtxRelationshipSchema } from '../../../src/context/scan/relationship-profiling.js'; +import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from '../../../src/context/scan/types.js'; class InMemorySqliteExecutor { readonly db = new Database(':memory:'); @@ -104,7 +100,7 @@ describe('relationship profiling', () => { }); it('keeps profiling on the batched table path', async () => { - const source = await readFile(new URL('relationship-profiling.ts', import.meta.url), 'utf-8'); + const source = await readFile(new URL('../../../src/context/scan/relationship-profiling.ts', import.meta.url), 'utf-8'); expect(source).not.toMatch(new RegExp('queryColumn' + 'Profile')); expect(source).not.toMatch(/for \(const column of table\.columns\)[\s\S]*executeReadOnly/); @@ -112,16 +108,6 @@ describe('relationship profiling', () => { expect(source).toMatch(/UNION ALL/); }); - it('quotes identifiers and formats table refs for supported local SQL drivers', () => { - expect(quoteKtxRelationshipIdentifier('sqlite', 'odd"name')).toBe('"odd""name"'); - expect(quoteKtxRelationshipIdentifier('mysql', 'odd`name')).toBe('`odd``name`'); - expect(quoteKtxRelationshipIdentifier('sqlserver', 'odd]name')).toBe('[odd]]name]'); - expect(formatKtxRelationshipTableRef('sqlite', { catalog: null, db: null, name: 'accounts' })).toBe('"accounts"'); - expect(formatKtxRelationshipTableRef('postgres', { catalog: null, db: 'analytics', name: 'accounts' })).toBe( - '"analytics"."accounts"', - ); - }); - it('profiles row count, null rate, uniqueness, sample values, and text lengths', async () => { executor = new InMemorySqliteExecutor(); executor.db.exec(` @@ -135,7 +121,7 @@ describe('relationship profiling', () => { const result = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: schema([ table('accounts', [ column('accounts', 'id', { primaryKey: false, nullable: false }), @@ -197,7 +183,7 @@ describe('relationship profiling', () => { const result = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: schema([ table('accounts', [ column('accounts', 'id', { nullable: false }), @@ -240,7 +226,7 @@ describe('relationship profiling', () => { const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: schema([ table('accounts', [ column('accounts', 'id', { nullable: false }), @@ -291,7 +277,7 @@ describe('relationship profiling', () => { const first = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: relationshipSchema, executor, ctx: { runId: 'profile-cache-run' }, @@ -299,7 +285,7 @@ describe('relationship profiling', () => { }); const second = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: relationshipSchema, executor, ctx: { runId: 'profile-cache-run' }, @@ -307,7 +293,7 @@ describe('relationship profiling', () => { }); const third = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: relationshipSchema, executor, ctx: { runId: 'profile-cache-fresh-run' }, @@ -323,7 +309,7 @@ describe('relationship profiling', () => { }); it('profiles the checked-in scale stress fixture with one query per table', async () => { - const fixtureRoot = new URL('../../test/fixtures/relationship-benchmarks/', import.meta.url); + const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture(join(fixtureRoot.pathname, 'scale_stress_no_declared_constraints')); if (!fixture.dataPath) { throw new Error('scale_stress_no_declared_constraints is missing data.sqlite'); @@ -336,7 +322,7 @@ describe('relationship profiling', () => { try { const result = await profileKtxRelationshipSchema({ connectionId: fixture.snapshot.connectionId, - driver: fixture.snapshot.driver, + dialect: getDialectForDriver(fixture.snapshot.driver), schema: snapshotToKtxEnrichedSchema(maskedSnapshot, new Map()), executor: scaleExecutor, ctx: { runId: 'scale-stress-profile-query-count' }, @@ -381,7 +367,7 @@ describe('relationship profiling', () => { await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: schemaWithTables(['accounts', 'orders', 'payments', 'refunds']), executor, ctx: { runId: 'profile-concurrency' }, @@ -417,7 +403,7 @@ describe('relationship profiling', () => { const result = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: schemaWithTables(['accounts', 'orders']), executor, ctx: { runId: 'profile-error-isolated' }, diff --git a/packages/cli/src/context/scan/relationship-scoring.test.ts b/packages/cli/test/context/scan/relationship-scoring.test.ts similarity index 98% rename from packages/cli/src/context/scan/relationship-scoring.test.ts rename to packages/cli/test/context/scan/relationship-scoring.test.ts index 30127913..bd683f55 100644 --- a/packages/cli/src/context/scan/relationship-scoring.test.ts +++ b/packages/cli/test/context/scan/relationship-scoring.test.ts @@ -5,7 +5,7 @@ import { normalizeKtxRelationshipScoreWeights, scoreKtxRelationshipCandidate, type KtxRelationshipSignalVector, -} from './relationship-scoring.js'; +} from '../../../src/context/scan/relationship-scoring.js'; function signals(overrides: Partial = {}): KtxRelationshipSignalVector { return { diff --git a/packages/cli/src/context/scan/relationship-validation.test.ts b/packages/cli/test/context/scan/relationship-validation.test.ts similarity index 93% rename from packages/cli/src/context/scan/relationship-validation.test.ts rename to packages/cli/test/context/scan/relationship-validation.test.ts index 856cf60a..cd7771d9 100644 --- a/packages/cli/src/context/scan/relationship-validation.test.ts +++ b/packages/cli/test/context/scan/relationship-validation.test.ts @@ -1,11 +1,12 @@ import Database from 'better-sqlite3'; import { afterEach, describe, expect, it } from 'vitest'; -import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from './enrichment-types.js'; -import { generateKtxRelationshipDiscoveryCandidates } from './relationship-candidates.js'; -import type { KtxRelationshipProfileArtifact } from './relationship-profiling.js'; -import { profileKtxRelationshipSchema } from './relationship-profiling.js'; -import { validateKtxRelationshipDiscoveryCandidates } from './relationship-validation.js'; -import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from './types.js'; +import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; +import type { KtxEnrichedColumn, KtxEnrichedSchema, KtxEnrichedTable } from '../../../src/context/scan/enrichment-types.js'; +import { generateKtxRelationshipDiscoveryCandidates } from '../../../src/context/scan/relationship-candidates.js'; +import type { KtxRelationshipProfileArtifact } from '../../../src/context/scan/relationship-profiling.js'; +import { profileKtxRelationshipSchema } from '../../../src/context/scan/relationship-profiling.js'; +import { validateKtxRelationshipDiscoveryCandidates } from '../../../src/context/scan/relationship-validation.js'; +import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from '../../../src/context/scan/types.js'; class InMemorySqliteExecutor { readonly db = new Database(':memory:'); @@ -99,7 +100,7 @@ describe('relationship validation', () => { const testSchema = schema(); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'validate-test' }, @@ -110,7 +111,7 @@ describe('relationship validation', () => { const validated = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates, profiles, executor, @@ -148,7 +149,7 @@ describe('relationship validation', () => { const testSchema = schema(); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'validate-test' }, @@ -159,7 +160,7 @@ describe('relationship validation', () => { const validated = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates, profiles, executor, @@ -198,7 +199,7 @@ describe('relationship validation', () => { const testSchema = schema(); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'validate-budget-profile' }, @@ -211,7 +212,7 @@ describe('relationship validation', () => { const validated = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates, profiles, executor, @@ -253,7 +254,7 @@ describe('relationship validation', () => { ]); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'validate-zero-budget-profile' }, @@ -263,7 +264,7 @@ describe('relationship validation', () => { const validated = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates, profiles, executor, @@ -300,7 +301,7 @@ describe('relationship validation', () => { ]); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'llm-rejected-validation' }, @@ -329,7 +330,7 @@ describe('relationship validation', () => { const [validated] = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates: [llmCandidate], profiles, executor, @@ -374,7 +375,7 @@ describe('relationship validation', () => { ]); const profiles = await profileKtxRelationshipSchema({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), schema: testSchema, executor, ctx: { runId: 'validation-concurrency-profile' }, @@ -383,7 +384,7 @@ describe('relationship validation', () => { await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates, profiles, executor: throttled, @@ -475,7 +476,7 @@ describe('relationship validation', () => { const [validated] = await validateKtxRelationshipDiscoveryCandidates({ connectionId: 'warehouse', - driver: 'sqlite', + dialect: getDialectForDriver('sqlite'), candidates: [candidate], profiles, executor, diff --git a/packages/cli/src/context/scan/table-ref.test.ts b/packages/cli/test/context/scan/table-ref.test.ts similarity index 98% rename from packages/cli/src/context/scan/table-ref.test.ts rename to packages/cli/test/context/scan/table-ref.test.ts index 510b6c82..233af18c 100644 --- a/packages/cli/src/context/scan/table-ref.test.ts +++ b/packages/cli/test/context/scan/table-ref.test.ts @@ -5,7 +5,7 @@ import { tableRefKey, tableRefSet, type KtxTableRefKey, -} from './table-ref.js'; +} from '../../../src/context/scan/table-ref.js'; describe('tableRefKey roundtrip', () => { it('encodes and decodes a three-part ref', () => { diff --git a/packages/cli/src/context/scan/type-normalization.test.ts b/packages/cli/test/context/scan/type-normalization.test.ts similarity index 92% rename from packages/cli/src/context/scan/type-normalization.test.ts rename to packages/cli/test/context/scan/type-normalization.test.ts index 5dc0adf2..fa19df32 100644 --- a/packages/cli/src/context/scan/type-normalization.test.ts +++ b/packages/cli/test/context/scan/type-normalization.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { inferKtxDimensionType, ktxColumnTypeMappingFromNative, normalizeKtxNativeType } from './type-normalization.js'; +import { inferKtxDimensionType, ktxColumnTypeMappingFromNative, normalizeKtxNativeType } from '../../../src/context/scan/type-normalization.js'; describe('KTX scan type normalization', () => { it('normalizes native database type strings', () => { diff --git a/packages/cli/src/context/scan/types.test.ts b/packages/cli/test/context/scan/types.test.ts similarity index 97% rename from packages/cli/src/context/scan/types.test.ts rename to packages/cli/test/context/scan/types.test.ts index 309db88e..8aa55dba 100644 --- a/packages/cli/src/context/scan/types.test.ts +++ b/packages/cli/test/context/scan/types.test.ts @@ -15,7 +15,7 @@ import { type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, -} from './types.js'; +} from '../../../src/context/scan/types.js'; describe('KTX scan contract types', () => { it('defaults to structural-only connector capabilities', () => { @@ -93,6 +93,8 @@ describe('KTX scan contract types', () => { expect(ctx.runId).toBe('scan-run-1'); return snapshot; }, + listSchemas: async () => [], + listTables: async () => [], }; await expect( @@ -164,6 +166,8 @@ describe('KTX scan contract types', () => { tables: [], }; }, + listSchemas: async () => [], + listTables: async () => [], }; await expect( diff --git a/packages/cli/src/context/scan/warehouse-catalog.test.ts b/packages/cli/test/context/scan/warehouse-catalog.test.ts similarity index 92% rename from packages/cli/src/context/scan/warehouse-catalog.test.ts rename to packages/cli/test/context/scan/warehouse-catalog.test.ts index 6ef1f03a..300eba91 100644 --- a/packages/cli/src/context/scan/warehouse-catalog.test.ts +++ b/packages/cli/test/context/scan/warehouse-catalog.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { WarehouseCatalogService } from './warehouse-catalog.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { WarehouseCatalogService } from '../../../src/context/scan/warehouse-catalog.js'; describe('WarehouseCatalogService', () => { let tempDir: string; @@ -156,6 +156,17 @@ describe('WarehouseCatalogService', () => { }); }); + it('keeps one-part table display fallback for loose catalog resolution', async () => { + await seedLiveDatabaseScan(); + const catalog = new WarehouseCatalogService({ fileStore: project.fileStore }); + + await expect(catalog.resolveDisplay('warehouse', 'orders')).resolves.toMatchObject({ + resolved: { catalog: null, db: 'public', name: 'orders' }, + candidates: [], + dialect: 'postgres', + }); + }); + it('treats two-part BigQuery identifiers as ambiguous instead of guessing', async () => { await seedLiveDatabaseScan('warehouse', 'sync-bigquery', 'bigquery'); const catalog = new WarehouseCatalogService({ fileStore: project.fileStore }); diff --git a/packages/cli/src/context/search/backend-conformance.test-utils.test.ts b/packages/cli/test/context/search/backend-conformance.test-utils.test.ts similarity index 95% rename from packages/cli/src/context/search/backend-conformance.test-utils.test.ts rename to packages/cli/test/context/search/backend-conformance.test-utils.test.ts index c9ecebb7..b49f866a 100644 --- a/packages/cli/src/context/search/backend-conformance.test-utils.test.ts +++ b/packages/cli/test/context/search/backend-conformance.test-utils.test.ts @@ -2,22 +2,22 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, it } from 'vitest'; -import { SqliteContextEvidenceStore } from '../ingest/context-evidence/sqlite-context-evidence-store.js'; -import type { JsonValue } from '../ingest/ports.js'; -import { initKtxProject, type KtxLocalProject } from '../project/project.js'; -import { type LocalSlSourceSearchResult, searchLocalSlSources, writeLocalSlSource } from '../sl/local-sl.js'; -import type { ContextEvidenceSearchResult } from '../tools/context-evidence-tool-store.js'; +import { SqliteContextEvidenceStore } from '../../../src/context/ingest/context-evidence/sqlite-context-evidence-store.js'; +import type { JsonValue } from '../../../src/context/ingest/ports.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { type LocalSlSourceSearchResult, searchLocalSlSources, writeLocalSlSource } from '../../../src/context/sl/local-sl.js'; +import type { ContextEvidenceSearchResult } from '../../../src/context/tools/context-evidence-tool-store.js'; import { type LocalKnowledgeSearchResult, searchLocalKnowledgePages, writeLocalKnowledgePage, -} from '../wiki/local-knowledge.js'; +} from '../../../src/context/wiki/local-knowledge.js'; import { assertSearchBackendCapabilities, assertSearchBackendConformanceCase, type SearchBackendConformanceResult, } from './backend-conformance.test-utils.js'; -import type { SearchBackendCapabilities } from './types.js'; +import type { SearchBackendCapabilities } from '../../../src/context/search/types.js'; const SQLITE_SEARCH_CAPABILITIES = { fts: true, diff --git a/packages/cli/src/context/search/backend-conformance.test-utils.ts b/packages/cli/test/context/search/backend-conformance.test-utils.ts similarity index 99% rename from packages/cli/src/context/search/backend-conformance.test-utils.ts rename to packages/cli/test/context/search/backend-conformance.test-utils.ts index fa6070b2..506cc22c 100644 --- a/packages/cli/src/context/search/backend-conformance.test-utils.ts +++ b/packages/cli/test/context/search/backend-conformance.test-utils.ts @@ -1,4 +1,4 @@ -import type { SearchBackendCapabilities, SearchLaneStatus } from './types.js'; +import type { SearchBackendCapabilities, SearchLaneStatus } from '../../../src/context/search/types.js'; export interface SearchBackendConformanceLane { lane: string; diff --git a/packages/cli/src/context/search/discover.test.ts b/packages/cli/test/context/search/discover.test.ts similarity index 97% rename from packages/cli/src/context/search/discover.test.ts rename to packages/cli/test/context/search/discover.test.ts index 931de2be..77e35e18 100644 --- a/packages/cli/src/context/search/discover.test.ts +++ b/packages/cli/test/context/search/discover.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { writeLocalKnowledgePage } from '../wiki/local-knowledge.js'; -import { createKtxDiscoverDataService } from './discover.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { writeLocalKnowledgePage } from '../../../src/context/wiki/local-knowledge.js'; +import { createKtxDiscoverDataService } from '../../../src/context/search/discover.js'; describe('createKtxDiscoverDataService', () => { let tempDir: string; diff --git a/packages/cli/src/context/search/hybrid-search-core.test.ts b/packages/cli/test/context/search/hybrid-search-core.test.ts similarity index 96% rename from packages/cli/src/context/search/hybrid-search-core.test.ts rename to packages/cli/test/context/search/hybrid-search-core.test.ts index 2350e2ed..5952e3ee 100644 --- a/packages/cli/src/context/search/hybrid-search-core.test.ts +++ b/packages/cli/test/context/search/hybrid-search-core.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { HybridSearchCore } from './hybrid-search-core.js'; -import type { SearchCandidateGenerator } from './types.js'; +import { HybridSearchCore } from '../../../src/context/search/hybrid-search-core.js'; +import type { SearchCandidateGenerator } from '../../../src/context/search/types.js'; function generator( lane: string, diff --git a/packages/cli/src/context/search/pglite-owner-process.test.ts b/packages/cli/test/context/search/pglite-owner-process.test.ts similarity index 98% rename from packages/cli/src/context/search/pglite-owner-process.test.ts rename to packages/cli/test/context/search/pglite-owner-process.test.ts index 3a15eea9..df3d096b 100644 --- a/packages/cli/src/context/search/pglite-owner-process.test.ts +++ b/packages/cli/test/context/search/pglite-owner-process.test.ts @@ -4,8 +4,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Client } from 'pg'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { assertSearchBackendConformanceCase } from '../../context/search/backend-conformance.test-utils.js'; -import { KtxPGliteOwnerProcess } from './pglite-owner-process.js'; +import { assertSearchBackendConformanceCase } from './backend-conformance.test-utils.js'; +import { KtxPGliteOwnerProcess } from '../../../src/context/search/pglite-owner-process.js'; async function allocatePort(): Promise { const server = createServer(); diff --git a/packages/cli/src/context/search/pglite-runtime-boundary.test.ts b/packages/cli/test/context/search/pglite-runtime-boundary.test.ts similarity index 96% rename from packages/cli/src/context/search/pglite-runtime-boundary.test.ts rename to packages/cli/test/context/search/pglite-runtime-boundary.test.ts index feb7443d..c9fe80d2 100644 --- a/packages/cli/src/context/search/pglite-runtime-boundary.test.ts +++ b/packages/cli/test/context/search/pglite-runtime-boundary.test.ts @@ -3,7 +3,7 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -const ktxRoot = fileURLToPath(new URL('../../../../../', import.meta.url)); +const ktxRoot = fileURLToPath(new URL('../../../../..', import.meta.url)); function readKtxFile(relativePath: string): string { return readFileSync(join(ktxRoot, relativePath), 'utf8'); diff --git a/packages/cli/src/context/search/pglite-spike.test.ts b/packages/cli/test/context/search/pglite-spike.test.ts similarity index 98% rename from packages/cli/src/context/search/pglite-spike.test.ts rename to packages/cli/test/context/search/pglite-spike.test.ts index 470000da..2183630b 100644 --- a/packages/cli/src/context/search/pglite-spike.test.ts +++ b/packages/cli/test/context/search/pglite-spike.test.ts @@ -5,8 +5,8 @@ import { PGlite, type PGliteInterface } from '@electric-sql/pglite'; import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm'; import { vector } from '@electric-sql/pglite/vector'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { assertSearchBackendCapabilities, assertSearchBackendConformanceCase } from '../../context/search/backend-conformance.test-utils.js'; -import type { SearchBackendCapabilities } from '../../context/search/types.js'; +import { assertSearchBackendCapabilities, assertSearchBackendConformanceCase } from './backend-conformance.test-utils.js'; +import type { SearchBackendCapabilities } from '../../../src/context/search/types.js'; type PGliteDb = PGliteInterface; diff --git a/packages/cli/src/context/search/query.test.ts b/packages/cli/test/context/search/query.test.ts similarity index 95% rename from packages/cli/src/context/search/query.test.ts rename to packages/cli/test/context/search/query.test.ts index 64f1fd0b..b8e7660f 100644 --- a/packages/cli/src/context/search/query.test.ts +++ b/packages/cli/test/context/search/query.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { defaultLaneCandidatePoolLimit, normalizeSearchQuery } from './query.js'; +import { defaultLaneCandidatePoolLimit, normalizeSearchQuery } from '../../../src/context/search/query.js'; describe('search query helpers', () => { it('normalizes punctuation and duplicate terms into stable lowercase tokens', () => { diff --git a/packages/cli/src/context/search/rrf.test.ts b/packages/cli/test/context/search/rrf.test.ts similarity index 90% rename from packages/cli/src/context/search/rrf.test.ts rename to packages/cli/test/context/search/rrf.test.ts index cbb4065b..42890989 100644 --- a/packages/cli/src/context/search/rrf.test.ts +++ b/packages/cli/test/context/search/rrf.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { compareFusedSearchCandidates, DEFAULT_SEARCH_LANE_WEIGHTS, rrfContribution } from './rrf.js'; -import type { FusedSearchCandidate } from './types.js'; +import { compareFusedSearchCandidates, DEFAULT_SEARCH_LANE_WEIGHTS, rrfContribution } from '../../../src/context/search/rrf.js'; +import type { FusedSearchCandidate } from '../../../src/context/search/types.js'; describe('RRF scoring', () => { it('uses the shared lane weights from the hybrid search spec', () => { diff --git a/packages/cli/src/context/skills/skills-registry.service.test.ts b/packages/cli/test/context/skills/skills-registry.service.test.ts similarity index 98% rename from packages/cli/src/context/skills/skills-registry.service.test.ts rename to packages/cli/test/context/skills/skills-registry.service.test.ts index 9bb716dd..2f6f1aaf 100644 --- a/packages/cli/src/context/skills/skills-registry.service.test.ts +++ b/packages/cli/test/context/skills/skills-registry.service.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SkillsRegistryService } from './skills-registry.service.js'; +import { SkillsRegistryService } from '../../../src/context/skills/skills-registry.service.js'; describe('SkillsRegistryService', () => { let service: SkillsRegistryService; diff --git a/packages/cli/src/context/sl/dictionary-search.test.ts b/packages/cli/test/context/sl/dictionary-search.test.ts similarity index 97% rename from packages/cli/src/context/sl/dictionary-search.test.ts rename to packages/cli/test/context/sl/dictionary-search.test.ts index 1838f0d9..b7a6beeb 100644 --- a/packages/cli/src/context/sl/dictionary-search.test.ts +++ b/packages/cli/test/context/sl/dictionary-search.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { createKtxDictionarySearchService } from './dictionary-search.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { createKtxDictionarySearchService } from '../../../src/context/sl/dictionary-search.js'; describe('createKtxDictionarySearchService', () => { let tempDir: string; diff --git a/packages/cli/src/context/sl/local-query.test.ts b/packages/cli/test/context/sl/local-query.test.ts similarity index 97% rename from packages/cli/src/context/sl/local-query.test.ts rename to packages/cli/test/context/sl/local-query.test.ts index 800bdb95..4137f596 100644 --- a/packages/cli/src/context/sl/local-query.test.ts +++ b/packages/cli/test/context/sl/local-query.test.ts @@ -2,9 +2,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { KtxSemanticLayerComputePort } from '../../context/daemon/semantic-layer-compute.js'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { compileLocalSlQuery } from './local-query.js'; +import type { KtxSemanticLayerComputePort } from '../../../src/context/daemon/semantic-layer-compute.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { compileLocalSlQuery } from '../../../src/context/sl/local-query.js'; describe('compileLocalSlQuery', () => { let tempDir: string; diff --git a/packages/cli/src/context/sl/local-sl.test.ts b/packages/cli/test/context/sl/local-sl.test.ts similarity index 98% rename from packages/cli/src/context/sl/local-sl.test.ts rename to packages/cli/test/context/sl/local-sl.test.ts index 3ba00a92..1115f387 100644 --- a/packages/cli/src/context/sl/local-sl.test.ts +++ b/packages/cli/test/context/sl/local-sl.test.ts @@ -2,14 +2,14 @@ import { access, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; import { listLocalSlSources, readLocalSlSource, searchLocalSlSources, validateLocalSlSource, writeLocalSlSource, -} from './local-sl.js'; +} from '../../../src/context/sl/local-sl.js'; const ORDERS_YAML = [ 'name: orders', diff --git a/packages/cli/src/context/sl/pglite-sl-search-prototype.test.ts b/packages/cli/test/context/sl/pglite-sl-search-prototype.test.ts similarity index 95% rename from packages/cli/src/context/sl/pglite-sl-search-prototype.test.ts rename to packages/cli/test/context/sl/pglite-sl-search-prototype.test.ts index 372f8668..8ebb8646 100644 --- a/packages/cli/src/context/sl/pglite-sl-search-prototype.test.ts +++ b/packages/cli/test/context/sl/pglite-sl-search-prototype.test.ts @@ -3,10 +3,10 @@ import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { assertSearchBackendConformanceCase } from '../../context/search/backend-conformance.test-utils.js'; -import { searchLocalSlSources, writeLocalSlSource, type LocalSlSourceSearchResult } from './local-sl.js'; -import { searchLocalSlSourcesWithPglitePrototype } from './pglite-sl-search-prototype.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { assertSearchBackendConformanceCase } from '../search/backend-conformance.test-utils.js'; +import { searchLocalSlSources, writeLocalSlSource, type LocalSlSourceSearchResult } from '../../../src/context/sl/local-sl.js'; +import { searchLocalSlSourcesWithPglitePrototype } from '../../../src/context/sl/pglite-sl-search-prototype.js'; const ORDERS_YAML = [ 'name: orders', diff --git a/packages/cli/src/context/sl/schemas.contract.test.ts b/packages/cli/test/context/sl/schemas.contract.test.ts similarity index 88% rename from packages/cli/src/context/sl/schemas.contract.test.ts rename to packages/cli/test/context/sl/schemas.contract.test.ts index 1b0dac20..9b1293f1 100644 --- a/packages/cli/src/context/sl/schemas.contract.test.ts +++ b/packages/cli/test/context/sl/schemas.contract.test.ts @@ -2,14 +2,14 @@ import { execFileSync } from 'node:child_process'; import { Ajv2020 } from 'ajv/dist/2020.js'; import { describe, expect, it } from 'vitest'; -import { resolvedSourceSchema } from './schemas.js'; -import { toResolvedWire } from './semantic-layer.service.js'; -import type { SemanticLayerSource } from './types.js'; +import { resolvedSourceSchema } from '../../../src/context/sl/schemas.js'; +import { toResolvedWire } from '../../../src/context/sl/semantic-layer.service.js'; +import type { SemanticLayerSource } from '../../../src/context/sl/types.js'; function loadPythonSourceDefinitionSchema(): Record | null { try { const stdout = execFileSync('uv', ['run', 'python', '-m', 'semantic_layer', 'dump-schema'], { - cwd: new URL('../../../../', import.meta.url), + cwd: new URL('../../../..', import.meta.url), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); diff --git a/packages/cli/src/context/sl/semantic-layer.service.test.ts b/packages/cli/test/context/sl/semantic-layer.service.test.ts similarity index 99% rename from packages/cli/src/context/sl/semantic-layer.service.test.ts rename to packages/cli/test/context/sl/semantic-layer.service.test.ts index cd14d66a..f8b919bb 100644 --- a/packages/cli/src/context/sl/semantic-layer.service.test.ts +++ b/packages/cli/test/context/sl/semantic-layer.service.test.ts @@ -11,9 +11,9 @@ import { SemanticLayerService, toResolvedWire, UnknownColumnOverrideError, -} from './semantic-layer.service.js'; -import { resolvedSourceSchema, sourceDefinitionSchema, sourceOverlaySchema } from './schemas.js'; -import type { SemanticLayerSource } from './types.js'; +} from '../../../src/context/sl/semantic-layer.service.js'; +import { resolvedSourceSchema, sourceDefinitionSchema, sourceOverlaySchema } from '../../../src/context/sl/schemas.js'; +import type { SemanticLayerSource } from '../../../src/context/sl/types.js'; const pythonPort = { validateSources: vi.fn(), diff --git a/packages/cli/src/context/sl/sl-dictionary-profile.test.ts b/packages/cli/test/context/sl/sl-dictionary-profile.test.ts similarity index 94% rename from packages/cli/src/context/sl/sl-dictionary-profile.test.ts rename to packages/cli/test/context/sl/sl-dictionary-profile.test.ts index f7aa3854..7a36924f 100644 --- a/packages/cli/src/context/sl/sl-dictionary-profile.test.ts +++ b/packages/cli/test/context/sl/sl-dictionary-profile.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; -import { loadLatestSlDictionaryEntries } from './sl-dictionary-profile.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; +import { loadLatestSlDictionaryEntries } from '../../../src/context/sl/sl-dictionary-profile.js'; describe('loadLatestSlDictionaryEntries', () => { let tempDir: string; diff --git a/packages/cli/src/context/sl/sl-search.service.test.ts b/packages/cli/test/context/sl/sl-search.service.test.ts similarity index 98% rename from packages/cli/src/context/sl/sl-search.service.test.ts rename to packages/cli/test/context/sl/sl-search.service.test.ts index 164c3954..052cdee9 100644 --- a/packages/cli/src/context/sl/sl-search.service.test.ts +++ b/packages/cli/test/context/sl/sl-search.service.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { buildSemanticLayerSourceSearchText, SlSearchService } from './sl-search.service.js'; -import type { SemanticLayerSource } from './types.js'; +import { buildSemanticLayerSourceSearchText, SlSearchService } from '../../../src/context/sl/sl-search.service.js'; +import type { SemanticLayerSource } from '../../../src/context/sl/types.js'; describe('SlSearchService', () => { it('builds search text from source, columns, measures, and joins', () => { diff --git a/packages/cli/src/context/sl/sqlite-sl-sources-index.test.ts b/packages/cli/test/context/sl/sqlite-sl-sources-index.test.ts similarity index 98% rename from packages/cli/src/context/sl/sqlite-sl-sources-index.test.ts rename to packages/cli/test/context/sl/sqlite-sl-sources-index.test.ts index 91a7727e..d0002dae 100644 --- a/packages/cli/src/context/sl/sqlite-sl-sources-index.test.ts +++ b/packages/cli/test/context/sl/sqlite-sl-sources-index.test.ts @@ -2,7 +2,7 @@ import { access, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SqliteSlSourcesIndex } from './sqlite-sl-sources-index.js'; +import { SqliteSlSourcesIndex } from '../../../src/context/sl/sqlite-sl-sources-index.js'; describe('SqliteSlSourcesIndex', () => { let tempDir: string; diff --git a/packages/cli/src/context/sl/tools/connection-id-schema.test.ts b/packages/cli/test/context/sl/tools/connection-id-schema.test.ts similarity index 87% rename from packages/cli/src/context/sl/tools/connection-id-schema.test.ts rename to packages/cli/test/context/sl/tools/connection-id-schema.test.ts index 48e023e5..1108ce96 100644 --- a/packages/cli/src/context/sl/tools/connection-id-schema.test.ts +++ b/packages/cli/test/context/sl/tools/connection-id-schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { slToolConnectionIdSchema } from './connection-id-schema.js'; +import { slToolConnectionIdSchema } from '../../../../src/context/sl/tools/connection-id-schema.js'; describe('slToolConnectionIdSchema', () => { it('accepts app UUIDs and local project connection ids', () => { diff --git a/packages/cli/src/context/sl/tools/sl-discover.tool.test.ts b/packages/cli/test/context/sl/tools/sl-discover.tool.test.ts similarity index 86% rename from packages/cli/src/context/sl/tools/sl-discover.tool.test.ts rename to packages/cli/test/context/sl/tools/sl-discover.tool.test.ts index 6dc30478..6a673d2c 100644 --- a/packages/cli/src/context/sl/tools/sl-discover.tool.test.ts +++ b/packages/cli/test/context/sl/tools/sl-discover.tool.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { SemanticLayerSource } from '../types.js'; -import { SlDiscoverTool } from './sl-discover.tool.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { SemanticLayerSource } from '../../../../src/context/sl/types.js'; +import { SlDiscoverTool } from '../../../../src/context/sl/tools/sl-discover.tool.js'; function makeTool() { const semanticLayerService = { diff --git a/packages/cli/src/context/sl/tools/sl-edit-source.tool.test.ts b/packages/cli/test/context/sl/tools/sl-edit-source.tool.test.ts similarity index 96% rename from packages/cli/src/context/sl/tools/sl-edit-source.tool.test.ts rename to packages/cli/test/context/sl/tools/sl-edit-source.tool.test.ts index cf66baf8..fee83ea6 100644 --- a/packages/cli/src/context/sl/tools/sl-edit-source.tool.test.ts +++ b/packages/cli/test/context/sl/tools/sl-edit-source.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources, hasTouchedSlSource } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { SlEditSourceTool } from './sl-edit-source.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources, hasTouchedSlSource } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { SlEditSourceTool } from '../../../../src/context/sl/tools/sl-edit-source.tool.js'; function makeTool(overrides: any = {}) { const semanticLayerService = { diff --git a/packages/cli/src/context/sl/tools/sl-read-source.tool.session.test.ts b/packages/cli/test/context/sl/tools/sl-read-source.tool.session.test.ts similarity index 87% rename from packages/cli/src/context/sl/tools/sl-read-source.tool.session.test.ts rename to packages/cli/test/context/sl/tools/sl-read-source.tool.session.test.ts index 481c4cfe..121a012b 100644 --- a/packages/cli/src/context/sl/tools/sl-read-source.tool.session.test.ts +++ b/packages/cli/test/context/sl/tools/sl-read-source.tool.session.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { SlReadSourceTool } from './sl-read-source.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { SlReadSourceTool } from '../../../../src/context/sl/tools/sl-read-source.tool.js'; function makeTool(overrides: Partial> = {}) { const semanticLayerService = { diff --git a/packages/cli/src/context/sl/tools/sl-rollback.tool.test.ts b/packages/cli/test/context/sl/tools/sl-rollback.tool.test.ts similarity index 90% rename from packages/cli/src/context/sl/tools/sl-rollback.tool.test.ts rename to packages/cli/test/context/sl/tools/sl-rollback.tool.test.ts index 5a1927a4..6d2d787a 100644 --- a/packages/cli/src/context/sl/tools/sl-rollback.tool.test.ts +++ b/packages/cli/test/context/sl/tools/sl-rollback.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources, hasTouchedSlSource } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { SlRollbackTool } from './sl-rollback.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources, hasTouchedSlSource } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { SlRollbackTool } from '../../../../src/context/sl/tools/sl-rollback.tool.js'; function makeSession(overrides: Partial = {}): ToolSession { return { diff --git a/packages/cli/src/context/sl/tools/sl-validate.tool.test.ts b/packages/cli/test/context/sl/tools/sl-validate.tool.test.ts similarity index 84% rename from packages/cli/src/context/sl/tools/sl-validate.tool.test.ts rename to packages/cli/test/context/sl/tools/sl-validate.tool.test.ts index f0c18eac..ff7cee5f 100644 --- a/packages/cli/src/context/sl/tools/sl-validate.tool.test.ts +++ b/packages/cli/test/context/sl/tools/sl-validate.tool.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import type { SemanticLayerService } from '../semantic-layer.service.js'; -import type { SemanticLayerSource } from '../types.js'; -import { SlValidateTool, validateSemanticLayerEndpoint } from './sl-validate.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import type { SemanticLayerService } from '../../../../src/context/sl/semantic-layer.service.js'; +import type { SemanticLayerSource } from '../../../../src/context/sl/types.js'; +import { SlValidateTool, validateSemanticLayerEndpoint } from '../../../../src/context/sl/tools/sl-validate.tool.js'; describe('validateSemanticLayerEndpoint', () => { it('uses the connection warehouse dialect, not hardcoded postgres', async () => { diff --git a/packages/cli/src/context/sl/tools/sl-warehouse-validation.test.ts b/packages/cli/test/context/sl/tools/sl-warehouse-validation.test.ts similarity index 98% rename from packages/cli/src/context/sl/tools/sl-warehouse-validation.test.ts rename to packages/cli/test/context/sl/tools/sl-warehouse-validation.test.ts index 5796cdb7..d8d45a81 100644 --- a/packages/cli/src/context/sl/tools/sl-warehouse-validation.test.ts +++ b/packages/cli/test/context/sl/tools/sl-warehouse-validation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { validateSingleSource } from './sl-warehouse-validation.js'; +import { validateSingleSource } from '../../../../src/context/sl/tools/sl-warehouse-validation.js'; function makeDeps(opts: { sourceYaml: string; executeQuery: ReturnType }) { return { diff --git a/packages/cli/src/context/sl/tools/sl-write-source.tool.test.ts b/packages/cli/test/context/sl/tools/sl-write-source.tool.test.ts similarity index 97% rename from packages/cli/src/context/sl/tools/sl-write-source.tool.test.ts rename to packages/cli/test/context/sl/tools/sl-write-source.tool.test.ts index f168095c..ab3ee308 100644 --- a/packages/cli/src/context/sl/tools/sl-write-source.tool.test.ts +++ b/packages/cli/test/context/sl/tools/sl-write-source.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources, hasTouchedSlSource } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { SlWriteSourceTool } from './sl-write-source.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources, hasTouchedSlSource } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { SlWriteSourceTool } from '../../../../src/context/sl/tools/sl-write-source.tool.js'; function makeTool(overrides: Partial> = {}) { const semanticLayerService = { diff --git a/packages/cli/src/context/sql-analysis/http-sql-analysis-port.test.ts b/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts similarity index 98% rename from packages/cli/src/context/sql-analysis/http-sql-analysis-port.test.ts rename to packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts index 2d759369..02b275a6 100644 --- a/packages/cli/src/context/sql-analysis/http-sql-analysis-port.test.ts +++ b/packages/cli/test/context/sql-analysis/http-sql-analysis-port.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createHttpSqlAnalysisPort } from './http-sql-analysis-port.js'; +import { createHttpSqlAnalysisPort } from '../../../src/context/sql-analysis/http-sql-analysis-port.js'; describe('createHttpSqlAnalysisPort', () => { it('calls the SQL-analysis fingerprint endpoint and maps snake_case response fields', async () => { diff --git a/packages/cli/src/context/test/make-local-git-repo.ts b/packages/cli/test/context/test/make-local-git-repo.ts similarity index 95% rename from packages/cli/src/context/test/make-local-git-repo.ts rename to packages/cli/test/context/test/make-local-git-repo.ts index a7b4c662..c60187ac 100644 --- a/packages/cli/src/context/test/make-local-git-repo.ts +++ b/packages/cli/test/context/test/make-local-git-repo.ts @@ -1,7 +1,7 @@ import { cp, mkdir, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { SimpleGit } from 'simple-git'; -import { createSimpleGit } from '../ingest/git-env.js'; +import { createSimpleGit } from '../../../src/context/ingest/git-env.js'; export interface LocalGitRepo { repoDir: string; diff --git a/packages/cli/src/context/tools/context-evidence-tools.test.ts b/packages/cli/test/context/tools/context-evidence-tools.test.ts similarity index 95% rename from packages/cli/src/context/tools/context-evidence-tools.test.ts rename to packages/cli/test/context/tools/context-evidence-tools.test.ts index 08a8654f..f8adb618 100644 --- a/packages/cli/src/context/tools/context-evidence-tools.test.ts +++ b/packages/cli/test/context/tools/context-evidence-tools.test.ts @@ -3,17 +3,17 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { KtxEmbeddingPort } from '../../context/core/embedding.js'; -import { SqliteContextEvidenceStore } from '../ingest/context-evidence/sqlite-context-evidence-store.js'; -import { ContextCandidateMarkTool } from './context-candidate-mark.tool.js'; -import { ContextCandidateWriteTool } from './context-candidate-write.tool.js'; -import { ContextEvidenceNeighborsTool } from './context-evidence-neighbors.tool.js'; -import { ContextEvidenceReadTool } from './context-evidence-read.tool.js'; -import { ContextEvidenceSearchTool } from './context-evidence-search.tool.js'; -import type { ContextEvidenceToolStorePort } from './context-evidence-tool-store.js'; -import { createTouchedSlSources } from '../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../context/tools/base-tool.js'; -import type { ToolSession } from '../../context/tools/tool-session.js'; +import type { KtxEmbeddingPort } from '../../../src/context/core/embedding.js'; +import { SqliteContextEvidenceStore } from '../../../src/context/ingest/context-evidence/sqlite-context-evidence-store.js'; +import { ContextCandidateMarkTool } from '../../../src/context/tools/context-candidate-mark.tool.js'; +import { ContextCandidateWriteTool } from '../../../src/context/tools/context-candidate-write.tool.js'; +import { ContextEvidenceNeighborsTool } from '../../../src/context/tools/context-evidence-neighbors.tool.js'; +import { ContextEvidenceReadTool } from '../../../src/context/tools/context-evidence-read.tool.js'; +import { ContextEvidenceSearchTool } from '../../../src/context/tools/context-evidence-search.tool.js'; +import type { ContextEvidenceToolStorePort } from '../../../src/context/tools/context-evidence-tool-store.js'; +import { createTouchedSlSources } from '../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../src/context/tools/base-tool.js'; +import type { ToolSession } from '../../../src/context/tools/tool-session.js'; const ingestContext = (): ToolContext => ({ sourceId: 'ingest', diff --git a/packages/cli/src/context/tools/touched-sl-sources.test.ts b/packages/cli/test/context/tools/touched-sl-sources.test.ts similarity index 96% rename from packages/cli/src/context/tools/touched-sl-sources.test.ts rename to packages/cli/test/context/tools/touched-sl-sources.test.ts index 818676d2..f84f5d6b 100644 --- a/packages/cli/src/context/tools/touched-sl-sources.test.ts +++ b/packages/cli/test/context/tools/touched-sl-sources.test.ts @@ -7,7 +7,7 @@ import { listTouchedSlSources, touchedSlSourceCount, touchedSlSourceNamesForConnection, -} from './touched-sl-sources.js'; +} from '../../../src/context/tools/touched-sl-sources.js'; describe('target-aware touched SL source helpers', () => { it('deduplicates by connectionId and sourceName while preserving target identity', () => { diff --git a/packages/cli/src/context/wiki/knowledge-wiki.service.test.ts b/packages/cli/test/context/wiki/knowledge-wiki.service.test.ts similarity index 98% rename from packages/cli/src/context/wiki/knowledge-wiki.service.test.ts rename to packages/cli/test/context/wiki/knowledge-wiki.service.test.ts index 88bd92ab..efc9c69c 100644 --- a/packages/cli/src/context/wiki/knowledge-wiki.service.test.ts +++ b/packages/cli/test/context/wiki/knowledge-wiki.service.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { KnowledgeWikiService, type WikiFrontmatter } from './knowledge-wiki.service.js'; +import { KnowledgeWikiService, type WikiFrontmatter } from '../../../src/context/wiki/knowledge-wiki.service.js'; function makeService() { const pagesRepository: Record> = { diff --git a/packages/cli/src/context/wiki/local-knowledge.test.ts b/packages/cli/test/context/wiki/local-knowledge.test.ts similarity index 98% rename from packages/cli/src/context/wiki/local-knowledge.test.ts rename to packages/cli/test/context/wiki/local-knowledge.test.ts index 8229d5e7..fa70bcc5 100644 --- a/packages/cli/src/context/wiki/local-knowledge.test.ts +++ b/packages/cli/test/context/wiki/local-knowledge.test.ts @@ -2,13 +2,13 @@ import { access, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { initKtxProject, type KtxLocalProject } from '../../context/project/project.js'; +import { initKtxProject, type KtxLocalProject } from '../../../src/context/project/project.js'; import { listLocalKnowledgePages, readLocalKnowledgePage, searchLocalKnowledgePages, writeLocalKnowledgePage, -} from './local-knowledge.js'; +} from '../../../src/context/wiki/local-knowledge.js'; class FakeEmbeddingPort { readonly maxBatchSize = 16; diff --git a/packages/cli/src/context/wiki/sqlite-knowledge-index.test.ts b/packages/cli/test/context/wiki/sqlite-knowledge-index.test.ts similarity index 98% rename from packages/cli/src/context/wiki/sqlite-knowledge-index.test.ts rename to packages/cli/test/context/wiki/sqlite-knowledge-index.test.ts index 940e9954..5a3b0dc1 100644 --- a/packages/cli/src/context/wiki/sqlite-knowledge-index.test.ts +++ b/packages/cli/test/context/wiki/sqlite-knowledge-index.test.ts @@ -2,7 +2,7 @@ import { access, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SqliteKnowledgeIndex, type SqliteKnowledgeIndexPage } from './sqlite-knowledge-index.js'; +import { SqliteKnowledgeIndex, type SqliteKnowledgeIndexPage } from '../../../src/context/wiki/sqlite-knowledge-index.js'; describe('SqliteKnowledgeIndex', () => { let tempDir: string; diff --git a/packages/cli/src/context/wiki/tools/wiki-list-tags.tool.test.ts b/packages/cli/test/context/wiki/tools/wiki-list-tags.tool.test.ts similarity index 91% rename from packages/cli/src/context/wiki/tools/wiki-list-tags.tool.test.ts rename to packages/cli/test/context/wiki/tools/wiki-list-tags.tool.test.ts index 49605c4f..6c6ec209 100644 --- a/packages/cli/src/context/wiki/tools/wiki-list-tags.tool.test.ts +++ b/packages/cli/test/context/wiki/tools/wiki-list-tags.tool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { WikiListTagsTool } from './wiki-list-tags.tool.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { WikiListTagsTool } from '../../../../src/context/wiki/tools/wiki-list-tags.tool.js'; describe('WikiListTagsTool', () => { const baseContext: ToolContext = { sourceId: 's', messageId: 'm', userId: 'u' }; diff --git a/packages/cli/src/context/wiki/tools/wiki-read.tool.test.ts b/packages/cli/test/context/wiki/tools/wiki-read.tool.test.ts similarity index 91% rename from packages/cli/src/context/wiki/tools/wiki-read.tool.test.ts rename to packages/cli/test/context/wiki/tools/wiki-read.tool.test.ts index ac75b174..f70b34ec 100644 --- a/packages/cli/src/context/wiki/tools/wiki-read.tool.test.ts +++ b/packages/cli/test/context/wiki/tools/wiki-read.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { WikiReadTool } from './wiki-read.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { WikiReadTool } from '../../../../src/context/wiki/tools/wiki-read.tool.js'; describe('WikiReadTool', () => { const baseContext: ToolContext = { sourceId: 's', messageId: 'm', userId: 'u' }; diff --git a/packages/cli/src/context/wiki/tools/wiki-remove.tool.test.ts b/packages/cli/test/context/wiki/tools/wiki-remove.tool.test.ts similarity index 93% rename from packages/cli/src/context/wiki/tools/wiki-remove.tool.test.ts rename to packages/cli/test/context/wiki/tools/wiki-remove.tool.test.ts index 8130613c..afabc8c0 100644 --- a/packages/cli/src/context/wiki/tools/wiki-remove.tool.test.ts +++ b/packages/cli/test/context/wiki/tools/wiki-remove.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { WikiRemoveTool } from './wiki-remove.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { WikiRemoveTool } from '../../../../src/context/wiki/tools/wiki-remove.tool.js'; describe('WikiRemoveTool', () => { const baseContext: ToolContext = { sourceId: 's', messageId: 'm', userId: 'u' }; diff --git a/packages/cli/src/context/wiki/tools/wiki-search.tool.test.ts b/packages/cli/test/context/wiki/tools/wiki-search.tool.test.ts similarity index 93% rename from packages/cli/src/context/wiki/tools/wiki-search.tool.test.ts rename to packages/cli/test/context/wiki/tools/wiki-search.tool.test.ts index 24840a4f..d21f9825 100644 --- a/packages/cli/src/context/wiki/tools/wiki-search.tool.test.ts +++ b/packages/cli/test/context/wiki/tools/wiki-search.tool.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { WikiSearchTool } from './wiki-search.tool.js'; +import { WikiSearchTool } from '../../../../src/context/wiki/tools/wiki-search.tool.js'; describe('WikiSearchTool', () => { it('searches through the injected wiki adapter port', async () => { diff --git a/packages/cli/src/context/wiki/tools/wiki-write.tool.test.ts b/packages/cli/test/context/wiki/tools/wiki-write.tool.test.ts similarity index 97% rename from packages/cli/src/context/wiki/tools/wiki-write.tool.test.ts rename to packages/cli/test/context/wiki/tools/wiki-write.tool.test.ts index ad2bc54b..deadd716 100644 --- a/packages/cli/src/context/wiki/tools/wiki-write.tool.test.ts +++ b/packages/cli/test/context/wiki/tools/wiki-write.tool.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import type { ToolSession } from '../../../context/tools/tool-session.js'; -import { createTouchedSlSources } from '../../../context/tools/touched-sl-sources.js'; -import type { ToolContext } from '../../../context/tools/base-tool.js'; -import { WikiWriteTool } from './wiki-write.tool.js'; +import type { ToolSession } from '../../../../src/context/tools/tool-session.js'; +import { createTouchedSlSources } from '../../../../src/context/tools/touched-sl-sources.js'; +import type { ToolContext } from '../../../../src/context/tools/base-tool.js'; +import { WikiWriteTool } from '../../../../src/context/wiki/tools/wiki-write.tool.js'; function makeTool(overrides: any = {}) { const wikiService = { diff --git a/packages/cli/src/context/wiki/wiki-ref-validation.test.ts b/packages/cli/test/context/wiki/wiki-ref-validation.test.ts similarity index 96% rename from packages/cli/src/context/wiki/wiki-ref-validation.test.ts rename to packages/cli/test/context/wiki/wiki-ref-validation.test.ts index 6e0e8563..b6fd0012 100644 --- a/packages/cli/src/context/wiki/wiki-ref-validation.test.ts +++ b/packages/cli/test/context/wiki/wiki-ref-validation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { findDanglingWikiRefsForActions } from './wiki-ref-validation.js'; +import { findDanglingWikiRefsForActions } from '../../../src/context/wiki/wiki-ref-validation.js'; function makeWikiService(pages: Record) { return { diff --git a/packages/cli/src/database-tree-picker.test.ts b/packages/cli/test/database-tree-picker.test.ts similarity index 73% rename from packages/cli/src/database-tree-picker.test.ts rename to packages/cli/test/database-tree-picker.test.ts index 4dd1dca3..182f0235 100644 --- a/packages/cli/src/database-tree-picker.test.ts +++ b/packages/cli/test/database-tree-picker.test.ts @@ -4,9 +4,9 @@ import { type DatabaseScopePromptAdapter, type DatabaseTreePickerRenderer, type PickDatabaseScopeArgs, -} from './database-tree-picker.js'; -import type { TreePickerChrome, TreePickerResult } from './tree-picker-tui.js'; -import type { PickerState } from './tree-picker-state.js'; +} from '../src/database-tree-picker.js'; +import type { TreePickerChrome, TreePickerResult } from '../src/tree-picker-tui.js'; +import type { PickerState } from '../src/tree-picker-state.js'; function makeIo() { let stdout = ''; @@ -52,10 +52,10 @@ function captureRenderer(): { } const discovered = [ - { schema: 'analytics', name: 'customers', kind: 'table' as const }, - { schema: 'analytics', name: 'orders', kind: 'table' as const }, - { schema: 'public', name: 'events', kind: 'view' as const }, - { schema: 'public', name: 'sessions', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'events', kind: 'view' as const }, + { catalog: null, schema: 'public', name: 'sessions', kind: 'table' as const }, ]; function promptAdapter(overrides: Partial = {}): DatabaseScopePromptAdapter { @@ -88,7 +88,7 @@ describe('pickDatabaseScope', () => { select: vi.fn(async () => 'save'), }); const listTablesForSchemas = vi.fn(async () => [ - { schema: 'analytics', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const }, ]); const result = await pickDatabaseScope( @@ -114,6 +114,58 @@ describe('pickDatabaseScope', () => { }); }); + it('emits fully-qualified catalog.schema.name ids for catalog-bearing drivers and round-trips existing selection', async () => { + const promptsSave = promptAdapter({ + autocompleteMultiselect: vi.fn(async () => ['analytics']), + select: vi.fn(async () => 'save'), + }); + const listTablesForSchemas = vi.fn(async () => [ + { catalog: 'project-1', schema: 'analytics', name: 'orders', kind: 'table' as const }, + { catalog: 'project-1', schema: 'analytics', name: 'customers', kind: 'table' as const }, + ]); + const saveResult = await pickDatabaseScope( + baseArgs({ + schemas: ['analytics'], + schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) }, + listTablesForSchemas, + prompts: promptsSave, + }), + makeIo().io, + captureRenderer().renderer, + ); + expect(saveResult).toEqual({ + kind: 'selected', + activeSchemas: ['analytics'], + enabledTables: ['project-1.analytics.orders', 'project-1.analytics.customers'], + }); + + const { renderer, capture, setResult } = captureRenderer(); + setResult({ + kind: 'save', + selectedIds: ['project-1.analytics.orders'], + }); + const refineResult = await pickDatabaseScope( + baseArgs({ + schemas: ['analytics'], + schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) }, + existing: { enabledTables: ['project-1.analytics.orders'] }, + listTablesForSchemas, + prompts: promptAdapter({ + autocompleteMultiselect: vi.fn(async () => ['analytics']), + select: vi.fn(async () => 'refine'), + }), + }), + makeIo().io, + renderer, + ); + expect(refineResult).toEqual({ + kind: 'selected', + activeSchemas: ['analytics'], + enabledTables: ['project-1.analytics.orders'], + }); + expect([...(capture.state?.checked ?? [])]).toContain('project-1.analytics.orders'); + }); + it('routes partial existing allowlists through Stage 2 so save preserves table selections', async () => { const { renderer, setResult } = captureRenderer(); setResult({ kind: 'save', selectedIds: ['analytics.customers'] }); @@ -122,8 +174,8 @@ describe('pickDatabaseScope', () => { select: vi.fn(async () => 'save'), }); const listTablesForSchemas = vi.fn(async () => [ - { schema: 'analytics', name: 'customers', kind: 'table' as const }, - { schema: 'analytics', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const }, ]); const result = await pickDatabaseScope( @@ -161,6 +213,7 @@ describe('pickDatabaseScope', () => { 'public.events', 'public.sessions', ]); + expect([...(capture.state?.expanded ?? [])].sort()).toEqual(['analytics', 'public']); expect(capture.state?.byId.get('public.events')?.title).toBe('events (view)'); }); diff --git a/packages/cli/src/demo-assets.test.ts b/packages/cli/test/demo-assets.test.ts similarity index 99% rename from packages/cli/src/demo-assets.test.ts rename to packages/cli/test/demo-assets.test.ts index 052eda83..80573d3d 100644 --- a/packages/cli/src/demo-assets.test.ts +++ b/packages/cli/test/demo-assets.test.ts @@ -10,7 +10,7 @@ import { defaultDemoProjectDir, ensureDemoProject, ensureSeededDemoProject, -} from './demo-assets.js'; +} from '../src/demo-assets.js'; const packagedDemoSource = 'packaged-orbit-demo'; diff --git a/packages/cli/src/demo-metrics.test.ts b/packages/cli/test/demo-metrics.test.ts similarity index 97% rename from packages/cli/src/demo-metrics.test.ts rename to packages/cli/test/demo-metrics.test.ts index 9e40be36..fcfe90c8 100644 --- a/packages/cli/src/demo-metrics.test.ts +++ b/packages/cli/test/demo-metrics.test.ts @@ -1,4 +1,4 @@ -import type { MemoryFlowEvent, MemoryFlowReplayInput } from './context/ingest/memory-flow/types.js'; +import type { MemoryFlowEvent, MemoryFlowReplayInput } from '../src/context/ingest/memory-flow/types.js'; import { describe, expect, it } from 'vitest'; import { buildDemoMetrics, @@ -8,7 +8,7 @@ import { formatTokens, formatTokensPerSec, progressBar, -} from './demo-metrics.js'; +} from '../src/demo-metrics.js'; function snapshot(events: MemoryFlowEvent[], overrides: Partial = {}): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/doctor.test.ts b/packages/cli/test/doctor.test.ts similarity index 99% rename from packages/cli/src/doctor.test.ts rename to packages/cli/test/doctor.test.ts index 64050623..e3871f28 100644 --- a/packages/cli/src/doctor.test.ts +++ b/packages/cli/test/doctor.test.ts @@ -7,7 +7,7 @@ import { runKtxDoctor, runSetupDoctorChecks, type DoctorCheck, -} from './doctor.js'; +} from '../src/doctor.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/embedding-resolution.test.ts b/packages/cli/test/embedding-resolution.test.ts similarity index 94% rename from packages/cli/src/embedding-resolution.test.ts rename to packages/cli/test/embedding-resolution.test.ts index 40c71538..d9546c36 100644 --- a/packages/cli/src/embedding-resolution.test.ts +++ b/packages/cli/test/embedding-resolution.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; -import type { KtxLocalProject } from './context/project/project.js'; -import { resolveProjectEmbeddingProvider } from './embedding-resolution.js'; -import type { ManagedLocalEmbeddingsDaemon } from './managed-local-embeddings.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; +import type { KtxLocalProject } from '../src/context/project/project.js'; +import { resolveProjectEmbeddingProvider } from '../src/embedding-resolution.js'; +import type { ManagedLocalEmbeddingsDaemon } from '../src/managed-local-embeddings.js'; function projectWithConfig(config: KtxProjectConfig): KtxLocalProject { return { diff --git a/packages/cli/src/example-smoke.test.ts b/packages/cli/test/example-smoke.test.ts similarity index 100% rename from packages/cli/src/example-smoke.test.ts rename to packages/cli/test/example-smoke.test.ts diff --git a/packages/cli/src/test/fixtures/lookml/extends-chain/orders.model.lkml b/packages/cli/test/fixtures/lookml/extends-chain/orders.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/extends-chain/orders.model.lkml rename to packages/cli/test/fixtures/lookml/extends-chain/orders.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/extends-chain/views/base.view.lkml b/packages/cli/test/fixtures/lookml/extends-chain/views/base.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/extends-chain/views/base.view.lkml rename to packages/cli/test/fixtures/lookml/extends-chain/views/base.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/extends-chain/views/orders.view.lkml b/packages/cli/test/fixtures/lookml/extends-chain/views/orders.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/extends-chain/views/orders.view.lkml rename to packages/cli/test/fixtures/lookml/extends-chain/views/orders.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/extends-chain/views/orders_ext.view.lkml b/packages/cli/test/fixtures/lookml/extends-chain/views/orders_ext.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/extends-chain/views/orders_ext.view.lkml rename to packages/cli/test/fixtures/lookml/extends-chain/views/orders_ext.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/multi-model/marketing.model.lkml b/packages/cli/test/fixtures/lookml/multi-model/marketing.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/multi-model/marketing.model.lkml rename to packages/cli/test/fixtures/lookml/multi-model/marketing.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/multi-model/orders.model.lkml b/packages/cli/test/fixtures/lookml/multi-model/orders.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/multi-model/orders.model.lkml rename to packages/cli/test/fixtures/lookml/multi-model/orders.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/multi-model/views/campaigns.view.lkml b/packages/cli/test/fixtures/lookml/multi-model/views/campaigns.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/multi-model/views/campaigns.view.lkml rename to packages/cli/test/fixtures/lookml/multi-model/views/campaigns.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/multi-model/views/orders.view.lkml b/packages/cli/test/fixtures/lookml/multi-model/views/orders.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/multi-model/views/orders.view.lkml rename to packages/cli/test/fixtures/lookml/multi-model/views/orders.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/multi-model/views/shared_dims.view.lkml b/packages/cli/test/fixtures/lookml/multi-model/views/shared_dims.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/multi-model/views/shared_dims.view.lkml rename to packages/cli/test/fixtures/lookml/multi-model/views/shared_dims.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/single-model/orders.model.lkml b/packages/cli/test/fixtures/lookml/single-model/orders.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/single-model/orders.model.lkml rename to packages/cli/test/fixtures/lookml/single-model/orders.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/single-model/views/customers.view.lkml b/packages/cli/test/fixtures/lookml/single-model/views/customers.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/single-model/views/customers.view.lkml rename to packages/cli/test/fixtures/lookml/single-model/views/customers.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/single-model/views/orders.view.lkml b/packages/cli/test/fixtures/lookml/single-model/views/orders.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/single-model/views/orders.view.lkml rename to packages/cli/test/fixtures/lookml/single-model/views/orders.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/billing.model.lkml b/packages/cli/test/fixtures/lookml/three-churn/billing.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/billing.model.lkml rename to packages/cli/test/fixtures/lookml/three-churn/billing.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/customers.model.lkml b/packages/cli/test/fixtures/lookml/three-churn/customers.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/customers.model.lkml rename to packages/cli/test/fixtures/lookml/three-churn/customers.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/support.model.lkml b/packages/cli/test/fixtures/lookml/three-churn/support.model.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/support.model.lkml rename to packages/cli/test/fixtures/lookml/three-churn/support.model.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/views/billing/billing_churn_risk.view.lkml b/packages/cli/test/fixtures/lookml/three-churn/views/billing/billing_churn_risk.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/views/billing/billing_churn_risk.view.lkml rename to packages/cli/test/fixtures/lookml/three-churn/views/billing/billing_churn_risk.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/views/customers/customer_churn_risk.view.lkml b/packages/cli/test/fixtures/lookml/three-churn/views/customers/customer_churn_risk.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/views/customers/customer_churn_risk.view.lkml rename to packages/cli/test/fixtures/lookml/three-churn/views/customers/customer_churn_risk.view.lkml diff --git a/packages/cli/src/test/fixtures/lookml/three-churn/views/support/support_churn_risk.view.lkml b/packages/cli/test/fixtures/lookml/three-churn/views/support/support_churn_risk.view.lkml similarity index 100% rename from packages/cli/src/test/fixtures/lookml/three-churn/views/support/support_churn_risk.view.lkml rename to packages/cli/test/fixtures/lookml/three-churn/views/support/support_churn_risk.view.lkml diff --git a/packages/cli/src/test/fixtures/metabase/card-ref/cards/10.json b/packages/cli/test/fixtures/metabase/card-ref/cards/10.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/card-ref/cards/10.json rename to packages/cli/test/fixtures/metabase/card-ref/cards/10.json diff --git a/packages/cli/src/test/fixtures/metabase/card-ref/cards/11.json b/packages/cli/test/fixtures/metabase/card-ref/cards/11.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/card-ref/cards/11.json rename to packages/cli/test/fixtures/metabase/card-ref/cards/11.json diff --git a/packages/cli/src/test/fixtures/metabase/card-ref/collections/5.json b/packages/cli/test/fixtures/metabase/card-ref/collections/5.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/card-ref/collections/5.json rename to packages/cli/test/fixtures/metabase/card-ref/collections/5.json diff --git a/packages/cli/src/test/fixtures/metabase/card-ref/databases/42.json b/packages/cli/test/fixtures/metabase/card-ref/databases/42.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/card-ref/databases/42.json rename to packages/cli/test/fixtures/metabase/card-ref/databases/42.json diff --git a/packages/cli/src/test/fixtures/metabase/card-ref/sync-config.json b/packages/cli/test/fixtures/metabase/card-ref/sync-config.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/card-ref/sync-config.json rename to packages/cli/test/fixtures/metabase/card-ref/sync-config.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/cards/1.json b/packages/cli/test/fixtures/metabase/multi-collection/cards/1.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/cards/1.json rename to packages/cli/test/fixtures/metabase/multi-collection/cards/1.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/cards/2.json b/packages/cli/test/fixtures/metabase/multi-collection/cards/2.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/cards/2.json rename to packages/cli/test/fixtures/metabase/multi-collection/cards/2.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/cards/3.json b/packages/cli/test/fixtures/metabase/multi-collection/cards/3.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/cards/3.json rename to packages/cli/test/fixtures/metabase/multi-collection/cards/3.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/collections/5.json b/packages/cli/test/fixtures/metabase/multi-collection/collections/5.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/collections/5.json rename to packages/cli/test/fixtures/metabase/multi-collection/collections/5.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/collections/6.json b/packages/cli/test/fixtures/metabase/multi-collection/collections/6.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/collections/6.json rename to packages/cli/test/fixtures/metabase/multi-collection/collections/6.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/databases/42.json b/packages/cli/test/fixtures/metabase/multi-collection/databases/42.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/databases/42.json rename to packages/cli/test/fixtures/metabase/multi-collection/databases/42.json diff --git a/packages/cli/src/test/fixtures/metabase/multi-collection/sync-config.json b/packages/cli/test/fixtures/metabase/multi-collection/sync-config.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/multi-collection/sync-config.json rename to packages/cli/test/fixtures/metabase/multi-collection/sync-config.json diff --git a/packages/cli/src/test/fixtures/metabase/simple/cards/1.json b/packages/cli/test/fixtures/metabase/simple/cards/1.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/simple/cards/1.json rename to packages/cli/test/fixtures/metabase/simple/cards/1.json diff --git a/packages/cli/src/test/fixtures/metabase/simple/cards/2.json b/packages/cli/test/fixtures/metabase/simple/cards/2.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/simple/cards/2.json rename to packages/cli/test/fixtures/metabase/simple/cards/2.json diff --git a/packages/cli/src/test/fixtures/metabase/simple/collections/5.json b/packages/cli/test/fixtures/metabase/simple/collections/5.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/simple/collections/5.json rename to packages/cli/test/fixtures/metabase/simple/collections/5.json diff --git a/packages/cli/src/test/fixtures/metabase/simple/databases/42.json b/packages/cli/test/fixtures/metabase/simple/databases/42.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/simple/databases/42.json rename to packages/cli/test/fixtures/metabase/simple/databases/42.json diff --git a/packages/cli/src/test/fixtures/metabase/simple/sync-config.json b/packages/cli/test/fixtures/metabase/simple/sync-config.json similarity index 100% rename from packages/cli/src/test/fixtures/metabase/simple/sync-config.json rename to packages/cli/test/fixtures/metabase/simple/sync-config.json diff --git a/packages/cli/src/test/fixtures/metricflow/dbt-mixed/dbt_project.yml b/packages/cli/test/fixtures/metricflow/dbt-mixed/dbt_project.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/dbt-mixed/dbt_project.yml rename to packages/cli/test/fixtures/metricflow/dbt-mixed/dbt_project.yml diff --git a/packages/cli/src/test/fixtures/metricflow/dbt-mixed/models/orders.yml b/packages/cli/test/fixtures/metricflow/dbt-mixed/models/orders.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/dbt-mixed/models/orders.yml rename to packages/cli/test/fixtures/metricflow/dbt-mixed/models/orders.yml diff --git a/packages/cli/src/test/fixtures/metricflow/extends-chain/metrics/orders_final.yml b/packages/cli/test/fixtures/metricflow/extends-chain/metrics/orders_final.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/extends-chain/metrics/orders_final.yml rename to packages/cli/test/fixtures/metricflow/extends-chain/metrics/orders_final.yml diff --git a/packages/cli/src/test/fixtures/metricflow/extends-chain/models/orders.yml b/packages/cli/test/fixtures/metricflow/extends-chain/models/orders.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/extends-chain/models/orders.yml rename to packages/cli/test/fixtures/metricflow/extends-chain/models/orders.yml diff --git a/packages/cli/src/test/fixtures/metricflow/extends-chain/models/orders_ext.yml b/packages/cli/test/fixtures/metricflow/extends-chain/models/orders_ext.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/extends-chain/models/orders_ext.yml rename to packages/cli/test/fixtures/metricflow/extends-chain/models/orders_ext.yml diff --git a/packages/cli/src/test/fixtures/metricflow/multi-component/models/marketing/campaigns.yml b/packages/cli/test/fixtures/metricflow/multi-component/models/marketing/campaigns.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/multi-component/models/marketing/campaigns.yml rename to packages/cli/test/fixtures/metricflow/multi-component/models/marketing/campaigns.yml diff --git a/packages/cli/src/test/fixtures/metricflow/multi-component/models/sales/orders.yml b/packages/cli/test/fixtures/metricflow/multi-component/models/sales/orders.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/multi-component/models/sales/orders.yml rename to packages/cli/test/fixtures/metricflow/multi-component/models/sales/orders.yml diff --git a/packages/cli/src/test/fixtures/metricflow/single-model/models/orders.yml b/packages/cli/test/fixtures/metricflow/single-model/models/orders.yml similarity index 100% rename from packages/cli/src/test/fixtures/metricflow/single-model/models/orders.yml rename to packages/cli/test/fixtures/metricflow/single-model/models/orders.yml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/abbreviated_old_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworks_oltp_with_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/adventureworkslt_with_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/analytical_warehouse_no_naming_convention/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/chinook_with_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/composite_keys_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/demo_b2b_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/mixed_case_within_schema_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/natural_keys_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/non_english_naming_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/northwind_with_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/orbit_style_product_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/plan_code_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/polymorphic_partial_overlap_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/sakila_with_declared_metadata/snapshot.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/data.sqlite.gz b/packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/data.sqlite.gz similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/data.sqlite.gz rename to packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/data.sqlite.gz diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/snapshot.json.gz b/packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/snapshot.json.gz similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/snapshot.json.gz rename to packages/cli/test/fixtures/relationship-benchmarks/scale_stress_no_declared_constraints/snapshot.json.gz diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/column-embeddings.json b/packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/column-embeddings.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/column-embeddings.json rename to packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/column-embeddings.json diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/data.sqlite b/packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/data.sqlite similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/data.sqlite rename to packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/data.sqlite diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/expected-links.yaml b/packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/expected-links.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/expected-links.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/expected-links.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/fixture.yaml b/packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/fixture.yaml similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/fixture.yaml rename to packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/fixture.yaml diff --git a/packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/snapshot.json b/packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/snapshot.json similarity index 100% rename from packages/cli/src/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/snapshot.json rename to packages/cli/test/fixtures/relationship-benchmarks/semantic_embedding_aliases_no_declared_constraints/snapshot.json diff --git a/packages/cli/src/index.test.ts b/packages/cli/test/index.test.ts similarity index 99% rename from packages/cli/src/index.test.ts rename to packages/cli/test/index.test.ts index c482e452..a60c48f2 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; +import { initKtxProject } from '../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -15,7 +15,7 @@ import { sanitizeMemoryFlowTuiError, startLiveMemoryFlowTui, warnVizFallbackOnce, -} from './index.js'; +} from '../src/index.js'; const require = createRequire(import.meta.url); diff --git a/packages/cli/src/ingest-query-executor.test.ts b/packages/cli/test/ingest-query-executor.test.ts similarity index 89% rename from packages/cli/src/ingest-query-executor.test.ts rename to packages/cli/test/ingest-query-executor.test.ts index 14b714d9..372cd362 100644 --- a/packages/cli/src/ingest-query-executor.test.ts +++ b/packages/cli/test/ingest-query-executor.test.ts @@ -1,7 +1,7 @@ -import type { KtxLocalProject } from './context/project/project.js'; -import { createKtxConnectorCapabilities, type KtxScanConnector } from './context/scan/types.js'; +import type { KtxLocalProject } from '../src/context/project/project.js'; +import { createKtxConnectorCapabilities, type KtxScanConnector } from '../src/context/scan/types.js'; import { describe, expect, it, vi } from 'vitest'; -import { createKtxCliIngestQueryExecutor } from './ingest-query-executor.js'; +import { createKtxCliIngestQueryExecutor } from '../src/ingest-query-executor.js'; function project(): KtxLocalProject { return { @@ -31,6 +31,8 @@ function connector(overrides: Partial = {}): KtxScanConnector })), cleanup: vi.fn(async () => {}), ...overrides, + listSchemas: overrides.listSchemas ?? vi.fn(async () => []), + listTables: overrides.listTables ?? vi.fn(async () => []), }; } diff --git a/packages/cli/src/ingest-report-file.test.ts b/packages/cli/test/ingest-report-file.test.ts similarity index 96% rename from packages/cli/src/ingest-report-file.test.ts rename to packages/cli/test/ingest-report-file.test.ts index 5183764b..071876cf 100644 --- a/packages/cli/src/ingest-report-file.test.ts +++ b/packages/cli/test/ingest-report-file.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { readIngestReportSnapshotFile } from './ingest-report-file.js'; +import { readIngestReportSnapshotFile } from '../src/ingest-report-file.js'; function reportSnapshot() { return { diff --git a/packages/cli/src/ingest-viz.test.ts b/packages/cli/test/ingest-viz.test.ts similarity index 99% rename from packages/cli/src/ingest-viz.test.ts rename to packages/cli/test/ingest-viz.test.ts index 17b35f75..a794043f 100644 --- a/packages/cli/src/ingest-viz.test.ts +++ b/packages/cli/test/ingest-viz.test.ts @@ -1,10 +1,10 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { LocalIngestResult, RunLocalIngestOptions } from './context/ingest/local-ingest.js'; -import type { MemoryFlowReplayInput } from './context/ingest/memory-flow/types.js'; +import type { LocalIngestResult, RunLocalIngestOptions } from '../src/context/ingest/local-ingest.js'; +import type { MemoryFlowReplayInput } from '../src/context/ingest/memory-flow/types.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxIngest } from './ingest.js'; +import { runKtxIngest } from '../src/ingest.js'; import { completedLocalBundleRun, emitLiveLocalMemoryFlow, @@ -14,7 +14,7 @@ import { writeBundleReportFile, writeWarehouseConfig, } from './ingest.test-utils.js'; -import { resetVizFallbackWarningsForTest } from './viz-fallback.js'; +import { resetVizFallbackWarningsForTest } from '../src/viz-fallback.js'; describe('runKtxIngest viz and replay', () => { let tempDir: string; diff --git a/packages/cli/src/ingest.test-utils.ts b/packages/cli/test/ingest.test-utils.ts similarity index 95% rename from packages/cli/src/ingest.test-utils.ts rename to packages/cli/test/ingest.test-utils.ts index 81600df7..7198ff5d 100644 --- a/packages/cli/src/ingest.test-utils.ts +++ b/packages/cli/test/ingest.test-utils.ts @@ -1,21 +1,21 @@ import { EventEmitter } from 'node:events'; import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import type { AgentRunnerPort, RunLoopParams } from './context/llm/runtime-port.js'; -import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from './context/ingest/adapters/metabase/local-source-state-store.js'; -import { MetabaseSourceAdapter } from './context/ingest/adapters/metabase/metabase.adapter.js'; -import { getLocalIngestStatus, type LocalIngestResult, type RunLocalIngestOptions } from './context/ingest/local-ingest.js'; -import type { ChunkResult, FetchContext, SourceAdapter } from './context/ingest/types.js'; -import type { IngestReportSnapshot } from './context/ingest/reports.js'; -import type { LookerMappingClient, LookerTableIdentifierParser } from './context/ingest/adapters/looker/mapping.js'; -import type { LookerRuntimeClient } from './context/ingest/adapters/looker/fetch.js'; -import type { MemoryFlowEventSink } from './context/ingest/memory-flow/types.js'; -import type { MetabaseCard, MetabaseCardSummary, MetabaseClientFactory, MetabaseRuntimeClient } from './context/ingest/adapters/metabase/client-port.js'; -import type { SqliteBundleIngestStore } from './context/ingest/sqlite-bundle-ingest-store.js'; -import { ktxLocalStateDbPath } from './context/project/local-state-db.js'; -import { loadKtxProject } from './context/project/project.js'; +import type { AgentRunnerPort, RunLoopParams } from '../src/context/llm/runtime-port.js'; +import { KtxYamlMetabaseSourceStateReader, LocalMetabaseDiscoveryCache } from '../src/context/ingest/adapters/metabase/local-source-state-store.js'; +import { MetabaseSourceAdapter } from '../src/context/ingest/adapters/metabase/metabase.adapter.js'; +import { getLocalIngestStatus, type LocalIngestResult, type RunLocalIngestOptions } from '../src/context/ingest/local-ingest.js'; +import type { ChunkResult, FetchContext, SourceAdapter } from '../src/context/ingest/types.js'; +import type { IngestReportSnapshot } from '../src/context/ingest/reports.js'; +import type { LookerMappingClient, LookerTableIdentifierParser } from '../src/context/ingest/adapters/looker/mapping.js'; +import type { LookerRuntimeClient } from '../src/context/ingest/adapters/looker/fetch.js'; +import type { MemoryFlowEventSink } from '../src/context/ingest/memory-flow/types.js'; +import type { MetabaseCard, MetabaseCardSummary, MetabaseClientFactory, MetabaseRuntimeClient } from '../src/context/ingest/adapters/metabase/client-port.js'; +import type { SqliteBundleIngestStore } from '../src/context/ingest/sqlite-bundle-ingest-store.js'; +import { ktxLocalStateDbPath } from '../src/context/project/local-state-db.js'; +import { loadKtxProject } from '../src/context/project/project.js'; import { expect, vi } from 'vitest'; -import { runKtxIngest } from './ingest.js'; +import { runKtxIngest } from '../src/ingest.js'; export function makeIo( options: { @@ -675,7 +675,7 @@ export function localFakeBundleReport( } export async function localBundleStore(projectDir: string, ids: [string, string]): Promise { - const { SqliteBundleIngestStore } = await import('./context/ingest/sqlite-bundle-ingest-store.js');; + const { SqliteBundleIngestStore } = await import('../src/context/ingest/sqlite-bundle-ingest-store.js');; const project = await loadKtxProject({ projectDir }); return new SqliteBundleIngestStore({ dbPath: ktxLocalStateDbPath(project), diff --git a/packages/cli/src/ingest.test.ts b/packages/cli/test/ingest.test.ts similarity index 98% rename from packages/cli/src/ingest.test.ts rename to packages/cli/test/ingest.test.ts index 6de72879..eef751ba 100644 --- a/packages/cli/src/ingest.test.ts +++ b/packages/cli/test/ingest.test.ts @@ -1,15 +1,15 @@ import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { LocalLookerRuntimeStore } from './context/ingest/adapters/looker/local-runtime-store.js'; -import { LocalMetabaseDiscoveryCache } from './context/ingest/adapters/metabase/local-source-state-store.js'; -import type { LocalIngestResult, LocalMetabaseFanoutProgress, RunLocalIngestOptions } from './context/ingest/local-ingest.js'; -import type { SourceAdapter } from './context/ingest/types.js'; -import { initKtxProject, loadKtxProject } from './context/project/project.js'; -import { ktxLocalStateDbPath } from './context/project/local-state-db.js'; +import { LocalLookerRuntimeStore } from '../src/context/ingest/adapters/looker/local-runtime-store.js'; +import { LocalMetabaseDiscoveryCache } from '../src/context/ingest/adapters/metabase/local-source-state-store.js'; +import type { LocalIngestResult, LocalMetabaseFanoutProgress, RunLocalIngestOptions } from '../src/context/ingest/local-ingest.js'; +import type { SourceAdapter } from '../src/context/ingest/types.js'; +import { initKtxProject, loadKtxProject } from '../src/context/project/project.js'; +import { ktxLocalStateDbPath } from '../src/context/project/local-state-db.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type KtxIngestArgs, type KtxIngestDeps, runKtxIngest } from './ingest.js'; -import type { KtxCliLocalIngestAdaptersOptions } from './local-adapters.js'; +import { type KtxIngestArgs, type KtxIngestDeps, runKtxIngest } from '../src/ingest.js'; +import type { KtxCliLocalIngestAdaptersOptions } from '../src/local-adapters.js'; import { CliLookerSlWritingAgentRunner, CliMetabaseAgentRunner, @@ -25,8 +25,8 @@ import { writeMetabaseConfig, writeWarehouseConfig, } from './ingest.test-utils.js'; -import { resetVizFallbackWarningsForTest } from './viz-fallback.js'; -import { runKtxSetup } from './setup.js'; +import { resetVizFallbackWarningsForTest } from '../src/viz-fallback.js'; +import { runKtxSetup } from '../src/setup.js'; describe('runKtxIngest', () => { let tempDir: string; diff --git a/packages/cli/src/io/logger.test.ts b/packages/cli/test/io/logger.test.ts similarity index 97% rename from packages/cli/src/io/logger.test.ts rename to packages/cli/test/io/logger.test.ts index bf21a150..46248d02 100644 --- a/packages/cli/src/io/logger.test.ts +++ b/packages/cli/test/io/logger.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { createCliOperationalLogger, createNoopOperationalLogger } from './logger.js'; +import { createCliOperationalLogger, createNoopOperationalLogger } from '../../src/io/logger.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/io/mode.test.ts b/packages/cli/test/io/mode.test.ts similarity index 95% rename from packages/cli/src/io/mode.test.ts rename to packages/cli/test/io/mode.test.ts index cfc9a9fc..4d36c37a 100644 --- a/packages/cli/src/io/mode.test.ts +++ b/packages/cli/test/io/mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { KtxCliIo } from '../cli-runtime.js'; -import { resolveOutputMode } from './mode.js'; +import type { KtxCliIo } from '../../src/cli-runtime.js'; +import { resolveOutputMode } from '../../src/io/mode.js'; function ioWith(isTTY: boolean | undefined): KtxCliIo { return { diff --git a/packages/cli/src/io/print-list.test.ts b/packages/cli/test/io/print-list.test.ts similarity index 98% rename from packages/cli/src/io/print-list.test.ts rename to packages/cli/test/io/print-list.test.ts index f084e519..f065d067 100644 --- a/packages/cli/src/io/print-list.test.ts +++ b/packages/cli/test/io/print-list.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { KtxCliIo } from '../cli-runtime.js'; -import { createRankBadgeFormatter, printList, type PrintListColumn } from './print-list.js'; -import { SYMBOLS } from './symbols.js'; +import type { KtxCliIo } from '../../src/cli-runtime.js'; +import { createRankBadgeFormatter, printList, type PrintListColumn } from '../../src/io/print-list.js'; +import { SYMBOLS } from '../../src/io/symbols.js'; function recorder(): { io: KtxCliIo; out: () => string; err: () => string } { let stdout = ''; diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/test/knowledge.test.ts similarity index 96% rename from packages/cli/src/knowledge.test.ts rename to packages/cli/test/knowledge.test.ts index 69581f0f..339eb659 100644 --- a/packages/cli/src/knowledge.test.ts +++ b/packages/cli/test/knowledge.test.ts @@ -2,11 +2,11 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { stripVTControlCharacters } from 'node:util'; -import { initKtxProject, loadKtxProject } from './context/project/project.js'; -import type { KtxEmbeddingPort } from './context/core/embedding.js'; -import { writeLocalKnowledgePage } from './context/wiki/local-knowledge.js'; +import { initKtxProject, loadKtxProject } from '../src/context/project/project.js'; +import type { KtxEmbeddingPort } from '../src/context/core/embedding.js'; +import { writeLocalKnowledgePage } from '../src/context/wiki/local-knowledge.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxKnowledge } from './knowledge.js'; +import { runKtxKnowledge } from '../src/knowledge.js'; function makeIo(options: { isTTY?: boolean } = {}) { let stdout = ''; diff --git a/packages/cli/src/llm/embedding-health.test.ts b/packages/cli/test/llm/embedding-health.test.ts similarity index 97% rename from packages/cli/src/llm/embedding-health.test.ts rename to packages/cli/test/llm/embedding-health.test.ts index 65956311..f659b2d6 100644 --- a/packages/cli/src/llm/embedding-health.test.ts +++ b/packages/cli/test/llm/embedding-health.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { runKtxEmbeddingHealthCheck } from './embedding-health.js'; +import { runKtxEmbeddingHealthCheck } from '../../src/llm/embedding-health.js'; describe('KTX embedding health check', () => { it('runs a one-shot OpenAI embedding check through the configured provider', async () => { diff --git a/packages/cli/src/llm/embedding-provider.test.ts b/packages/cli/test/llm/embedding-provider.test.ts similarity index 96% rename from packages/cli/src/llm/embedding-provider.test.ts rename to packages/cli/test/llm/embedding-provider.test.ts index c649a948..071a17b0 100644 --- a/packages/cli/src/llm/embedding-provider.test.ts +++ b/packages/cli/test/llm/embedding-provider.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { createKtxEmbeddingProvider } from './embedding-provider.js'; -import type { KtxEmbeddingConfig } from './types.js'; +import { createKtxEmbeddingProvider } from '../../src/llm/embedding-provider.js'; +import type { KtxEmbeddingConfig } from '../../src/llm/types.js'; describe('createKtxEmbeddingProvider', () => { it('rejects deterministic embeddings', () => { diff --git a/packages/cli/src/llm/message-builder.test.ts b/packages/cli/test/llm/message-builder.test.ts similarity index 97% rename from packages/cli/src/llm/message-builder.test.ts rename to packages/cli/test/llm/message-builder.test.ts index 60f7d948..5ebbb590 100644 --- a/packages/cli/src/llm/message-builder.test.ts +++ b/packages/cli/test/llm/message-builder.test.ts @@ -1,7 +1,7 @@ import type { ModelMessage } from 'ai'; import { describe, expect, it } from 'vitest'; -import { KtxMessageBuilder, splitKtxSystemMessages } from './message-builder.js'; -import { createKtxLlmProvider } from './model-provider.js'; +import { KtxMessageBuilder, splitKtxSystemMessages } from '../../src/llm/message-builder.js'; +import { createKtxLlmProvider } from '../../src/llm/model-provider.js'; function makeBuilder(overrides: Parameters[0]['promptCaching'] = {}) { const provider = createKtxLlmProvider({ diff --git a/packages/cli/src/llm/model-health.test.ts b/packages/cli/test/llm/model-health.test.ts similarity index 97% rename from packages/cli/src/llm/model-health.test.ts rename to packages/cli/test/llm/model-health.test.ts index 8cf7a7ee..0af6f8e8 100644 --- a/packages/cli/src/llm/model-health.test.ts +++ b/packages/cli/test/llm/model-health.test.ts @@ -1,6 +1,6 @@ import { wrapLanguageModel as defaultWrapLanguageModel } from 'ai'; import { describe, expect, it, vi } from 'vitest'; -import { runKtxLlmHealthCheck } from './model-health.js'; +import { runKtxLlmHealthCheck } from '../../src/llm/model-health.js'; const anthropicModel = { modelId: 'claude-sonnet-4-6' } as never; diff --git a/packages/cli/src/llm/model-provider.test.ts b/packages/cli/test/llm/model-provider.test.ts similarity index 99% rename from packages/cli/src/llm/model-provider.test.ts rename to packages/cli/test/llm/model-provider.test.ts index 1a61d0a1..0e3ef045 100644 --- a/packages/cli/src/llm/model-provider.test.ts +++ b/packages/cli/test/llm/model-provider.test.ts @@ -1,7 +1,7 @@ import { devToolsMiddleware as defaultDevToolsMiddleware } from '@ai-sdk/devtools'; import { wrapLanguageModel as defaultWrapLanguageModel, type LanguageModel } from 'ai'; import { describe, expect, it, vi } from 'vitest'; -import { createKtxLlmProvider, type KtxLlmProviderFactoryDeps } from './model-provider.js'; +import { createKtxLlmProvider, type KtxLlmProviderFactoryDeps } from '../../src/llm/model-provider.js'; const languageModel = (modelId: string, provider = 'test'): LanguageModel => ({ modelId, provider }) as LanguageModel; const devtoolsMiddleware = (): ReturnType => ({ specificationVersion: 'v3' }); diff --git a/packages/cli/src/llm/repair.test.ts b/packages/cli/test/llm/repair.test.ts similarity index 97% rename from packages/cli/src/llm/repair.test.ts rename to packages/cli/test/llm/repair.test.ts index bef53a46..743039a9 100644 --- a/packages/cli/src/llm/repair.test.ts +++ b/packages/cli/test/llm/repair.test.ts @@ -1,6 +1,6 @@ import { NoSuchToolError, type LanguageModel } from 'ai'; import { describe, expect, it, vi } from 'vitest'; -import { createKtxToolCallRepairHandler } from './repair.js'; +import { createKtxToolCallRepairHandler } from '../../src/llm/repair.js'; const repairModel = { modelId: 'claude-repair', provider: 'anthropic' } as LanguageModel; diff --git a/packages/cli/src/local-adapters.test.ts b/packages/cli/test/local-adapters.test.ts similarity index 97% rename from packages/cli/src/local-adapters.test.ts rename to packages/cli/test/local-adapters.test.ts index a4e856b4..c7ae58cc 100644 --- a/packages/cli/src/local-adapters.test.ts +++ b/packages/cli/test/local-adapters.test.ts @@ -1,9 +1,9 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { loadKtxProject } from './context/project/project.js'; +import { loadKtxProject } from '../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { createKtxCliLocalIngestAdapters } from './local-adapters.js'; +import { createKtxCliLocalIngestAdapters } from '../src/local-adapters.js'; function sqlAnalysisStub() { return { diff --git a/packages/cli/src/local-scan-connectors.test.ts b/packages/cli/test/local-scan-connectors.test.ts similarity index 94% rename from packages/cli/src/local-scan-connectors.test.ts rename to packages/cli/test/local-scan-connectors.test.ts index a993faa2..1dadb6c4 100644 --- a/packages/cli/src/local-scan-connectors.test.ts +++ b/packages/cli/test/local-scan-connectors.test.ts @@ -1,9 +1,9 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject, loadKtxProject } from './context/project/project.js'; +import { initKtxProject, loadKtxProject } from '../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createKtxCliScanConnector } from './local-scan-connectors.js'; +import { createKtxCliScanConnector } from '../src/local-scan-connectors.js'; const bigQueryMock = vi.hoisted(() => ({ constructorInputs: [] as Array<{ @@ -12,7 +12,7 @@ const bigQueryMock = vi.hoisted(() => ({ }>, })); -vi.mock('./connectors/bigquery/connector.js', () => ({ +vi.mock('../src/connectors/bigquery/connector.js', () => ({ isKtxBigQueryConnectionConfig: (connection: { driver?: unknown } | undefined) => String(connection?.driver ?? '').toLowerCase() === 'bigquery', KtxBigQueryScanConnector: class { diff --git a/packages/cli/src/managed-local-embeddings.test.ts b/packages/cli/test/managed-local-embeddings.test.ts similarity index 96% rename from packages/cli/src/managed-local-embeddings.test.ts rename to packages/cli/test/managed-local-embeddings.test.ts index 9ee938fb..9c78e177 100644 --- a/packages/cli/src/managed-local-embeddings.test.ts +++ b/packages/cli/test/managed-local-embeddings.test.ts @@ -3,10 +3,10 @@ import { ensureManagedLocalEmbeddingsDaemon, managedLocalEmbeddingHealthConfig, tryUseManagedLocalEmbeddingsDaemon, -} from './managed-local-embeddings.js'; -import type { ManagedPythonCommandRuntime } from './managed-python-command.js'; -import type { ManagedPythonDaemonStartResult } from './managed-python-daemon.js'; -import type { ManagedPythonDaemonLayout } from './managed-python-runtime.js'; +} from '../src/managed-local-embeddings.js'; +import type { ManagedPythonCommandRuntime } from '../src/managed-python-command.js'; +import type { ManagedPythonDaemonStartResult } from '../src/managed-python-daemon.js'; +import type { ManagedPythonDaemonLayout } from '../src/managed-python-runtime.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/managed-mcp-daemon.test.ts b/packages/cli/test/managed-mcp-daemon.test.ts similarity index 99% rename from packages/cli/src/managed-mcp-daemon.test.ts rename to packages/cli/test/managed-mcp-daemon.test.ts index d72bb6a4..81566d40 100644 --- a/packages/cli/src/managed-mcp-daemon.test.ts +++ b/packages/cli/test/managed-mcp-daemon.test.ts @@ -9,7 +9,7 @@ import { stopKtxMcpDaemon, type KtxMcpDaemonChild, type KtxMcpDaemonState, -} from './managed-mcp-daemon.js'; +} from '../src/managed-mcp-daemon.js'; type KtxMcpDaemonStartOptions = Parameters[0]; diff --git a/packages/cli/src/managed-python-command.test.ts b/packages/cli/test/managed-python-command.test.ts similarity index 99% rename from packages/cli/src/managed-python-command.test.ts rename to packages/cli/test/managed-python-command.test.ts index 717accf4..f08589f1 100644 --- a/packages/cli/src/managed-python-command.test.ts +++ b/packages/cli/test/managed-python-command.test.ts @@ -4,14 +4,14 @@ import { ensureManagedPythonCommandRuntime, managedRuntimeInstallCommand, runtimeInstallPolicyFromFlags, -} from './managed-python-command.js'; +} from '../src/managed-python-command.js'; import type { InstalledKtxRuntimeManifest, KtxRuntimeFeature, ManagedPythonRuntimeInstallResult, ManagedPythonRuntimeLayout, ManagedPythonRuntimeStatus, -} from './managed-python-runtime.js'; +} from '../src/managed-python-runtime.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/managed-python-daemon.test.ts b/packages/cli/test/managed-python-daemon.test.ts similarity index 83% rename from packages/cli/src/managed-python-daemon.test.ts rename to packages/cli/test/managed-python-daemon.test.ts index d56a27b1..4684c731 100644 --- a/packages/cli/src/managed-python-daemon.test.ts +++ b/packages/cli/test/managed-python-daemon.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { + ManagedPythonDaemonStartError, readManagedPythonDaemonStatus, startManagedPythonDaemon, stopAllManagedPythonDaemons, @@ -12,13 +13,13 @@ import { type ManagedPythonDaemonProcessInfo, type ManagedPythonDaemonSpawn, type ManagedPythonDaemonState, -} from './managed-python-daemon.js'; +} from '../src/managed-python-daemon.js'; import type { InstalledKtxRuntimeManifest, ManagedPythonDaemonLayout, ManagedPythonRuntimeInstallResult, ManagedPythonRuntimeLayout, -} from './managed-python-runtime.js'; +} from '../src/managed-python-runtime.js'; function layout(root: string): ManagedPythonDaemonLayout { const projectDir = join(root, 'project'); @@ -244,6 +245,76 @@ describe('KTX daemon lifecycle', () => { }); }); + it('kills the spawned daemon when the startup health check times out', async () => { + const spawnDaemon = makeSpawn(7777); + const killProcess = vi.fn(); + const fetch = vi.fn().mockRejectedValue(new Error('fetch failed')); + + await expect( + startManagedPythonDaemon({ + ...daemonOptionsBase(tempDir), + features: ['core'], + installRuntime: vi.fn(async () => installResult(tempDir)), + spawnDaemon, + fetch, + processAlive: vi.fn(() => true), + killProcess, + allocatePort: vi.fn(async () => 61234), + now: () => new Date('2026-05-11T00:00:00.000Z'), + startupTimeoutMs: 5, + pollIntervalMs: 1, + }), + ).rejects.toBeInstanceOf(ManagedPythonDaemonStartError); + + expect(killProcess).toHaveBeenCalledWith(7777); + await expect(readFile(layout(tempDir).daemonStatePath, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); + }); + + it('surfaces the underlying fetch cause in the startup failure message', async () => { + const cause = new Error('connect ECONNREFUSED 127.0.0.1:61234'); + const fetchError = new Error('fetch failed'); + (fetchError as Error & { cause?: unknown }).cause = cause; + + const error = await startManagedPythonDaemon({ + ...daemonOptionsBase(tempDir), + features: ['core'], + installRuntime: vi.fn(async () => installResult(tempDir)), + spawnDaemon: makeSpawn(7778), + fetch: vi.fn().mockRejectedValue(fetchError), + processAlive: vi.fn(() => false), + killProcess: vi.fn(), + allocatePort: vi.fn(async () => 61234), + now: () => new Date('2026-05-11T00:00:00.000Z'), + startupTimeoutMs: 5, + pollIntervalMs: 1, + }).catch((value: unknown) => value); + + expect(error).toBeInstanceOf(ManagedPythonDaemonStartError); + const startError = error as ManagedPythonDaemonStartError; + expect(startError.detail).toContain('fetch failed'); + expect(startError.detail).toContain('ECONNREFUSED'); + expect(startError.message).toContain('ECONNREFUSED'); + }); + + it('exposes the daemon stderr log path on startup failure', async () => { + const error = await startManagedPythonDaemon({ + ...daemonOptionsBase(tempDir), + features: ['core'], + installRuntime: vi.fn(async () => installResult(tempDir)), + spawnDaemon: makeSpawn(7779), + fetch: vi.fn().mockRejectedValue(new Error('fetch failed')), + processAlive: vi.fn(() => false), + killProcess: vi.fn(), + allocatePort: vi.fn(async () => 61234), + now: () => new Date('2026-05-11T00:00:00.000Z'), + startupTimeoutMs: 5, + pollIntervalMs: 1, + }).catch((value: unknown) => value); + + expect(error).toBeInstanceOf(ManagedPythonDaemonStartError); + expect((error as ManagedPythonDaemonStartError).stderrLog).toBe(layout(tempDir).daemonStderrPath); + }); + it('reuses a healthy daemon with the requested feature set', async () => { await mkdir(layout(tempDir).daemonStateDir, { recursive: true }); await writeFile(layout(tempDir).daemonStatePath, `${JSON.stringify(runningState(tempDir), null, 2)}\n`); diff --git a/packages/cli/src/managed-python-http.test.ts b/packages/cli/test/managed-python-http.test.ts similarity index 99% rename from packages/cli/src/managed-python-http.test.ts rename to packages/cli/test/managed-python-http.test.ts index 73cae844..f19b6d72 100644 --- a/packages/cli/src/managed-python-http.test.ts +++ b/packages/cli/test/managed-python-http.test.ts @@ -5,7 +5,7 @@ import { createManagedDaemonSqlAnalysisPort, createManagedPythonDaemonBaseUrlResolver, managedDaemonDatabaseIntrospectionOptions, -} from './managed-python-http.js'; +} from '../src/managed-python-http.js'; function io() { let stderr = ''; diff --git a/packages/cli/src/managed-python-runtime.test.ts b/packages/cli/test/managed-python-runtime.test.ts similarity index 99% rename from packages/cli/src/managed-python-runtime.test.ts rename to packages/cli/test/managed-python-runtime.test.ts index 92e34e35..143802ad 100644 --- a/packages/cli/src/managed-python-runtime.test.ts +++ b/packages/cli/test/managed-python-runtime.test.ts @@ -13,7 +13,7 @@ import { readManagedPythonRuntimeStatus, verifyRuntimeAsset, type ManagedPythonRuntimeExec, -} from './managed-python-runtime.js'; +} from '../src/managed-python-runtime.js'; function runtimeWheelContents(input: { label?: string; requiresPython?: string | null } = {}): Buffer { const label = input.label ?? 'runtime-wheel'; diff --git a/packages/cli/src/mcp-http-server.test.ts b/packages/cli/test/mcp-http-server.test.ts similarity index 99% rename from packages/cli/src/mcp-http-server.test.ts rename to packages/cli/test/mcp-http-server.test.ts index d34f0c0c..ddd0bf0f 100644 --- a/packages/cli/src/mcp-http-server.test.ts +++ b/packages/cli/test/mcp-http-server.test.ts @@ -7,7 +7,7 @@ import { isMcpRequestAuthorized, normalizeHostHeader, runKtxMcpHttpServer, -} from './mcp-http-server.js'; +} from '../src/mcp-http-server.js'; describe('normalizeHostHeader', () => { it('normalizes host headers before allow-list comparison', () => { diff --git a/packages/cli/src/mcp-server-factory.test.ts b/packages/cli/test/mcp-server-factory.test.ts similarity index 85% rename from packages/cli/src/mcp-server-factory.test.ts rename to packages/cli/test/mcp-server-factory.test.ts index 64c7275d..05ac5aac 100644 --- a/packages/cli/src/mcp-server-factory.test.ts +++ b/packages/cli/test/mcp-server-factory.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { createDefaultKtxMcpServer } from './context/mcp/server.js'; -import { createLocalProjectMcpContextPorts } from './context/mcp/local-project-ports.js'; -import { createLocalProjectMemoryIngest } from './context/memory/local-memory.js'; -import { resolveProjectEmbeddingProvider } from './embedding-resolution.js'; -import { createKtxCliScanConnector } from './local-scan-connectors.js'; -import { createKtxMcpServerFactory } from './mcp-server-factory.js'; +import { createDefaultKtxMcpServer } from '../src/context/mcp/server.js'; +import { createLocalProjectMcpContextPorts } from '../src/context/mcp/local-project-ports.js'; +import { createLocalProjectMemoryIngest } from '../src/context/memory/local-memory.js'; +import { resolveProjectEmbeddingProvider } from '../src/embedding-resolution.js'; +import { createKtxCliScanConnector } from '../src/local-scan-connectors.js'; +import { createKtxMcpServerFactory } from '../src/mcp-server-factory.js'; type FakeEmbeddingProvider = { maxBatchSize: number; @@ -19,7 +19,7 @@ const mocks = vi.hoisted(() => ({ memoryIngest: { ingest: vi.fn(), status: vi.fn(), waitForRun: vi.fn() }, })); -vi.mock('./context/llm/embedding-port.js', () => ({ +vi.mock('../src/context/llm/embedding-port.js', () => ({ KtxIngestEmbeddingPortAdapter: class { readonly maxBatchSize: number; @@ -37,35 +37,35 @@ vi.mock('./context/llm/embedding-port.js', () => ({ }, })); -vi.mock('./context/mcp/server.js', () => ({ +vi.mock('../src/context/mcp/server.js', () => ({ createDefaultKtxMcpServer: vi.fn(() => ({ kind: 'mcp-server' })), })); -vi.mock('./context/mcp/local-project-ports.js', () => ({ +vi.mock('../src/context/mcp/local-project-ports.js', () => ({ createLocalProjectMcpContextPorts: vi.fn(() => ({ context_tool: { name: 'context_tool' } })), })); -vi.mock('./context/memory/local-memory.js', () => ({ +vi.mock('../src/context/memory/local-memory.js', () => ({ createLocalProjectMemoryIngest: vi.fn(() => mocks.memoryIngest), })); -vi.mock('./embedding-resolution.js', () => ({ +vi.mock('../src/embedding-resolution.js', () => ({ resolveProjectEmbeddingProvider: vi.fn(), })); -vi.mock('./ingest-query-executor.js', () => ({ +vi.mock('../src/ingest-query-executor.js', () => ({ createKtxCliIngestQueryExecutor: vi.fn(() => mocks.queryExecutor), })); -vi.mock('./local-scan-connectors.js', () => ({ +vi.mock('../src/local-scan-connectors.js', () => ({ createKtxCliScanConnector: vi.fn(() => ({ source: 'fake-scan-connector' })), })); -vi.mock('./managed-python-command.js', () => ({ +vi.mock('../src/managed-python-command.js', () => ({ createManagedPythonSemanticLayerComputePort: vi.fn(async () => mocks.semanticLayerCompute), })); -vi.mock('./managed-python-http.js', () => ({ +vi.mock('../src/managed-python-http.js', () => ({ createManagedDaemonSqlAnalysisPort: vi.fn(() => mocks.sqlAnalysis), })); diff --git a/packages/cli/src/memory-flow-interactive.test.ts b/packages/cli/test/memory-flow-interactive.test.ts similarity index 97% rename from packages/cli/src/memory-flow-interactive.test.ts rename to packages/cli/test/memory-flow-interactive.test.ts index d6976a03..befc7f01 100644 --- a/packages/cli/src/memory-flow-interactive.test.ts +++ b/packages/cli/test/memory-flow-interactive.test.ts @@ -1,7 +1,7 @@ import { EventEmitter } from 'node:events'; -import type { MemoryFlowReplayInput } from './context/ingest/memory-flow/types.js'; +import type { MemoryFlowReplayInput } from '../src/context/ingest/memory-flow/types.js'; import { describe, expect, it, vi } from 'vitest'; -import { memoryFlowCommandForKey, renderMemoryFlowInteractively } from './memory-flow-interactive.js'; +import { memoryFlowCommandForKey, renderMemoryFlowInteractively } from '../src/memory-flow-interactive.js'; class FakeStdin extends EventEmitter { isTTY = true; diff --git a/packages/cli/src/memory-flow-tui.test.tsx b/packages/cli/test/memory-flow-tui.test.tsx similarity index 99% rename from packages/cli/src/memory-flow-tui.test.tsx rename to packages/cli/test/memory-flow-tui.test.tsx index 09d50125..1bb38b72 100644 --- a/packages/cli/src/memory-flow-tui.test.tsx +++ b/packages/cli/test/memory-flow-tui.test.tsx @@ -1,5 +1,5 @@ /* @jsxImportSource react */ -import type { MemoryFlowReplayInput } from './context/ingest/memory-flow/types.js'; +import type { MemoryFlowReplayInput } from '../src/context/ingest/memory-flow/types.js'; import { render as renderInkTest } from 'ink-testing-library'; import React, { type ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; @@ -11,7 +11,7 @@ import { startLiveMemoryFlowTui, type KtxMemoryFlowTuiIo, type MemoryFlowInkInstance, -} from './memory-flow-tui.js'; +} from '../src/memory-flow-tui.js'; function replayInput(): MemoryFlowReplayInput { return { diff --git a/packages/cli/src/next-steps.test.ts b/packages/cli/test/next-steps.test.ts similarity index 99% rename from packages/cli/src/next-steps.test.ts rename to packages/cli/test/next-steps.test.ts index 8a3e5e2a..c700de9e 100644 --- a/packages/cli/src/next-steps.test.ts +++ b/packages/cli/test/next-steps.test.ts @@ -4,7 +4,7 @@ import { KTX_NEXT_STEP_COMMANDS, formatNextStepLines, formatSetupNextStepLines, -} from './next-steps.js'; +} from '../src/next-steps.js'; describe('KTX demo next steps', () => { it('uses supported context-build commands before agent usage', () => { diff --git a/packages/cli/src/notion-page-picker.test.ts b/packages/cli/test/notion-page-picker.test.ts similarity index 98% rename from packages/cli/src/notion-page-picker.test.ts rename to packages/cli/test/notion-page-picker.test.ts index 29f5a352..dda695cb 100644 --- a/packages/cli/src/notion-page-picker.test.ts +++ b/packages/cli/test/notion-page-picker.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import type { PickerState } from './tree-picker-state.js'; -import type { TreePickerChrome, TreePickerResult, TreePickerTuiIo } from './tree-picker-tui.js'; +import type { PickerState } from '../src/tree-picker-state.js'; +import type { TreePickerChrome, TreePickerResult, TreePickerTuiIo } from '../src/tree-picker-tui.js'; import { discoverNotionPickerPages, notionPickerPageFromSearchResult, @@ -8,7 +8,7 @@ import { pickNotionRootPages, resolveNotionWorkspaceLabel, type NotionPickerApi, -} from './notion-page-picker.js'; +} from '../src/notion-page-picker.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/print-command-tree.test.ts b/packages/cli/test/print-command-tree.test.ts similarity index 96% rename from packages/cli/src/print-command-tree.test.ts rename to packages/cli/test/print-command-tree.test.ts index edd0b69a..688818ab 100644 --- a/packages/cli/src/print-command-tree.test.ts +++ b/packages/cli/test/print-command-tree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { renderKtxCommandTree } from './print-command-tree.js'; +import { renderKtxCommandTree } from '../src/print-command-tree.js'; describe('renderKtxCommandTree', () => { it('renders an indented tree rooted at "ktx" with known top-level commands', () => { diff --git a/packages/cli/src/project-dir.test.ts b/packages/cli/test/project-dir.test.ts similarity index 98% rename from packages/cli/src/project-dir.test.ts rename to packages/cli/test/project-dir.test.ts index 25a9b585..14c3ceb7 100644 --- a/packages/cli/src/project-dir.test.ts +++ b/packages/cli/test/project-dir.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxCli, type KtxCliDeps } from './index.js'; +import { runKtxCli, type KtxCliDeps } from '../src/index.js'; async function makeFixtureProject(prefix: string): Promise { const dir = await mkdtemp(join(tmpdir(), prefix)); diff --git a/packages/cli/src/project-resolver.test.ts b/packages/cli/test/project-resolver.test.ts similarity index 98% rename from packages/cli/src/project-resolver.test.ts rename to packages/cli/test/project-resolver.test.ts index 39dab27b..9680fd78 100644 --- a/packages/cli/src/project-resolver.test.ts +++ b/packages/cli/test/project-resolver.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js'; +import { findNearestKtxProjectDir, resolveKtxProjectDir } from '../src/project-resolver.js'; describe('resolveKtxProjectDir', () => { let tempDir: string; diff --git a/packages/cli/src/prompt-navigation.test.ts b/packages/cli/test/prompt-navigation.test.ts similarity index 97% rename from packages/cli/src/prompt-navigation.test.ts rename to packages/cli/test/prompt-navigation.test.ts index 9338b56e..e0b4cf8b 100644 --- a/packages/cli/src/prompt-navigation.test.ts +++ b/packages/cli/test/prompt-navigation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { withMenuOptionSpacing, withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js'; +import { withMenuOptionSpacing, withMultiselectNavigation, withTextInputNavigation } from '../src/prompt-navigation.js'; describe('prompt navigation helpers', () => { it('leaves compact single-line menu prompts unchanged', () => { diff --git a/packages/cli/src/proxy-env.test.ts b/packages/cli/test/proxy-env.test.ts similarity index 92% rename from packages/cli/src/proxy-env.test.ts rename to packages/cli/test/proxy-env.test.ts index 1da7bc91..38279a04 100644 --- a/packages/cli/src/proxy-env.test.ts +++ b/packages/cli/test/proxy-env.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { sanitizeChildProxyEnv } from './proxy-env.js'; +import { sanitizeChildProxyEnv } from '../src/proxy-env.js'; describe('sanitizeChildProxyEnv', () => { it('drops IPv6 CIDR no-proxy entries and normalizes both env keys', () => { diff --git a/packages/cli/src/public-ingest-copy.test.ts b/packages/cli/test/public-ingest-copy.test.ts similarity index 98% rename from packages/cli/src/public-ingest-copy.test.ts rename to packages/cli/test/public-ingest-copy.test.ts index d13696df..539d76ce 100644 --- a/packages/cli/src/public-ingest-copy.test.ts +++ b/packages/cli/test/public-ingest-copy.test.ts @@ -3,7 +3,7 @@ import { publicDatabaseIngestMessage, publicIngestOutputLine, publicQueryHistoryMessage, -} from './public-ingest-copy.js'; +} from '../src/public-ingest-copy.js'; describe('public ingest copy sanitizers', () => { it('maps database scan progress into schema-context wording', () => { diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/test/public-ingest.test.ts similarity index 99% rename from packages/cli/src/public-ingest.test.ts rename to packages/cli/test/public-ingest.test.ts index d6ced94d..b926793c 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/test/public-ingest.test.ts @@ -1,16 +1,16 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; -import { initKtxProject } from './context/project/project.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; +import { initKtxProject } from '../src/context/project/project.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { buildPublicIngestPlan, type KtxPublicIngestDeps, type KtxPublicIngestProject, runKtxPublicIngest, -} from './public-ingest.js'; -import type { ManagedPythonCommandRuntime } from './managed-python-command.js'; +} from '../src/public-ingest.js'; +import type { ManagedPythonCommandRuntime } from '../src/managed-python-command.js'; function makeIo(options: { isTTY?: boolean; interactive?: boolean } = {}) { let stdout = ''; diff --git a/packages/cli/src/runtime-requirements.test.ts b/packages/cli/test/runtime-requirements.test.ts similarity index 97% rename from packages/cli/src/runtime-requirements.test.ts rename to packages/cli/test/runtime-requirements.test.ts index 35e94eae..0a951056 100644 --- a/packages/cli/src/runtime-requirements.test.ts +++ b/packages/cli/test/runtime-requirements.test.ts @@ -1,9 +1,9 @@ -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; import { describe, expect, it } from 'vitest'; import { resolveProjectRuntimeRequirements, resolvePublicIngestRuntimeRequirements, -} from './runtime-requirements.js'; +} from '../src/runtime-requirements.js'; describe('runtime requirement detection', () => { it('does not require runtime for agent/MCP setup alone', () => { diff --git a/packages/cli/src/runtime.test.ts b/packages/cli/test/runtime.test.ts similarity index 99% rename from packages/cli/src/runtime.test.ts rename to packages/cli/test/runtime.test.ts index 266359e4..4525bf83 100644 --- a/packages/cli/src/runtime.test.ts +++ b/packages/cli/test/runtime.test.ts @@ -3,13 +3,13 @@ import type { ManagedPythonDaemonStopAllResult, ManagedPythonDaemonStartResult, ManagedPythonDaemonStopResult, -} from './managed-python-daemon.js'; +} from '../src/managed-python-daemon.js'; import type { ManagedPythonRuntimeDoctorCheck, ManagedPythonRuntimeInstallResult, ManagedPythonRuntimeStatus, -} from './managed-python-runtime.js'; -import { runKtxRuntime, type KtxRuntimeDeps } from './runtime.js'; +} from '../src/managed-python-runtime.js'; +import { runKtxRuntime, type KtxRuntimeDeps } from '../src/runtime.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/scan.test.ts b/packages/cli/test/scan.test.ts similarity index 98% rename from packages/cli/src/scan.test.ts rename to packages/cli/test/scan.test.ts index 6db8243a..837acb10 100644 --- a/packages/cli/src/scan.test.ts +++ b/packages/cli/test/scan.test.ts @@ -1,12 +1,12 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { SourceAdapter } from './context/ingest/types.js'; -import { initKtxProject } from './context/project/project.js'; -import type { KtxScanReport } from './context/scan/types.js'; -import type { LocalScanRunResult, RunLocalScanOptions } from './context/scan/local-scan.js'; +import type { SourceAdapter } from '../src/context/ingest/types.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import type { KtxScanReport } from '../src/context/scan/types.js'; +import type { LocalScanRunResult, RunLocalScanOptions } from '../src/context/scan/local-scan.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createCliScanProgress, runKtxScan, type KtxScanDeps } from './scan.js'; +import { createCliScanProgress, runKtxScan, type KtxScanDeps } from '../src/scan.js'; const sqlServerExtractSchema = vi.hoisted(() => vi.fn(async (connectionId: string) => ({ @@ -138,35 +138,35 @@ const KtxPostgresScanConnector = vi.hoisted( }, ); -vi.mock('./connectors/sqlserver/connector.js', () => ({ +vi.mock('../src/connectors/sqlserver/connector.js', () => ({ isKtxSqlServerConnectionConfig, KtxSqlServerScanConnector, })); -vi.mock('./connectors/sqlserver/live-database-introspection.js', () => ({ +vi.mock('../src/connectors/sqlserver/live-database-introspection.js', () => ({ createSqlServerLiveDatabaseIntrospection, })); -vi.mock('./connectors/bigquery/connector.js', () => ({ +vi.mock('../src/connectors/bigquery/connector.js', () => ({ isKtxBigQueryConnectionConfig, KtxBigQueryScanConnector, })); -vi.mock('./connectors/bigquery/live-database-introspection.js', () => ({ +vi.mock('../src/connectors/bigquery/live-database-introspection.js', () => ({ createBigQueryLiveDatabaseIntrospection, })); -vi.mock('./connectors/snowflake/connector.js', () => ({ +vi.mock('../src/connectors/snowflake/connector.js', () => ({ isKtxSnowflakeConnectionConfig, KtxSnowflakeScanConnector, })); -vi.mock('./connectors/snowflake/live-database-introspection.js', () => ({ +vi.mock('../src/connectors/snowflake/live-database-introspection.js', () => ({ createSnowflakeLiveDatabaseIntrospection, })); -vi.mock('./connectors/postgres/connector.js', () => ({ +vi.mock('../src/connectors/postgres/connector.js', () => ({ isKtxPostgresConnectionConfig, KtxPostgresScanConnector, })); -vi.mock('./connectors/postgres/live-database-introspection.js', () => ({ +vi.mock('../src/connectors/postgres/live-database-introspection.js', () => ({ createPostgresLiveDatabaseIntrospection, })); diff --git a/packages/cli/src/setup-agents.test.ts b/packages/cli/test/setup-agents.test.ts similarity index 99% rename from packages/cli/src/setup-agents.test.ts rename to packages/cli/test/setup-agents.test.ts index bd521787..c6c2d7c4 100644 --- a/packages/cli/src/setup-agents.test.ts +++ b/packages/cli/test/setup-agents.test.ts @@ -1,7 +1,7 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { readKtxSetupState } from './context/project/setup-config.js'; +import { readKtxSetupState } from '../src/context/project/setup-config.js'; import { strFromU8, unzipSync } from 'fflate'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -11,7 +11,7 @@ import { readKtxAgentInstallManifest, removeKtxAgentInstall, runKtxSetupAgentsStep, -} from './setup-agents.js'; +} from '../src/setup-agents.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/setup-context.test.ts b/packages/cli/test/setup-context.test.ts similarity index 99% rename from packages/cli/src/setup-context.test.ts rename to packages/cli/test/setup-context.test.ts index 7bcad93e..9757cc62 100644 --- a/packages/cli/src/setup-context.test.ts +++ b/packages/cli/test/setup-context.test.ts @@ -1,8 +1,8 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState, writeKtxSetupState } from './context/project/setup-config.js'; +import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState, writeKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -11,7 +11,7 @@ import { runKtxSetupContextStep, type KtxSetupContextDeps, writeKtxSetupContextState, -} from './setup-context.js'; +} from '../src/setup-context.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/setup-databases.test.ts b/packages/cli/test/setup-databases.test.ts similarity index 94% rename from packages/cli/src/setup-databases.test.ts rename to packages/cli/test/setup-databases.test.ts index 57f507d5..15d27e3c 100644 --- a/packages/cli/src/setup-databases.test.ts +++ b/packages/cli/test/setup-databases.test.ts @@ -1,21 +1,22 @@ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; -import { initKtxProject, loadKtxProject } from './context/project/project.js'; -import { parseKtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState, writeKtxSetupState } from './context/project/setup-config.js'; +import { initKtxProject, loadKtxProject } from '../src/context/project/project.js'; +import { parseKtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState, writeKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { type KtxSetupDatabaseDriver, type KtxSetupDatabasesDeps, type KtxSetupDatabasesPromptAdapter, runKtxSetupDatabasesStep, -} from './setup-databases.js'; -import type { KtxCliIo } from './cli-runtime.js'; +} from '../src/setup-databases.js'; +import type { KtxCliIo } from '../src/cli-runtime.js'; import type { DatabaseScopePickResult, PickDatabaseScopeArgs, -} from './database-tree-picker.js'; +} from '../src/database-tree-picker.js'; +import type { KtxSetupPromptOption } from '../src/setup-prompts.js'; function makeIo() { let stdout = ''; @@ -164,13 +165,13 @@ describe('setup databases step', () => { 'Which databases should KTX connect to?\n' + 'Use Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.', options: [ - { value: 'sqlite', label: 'SQLite' }, { value: 'postgres', label: 'PostgreSQL' }, + { value: 'bigquery', label: 'BigQuery' }, + { value: 'snowflake', label: 'Snowflake' }, { value: 'mysql', label: 'MySQL' }, { value: 'clickhouse', label: 'ClickHouse' }, { value: 'sqlserver', label: 'SQL Server' }, - { value: 'bigquery', label: 'BigQuery' }, - { value: 'snowflake', label: 'Snowflake' }, + { value: 'sqlite', label: 'SQLite' }, ], required: true, }); @@ -381,12 +382,16 @@ describe('setup databases step', () => { it('emits debug telemetry when setup writes a database connection', async () => { vi.stubEnv('KTX_TELEMETRY_DEBUG', '1'); + vi.stubEnv('KTX_TELEMETRY_DISABLED', ''); + vi.stubEnv('DO_NOT_TRACK', ''); vi.stubEnv('CI', ''); const io = makeIo(); const prompts = makePromptAdapter({ selectValues: ['url'], textValues: ['', 'env:DATABASE_URL'], }); + const listSchemas = vi.fn(async () => []); + const listTables = vi.fn(async () => []); const result = await runKtxSetupDatabasesStep( { @@ -397,7 +402,13 @@ describe('setup databases step', () => { skipDatabases: false, }, io.io, - { prompts, testConnection: vi.fn(async () => 0), scanConnection: vi.fn(async () => 0) }, + { + prompts, + testConnection: vi.fn(async () => 0), + scanConnection: vi.fn(async () => 0), + listSchemas, + listTables, + }, ); expect(result.status).toBe('ready'); @@ -1029,7 +1040,7 @@ describe('setup databases step', () => { const testConnection = vi.fn(async () => 0); const scanConnection = vi.fn(async () => 0); const listSchemas = vi.fn(async () => ['analytics', 'public']); - const listTables = vi.fn(async () => [{ schema: 'analytics', name: 'customers', kind: 'table' as const }]); + const listTables = vi.fn(async () => [{ catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const }]); const pickers = makePickerStubs({ scopes: [{ schemas: ['analytics'], tables: ['analytics.customers'] }], }); @@ -1100,9 +1111,9 @@ describe('setup databases step', () => { }); const listSchemas = vi.fn(async () => ['orbit_analytics', 'orbit_raw', 'public']); const listTables = vi.fn(async () => [ - { schema: 'public', name: 'customers', kind: 'table' as const }, - { schema: 'public', name: 'orders', kind: 'table' as const }, - { schema: 'public', name: 'products', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'products', kind: 'table' as const }, ]); const pickers = makePickerStubs({ scopes: [{ schemas: ['public'], tables: ['public.customers', 'public.orders'] }], @@ -1174,8 +1185,8 @@ describe('setup databases step', () => { const scanConnection = vi.fn(async () => 0); const listSchemas = vi.fn(async () => ['analytics', 'public']); const listTables = vi.fn(async () => [ - { schema: 'analytics', name: 'customers', kind: 'table' as const }, - { schema: 'public', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'orders', kind: 'table' as const }, ]); const pickers = makePickerStubs({ scopes: ['back'] }); @@ -1240,8 +1251,8 @@ describe('setup databases step', () => { const scanConnection = vi.fn(async () => 0); const listSchemas = vi.fn(async () => ['public']); const listTables = vi.fn(async () => [ - { schema: 'public', name: 'customers', kind: 'table' as const }, - { schema: 'public', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'orders', kind: 'table' as const }, ]); const pickers = makePickerStubs({ scopes: [{ schemas: ['public'], tables: 'back' }] }); @@ -1301,8 +1312,8 @@ describe('setup databases step', () => { return 'back'; }); const listTables = vi.fn(async () => [ - { schema: 'public', name: 'customers', kind: 'table' as const }, - { schema: 'public', name: 'orders', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'customers', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'orders', kind: 'table' as const }, ]); const pickers = makePickerStubs({ scopes: ['enable-all'] }); @@ -1600,7 +1611,7 @@ describe('setup databases step', () => { }); const listSchemas = vi.fn(async () => ['analytics', 'mart']); const listTables = vi.fn(async (_projectDir: string, _connectionId: string, schemas?: string[]) => - (schemas ?? []).map((schema) => ({ schema, name: 'orders', kind: 'table' as const })), + (schemas ?? []).map((schema) => ({ catalog: null, schema, name: 'orders', kind: 'table' as const })), ); const pickDatabaseScope = vi.fn(async (args: PickDatabaseScopeArgs) => { const scopedArgs = args as PickDatabaseScopeArgs & { @@ -1657,7 +1668,7 @@ describe('setup databases step', () => { textValues: ['bigquery-warehouse', '/tmp/service-account.json', 'US'], }); const listSchemas = vi.fn(async () => ['analytics']); - const listTables = vi.fn(async () => [{ schema: 'analytics', name: 'orders', kind: 'table' as const }]); + const listTables = vi.fn(async () => [{ catalog: 'project-1', schema: 'analytics', name: 'orders', kind: 'table' as const }]); const pickDatabaseScope = vi.fn(async () => ({ kind: 'selected' as const, activeSchemas: ['analytics'], @@ -1690,9 +1701,9 @@ describe('setup databases step', () => { }); const listSchemas = vi.fn(async () => ['orbit_analytics', 'orbit_raw', 'public']); const listTables = vi.fn(async () => [ - { schema: 'orbit_analytics', name: 'events', kind: 'table' as const }, - { schema: 'orbit_raw', name: 'inputs', kind: 'table' as const }, - { schema: 'public', name: 'misc', kind: 'table' as const }, + { catalog: null, schema: 'orbit_analytics', name: 'events', kind: 'table' as const }, + { catalog: null, schema: 'orbit_raw', name: 'inputs', kind: 'table' as const }, + { catalog: null, schema: 'public', name: 'misc', kind: 'table' as const }, ]); const pickers = makePickerStubs({ scopes: [ @@ -1751,7 +1762,7 @@ describe('setup databases step', () => { throw new Error('permission denied to list schemas'); }); const listTables = vi.fn(async (_projectDir: string, _connectionId: string, schemas?: string[]) => - (schemas ?? []).map((schema) => ({ schema, name: 'events', kind: 'table' as const })), + (schemas ?? []).map((schema) => ({ catalog: null, schema, name: 'events', kind: 'table' as const })), ); const pickers = makePickerStubs({ scopes: [ @@ -1798,18 +1809,18 @@ describe('setup databases step', () => { it('passes schemas and a lazy table callback to the scope picker instead of eager table discovery', async () => { const listSchemas = vi.fn(async () => ['analytics', 'raw']); const listTables = vi.fn(async (_projectDir: string, _connectionId: string, schemas?: string[]) => - (schemas ?? []).map((schema) => ({ schema, name: 'orders', kind: 'table' as const })), + (schemas ?? []).map((schema) => ({ catalog: null, schema, name: 'orders', kind: 'table' as const })), ); const pickDatabaseScope = vi.fn(async (args: PickDatabaseScopeArgs) => { const lazyArgs = args as PickDatabaseScopeArgs & { schemas: string[]; - listTablesForSchemas: (schemas: string[]) => Promise>; + listTablesForSchemas: (schemas: string[]) => Promise>; }; expect(lazyArgs.schemas).toEqual(['analytics', 'raw']); expect(args).not.toHaveProperty('discovered'); expect(listTables).not.toHaveBeenCalled(); const tables = await lazyArgs.listTablesForSchemas(['analytics']); - expect(tables).toEqual([{ schema: 'analytics', name: 'orders', kind: 'table' }]); + expect(tables).toEqual([{ catalog: null, schema: 'analytics', name: 'orders', kind: 'table' }]); return { kind: 'selected' as const, activeSchemas: ['analytics'], enabledTables: ['analytics.orders'] }; }); @@ -2547,6 +2558,81 @@ describe('setup databases step', () => { expect(io.stdout()).toContain('Setup written; query history will be skipped until fixed.'); }); + it('lets interactive BigQuery setup disable unavailable query history and retry after scan failure', async () => { + const io = makeIo(); + const failurePromptOptions: KtxSetupPromptOption[][] = []; + let failurePromptCount = 0; + const prompts = makePromptAdapter({ + textValues: ['/tmp/service-account.json', 'US'], + }); + vi.mocked(prompts.select).mockImplementation(async ({ message, options }) => { + if (message.startsWith('Enable query-history ingest')) return 'yes'; + if (message.includes('How much database context should KTX build?')) return 'fast'; + if (message.startsWith('Database setup failed for analytics')) { + failurePromptCount += 1; + failurePromptOptions.push(options); + if (failurePromptCount === 1) return 'disable-query-history'; + throw new Error('setup did not disable query history before retrying'); + } + throw new Error(`unexpected select prompt: ${message}`); + }); + const runner = { + ...fakeHistoricSqlRunner('bigquery', 'INFORMATION_SCHEMA.JOBS_BY_PROJECT'), + fixAdvice: () => ({ + failHeadline: 'BigQuery principal cannot read INFORMATION_SCHEMA.JOBS_BY_PROJECT', + remediation: + 'Grant roles/bigquery.resourceViewer on the BigQuery project, or grant a custom role containing bigquery.jobs.listAll.', + }), + }; + const historicSqlReadinessProbe = vi.fn(async () => ({ + ok: false as const, + dialect: 'bigquery' as const, + runner, + error: new Error('access denied'), + })); + let scanAttempts = 0; + const scanConnection = vi.fn(async () => { + scanAttempts += 1; + return scanAttempts === 1 ? 1 : 0; + }); + + const result = await runKtxSetupDatabasesStep( + { + projectDir: tempDir, + inputMode: 'auto', + databaseDrivers: ['bigquery'], + databaseConnectionId: 'analytics', + databaseSchemas: [], + skipDatabases: false, + }, + io.io, + { + prompts, + testConnection: vi.fn(async () => 0), + scanConnection, + historicSqlReadinessProbe, + listSchemas: vi.fn(async () => ['analytics']), + listTables: vi.fn(async () => [{ catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const }]), + }, + ); + + expect(result.status).toBe('ready'); + expect(scanConnection).toHaveBeenCalledTimes(2); + expect(historicSqlReadinessProbe).toHaveBeenCalledTimes(1); + expect(failurePromptOptions[0]).toContainEqual({ + value: 'disable-query-history', + label: 'Disable query history and retry', + }); + const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')); + expect(config.connections.analytics).toMatchObject({ + context: { + queryHistory: { + enabled: false, + }, + }, + }); + }); + it('enables query history on an existing Postgres connection', async () => { await writeFile( join(tempDir, 'ktx.yaml'), diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/test/setup-demo-tour.test.ts similarity index 98% rename from packages/cli/src/setup-demo-tour.test.ts rename to packages/cli/test/setup-demo-tour.test.ts index 1d57b010..3916076c 100644 --- a/packages/cli/src/setup-demo-tour.test.ts +++ b/packages/cli/test/setup-demo-tour.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import type { KtxSetupAgentsResult } from './setup-agents.js'; +import type { KtxSetupAgentsResult } from '../src/setup-agents.js'; import { buildDemoReplayTimeline, DEMO_REPLAY_TARGETS, @@ -8,7 +8,7 @@ import { renderDemoCardContent, renderDemoCompletionSummary, runDemoTour, -} from './setup-demo-tour.js'; +} from '../src/setup-demo-tour.js'; /** Strip ANSI escape sequences for plain-text assertions. */ function stripAnsi(text: string): string { diff --git a/packages/cli/src/setup-embeddings.test.ts b/packages/cli/test/setup-embeddings.test.ts similarity index 92% rename from packages/cli/src/setup-embeddings.test.ts rename to packages/cli/test/setup-embeddings.test.ts index bf9e2b2d..9af9f913 100644 --- a/packages/cli/src/setup-embeddings.test.ts +++ b/packages/cli/test/setup-embeddings.test.ts @@ -1,11 +1,12 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; -import { parseKtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState, writeKtxSetupState } from './context/project/setup-config.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { parseKtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState, writeKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { type KtxSetupEmbeddingsPromptAdapter, runKtxSetupEmbeddingsStep } from './setup-embeddings.js'; +import { ManagedPythonDaemonStartError } from '../src/managed-python-daemon.js'; +import { type KtxSetupEmbeddingsPromptAdapter, runKtxSetupEmbeddingsStep } from '../src/setup-embeddings.js'; const EMBEDDING_OPTION_PROMPT_MESSAGE = [ 'Which embedding option should KTX use?', @@ -366,6 +367,40 @@ describe('setup embeddings step', () => { expect(io.stderr()).not.toContain('daemon traceback line 5'); }); + it('prints the daemon stderr tail when the daemon fails to start', async () => { + const io = makeIo(); + const stderrLog = join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log'); + await mkdir(join(tempDir, '.ktx', 'runtime'), { recursive: true }); + await writeFile( + stderrLog, + Array.from({ length: 45 }, (_value, index) => `daemon startup traceback ${index + 1}`).join('\n'), + ); + + const result = await runKtxSetupEmbeddingsStep( + { + projectDir: tempDir, + inputMode: 'disabled', + cliVersion: '0.2.0', + runtimeInstallPolicy: 'auto', + skipEmbeddings: false, + }, + io.io, + { + env: {}, + ensureLocalEmbeddings: vi.fn(async () => { + throw new ManagedPythonDaemonStartError('fetch failed: connect ECONNREFUSED 127.0.0.1:61234', stderrLog); + }), + }, + ); + + expect(result.status).toBe('failed'); + expect(io.stderr()).toContain('Local embedding health check failed: fetch failed: connect ECONNREFUSED'); + expect(io.stderr()).toContain('Recent KTX daemon stderr:'); + expect(io.stderr()).toContain('daemon startup traceback 6'); + expect(io.stderr()).toContain('daemon startup traceback 45'); + expect(io.stderr()).not.toContain('daemon startup traceback 5'); + }); + it('does not print daemon stderr diagnostics when the log is unavailable or empty', async () => { const io = makeIo(); diff --git a/packages/cli/src/setup-interrupt.test.ts b/packages/cli/test/setup-interrupt.test.ts similarity index 99% rename from packages/cli/src/setup-interrupt.test.ts rename to packages/cli/test/setup-interrupt.test.ts index 62917db6..a1ff6b10 100644 --- a/packages/cli/src/setup-interrupt.test.ts +++ b/packages/cli/test/setup-interrupt.test.ts @@ -4,7 +4,7 @@ import { KtxSetupExitError, withSetupInterruptConfirmation, type SetupInterruptTracker, -} from './setup-interrupt.js'; +} from '../src/setup-interrupt.js'; const CANCEL = Symbol('cancel'); diff --git a/packages/cli/src/setup-models.test.ts b/packages/cli/test/setup-models.test.ts similarity index 99% rename from packages/cli/src/setup-models.test.ts rename to packages/cli/test/setup-models.test.ts index 444c3b4d..f054beff 100644 --- a/packages/cli/src/setup-models.test.ts +++ b/packages/cli/test/setup-models.test.ts @@ -1,16 +1,16 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; -import { parseKtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState, writeKtxSetupState } from './context/project/setup-config.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { parseKtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState, writeKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BUNDLED_ANTHROPIC_MODELS, fetchAnthropicModels, type KtxSetupModelPromptAdapter, runKtxSetupAnthropicModelStep, -} from './setup-models.js'; +} from '../src/setup-models.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/setup-project.test.ts b/packages/cli/test/setup-project.test.ts similarity index 98% rename from packages/cli/src/setup-project.test.ts rename to packages/cli/test/setup-project.test.ts index 89663bbf..c77a2080 100644 --- a/packages/cli/src/setup-project.test.ts +++ b/packages/cli/test/setup-project.test.ts @@ -1,10 +1,10 @@ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { readKtxSetupState } from './context/project/setup-config.js'; +import { readKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { gray } from './io/symbols.js'; -import { type KtxSetupProjectPromptAdapter, runKtxSetupProjectStep } from './setup-project.js'; +import { gray } from '../src/io/symbols.js'; +import { type KtxSetupProjectPromptAdapter, runKtxSetupProjectStep } from '../src/setup-project.js'; function makeIo(options: { stdoutIsTty?: boolean } = {}) { let stdout = ''; diff --git a/packages/cli/src/setup-prompts.test.ts b/packages/cli/test/setup-prompts.test.ts similarity index 97% rename from packages/cli/src/setup-prompts.test.ts rename to packages/cli/test/setup-prompts.test.ts index 95f4b68b..46628b1c 100644 --- a/packages/cli/src/setup-prompts.test.ts +++ b/packages/cli/test/setup-prompts.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createKtxSetupPromptAdapter, type KtxSetupPromptOption, -} from './setup-prompts.js'; +} from '../src/setup-prompts.js'; const mocks = vi.hoisted(() => { const cancelSymbol = Symbol('cancel'); @@ -39,7 +39,7 @@ vi.mock('@clack/prompts', () => ({ text: mocks.text, })); -vi.mock('./setup-interrupt.js', () => ({ +vi.mock('../src/setup-interrupt.js', () => ({ withSetupInterruptConfirmation: mocks.withSetupInterruptConfirmation, })); @@ -213,7 +213,7 @@ describe('setup prompt adapter', () => { }); it('keeps setup intro and note plain for non-stream output', async () => { - const { createKtxSetupUiAdapter } = await import('./setup-prompts.js'); + const { createKtxSetupUiAdapter } = await import('../src/setup-prompts.js'); const chunks: string[] = []; const io = { stdout: { @@ -235,7 +235,7 @@ describe('setup prompt adapter', () => { }); it('uses Clack intro and note for writable TTY output', async () => { - const { createKtxSetupUiAdapter } = await import('./setup-prompts.js'); + const { createKtxSetupUiAdapter } = await import('../src/setup-prompts.js'); const output = { columns: 80, isTTY: true, diff --git a/packages/cli/src/setup-ready-menu.test.ts b/packages/cli/test/setup-ready-menu.test.ts similarity index 96% rename from packages/cli/src/setup-ready-menu.test.ts rename to packages/cli/test/setup-ready-menu.test.ts index 028b94ee..82c92a1c 100644 --- a/packages/cli/src/setup-ready-menu.test.ts +++ b/packages/cli/test/setup-ready-menu.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { isKtxPreAgentSetupReady, isKtxSetupReady, runKtxSetupReadyChangeMenu } from './setup-ready-menu.js'; -import type { KtxSetupStatus } from './setup.js'; +import { isKtxPreAgentSetupReady, isKtxSetupReady, runKtxSetupReadyChangeMenu } from '../src/setup-ready-menu.js'; +import type { KtxSetupStatus } from '../src/setup.js'; const readyStatus: KtxSetupStatus = { project: { path: '/tmp/revenue', ready: true }, diff --git a/packages/cli/src/setup-runtime.test.ts b/packages/cli/test/setup-runtime.test.ts similarity index 95% rename from packages/cli/src/setup-runtime.test.ts rename to packages/cli/test/setup-runtime.test.ts index 2fb1f1f2..ab5777e4 100644 --- a/packages/cli/src/setup-runtime.test.ts +++ b/packages/cli/test/setup-runtime.test.ts @@ -1,10 +1,10 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState } from './context/project/setup-config.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxSetupRuntimeStep } from './setup-runtime.js'; +import { runKtxSetupRuntimeStep } from '../src/setup-runtime.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/setup-secrets.test.ts b/packages/cli/test/setup-secrets.test.ts similarity index 97% rename from packages/cli/src/setup-secrets.test.ts rename to packages/cli/test/setup-secrets.test.ts index 16589db0..67a288f3 100644 --- a/packages/cli/src/setup-secrets.test.ts +++ b/packages/cli/test/setup-secrets.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { envCredentialReference, writeProjectLocalSecretReference } from './setup-secrets.js'; +import { envCredentialReference, writeProjectLocalSecretReference } from '../src/setup-secrets.js'; describe('setup secrets', () => { let tempDir: string; diff --git a/packages/cli/src/setup-sources-notion.test.ts b/packages/cli/test/setup-sources-notion.test.ts similarity index 90% rename from packages/cli/src/setup-sources-notion.test.ts rename to packages/cli/test/setup-sources-notion.test.ts index 1306b07b..ce9210c1 100644 --- a/packages/cli/src/setup-sources-notion.test.ts +++ b/packages/cli/test/setup-sources-notion.test.ts @@ -1,13 +1,13 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; -import { type KtxProjectConnectionConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './context/project/config.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { type KtxProjectConnectionConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from '../src/context/project/config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { runKtxSetupSourcesStep, type KtxSetupSourcesPromptAdapter, -} from './setup-sources.js'; +} from '../src/setup-sources.js'; const notionMocks = vi.hoisted(() => ({ tokens: [] as string[], @@ -15,8 +15,8 @@ const notionMocks = vi.hoisted(() => ({ retrievePage: vi.fn(async () => ({ id: 'page-1' })), })); -vi.mock('./context/ingest/adapters/notion/notion-client.js', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('../src/context/ingest/adapters/notion/notion-client.js', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, NotionClient: vi.fn().mockImplementation(function NotionClient(token: string) { diff --git a/packages/cli/src/setup-sources.test.ts b/packages/cli/test/setup-sources.test.ts similarity index 99% rename from packages/cli/src/setup-sources.test.ts rename to packages/cli/test/setup-sources.test.ts index c0b2c781..b426ad10 100644 --- a/packages/cli/src/setup-sources.test.ts +++ b/packages/cli/test/setup-sources.test.ts @@ -1,17 +1,17 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; -import { type KtxProjectConnectionConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './context/project/config.js'; -import { readKtxSetupState } from './context/project/setup-config.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { type KtxProjectConnectionConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from '../src/context/project/config.js'; +import { readKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { KtxCliIo } from './cli-runtime.js'; +import type { KtxCliIo } from '../src/cli-runtime.js'; import { runKtxSetupSourcesStep, type KtxSetupSourcesDeps, type KtxSetupSourcesPromptAdapter, type KtxSetupSourceType, -} from './setup-sources.js'; +} from '../src/setup-sources.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/setup.test.ts b/packages/cli/test/setup.test.ts similarity index 99% rename from packages/cli/src/setup.test.ts rename to packages/cli/test/setup.test.ts index 26dc0324..6c928033 100644 --- a/packages/cli/src/setup.test.ts +++ b/packages/cli/test/setup.test.ts @@ -3,15 +3,15 @@ import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; -import { writeKtxSetupState } from './context/project/setup-config.js'; +import { writeKtxSetupState } from '../src/context/project/setup-config.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js'; -import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js'; -import { runDemoTour } from './setup-demo-tour.js'; -import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js'; +import { contextBuildCommands, writeKtxSetupContextState } from '../src/setup-context.js'; +import { runDemoTour } from '../src/setup-demo-tour.js'; +import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from '../src/setup.js'; -vi.mock('./setup-demo-tour.js', () => ({ +vi.mock('../src/setup-demo-tour.js', () => ({ runDemoTour: vi.fn(async () => 0), })); diff --git a/packages/cli/src/sl.test.ts b/packages/cli/test/sl.test.ts similarity index 99% rename from packages/cli/src/sl.test.ts rename to packages/cli/test/sl.test.ts index 7fa855d0..7b4b7795 100644 --- a/packages/cli/src/sl.test.ts +++ b/packages/cli/test/sl.test.ts @@ -3,9 +3,9 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { stripVTControlCharacters } from 'node:util'; import Database from 'better-sqlite3'; -import { initKtxProject } from './context/project/project.js'; +import { initKtxProject } from '../src/context/project/project.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxSl } from './sl.js'; +import { runKtxSl } from '../src/sl.js'; const ORDERS_YAML = [ 'name: orders', diff --git a/packages/cli/src/source-mapping.test.ts b/packages/cli/test/source-mapping.test.ts similarity index 94% rename from packages/cli/src/source-mapping.test.ts rename to packages/cli/test/source-mapping.test.ts index 83f9496b..5099d4c0 100644 --- a/packages/cli/src/source-mapping.test.ts +++ b/packages/cli/test/source-mapping.test.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import type { KtxCliIo } from './cli-runtime.js'; -import { runKtxSourceMapping } from './source-mapping.js'; +import type { KtxCliIo } from '../src/cli-runtime.js'; +import { runKtxSourceMapping } from '../src/source-mapping.js'; function makeIo() { let stdout = ''; diff --git a/packages/cli/src/sql.test.ts b/packages/cli/test/sql.test.ts similarity index 95% rename from packages/cli/src/sql.test.ts rename to packages/cli/test/sql.test.ts index 51cfe920..b48ebe5b 100644 --- a/packages/cli/src/sql.test.ts +++ b/packages/cli/test/sql.test.ts @@ -1,12 +1,12 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from './context/project/project.js'; -import { parseKtxProjectConfig, serializeKtxProjectConfig } from './context/project/config.js'; -import type { KtxScanConnector } from './context/scan/types.js'; -import type { SqlAnalysisPort } from './context/sql-analysis/ports.js'; +import { initKtxProject } from '../src/context/project/project.js'; +import { parseKtxProjectConfig, serializeKtxProjectConfig } from '../src/context/project/config.js'; +import type { KtxScanConnector } from '../src/context/scan/types.js'; +import type { SqlAnalysisPort } from '../src/context/sql-analysis/ports.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { runKtxSql } from './sql.js'; +import { runKtxSql } from '../src/sql.js'; function makeIo(options: { isTTY?: boolean } = {}) { let stdout = ''; @@ -66,6 +66,8 @@ function makeConnector(overrides: Partial = {}): KtxScanConnec })), cleanup: vi.fn(async () => undefined), ...overrides, + listSchemas: overrides.listSchemas ?? vi.fn(async () => []), + listTables: overrides.listTables ?? vi.fn(async () => []), }; } diff --git a/packages/cli/src/standalone-smoke.test.ts b/packages/cli/test/standalone-smoke.test.ts similarity index 99% rename from packages/cli/src/standalone-smoke.test.ts rename to packages/cli/test/standalone-smoke.test.ts index 7e6ed56e..4007afcb 100644 --- a/packages/cli/src/standalone-smoke.test.ts +++ b/packages/cli/test/standalone-smoke.test.ts @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { promisify } from 'node:util'; -import { parseKtxProjectConfig } from './context/project/config.js'; +import { parseKtxProjectConfig } from '../src/context/project/config.js'; import Database from 'better-sqlite3'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; diff --git a/packages/cli/src/status-project.test.ts b/packages/cli/test/status-project.test.ts similarity index 99% rename from packages/cli/src/status-project.test.ts rename to packages/cli/test/status-project.test.ts index 8f35cfe8..38d5aa6f 100644 --- a/packages/cli/src/status-project.test.ts +++ b/packages/cli/test/status-project.test.ts @@ -3,13 +3,13 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import Database from 'better-sqlite3'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from './context/project/config.js'; -import type { KtxLocalProject } from './context/project/project.js'; +import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '../src/context/project/config.js'; +import type { KtxLocalProject } from '../src/context/project/project.js'; import { buildLocalStatsStatus, buildProjectStatus, renderProjectStatus, -} from './status-project.js'; +} from '../src/status-project.js'; function projectWithConfig(config: KtxProjectConfig): KtxLocalProject { return { diff --git a/packages/cli/src/telemetry/command-hook.test.ts b/packages/cli/test/telemetry/command-hook.test.ts similarity index 95% rename from packages/cli/src/telemetry/command-hook.test.ts rename to packages/cli/test/telemetry/command-hook.test.ts index ffd0485b..92105151 100644 --- a/packages/cli/src/telemetry/command-hook.test.ts +++ b/packages/cli/test/telemetry/command-hook.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { beginCommandSpan, completeCommandSpan, resetCommandSpan } from './command-hook.js'; +import { beginCommandSpan, completeCommandSpan, resetCommandSpan } from '../../src/telemetry/command-hook.js'; describe('telemetry command hook', () => { it('builds a completed command event from a span', () => { diff --git a/packages/cli/src/telemetry/demo-detect.test.ts b/packages/cli/test/telemetry/demo-detect.test.ts similarity index 91% rename from packages/cli/src/telemetry/demo-detect.test.ts rename to packages/cli/test/telemetry/demo-detect.test.ts index b371694e..4640766f 100644 --- a/packages/cli/src/telemetry/demo-detect.test.ts +++ b/packages/cli/test/telemetry/demo-detect.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { isDemoConnection } from './demo-detect.js'; +import { isDemoConnection } from '../../src/telemetry/demo-detect.js'; describe('isDemoConnection', () => { it('detects only the packaged Orbit SQLite demo recipe', () => { diff --git a/packages/cli/src/telemetry/emitter.test.ts b/packages/cli/test/telemetry/emitter.test.ts similarity index 96% rename from packages/cli/src/telemetry/emitter.test.ts rename to packages/cli/test/telemetry/emitter.test.ts index 9c732997..98400860 100644 --- a/packages/cli/src/telemetry/emitter.test.ts +++ b/packages/cli/test/telemetry/emitter.test.ts @@ -4,8 +4,8 @@ import { __resetTelemetryEmitterForTests, shutdownTelemetryEmitter, trackTelemetryEvent, -} from './emitter.js'; -import type { BuiltTelemetryEvent } from './events.js'; +} from '../../src/telemetry/emitter.js'; +import type { BuiltTelemetryEvent } from '../../src/telemetry/events.js'; const captures: unknown[] = []; const shutdown = vi.fn(async () => {}); diff --git a/packages/cli/src/telemetry/events.snapshot.test.ts b/packages/cli/test/telemetry/events.snapshot.test.ts similarity index 99% rename from packages/cli/src/telemetry/events.snapshot.test.ts rename to packages/cli/test/telemetry/events.snapshot.test.ts index 1df95aa0..7a1b76f7 100644 --- a/packages/cli/src/telemetry/events.snapshot.test.ts +++ b/packages/cli/test/telemetry/events.snapshot.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildTelemetryEvent, type TelemetryCommonEnvelope } from './events.js'; +import { buildTelemetryEvent, type TelemetryCommonEnvelope } from '../../src/telemetry/events.js'; const BLACKLIST = [ '/Users/', diff --git a/packages/cli/src/telemetry/events.test.ts b/packages/cli/test/telemetry/events.test.ts similarity index 99% rename from packages/cli/src/telemetry/events.test.ts rename to packages/cli/test/telemetry/events.test.ts index 3726ddde..29108600 100644 --- a/packages/cli/src/telemetry/events.test.ts +++ b/packages/cli/test/telemetry/events.test.ts @@ -5,7 +5,7 @@ import { telemetryEventCatalog, telemetryEventSchemas, type TelemetryCommonEnvelope, -} from './events.js'; +} from '../../src/telemetry/events.js'; const envelope: TelemetryCommonEnvelope = { cliVersion: '0.4.1', diff --git a/packages/cli/src/telemetry/identity.test.ts b/packages/cli/test/telemetry/identity.test.ts similarity index 99% rename from packages/cli/src/telemetry/identity.test.ts rename to packages/cli/test/telemetry/identity.test.ts index 06d76043..31c3bfb5 100644 --- a/packages/cli/src/telemetry/identity.test.ts +++ b/packages/cli/test/telemetry/identity.test.ts @@ -9,7 +9,7 @@ import { readExistingTelemetryProjectId, TELEMETRY_NOTICE, type TelemetryIdentityEnv, -} from './identity.js'; +} from '../../src/telemetry/identity.js'; function makeIo(stdoutIsTTY = true) { let stderr = ''; diff --git a/packages/cli/test/telemetry/index.test.ts b/packages/cli/test/telemetry/index.test.ts new file mode 100644 index 00000000..8d8f932b --- /dev/null +++ b/packages/cli/test/telemetry/index.test.ts @@ -0,0 +1,63 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { KtxCliIo } from '../../src/cli-runtime.js'; +import { emitTelemetryEvent } from '../../src/telemetry/index.js'; + +function makeIo(): { io: KtxCliIo; stderr: () => string } { + let stderr = ''; + return { + io: { + stdout: { + isTTY: true, + write: () => {}, + }, + stderr: { + write: (chunk) => { + stderr += chunk; + }, + }, + }, + stderr: () => stderr, + }; +} + +describe('emitTelemetryEvent', () => { + let homeDir: string; + + beforeEach(async () => { + homeDir = await mkdtemp(join(tmpdir(), 'ktx-telemetry-index-')); + vi.stubEnv('HOME', homeDir); + }); + + afterEach(async () => { + vi.unstubAllEnvs(); + await rm(homeDir, { recursive: true, force: true }); + }); + + it('prints debug telemetry when live telemetry is disabled without creating an identity file', async () => { + vi.stubEnv('KTX_TELEMETRY_DEBUG', '1'); + vi.stubEnv('KTX_TELEMETRY_DISABLED', '1'); + vi.stubEnv('DO_NOT_TRACK', '1'); + const testIo = makeIo(); + const projectDir = join(homeDir, 'private-project'); + + await emitTelemetryEvent({ + name: 'connection_added', + projectDir, + io: testIo.io, + packageInfo: { name: '@kaelio/ktx', version: '0.0.0-test' }, + fields: { + driver: 'sqlite', + isDemoConnection: false, + }, + }); + + expect(testIo.stderr()).toContain('[telemetry]'); + expect(testIo.stderr()).toContain('"event":"connection_added"'); + expect(testIo.stderr()).not.toContain(projectDir); + await expect(readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')).rejects.toThrow(); + }); +}); diff --git a/packages/cli/src/telemetry/project-snapshot.test.ts b/packages/cli/test/telemetry/project-snapshot.test.ts similarity index 96% rename from packages/cli/src/telemetry/project-snapshot.test.ts rename to packages/cli/test/telemetry/project-snapshot.test.ts index a1c06472..4a868426 100644 --- a/packages/cli/src/telemetry/project-snapshot.test.ts +++ b/packages/cli/test/telemetry/project-snapshot.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { buildProjectStackSnapshotFields } from './project-snapshot.js'; +import { buildProjectStackSnapshotFields } from '../../src/telemetry/project-snapshot.js'; describe('buildProjectStackSnapshotFields', () => { let projectDir: string; diff --git a/packages/cli/src/telemetry/schema-writer.test.ts b/packages/cli/test/telemetry/schema-writer.test.ts similarity index 90% rename from packages/cli/src/telemetry/schema-writer.test.ts rename to packages/cli/test/telemetry/schema-writer.test.ts index a6539421..498869f9 100644 --- a/packages/cli/src/telemetry/schema-writer.test.ts +++ b/packages/cli/test/telemetry/schema-writer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { buildTelemetrySchemaArtifact } from './schema-writer.js'; +import { buildTelemetrySchemaArtifact } from '../../src/telemetry/schema-writer.js'; describe('telemetry schema writer', () => { it('exports a schema artifact with the full catalog and strict metadata', () => { diff --git a/packages/cli/src/telemetry/scrubber.test.ts b/packages/cli/test/telemetry/scrubber.test.ts similarity index 94% rename from packages/cli/src/telemetry/scrubber.test.ts rename to packages/cli/test/telemetry/scrubber.test.ts index 87eb74d4..a12946d4 100644 --- a/packages/cli/src/telemetry/scrubber.test.ts +++ b/packages/cli/test/telemetry/scrubber.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { scrubErrorClass } from './scrubber.js'; +import { scrubErrorClass } from '../../src/telemetry/scrubber.js'; class KtxProjectMissingAbortError extends Error {} diff --git a/packages/cli/src/text-ingest.test.ts b/packages/cli/test/text-ingest.test.ts similarity index 97% rename from packages/cli/src/text-ingest.test.ts rename to packages/cli/test/text-ingest.test.ts index b7737f36..5122208e 100644 --- a/packages/cli/src/text-ingest.test.ts +++ b/packages/cli/test/text-ingest.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { MemoryIngestStatus } from './context/memory/memory-runs.js'; -import type { KtxLocalProject } from './context/project/project.js'; -import { runKtxTextIngest, type TextMemoryIngestPort } from './text-ingest.js'; +import type { MemoryIngestStatus } from '../src/context/memory/memory-runs.js'; +import type { KtxLocalProject } from '../src/context/project/project.js'; +import { runKtxTextIngest, type TextMemoryIngestPort } from '../src/text-ingest.js'; function makeIo(options: { isTTY?: boolean } = {}) { let stdout = ''; diff --git a/packages/cli/src/tree-picker-state.test.ts b/packages/cli/test/tree-picker-state.test.ts similarity index 99% rename from packages/cli/src/tree-picker-state.test.ts rename to packages/cli/test/tree-picker-state.test.ts index e8d6afd1..50d47d89 100644 --- a/packages/cli/src/tree-picker-state.test.ts +++ b/packages/cli/test/tree-picker-state.test.ts @@ -14,7 +14,7 @@ import { toggleChecked, visibleNodeIds, type TreePickerNodeInput, -} from './tree-picker-state.js'; +} from '../src/tree-picker-state.js'; const IDS = { engineering: '11111111-1111-1111-1111-111111111111', diff --git a/packages/cli/src/tree-picker-tui.test.tsx b/packages/cli/test/tree-picker-tui.test.tsx similarity index 99% rename from packages/cli/src/tree-picker-tui.test.tsx rename to packages/cli/test/tree-picker-tui.test.tsx index 18877778..f4f642c0 100644 --- a/packages/cli/src/tree-picker-tui.test.tsx +++ b/packages/cli/test/tree-picker-tui.test.tsx @@ -2,7 +2,7 @@ import { render as renderInkTest } from 'ink-testing-library'; import { type ReactNode } from 'react'; import { describe, expect, it, vi } from 'vitest'; -import { buildInitialState, buildPickerTree, type TreePickerNodeInput } from './tree-picker-state.js'; +import { buildInitialState, buildPickerTree, type TreePickerNodeInput } from '../src/tree-picker-state.js'; import { TreePickerApp, renderTreePickerTui, @@ -14,7 +14,7 @@ import { type TreePickerChrome, type TreePickerInkInstance, type TreePickerInkRenderOptions, -} from './tree-picker-tui.js'; +} from '../src/tree-picker-tui.js'; const IDS = { engineering: '11111111-1111-1111-1111-111111111111', diff --git a/packages/cli/src/viz-fallback.test.ts b/packages/cli/test/viz-fallback.test.ts similarity index 99% rename from packages/cli/src/viz-fallback.test.ts rename to packages/cli/test/viz-fallback.test.ts index f42eb440..b7b38158 100644 --- a/packages/cli/src/viz-fallback.test.ts +++ b/packages/cli/test/viz-fallback.test.ts @@ -4,7 +4,7 @@ import { resetVizFallbackWarningsForTest, resolveVizFallback, warnVizFallbackOnce, -} from './viz-fallback.js'; +} from '../src/viz-fallback.js'; function io(options: { stdoutTty?: boolean; stdinTty?: boolean; rawMode?: boolean }) { return { diff --git a/packages/cli/tsconfig.test.json b/packages/cli/tsconfig.test.json new file mode 100644 index 00000000..e4b4d755 --- /dev/null +++ b/packages/cli/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "tsBuildInfoFile": "./dist/.tsbuildinfo.test" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index ca1a6b26..9260a927 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }, test: { root: '.', - include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + include: ['test/**/*.test.ts', 'test/**/*.test.tsx'], testTimeout: 30_000, }, }); diff --git a/scripts/anti-fixture-conditional.test.mjs b/scripts/anti-fixture-conditional.test.mjs index b4a3241c..44728c48 100644 --- a/scripts/anti-fixture-conditional.test.mjs +++ b/scripts/anti-fixture-conditional.test.mjs @@ -19,7 +19,7 @@ const RELATIONSHIP_RUNTIME_SOURCES = Object.freeze([ ]); async function checkedInFixtureIds() { - const fixtureRoot = new URL('packages/cli/src/test/fixtures/relationship-benchmarks/', KTX_ROOT); + const fixtureRoot = new URL('packages/cli/test/fixtures/relationship-benchmarks/', KTX_ROOT); const entries = await readdir(fixtureRoot, { withFileTypes: true }); return entries .filter((entry) => entry.isDirectory()) diff --git a/scripts/build-benchmark-snapshot.test.mjs b/scripts/build-benchmark-snapshot.test.mjs index 6e3f5189..acea6ff9 100644 --- a/scripts/build-benchmark-snapshot.test.mjs +++ b/scripts/build-benchmark-snapshot.test.mjs @@ -257,7 +257,7 @@ describe('buildBenchmarkSnapshot', () => { assert.equal( packageJson.scripts['relationships:benchmarks:test'], - 'KTX_RUN_RELATIONSHIP_BENCHMARKS=1 vitest run src/context/scan/relationship-benchmarks.test.ts', + 'KTX_RUN_RELATIONSHIP_BENCHMARKS=1 vitest run test/context/scan/relationship-benchmarks.test.ts', ); }); }); diff --git a/scripts/check-boundaries.mjs b/scripts/check-boundaries.mjs index 5766ac78..97ffef64 100644 --- a/scripts/check-boundaries.mjs +++ b/scripts/check-boundaries.mjs @@ -68,6 +68,13 @@ const contextProductionLlmBoundaryPatterns = [ }, ]; +const concreteDialectImportPatterns = [ + /from\s+['"][^'"]*connectors\/[^'"]+\/dialect\.js['"]/, + /import\s*\(\s*['"][^'"]*connectors\/[^'"]+\/dialect\.js['"]\s*\)/, + /from\s+['"]\.\/dialect\.js['"]/, + /import\s*\(\s*['"]\.\/dialect\.js['"]\s*\)/, +]; + function normalizePath(filePath) { return filePath.split(path.sep).join('/'); } @@ -99,6 +106,13 @@ function scansForContextProductionLlmBoundaries(relativePath) { return scansForLlmBoundaries(relativePath) && !isTestSource(relativePath); } +function scansForConcreteDialectImportBoundaries(relativePath) { + return ( + relativePath.startsWith('packages/cli/src/context/scan/') || + /^packages\/cli\/src\/connectors\/[^/]+\/connector\.ts$/.test(relativePath) + ); +} + function scansForForbiddenIdentifiers(relativePath) { return (isCodeSource(relativePath) && !isTestSource(relativePath)) || isRuntimeAsset(relativePath); } @@ -151,6 +165,19 @@ export function scanFileContent(relativePath, content) { } } + if (scansForConcreteDialectImportBoundaries(normalizedPath)) { + for (const pattern of concreteDialectImportPatterns) { + if (pattern.test(content)) { + violations.push({ + file: normalizedPath, + kind: 'dialect-boundary', + message: + 'Forbidden concrete connector dialect import; use getDialectForDriver() from context/connections/dialects.ts', + }); + } + } + } + if ( scansForForbiddenIdentifiers(normalizedPath) && !skipsIdentifierScan(normalizedPath) && diff --git a/scripts/check-boundaries.test.mjs b/scripts/check-boundaries.test.mjs index 25cd0f85..f279313d 100644 --- a/scripts/check-boundaries.test.mjs +++ b/scripts/check-boundaries.test.mjs @@ -68,8 +68,8 @@ describe('scanFileContent', () => { it('allows product identifiers in test fixtures', () => { const name = lowerProductName(); - assert.equal(scanFileContent('packages/cli/src/setup.test.ts', `project: ${name}-dev`).length, 0); - assert.equal(scanFileContent('packages/cli/src/context/ingest/importer.test.ts', `email: system@${name}.dev`).length, 0); + assert.equal(scanFileContent('packages/cli/test/setup.test.ts', `project: ${name}-dev`).length, 0); + assert.equal(scanFileContent('packages/cli/test/context/ingest/importer.test.ts', `email: system@${name}.dev`).length, 0); assert.equal(scanFileContent('python/ktx-daemon/tests/test_package.py', `${name}-ktx`).length, 0); }); @@ -112,6 +112,43 @@ describe('scanFileContent', () => { ); }); + it('rejects concrete connector dialect imports from scan workflow and connector classes', () => { + const violations = [ + ...scanFileContent( + 'packages/cli/src/context/scan/relationship-profiling.ts', + "import { KtxPostgresDialect } from '../../connectors/postgres/dialect.js';", + ), + ...scanFileContent( + 'packages/cli/src/connectors/postgres/connector.ts', + "import { KtxPostgresDialect } from './dialect.js';", + ), + ]; + + assert.deepEqual( + violations.map((violation) => violation.kind), + ['dialect-boundary', 'dialect-boundary'], + ); + assert.equal( + violations[0]?.message, + 'Forbidden concrete connector dialect import; use getDialectForDriver() from context/connections/dialects.ts', + ); + + assert.deepEqual( + scanFileContent( + 'packages/cli/src/context/connections/dialects.ts', + "import { KtxPostgresDialect } from '../../connectors/postgres/dialect.js';", + ), + [], + ); + assert.deepEqual( + scanFileContent( + 'packages/cli/test/connectors/postgres/dialect.test.ts', + "import { KtxPostgresDialect } from './dialect.js';", + ), + [], + ); + }); + it('rejects old KTX LLM port declarations in context', () => { const violations = [ ...scanFileContent('packages/cli/src/context/agent/agent-runner.service.ts', 'export interface LlmProviderPort {}'), @@ -150,7 +187,7 @@ describe('scanFileContent', () => { assert.deepEqual( scanFileContent( - 'packages/cli/src/context/ingest/page-triage/page-triage.service.test.ts', + 'packages/cli/test/context/ingest/page-triage/page-triage.service.test.ts', "const model = this.deps.llmProvider.getModelByName('test-model');", ), [], diff --git a/scripts/examples-docs.test.mjs b/scripts/examples-docs.test.mjs index 41b6d346..53413ea5 100644 --- a/scripts/examples-docs.test.mjs +++ b/scripts/examples-docs.test.mjs @@ -309,10 +309,10 @@ describe('standalone example docs', () => { it('runs the example smoke in the cli smoke script', async () => { const packageJson = JSON.parse(await readText('packages/cli/package.json')); - assert.match(packageJson.scripts.smoke, /src\/standalone-smoke\.test\.ts/); - assert.match(packageJson.scripts.smoke, /src\/example-smoke\.test\.ts/); - assert.match(packageJson.scripts.test, /--exclude src\/standalone-smoke\.test\.ts/); - assert.match(packageJson.scripts.test, /--exclude src\/example-smoke\.test\.ts/); + assert.match(packageJson.scripts.smoke, /test\/standalone-smoke\.test\.ts/); + assert.match(packageJson.scripts.smoke, /test\/example-smoke\.test\.ts/); + assert.match(packageJson.scripts.test, /--exclude test\/standalone-smoke\.test\.ts/); + assert.match(packageJson.scripts.test, /--exclude test\/example-smoke\.test\.ts/); }); it('documents daemon HTTP database, source generation, LookML, embedding, and code execution support', async () => { diff --git a/scripts/relationship-benchmark-report.mjs b/scripts/relationship-benchmark-report.mjs index 7af82d57..9901d77c 100644 --- a/scripts/relationship-benchmark-report.mjs +++ b/scripts/relationship-benchmark-report.mjs @@ -14,7 +14,7 @@ import { const scriptDir = dirname(fileURLToPath(import.meta.url)); const ktxRoot = resolve(scriptDir, '..'); -const fixtureRoot = join(ktxRoot, 'packages/cli/src/test/fixtures/relationship-benchmarks'); +const fixtureRoot = join(ktxRoot, 'packages/cli/test/fixtures/relationship-benchmarks'); async function buildDetector() { const backend = process.env.KTX_BENCHMARK_LLM_BACKEND; diff --git a/scripts/test-tiering.test.mjs b/scripts/test-tiering.test.mjs index bd43ce71..a54c6a16 100644 --- a/scripts/test-tiering.test.mjs +++ b/scripts/test-tiering.test.mjs @@ -14,41 +14,41 @@ function assertScriptContainsAll(script, expected) { describe('test tiering', () => { const cliSlowTests = [ - 'src/setup-databases.test.ts', - 'src/scan.test.ts', - 'src/commands/connection-metabase-setup.test.ts', - 'src/setup-models.test.ts', - 'src/setup-sources.test.ts', - 'src/setup.test.ts', - 'src/connection.test.ts', - 'src/setup-embeddings.test.ts', - 'src/ingest.test.ts', - 'src/commands/connection-mapping.test.ts', - 'src/ingest-viz.test.ts', - 'src/demo.test.ts', - 'src/setup-project.test.ts', - 'src/sl.test.ts', - 'src/local-scan-connectors.test.ts', - 'src/commands/connection-notion.test.ts', + 'test/setup-databases.test.ts', + 'test/scan.test.ts', + 'test/commands/connection-metabase-setup.test.ts', + 'test/setup-models.test.ts', + 'test/setup-sources.test.ts', + 'test/setup.test.ts', + 'test/connection.test.ts', + 'test/setup-embeddings.test.ts', + 'test/ingest.test.ts', + 'test/commands/connection-mapping.test.ts', + 'test/ingest-viz.test.ts', + 'test/demo.test.ts', + 'test/setup-project.test.ts', + 'test/sl.test.ts', + 'test/local-scan-connectors.test.ts', + 'test/commands/connection-notion.test.ts', ]; const contextSlowTests = [ - 'src/context/scan/local-scan.test.ts', - 'src/context/mcp/local-project-ports.test.ts', - 'src/context/ingest/local-stage-ingest.test.ts', - 'src/context/sl/pglite-sl-search-prototype.test.ts', - 'src/context/core/git.service.test.ts', - 'src/context/ingest/local-adapters.test.ts', - 'src/context/ingest/local-bundle-ingest.test.ts', - 'src/context/ingest/local-metabase-ingest.test.ts', - 'src/context/sl/local-sl.test.ts', - 'src/context/search/pglite-owner-process.test.ts', - 'src/context/scan/local-enrichment-artifacts.test.ts', - 'src/context/search/pglite-spike.test.ts', - 'src/context/wiki/local-knowledge.test.ts', - 'src/context/sl/local-query.test.ts', - 'src/context/scan/relationship-review-decisions.test.ts', - 'src/context/scan/relationship-profiling.test.ts', + 'test/context/scan/local-scan.test.ts', + 'test/context/mcp/local-project-ports.test.ts', + 'test/context/ingest/local-stage-ingest.test.ts', + 'test/context/sl/pglite-sl-search-prototype.test.ts', + 'test/context/core/git.service.test.ts', + 'test/context/ingest/local-adapters.test.ts', + 'test/context/ingest/local-bundle-ingest.test.ts', + 'test/context/ingest/local-metabase-ingest.test.ts', + 'test/context/sl/local-sl.test.ts', + 'test/context/search/pglite-owner-process.test.ts', + 'test/context/scan/local-enrichment-artifacts.test.ts', + 'test/context/search/pglite-spike.test.ts', + 'test/context/wiki/local-knowledge.test.ts', + 'test/context/sl/local-query.test.ts', + 'test/context/scan/relationship-review-decisions.test.ts', + 'test/context/scan/relationship-profiling.test.ts', ]; it('keeps slow package tests out of default local package test scripts', async () => {