From 416e440e43556944ca9c649233eeaae3c404bfa8 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 11:25:24 +0200 Subject: [PATCH] test: cover public kaelio ktx package invocations --- scripts/package-artifacts.mjs | 527 ++------------------- scripts/package-artifacts.test.mjs | 149 +----- scripts/published-package-smoke-config.mjs | 56 ++- scripts/published-package-smoke.mjs | 86 +--- scripts/published-package-smoke.test.mjs | 99 ++-- 5 files changed, 167 insertions(+), 750 deletions(-) diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index ffac1020..80862562 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -560,28 +560,16 @@ function runCommand(command, args, options = {}) { }); } -function npmTarballDependencyEntries(layout) { - return Object.fromEntries( - NPM_ARTIFACT_PACKAGES.map((packageInfo) => [ - packageInfo.name, - `file:${layout.npmTarballs[packageInfo.name]}`, - ]), - ); -} - export function npmSmokePackageJson(layout) { - const npmTarballDependencies = npmTarballDependencyEntries(layout); return { name: 'ktx-artifact-npm-smoke', version: '0.0.0', private: true, type: 'module', dependencies: { - ...npmTarballDependencies, - '@modelcontextprotocol/sdk': '^1.27.1', + '@kaelio/ktx': `file:${layout.cliTarball}`, }, pnpm: { - overrides: npmTarballDependencies, onlyBuiltDependencies: ['better-sqlite3'], }, }; @@ -589,103 +577,13 @@ export function npmSmokePackageJson(layout) { export function npmVerifySource() { return ` -const context = await import('@ktx/context'); -const project = await import('@ktx/context/project'); -const mcp = await import('@ktx/context/mcp'); -const memory = await import('@ktx/context/memory'); -const daemon = await import('@ktx/context/daemon'); -const ingest = await import('@ktx/context/ingest'); -const search = await import('@ktx/context/search'); -const llm = await import('@ktx/llm'); -const cli = await import('@ktx/cli'); -const bigqueryConnector = await import('@ktx/connector-bigquery'); -const clickhouseConnector = await import('@ktx/connector-clickhouse'); -const mysqlConnector = await import('@ktx/connector-mysql'); -const postgresConnector = await import('@ktx/connector-postgres'); -const posthogConnector = await import('@ktx/connector-posthog'); -const snowflakeConnector = await import('@ktx/connector-snowflake'); -const sqliteConnector = await import('@ktx/connector-sqlite'); -const sqlserverConnector = await import('@ktx/connector-sqlserver'); +const cli = await import('@kaelio/ktx'); -if (context.ktxContextPackageInfo.name !== '@ktx/context') { - throw new Error('Unexpected @ktx/context package info'); +if (cli.getKtxCliPackageInfo().name !== '@kaelio/ktx') { + throw new Error('Unexpected @kaelio/ktx package info'); } -if (typeof llm.createKtxLlmProvider !== 'function') { - throw new Error('Missing createKtxLlmProvider export'); -} -if (typeof llm.KtxMessageBuilder !== 'function') { - throw new Error('Missing KtxMessageBuilder export'); -} -if (typeof llm.createKtxEmbeddingProvider !== 'function') { - throw new Error('Missing createKtxEmbeddingProvider export'); -} -if (typeof project.initKtxProject !== 'function') { - throw new Error('Missing initKtxProject export'); -} -if (typeof mcp.createDefaultKtxMcpServer !== 'function') { - throw new Error('Missing createDefaultKtxMcpServer export'); -} -if (typeof memory.createLocalProjectMemoryCapture !== 'function') { - throw new Error('Missing createLocalProjectMemoryCapture export'); -} -if (typeof search.HybridSearchCore !== 'function') { - throw new Error('Missing HybridSearchCore export from @ktx/context/search'); -} -if (typeof search.assertSearchBackendConformanceCase !== 'function') { - throw new Error('Missing assertSearchBackendConformanceCase export from @ktx/context/search'); -} -if (typeof search.assertSearchBackendCapabilities !== 'function') { - throw new Error('Missing assertSearchBackendCapabilities export from @ktx/context/search'); -} -if (typeof daemon.createPythonSemanticLayerComputePort !== 'function') { - throw new Error('Missing createPythonSemanticLayerComputePort export'); -} -const dbtExtractionExports = [ - ['parseMetricflowFiles', ingest.parseMetricflowFiles], - ['parseMetricflowPullConfig', ingest.parseMetricflowPullConfig], - ['importMetricflowSemanticModels', ingest.importMetricflowSemanticModels], - ['parseDbtSchemaFiles', ingest.parseDbtSchemaFiles], - ['toDescriptionUpdates', ingest.toDescriptionUpdates], - ['toRelationshipUpdates', ingest.toRelationshipUpdates], - ['mergeSemanticModelTables', ingest.mergeSemanticModelTables], - ['loadProjectInfo', ingest.loadProjectInfo], - ['loadDbtSchemaFiles', ingest.loadDbtSchemaFiles], -]; - -for (const [exportName, exportValue] of dbtExtractionExports) { - if (typeof exportValue !== 'function') { - throw new Error('Missing dbt extraction export: ' + exportName); - } -} - -const metricflowConfig = ingest.parseMetricflowPullConfig({ - repoUrl: 'https://example.com/acme/analytics.git', -}); -if (metricflowConfig.branch !== 'main' || metricflowConfig.path !== null) { - throw new Error('Unexpected MetricFlow pull-config defaults from installed @ktx/context/ingest'); -} -if (cli.getKtxCliPackageInfo().name !== '@ktx/cli') { - throw new Error('Unexpected @ktx/cli package info'); -} - -const connectorExports = [ - ['@ktx/connector-bigquery', bigqueryConnector.KtxBigQueryScanConnector, bigqueryConnector.KtxBigQueryDialect], - ['@ktx/connector-clickhouse', clickhouseConnector.KtxClickHouseScanConnector, clickhouseConnector.KtxClickHouseDialect], - ['@ktx/connector-mysql', mysqlConnector.KtxMysqlScanConnector, mysqlConnector.KtxMysqlDialect], - ['@ktx/connector-postgres', postgresConnector.KtxPostgresScanConnector, postgresConnector.KtxPostgresDialect], - ['@ktx/connector-posthog', posthogConnector.KtxPostHogScanConnector, posthogConnector.KtxPostHogDialect], - ['@ktx/connector-snowflake', snowflakeConnector.KtxSnowflakeScanConnector, snowflakeConnector.KtxSnowflakeDialect], - ['@ktx/connector-sqlite', sqliteConnector.KtxSqliteScanConnector, sqliteConnector.KtxSqliteDialect], - ['@ktx/connector-sqlserver', sqlserverConnector.KtxSqlServerScanConnector, sqlserverConnector.KtxSqlServerDialect], -]; - -for (const [packageName, ScanConnector, Dialect] of connectorExports) { - if (typeof ScanConnector !== 'function') { - throw new Error('Missing scan connector export from ' + packageName); - } - if (typeof Dialect !== 'function') { - throw new Error('Missing dialect export from ' + packageName); - } +if (typeof cli.runKtxCli !== 'function') { + throw new Error('Missing runKtxCli export'); } `; } @@ -693,29 +591,13 @@ for (const [packageName, ScanConnector, Dialect] of connectorExports) { export function npmRuntimeSmokeSource() { return ` import assert from 'node:assert/strict'; -import { spawn, execFile } from 'node:child_process'; -import { once } from 'node:events'; +import { execFile } from 'node:child_process'; import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { request as httpRequest } from 'node:http'; -import { createServer } from 'node:net'; -import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { join } from 'node:path'; import { promisify } from 'node:util'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { - createDaemonLookerTableIdentifierParser, - LocalLookerRuntimeStore, -} from '@ktx/context/ingest'; const execFileAsync = promisify(execFile); -const require = createRequire(import.meta.url); -const contextPackageRoot = dirname(require.resolve('@ktx/context/package.json')); - -async function requireContextRuntimeAsset(relativePath) { - await access(join(contextPackageRoot, relativePath)); -} async function run(command, args, options = {}) { process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n'); @@ -770,140 +652,6 @@ function getRunId(stdout) { return match[1]; } -function requireToolNames(tools, expectedNames) { - const names = tools.tools.map((tool) => tool.name).sort(); - for (const expectedName of expectedNames) { - assert.ok(names.includes(expectedName), 'MCP tool list did not include ' + expectedName + ': ' + names.join(', ')); - } -} - -function structuredContent(result) { - assert.ok(result.structuredContent, 'MCP result did not include structuredContent'); - return result.structuredContent; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function getAvailablePort() { - const server = createServer(); - server.listen(0, '127.0.0.1'); - await once(server, 'listening'); - const address = server.address(); - if (!address || typeof address === 'string') { - server.close(); - throw new Error('expected TCP server address for daemon smoke'); - } - const port = address.port; - server.close(); - await once(server, 'close'); - return port; -} - -function httpGetOk(url) { - return new Promise((resolve, reject) => { - const request = httpRequest(url, { method: 'GET' }, (response) => { - response.resume(); - response.on('end', () => resolve((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300)); - }); - request.on('error', reject); - request.end(); - }); -} - -function spawnLogged(command, args, options = {}) { - const stdout = []; - const stderr = []; - let spawnError; - const child = spawn(command, args, { - cwd: options.cwd, - env: options.env ?? process.env, - stdio: ['ignore', 'pipe', 'pipe'], - }); - child.stdout.on('data', (chunk) => stdout.push(chunk)); - child.stderr.on('data', (chunk) => stderr.push(chunk)); - child.on('error', (error) => { - spawnError = error; - }); - return { - child, - error() { - return spawnError; - }, - output() { - return { - stdout: Buffer.concat(stdout).toString('utf8'), - stderr: Buffer.concat(stderr).toString('utf8'), - }; - }, - }; -} - -async function waitForHttpHealth(url, daemon) { - const deadline = Date.now() + 15_000; - while (Date.now() < deadline) { - if (daemon.error()) { - const output = daemon.output(); - throw new Error( - 'Failed to start ktx-daemon serve-http: ' + - daemon.error().message + - '\\nstdout:\\n' + - output.stdout + - '\\nstderr:\\n' + - output.stderr, - ); - } - if (daemon.child.exitCode !== null || daemon.child.signalCode !== null) { - const output = daemon.output(); - throw new Error( - 'ktx-daemon serve-http exited before health check passed\\nstdout:\\n' + - output.stdout + - '\\nstderr:\\n' + - output.stderr, - ); - } - try { - if (await httpGetOk(url)) { - return; - } - } catch { - await sleep(100); - continue; - } - await sleep(100); - } - const output = daemon.output(); - throw new Error('Timed out waiting for ' + url + '\\nstdout:\\n' + output.stdout + '\\nstderr:\\n' + output.stderr); -} - -async function startSemanticDaemon(port) { - const daemon = spawnLogged('ktx-daemon', [ - 'serve-http', - '--host', - '127.0.0.1', - '--port', - String(port), - '--log-level', - 'warning', - ]); - await waitForHttpHealth('http://127.0.0.1:' + port + '/health', daemon); - return daemon; -} - -async function stopSemanticDaemon(daemon) { - if (daemon.child.exitCode !== null || daemon.child.signalCode !== null) { - return; - } - daemon.child.kill('SIGTERM'); - const closed = once(daemon.child, 'close').then(() => true); - const timedOut = sleep(5_000).then(() => false); - if (!(await Promise.race([closed, timedOut]))) { - daemon.child.kill('SIGKILL'); - await once(daemon.child, 'close'); - } -} - async function writeSqliteWarehouse(projectDir) { const createDb = await run('python', [ '-c', @@ -928,16 +676,15 @@ async function writeSqliteWarehouse(projectDir) { requireSuccess('create sqlite warehouse', createDb); } -await requireContextRuntimeAsset('skills/notion_synthesize/SKILL.md'); -await requireContextRuntimeAsset('prompts/skills/page_triage_classifier.md'); -await requireContextRuntimeAsset('prompts/skills/light_extraction.md'); -process.stdout.write('packaged ingest runtime assets verified\\n'); - const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-')); try { const projectDir = join(root, 'project'); const sourceDir = join(root, 'source'); + const version = await run('pnpm', ['exec', 'ktx', '--version']); + requireSuccess('ktx public package version', version); + requireOutput('ktx public package version', version, /@kaelio\\/ktx 0\\.0\\.0-private/); + const missingProjectDir = join(root, 'missing-project'); await mkdir(missingProjectDir, { recursive: true }); const missingProjectSearch = await run('pnpm', [ @@ -1044,22 +791,6 @@ try { ); await writeSqliteWarehouse(projectDir); - const lookerStore = new LocalLookerRuntimeStore({ dbPath: join(projectDir, '.ktx', 'db.sqlite') }); - await lookerStore.setCursors('prod-looker', { - dashboardsLastSyncedAt: null, - looksLastSyncedAt: null, - }); - await lookerStore.upsertConnectionMapping({ - lookerConnectionId: 'prod-looker', - lookerConnectionName: 'analytics', - ktxConnectionId: 'warehouse', - source: 'cli', - }); - const lookerMappings = await lookerStore.readMappings('prod-looker'); - assert.equal(lookerMappings.length, 1); - assert.equal(lookerMappings[0].ktxConnectionId, 'warehouse'); - process.stdout.write('Looker local runtime store verified\\n'); - await mkdir(join(projectDir, 'knowledge', 'global'), { recursive: true }); await writeFile( join(projectDir, 'knowledge', 'global', 'revenue.md'), @@ -1168,40 +899,41 @@ try { requireIncludes(agentSlSearchJson.sources[0].matchReasons, 'lexical', 'agent sl search match reasons'); process.stdout.write('ktx agent sl list hybrid metadata verified\\n'); - const slQueryFile = join(projectDir, 'sl-query.json'); - await writeFile(slQueryFile, '{"measures":["orders.order_count"],"dimensions":[]}\\n', 'utf-8'); - - const slQuery = await run('pnpm', ['exec', 'ktx', 'agent', 'sl', 'query', - '--json', + const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query', '--connection-id', 'warehouse', - '--query-file', - slQueryFile, + '--measure', + 'orders.order_count', + '--format', + 'json', + '--yes', '--project-dir', projectDir, ]); - requireSuccess('ktx agent sl query', slQuery); - requireOutput('ktx agent sl query', slQuery, /"mode": "compile_only"/); - requireOutput('ktx agent sl query', slQuery, /orders/); + requireSuccess('ktx sl query', slQuery); + requireOutput('ktx sl query', slQuery, /"mode": "compile_only"/); + requireOutput('ktx sl query', slQuery, /orders/); - const sqliteSlQuery = await run('pnpm', ['exec', 'ktx', 'agent', 'sl', 'query', - '--json', + const sqliteSlQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query', '--connection-id', 'warehouse', - '--query-file', - slQueryFile, + '--measure', + 'orders.order_count', + '--format', + 'json', '--execute', '--max-rows', '100', + '--yes', '--project-dir', projectDir, ]); - requireSuccess('ktx agent sl query sqlite execute', sqliteSlQuery); - requireOutput('ktx agent sl query sqlite execute', sqliteSlQuery, /"dialect": "sqlite"/); - requireOutput('ktx agent sl query sqlite execute', sqliteSlQuery, /"mode": "executed"/); - requireOutput('ktx agent sl query sqlite execute', sqliteSlQuery, /"driver": "sqlite"/); - requireOutput('ktx agent sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/); - process.stdout.write('ktx agent sl query sqlite execute verified\\n'); + requireSuccess('ktx sl query sqlite execute', sqliteSlQuery); + requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"dialect": "sqlite"/); + requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"mode": "executed"/); + requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"driver": "sqlite"/); + requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/); + process.stdout.write('ktx sl query sqlite execute verified\\n'); const structuralScan = await run('pnpm', ['exec', 'ktx', 'dev', 'scan', 'warehouse', '--project-dir', @@ -1283,195 +1015,6 @@ try { await access(join(projectDir, '.ktx', 'db.sqlite')); process.stdout.write('ktx dev ingest provider guard verified\\n'); - - await writeFile( - join(projectDir, 'ktx.yaml'), - [ - 'project: warehouse', - 'connections:', - ' warehouse:', - ' driver: sqlite', - ' path: warehouse.db', - ' readonly: true', - 'storage:', - ' state: sqlite', - ' search: sqlite-fts5', - 'scan:', - ' enrichment:', - ' mode: deterministic', - 'llm:', - ' provider:', - ' backend: gateway', - ' gateway:', - ' api_key: env:AI_GATEWAY_API_KEY', - ' models:', - ' default: smoke/provider', - 'ingest:', - ' adapters:', - ' - fake', - ' - live-database', - '', - ].join('\\n'), - 'utf-8', - ); - - const daemonPort = await getAvailablePort(); - const semanticComputeUrl = 'http://127.0.0.1:' + daemonPort; - process.stdout.write('ktx-daemon serve-http --host 127.0.0.1 --port ' + daemonPort + '\\n'); - const daemon = await startSemanticDaemon(daemonPort); - const lookerParser = createDaemonLookerTableIdentifierParser({ baseUrl: semanticComputeUrl }); - const parsedLookerTables = await lookerParser.parse([ - { key: 'orders', sql_table_name: 'orders', dialect: 'sqlite' }, - ]); - assert.equal(parsedLookerTables.orders.ok, true); - assert.equal(parsedLookerTables.orders.name, 'orders'); - assert.equal(parsedLookerTables.orders.canonical_table, 'orders'); - process.stdout.write('Looker daemon table identifier parser verified\\n'); - const client = new Client({ name: 'ktx-artifact-smoke-client', version: '0.0.0' }); - process.stdout.write('ktx serve --mcp stdio --semantic-compute-url ' + semanticComputeUrl + ' --execute-queries\\n'); - const transport = new StdioClientTransport({ - command: 'pnpm', - args: [ - 'exec', - 'ktx', - 'serve', '--mcp', 'stdio', - '--project-dir', - projectDir, - '--user-id', - 'artifact-smoke-user', - '--semantic-compute-url', - semanticComputeUrl, - '--execute-queries', - '--memory-capture', '--memory-model', 'smoke/provider', - ], - cwd: process.cwd(), - stderr: 'pipe', - env: { - ...process.env, - AI_GATEWAY_API_KEY: process.env.AI_GATEWAY_API_KEY ?? 'artifact-smoke-token', - }, - }); - const mcpServerStderr = []; - transport.stderr?.on('data', (chunk) => mcpServerStderr.push(chunk)); - - try { - await client.connect(transport); - const tools = await client.listTools(); - requireToolNames(tools, [ - 'connection_list', - 'connection_test', - 'ingest_status', - 'ingest_trigger', - 'knowledge_read', - 'knowledge_search', - 'knowledge_write', - 'memory_capture', - 'memory_capture_status', - 'scan_list_artifacts', - 'scan_read_artifact', - 'scan_report', - 'scan_status', - 'scan_trigger', - 'sl_list_sources', - 'sl_query', - 'sl_read_source', - 'sl_validate', - 'sl_write_source', - ]); - const slValidateResult = structuredContent(await client.callTool({ - name: 'sl_validate', - arguments: { - connectionId: 'warehouse', - names: ['orders'], - }, - })); - assert.equal(slValidateResult.success, true); - assert.deepEqual(slValidateResult.errors, []); - const slQueryResult = structuredContent(await client.callTool({ - name: 'sl_query', - arguments: { - connectionId: 'warehouse', - measures: ['orders.order_count'], - limit: 5, - }, - })); - assert.equal(slQueryResult.connectionId, 'warehouse'); - assert.equal(slQueryResult.dialect, 'sqlite'); - assert.match(slQueryResult.sql, /orders/); - assert.deepEqual(slQueryResult.headers, ['order_count']); - assert.deepEqual(slQueryResult.rows, [[3]]); - assert.equal(slQueryResult.totalRows, 1); - assert.equal(slQueryResult.plan.execution.mode, 'executed'); - assert.equal(slQueryResult.plan.execution.driver, 'sqlite'); - - const connectionTest = structuredContent(await client.callTool({ - name: 'connection_test', - arguments: { - connectionId: 'warehouse', - }, - })); - assert.equal(connectionTest.id, 'warehouse'); - assert.equal(connectionTest.ok, true); - - const mcpScanTrigger = structuredContent(await client.callTool({ - name: 'scan_trigger', - arguments: { - connectionId: 'warehouse', - mode: 'structural', - }, - })); - assert.equal(mcpScanTrigger.connectionId, 'warehouse'); - assert.equal(mcpScanTrigger.report.mode, 'structural'); - assert.equal(mcpScanTrigger.report.manifestShardsWritten, 1); - - const mcpScanStatus = structuredContent(await client.callTool({ - name: 'scan_status', - arguments: { - runId: mcpScanTrigger.runId, - }, - })); - assert.equal(mcpScanStatus.runId, mcpScanTrigger.runId); - assert.equal(mcpScanStatus.status, 'done'); - - const mcpScanReport = structuredContent(await client.callTool({ - name: 'scan_report', - arguments: { - runId: mcpScanTrigger.runId, - }, - })); - assert.equal(mcpScanReport.runId, mcpScanTrigger.runId); - assert.deepEqual(mcpScanReport.artifactPaths.manifestShards, ['semantic-layer/warehouse/_schema/public.yaml']); - - const mcpScanArtifacts = structuredContent(await client.callTool({ - name: 'scan_list_artifacts', - arguments: { - runId: mcpScanTrigger.runId, - }, - })); - const manifestArtifact = mcpScanArtifacts.artifacts.find((artifact) => artifact.type === 'manifest_shard'); - assert.ok(manifestArtifact, 'scan_list_artifacts did not include a manifest shard'); - assert.equal(manifestArtifact.path, 'semantic-layer/warehouse/_schema/public.yaml'); - - const mcpManifestRead = structuredContent(await client.callTool({ - name: 'scan_read_artifact', - arguments: { - runId: mcpScanTrigger.runId, - path: manifestArtifact.path, - }, - })); - assert.equal(mcpManifestRead.path, 'semantic-layer/warehouse/_schema/public.yaml'); - assert.equal(mcpManifestRead.type, 'manifest_shard'); - assert.match(mcpManifestRead.content, /orders:/); - } catch (error) { - const stderr = Buffer.concat(mcpServerStderr).toString('utf8'); - if (stderr) { - error.message += '\\nktx serve stderr:\\n' + stderr; - } - throw error; - } finally { - await client.close(); - await stopSemanticDaemon(daemon); - } } finally { await rm(root, { recursive: true, force: true }); } @@ -1482,7 +1025,7 @@ export function npmDemoSmokeSource() { return ` import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -1523,6 +1066,8 @@ function requireStdout(label, result, pattern) { const root = await mkdtemp(join(tmpdir(), 'ktx-packed-demo-smoke-')); try { const projectDir = join(root, 'demo-project'); + const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')); + assert.deepEqual(Object.keys(packageJson.dependencies), ['@kaelio/ktx']); const help = await run('pnpm', ['exec', 'ktx', '--help']); requireSuccess('ktx --help', help); diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index fddd08a8..17b1ad96 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -505,16 +505,12 @@ describe('npmSmokePythonEnv', () => { }); describe('verification snippets', () => { - it('pins smoke dependencies and connector packages to clean-install-safe artifacts', () => { + it('pins the smoke project to the public package artifact', () => { const layout = packageArtifactLayout('/repo/ktx'); - const packageJson = npmSmokePackageJson(layout); - for (const packageInfo of NPM_ARTIFACT_PACKAGES) { - assert.equal(packageJson.dependencies[packageInfo.name], `file:${layout.npmTarballs[packageInfo.name]}`); - assert.equal(packageJson.pnpm.overrides[packageInfo.name], `file:${layout.npmTarballs[packageInfo.name]}`); - } - assert.equal(packageJson.dependencies['@modelcontextprotocol/sdk'], '^1.27.1'); - assert.deepEqual(packageJson.pnpm.onlyBuiltDependencies, ['better-sqlite3']); + assert.deepEqual(npmSmokePackageJson(layout).dependencies, { + '@kaelio/ktx': `file:${layout.cliTarball}`, + }); }); it('exposes manifest verification as a package artifact command', async () => { @@ -527,116 +523,40 @@ describe('verification snippets', () => { assert.equal(packageJson.scripts['artifacts:verify-manifest'], 'node scripts/package-artifacts.mjs verify-manifest'); }); - it('verifies installed dbt extraction exports from @ktx/context/ingest', () => { - const source = npmVerifySource(); - - assert.match(source, /const ingest = await import\('@ktx\/context\/ingest'\);/); - assert.match(source, /const dbtExtractionExports = \[/); - assert.match(source, /throw new Error\('Missing dbt extraction export: ' \+ exportName\);/); - - for (const exportName of [ - 'parseMetricflowFiles', - 'parseMetricflowPullConfig', - 'importMetricflowSemanticModels', - 'parseDbtSchemaFiles', - 'toDescriptionUpdates', - 'toRelationshipUpdates', - 'mergeSemanticModelTables', - 'loadProjectInfo', - 'loadDbtSchemaFiles', - ]) { - assert.match(source, new RegExp(`\\['${exportName}', ingest\\.${exportName}\\]`)); - } - }); - - it('asserts the public npm and connector entry points that clean installs must expose', () => { - const source = npmVerifySource(); - - assert.match(source, /@ktx\/context/); - assert.match(source, /@ktx\/context\/project/); - assert.match(source, /@ktx\/context\/mcp/); - assert.match(source, /@ktx\/context\/memory/); - assert.match(source, /@ktx\/context\/daemon/); - assert.match(source, /@ktx\/cli/); - assert.match(source, /@ktx\/llm/); - assert.match(source, /createKtxLlmProvider/); - assert.match(source, /KtxMessageBuilder/); - assert.match(source, /createKtxEmbeddingProvider/); - assert.doesNotMatch(source, /createGatewayLlmProvider/); - assert.match(source, /createLocalProjectMemoryCapture/); - for (const packageName of CONNECTOR_PACKAGE_NAMES) { - assert.match(source, new RegExp(packageName.replace('/', '\\/'))); - } - assert.match(source, /KtxSqliteScanConnector/); - assert.match(source, /KtxPostgresScanConnector/); - assert.match(source, /KtxBigQueryScanConnector/); - assert.match(source, /KtxSnowflakeScanConnector/); - assert.match(source, /KtxPostHogScanConnector/); - }); - - it('asserts installed hybrid search exports and CLI smoke coverage', () => { + it('asserts the public npm entry point that clean installs must expose', () => { const verifySource = npmVerifySource(); - const runtimeSource = npmRuntimeSmokeSource(); - const demoSource = npmDemoSmokeSource(); - assert.match(verifySource, /const search = await import\('@ktx\/context\/search'\);/); - assert.match(verifySource, /HybridSearchCore/); - assert.match(verifySource, /assertSearchBackendConformanceCase/); - assert.match(verifySource, /assertSearchBackendCapabilities/); - - assert.match(runtimeSource, /ktx agent wiki search hybrid metadata verified/); - assert.match(runtimeSource, /ktx agent sl list hybrid metadata verified/); - assert.match(runtimeSource, /agent_sl_search_missing_project/); - assert.match(runtimeSource, /agent_sl_search_no_connections/); - assert.match(runtimeSource, /agent_sl_search_no_indexed_sources/); - - assert.match(demoSource, /ktx seeded demo agent wiki search verified/); - assert.match(demoSource, /ktx seeded demo agent sl search verified/); + assert.match(verifySource, /const cli = await import\('@kaelio\/ktx'\);/); + assert.match(verifySource, /getKtxCliPackageInfo/); + assert.match(verifySource, /runKtxCli/); + assert.doesNotMatch(verifySource, /@ktx\/context/); + assert.doesNotMatch(verifySource, /@ktx\/llm/); + assert.doesNotMatch(verifySource, /@ktx\/connector-/); }); - it('runs installed CLI commands and MCP through an installed daemon HTTP server', () => { + it('runs installed CLI commands through the public package runtime', () => { const source = npmRuntimeSmokeSource(); - assert.match(source, /@modelcontextprotocol\/sdk\/client\/index\.js/); - assert.match(source, /@modelcontextprotocol\/sdk\/client\/stdio\.js/); - assert.match(source, /spawn\(command, args/); - assert.match(source, /createServer/); - assert.match(source, /request as httpRequest/); - assert.match(source, /getAvailablePort/); - assert.match(source, /startSemanticDaemon/); - assert.match(source, /waitForHttpHealth/); - assert.match(source, /stopSemanticDaemon/); - assert.match(source, /'ktx-daemon'/); - assert.match(source, /'serve-http'/); - assert.match(source, /'--host'/); - assert.match(source, /'127\.0\.0\.1'/); - assert.match(source, /'--port'/); - assert.match(source, /\/health/); - assert.match(source, /--semantic-compute-url/); - assert.match(source, /createDaemonLookerTableIdentifierParser/); - assert.match(source, /LocalLookerRuntimeStore/); - assert.match(source, /Looker daemon table identifier parser verified/); - assert.match(source, /Looker local runtime store verified/); - assert.match(source, /semanticComputeUrl/); + assert.match(source, /ktx public package version/); + assert.match(source, /@kaelio\\\/ktx 0\\\.0\\\.0-private/); + assert.match(source, /'ktx', 'sl', 'query'/); + assert.doesNotMatch(source, /@ktx\/context/); + assert.doesNotMatch(source, /@modelcontextprotocol/); + assert.doesNotMatch(source, /startSemanticDaemon/); assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'setup'/); assert.match(source, /knowledge', 'global', 'revenue\.md'/); assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'agent',\s*'wiki',\s*'search'/); assert.match(source, /semantic-layer', 'warehouse', 'orders\.yaml'/); assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'agent',\s*'sl',\s*'list'/); - assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'agent',\s*'sl',\s*'query'/); assert.match(source, /orders\.order_count/); assert.match(source, /sqlite3/); assert.match(source, /driver: sqlite/); assert.match(source, /path: warehouse\.db/); assert.match(source, /live-database/); assert.match(source, /'--execute'/); - assert.match(source, /'--execute-queries'/); - assert.match(source, /slValidateResult\.success, true/); - assert.match(source, /slQueryResult\.dialect, 'sqlite'/); - assert.match(source, /slQueryResult\.plan\.execution\.driver, 'sqlite'/); assert.match(source, /"mode": "compile_only"/); assert.match(source, /"mode": "executed"/); - assert.match(source, /ktx agent sl query sqlite execute/); + assert.match(source, /ktx sl query sqlite execute/); assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'dev',\s*'scan',\s*'warehouse'/); assert.match(source, /'--mode',\s*'enriched'/); assert.doesNotMatch(source, /'--enrich'/); @@ -646,28 +566,7 @@ describe('verification snippets', () => { assert.match(source, /scanReportJson\.artifactPaths\.enrichmentArtifacts/); assert.match(source, /enrichment:/); assert.match(source, /mode: deterministic/); - assert.match(source, /backend: gateway/); - assert.match(source, /models:/); - assert.match(source, /default: smoke\/provider/); - assert.match(source, /api_key: env:AI_GATEWAY_API_KEY/); assert.match(source, /run\('pnpm', \['exec', 'ktx', 'dev', 'ingest', 'run'/); - assert.match(source, /'serve', '--mcp', 'stdio'/); - assert.doesNotMatch(source, /'--semantic-compute',\n\s*'--execute-queries'/); - assert.match(source, /'--memory-capture', '--memory-model', 'smoke\/provider'/); - assert.match(source, /mcpServerStderr/); - assert.match(source, /ktx serve stderr/); - assert.match(source, /sl_validate/); - assert.match(source, /sl_query/); - assert.match(source, /memory_capture/); - assert.match(source, /memory_capture_status/); - assert.match(source, /connection_test/); - assert.match(source, /scan_trigger/); - assert.match(source, /scan_status/); - assert.match(source, /scan_report/); - assert.match(source, /scan_list_artifacts/); - assert.match(source, /scan_read_artifact/); - assert.match(source, /mcpScanArtifacts\.artifacts\.find/); - assert.match(source, /AI_GATEWAY_API_KEY/); assert.match(source, /access\(join\(projectDir, '\.ktx', 'db\.sqlite'\)\)/); assert.match(source, /SQLite knowledge index/); assert.match(source, /ktx dev ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway/); @@ -688,17 +587,11 @@ describe('verification snippets', () => { assert.match(source, /'dev', 'doctor', 'setup', '--no-input'/); assert.match(source, /'--plain'/); assert.match(source, /ktx setup demo seeded wrote unexpected stderr/); + assert.match(source, /Object\.keys\(packageJson\.dependencies\)/); + assert.match(source, /'@kaelio\/ktx'/); }); }); - it('checks packaged ingest runtime assets in the installed npm smoke', () => { - const source = npmRuntimeSmokeSource(); - - assert.match(source, /notion_synthesize\/SKILL\.md/); - assert.match(source, /skills\/page_triage_classifier\.md/); - assert.match(source, /skills\/light_extraction\.md/); - }); - it('asserts the Python modules that clean installs must expose', () => { const source = pythonVerifySource(); diff --git a/scripts/published-package-smoke-config.mjs b/scripts/published-package-smoke-config.mjs index 71bcd862..2316117a 100644 --- a/scripts/published-package-smoke-config.mjs +++ b/scripts/published-package-smoke-config.mjs @@ -125,28 +125,54 @@ export function buildPublishedPackageNpxCommand(config, args, label = 'published }; } -export function buildPublishedPackageSmokeCommands(config, projectDir, emptyProjectDir) { +export function buildPublishedPackageSmokeCommands(config, projectDir) { return [ buildPublishedPackageNpxCommand(config, ['--version'], 'published package version'), buildPublishedPackageNpxCommand( config, - ['demo', '--project-dir', projectDir, '--no-input', '--plain'], - 'published package demo', + ['setup', 'demo', '--project-dir', projectDir, '--no-input', '--plain'], + 'published package setup demo', ), buildPublishedPackageNpxCommand( config, - ['agent', 'wiki', 'search', 'ARR contract', '--json', '--limit', '5', '--project-dir', projectDir], - 'published package wiki hybrid search', - ), - buildPublishedPackageNpxCommand( - config, - ['agent', 'sl', 'list', '--json', '--query', 'ARR', '--project-dir', projectDir], - 'published package semantic-layer hybrid search', - ), - buildPublishedPackageNpxCommand( - config, - ['agent', 'sl', 'list', '--json', '--query', 'revenue', '--project-dir', emptyProjectDir], - 'published package missing-project readiness', + [ + 'sl', + 'query', + '--project-dir', + projectDir, + '--connection-id', + 'orbit_demo', + '--measure', + 'contracts.contract_count', + '--format', + 'sql', + '--yes', + ], + 'published package sl query', ), + { + label: 'published package local install', + command: 'pnpm', + args: ['add', publishedPackageSpec(config)], + env: config.registry ? { npm_config_registry: config.registry } : {}, + }, + { + label: 'published package local binary', + command: 'pnpm', + args: ['exec', 'ktx', '--version'], + env: config.registry ? { npm_config_registry: config.registry } : {}, + }, + { + label: 'published package global install', + command: 'pnpm', + args: ['add', '--global', publishedPackageSpec(config)], + env: config.registry ? { npm_config_registry: config.registry } : {}, + }, + { + label: 'published package global binary', + command: 'ktx', + args: ['--version'], + env: config.registry ? { npm_config_registry: config.registry } : {}, + }, ]; } diff --git a/scripts/published-package-smoke.mjs b/scripts/published-package-smoke.mjs index d97cdda0..9bd7329d 100644 --- a/scripts/published-package-smoke.mjs +++ b/scripts/published-package-smoke.mjs @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; -import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -59,78 +59,38 @@ function requireSuccess(label, result) { ); } -function parseJson(label, text) { - try { - return JSON.parse(text); - } catch (error) { - throw new Error(`${label} did not produce JSON: ${error instanceof Error ? error.message : String(error)}\n${text}`); - } -} - -function assertHybridWikiSearch(result) { - const payload = parseJson('published package wiki search', result.stdout); - assert.ok(payload.totalFound > 0, 'published package wiki search should return results'); - assert.ok( - payload.results.some((entry) => Array.isArray(entry.matchReasons) && entry.matchReasons.length > 0), - 'published package wiki search should expose match reasons', - ); -} - -function assertHybridSlSearch(result) { - const payload = parseJson('published package semantic-layer search', result.stdout); - assert.ok(payload.totalSources > 0, 'published package semantic-layer search should return sources'); - assert.ok( - payload.sources.some((entry) => Array.isArray(entry.matchReasons) && entry.matchReasons.length > 0), - 'published package semantic-layer search should expose match reasons', - ); -} - -function assertMissingProjectReadiness(result, emptyProjectDir) { - assert.equal(result.code, 1, 'missing-project semantic-layer search should exit 1'); - assert.equal(result.stdout, '', 'missing-project semantic-layer search should not write JSON errors to stdout'); - - const payload = parseJson('published package missing-project semantic-layer search', result.stderr); - assert.deepEqual(payload, { - ok: false, - error: { - code: 'agent_sl_search_missing_project', - message: `Semantic-layer search needs an initialized KTX project at ${emptyProjectDir}.`, - nextSteps: [ - 'ktx demo', - `ktx setup --project-dir ${emptyProjectDir}`, - 'ktx ingest ', - `ktx agent sl list --json --query "revenue" --project-dir ${emptyProjectDir}`, - ], - }, - }); -} - export async function runPublishedPackageSmoke(config) { const root = await mkdtemp(join(tmpdir(), 'ktx-published-package-smoke-')); try { const projectDir = join(root, 'demo-project'); - const emptyProjectDir = join(root, 'empty-project'); - await mkdir(emptyProjectDir, { recursive: true }); - const commands = buildPublishedPackageSmokeCommands(config, projectDir, emptyProjectDir); - for (const command of commands.slice(0, 4)) { - const result = await runCommand(command.command, command.args, { env: command.env }); + const commands = buildPublishedPackageSmokeCommands(config, projectDir); + const pnpmHome = join(root, 'pnpm-home'); + const globalEnv = { + PNPM_HOME: pnpmHome, + PATH: `${pnpmHome}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`, + }; + for (const command of commands) { + const isGlobalCommand = command.label.includes('global'); + const result = await runCommand(command.command, command.args, { + cwd: command.label.includes('local') || isGlobalCommand ? root : undefined, + env: isGlobalCommand ? { ...globalEnv, ...command.env } : command.env, + }); requireSuccess(command.label, result); - if (command.label === 'published package wiki hybrid search') { - assertHybridWikiSearch(result); + if ( + command.label === 'published package version' || + command.label === 'published package local binary' || + command.label === 'published package global binary' + ) { + assert.match(result.stdout, /@kaelio\/ktx /); } - if (command.label === 'published package semantic-layer hybrid search') { - assertHybridSlSearch(result); + if (command.label === 'published package sl query') { + assert.match(result.stdout, /SELECT/i); + assert.match(result.stdout, /contracts/i); } } - const missingProjectCommand = commands[4]; - const missingProject = await runCommand(missingProjectCommand.command, missingProjectCommand.args, { - env: missingProjectCommand.env, - }); - assertMissingProjectReadiness(missingProject, emptyProjectDir); - - process.stdout.write('published package hybrid search smoke verified\n'); + process.stdout.write('published package invocation smoke verified\n'); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/scripts/published-package-smoke.test.mjs b/scripts/published-package-smoke.test.mjs index 66fa6670..252b55a6 100644 --- a/scripts/published-package-smoke.test.mjs +++ b/scripts/published-package-smoke.test.mjs @@ -32,7 +32,7 @@ describe('published package smoke config', () => { assert.deepEqual( readPublishedPackageSmokeConfig( { - KTX_PUBLISHED_KTX_PACKAGE: '@ktx/cli-public', + KTX_PUBLISHED_KTX_PACKAGE: '@kaelio/ktx', KTX_PUBLISHED_KTX_VERSION: 'latest', KTX_PUBLISHED_KTX_REGISTRY: 'https://registry.npmjs.org/', }, @@ -42,7 +42,7 @@ describe('published package smoke config', () => { enabled: true, requireConfig: false, configSource: 'environment', - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', packageVersion: 'latest', registry: 'https://registry.npmjs.org/', }, @@ -55,7 +55,7 @@ describe('published package smoke config', () => { {}, [], { - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -64,7 +64,7 @@ describe('published package smoke config', () => { enabled: true, requireConfig: false, configSource: 'release-policy', - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', packageVersion: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -75,12 +75,12 @@ describe('published package smoke config', () => { assert.deepEqual( readPublishedPackageSmokeConfig( { - KTX_PUBLISHED_KTX_PACKAGE: '@ktx/cli-from-env', + KTX_PUBLISHED_KTX_PACKAGE: '@kaelio/ktx', KTX_PUBLISHED_KTX_VERSION: 'latest', }, [], { - packageName: '@ktx/cli-from-policy', + packageName: '@kaelio/ktx', version: '2026.5.8', registry: 'https://registry.npmjs.org/', }, @@ -89,7 +89,7 @@ describe('published package smoke config', () => { enabled: true, requireConfig: false, configSource: 'environment', - packageName: '@ktx/cli-from-env', + packageName: '@kaelio/ktx', packageVersion: 'latest', registry: 'https://registry.npmjs.org/', }, @@ -125,7 +125,7 @@ describe('published package smoke config', () => { () => readPublishedPackageSmokeConfig( { - KTX_PUBLISHED_KTX_PACKAGE: '@ktx/cli-public', + KTX_PUBLISHED_KTX_PACKAGE: '@kaelio/ktx', KTX_PUBLISHED_KTX_VERSION: '--tag latest', }, [], @@ -136,7 +136,7 @@ describe('published package smoke config', () => { () => readPublishedPackageSmokeConfig( { - KTX_PUBLISHED_KTX_PACKAGE: '@ktx/cli-public', + KTX_PUBLISHED_KTX_PACKAGE: '@kaelio/ktx', KTX_PUBLISHED_KTX_REGISTRY: 'file:///tmp/npm', }, [], @@ -150,38 +150,39 @@ describe('published package smoke command construction', () => { const config = { enabled: true, requireConfig: false, - packageName: '@ktx/cli-public', + packageName: '@kaelio/ktx', packageVersion: 'latest', registry: 'https://registry.npmjs.org/', }; it('builds the npx package spec from package name and version tag', () => { - assert.equal(publishedPackageSpec(config), '@ktx/cli-public@latest'); + assert.equal(publishedPackageSpec(config), '@kaelio/ktx@latest'); }); it('builds npx commands with a registry env patch instead of shell interpolation', () => { assert.deepEqual(buildPublishedPackageNpxCommand(config, ['--version']), { label: 'published package command', command: 'npx', - args: ['--yes', '@ktx/cli-public@latest', '--version'], + args: ['--yes', '@kaelio/ktx@latest', '--version'], env: { npm_config_registry: 'https://registry.npmjs.org/' }, }); }); - it('builds the full hybrid-search smoke command list', () => { + it('builds the full public package smoke command list', () => { assert.deepEqual(buildPublishedPackageSmokeCommands(config, '/tmp/ktx-smoke/demo', '/tmp/ktx-smoke/empty'), [ { label: 'published package version', command: 'npx', - args: ['--yes', '@ktx/cli-public@latest', '--version'], + args: ['--yes', '@kaelio/ktx@latest', '--version'], env: { npm_config_registry: 'https://registry.npmjs.org/' }, }, { - label: 'published package demo', + label: 'published package setup demo', command: 'npx', args: [ '--yes', - '@ktx/cli-public@latest', + '@kaelio/ktx@latest', + 'setup', 'demo', '--project-dir', '/tmp/ktx-smoke/demo', @@ -191,55 +192,47 @@ describe('published package smoke command construction', () => { env: { npm_config_registry: 'https://registry.npmjs.org/' }, }, { - label: 'published package wiki hybrid search', + label: 'published package sl query', command: 'npx', args: [ '--yes', - '@ktx/cli-public@latest', - 'agent', - 'wiki', - 'search', - 'ARR contract', - '--json', - '--limit', - '5', + '@kaelio/ktx@latest', + 'sl', + 'query', '--project-dir', '/tmp/ktx-smoke/demo', + '--connection-id', + 'orbit_demo', + '--measure', + 'contracts.contract_count', + '--format', + 'sql', + '--yes', ], env: { npm_config_registry: 'https://registry.npmjs.org/' }, }, { - label: 'published package semantic-layer hybrid search', - command: 'npx', - args: [ - '--yes', - '@ktx/cli-public@latest', - 'agent', - 'sl', - 'list', - '--json', - '--query', - 'ARR', - '--project-dir', - '/tmp/ktx-smoke/demo', - ], + label: 'published package local install', + command: 'pnpm', + args: ['add', '@kaelio/ktx@latest'], env: { npm_config_registry: 'https://registry.npmjs.org/' }, }, { - label: 'published package missing-project readiness', - command: 'npx', - args: [ - '--yes', - '@ktx/cli-public@latest', - 'agent', - 'sl', - 'list', - '--json', - '--query', - 'revenue', - '--project-dir', - '/tmp/ktx-smoke/empty', - ], + label: 'published package local binary', + command: 'pnpm', + args: ['exec', 'ktx', '--version'], + env: { npm_config_registry: 'https://registry.npmjs.org/' }, + }, + { + label: 'published package global install', + command: 'pnpm', + args: ['add', '--global', '@kaelio/ktx@latest'], + env: { npm_config_registry: 'https://registry.npmjs.org/' }, + }, + { + label: 'published package global binary', + command: 'ktx', + args: ['--version'], env: { npm_config_registry: 'https://registry.npmjs.org/' }, }, ]);