mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
test: cover public kaelio ktx package invocations
This commit is contained in:
parent
ba47ab95e7
commit
416e440e43
5 changed files with 167 additions and 750 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } : {},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <connection>',
|
||||
`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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/' },
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue