2026-05-12 16:56:58 -04:00
|
|
|
import { execFile } from 'node:child_process';
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
2026-05-12 16:56:58 -04:00
|
|
|
import { promisify } from 'node:util';
|
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
|
|
|
import { writeKtxSetupState } from '../src/context/project/setup-config.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
|
|
2026-05-12 01:07:47 +02:00
|
|
|
import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js';
|
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
|
|
|
import { contextBuildCommands, writeKtxSetupContextState } from '../src/setup-context.js';
|
|
|
|
|
import { runDemoTour } from '../src/setup-demo-tour.js';
|
|
|
|
|
import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from '../src/setup.js';
|
2026-05-10 23:12:26 +02:00
|
|
|
|
test: split cli tests from source tree (#216)
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
2026-05-26 08:49:05 +02:00
|
|
|
vi.mock('../src/setup-demo-tour.js', () => ({
|
2026-05-11 22:03:20 -07:00
|
|
|
runDemoTour: vi.fn(async () => 0),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-12 16:56:58 -04:00
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
function makeIo() {
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stderr = '';
|
|
|
|
|
return {
|
|
|
|
|
io: {
|
|
|
|
|
stdout: {
|
2026-05-22 18:18:47 +02:00
|
|
|
isTTY: false,
|
2026-05-10 23:12:26 +02:00
|
|
|
write: (chunk: string) => {
|
|
|
|
|
stdout += chunk;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
stderr: {
|
|
|
|
|
write: (chunk: string) => {
|
|
|
|
|
stderr += chunk;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
stdout: () => stdout,
|
|
|
|
|
stderr: () => stderr,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
function runtimeReady(projectDir: string) {
|
|
|
|
|
return { status: 'ready' as const, projectDir, requirements: { features: ['core' as const], requirements: [] } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function writeReadyRuntime(rootDir: string, cliVersion = '0.2.0') {
|
|
|
|
|
const runtimeRoot = join(rootDir, '.runtime');
|
|
|
|
|
const versionDir = join(runtimeRoot, cliVersion);
|
|
|
|
|
const pythonPath = join(versionDir, '.venv', 'bin', 'python');
|
|
|
|
|
const daemonPath = join(versionDir, '.venv', 'bin', 'ktx-daemon');
|
|
|
|
|
await mkdir(join(versionDir, '.venv', 'bin'), { recursive: true });
|
|
|
|
|
await writeFile(pythonPath, '', 'utf-8');
|
|
|
|
|
await writeFile(daemonPath, '', 'utf-8');
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(versionDir, 'manifest.json'),
|
|
|
|
|
`${JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
cliVersion,
|
|
|
|
|
installedAt: '2026-05-09T10:02:00.000Z',
|
|
|
|
|
asset: {
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
distributionName: 'kaelio-ktx',
|
|
|
|
|
normalizedName: 'kaelio_ktx',
|
|
|
|
|
version: '0.1.0',
|
|
|
|
|
wheel: {
|
|
|
|
|
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
|
|
|
|
|
sha256: '0'.repeat(64),
|
|
|
|
|
bytes: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
features: ['core'],
|
|
|
|
|
python: {
|
|
|
|
|
executable: pythonPath,
|
|
|
|
|
daemonExecutable: daemonPath,
|
|
|
|
|
},
|
|
|
|
|
installLog: join(versionDir, 'install.log'),
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
)}\n`,
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
return runtimeRoot;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
describe('setup status', () => {
|
|
|
|
|
let tempDir: string;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
2026-05-10 23:51:24 +02:00
|
|
|
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-status-'));
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
2026-05-22 18:18:47 +02:00
|
|
|
vi.unstubAllEnvs();
|
2026-05-10 23:12:26 +02:00
|
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports a missing project without creating files', async () => {
|
2026-05-10 23:51:24 +02:00
|
|
|
const status = await readKtxSetupStatus(tempDir);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
expect(status).toMatchObject({
|
|
|
|
|
project: { path: tempDir, ready: false },
|
|
|
|
|
llm: { ready: false },
|
|
|
|
|
embeddings: { ready: false },
|
|
|
|
|
databases: [],
|
|
|
|
|
sources: [],
|
|
|
|
|
context: { ready: false, status: 'not_started' },
|
|
|
|
|
agents: [],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 16:40:01 +02:00
|
|
|
it('reports disabled default embeddings as not setup-ready', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
await mkdir(tempDir, { recursive: true });
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
' backend: anthropic',
|
|
|
|
|
' anthropic:',
|
2026-05-13 08:42:38 -04:00
|
|
|
' api_key: env:ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-10 23:12:26 +02:00
|
|
|
' models:',
|
|
|
|
|
' default: claude-sonnet-4-6',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
2026-05-19 16:40:01 +02:00
|
|
|
' backend: none',
|
2026-05-10 23:12:26 +02:00
|
|
|
' dimensions: 8',
|
|
|
|
|
'connections: {}',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
project: { path: tempDir, ready: true },
|
|
|
|
|
llm: { backend: 'anthropic', ready: true, model: 'claude-sonnet-4-6' },
|
2026-05-19 16:40:01 +02:00
|
|
|
embeddings: { backend: 'none', ready: false, dimensions: 8 },
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-12 01:05:28 +02:00
|
|
|
it.each([
|
|
|
|
|
{
|
|
|
|
|
backend: 'vertex',
|
|
|
|
|
providerLines: [' backend: vertex', ' vertex:', ' project: kaelio-dev', ' location: us-east5'],
|
|
|
|
|
model: 'claude-sonnet-4-6',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
backend: 'gateway',
|
|
|
|
|
providerLines: [' backend: gateway', ' gateway:', ' api_key: env:AI_GATEWAY_API_KEY'],
|
|
|
|
|
model: 'anthropic/claude-sonnet-4-6',
|
|
|
|
|
},
|
|
|
|
|
])('reports configured $backend llm backends as setup-ready', async (fixture) => {
|
|
|
|
|
await mkdir(tempDir, { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
...fixture.providerLines,
|
|
|
|
|
' models:',
|
|
|
|
|
` default: ${fixture.model}`,
|
|
|
|
|
'connections: {}',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
|
|
|
|
llm: { backend: fixture.backend, ready: true, model: fixture.model },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('uses setup database connection ids when present', async () => {
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
' - analytics',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:WAREHOUSE_URL',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
|
|
|
|
' backend: openai',
|
|
|
|
|
' model: text-embedding-3-small',
|
|
|
|
|
' dimensions: 1536',
|
|
|
|
|
' openai:',
|
2026-05-13 08:42:38 -04:00
|
|
|
' api_key: env:OPENAI_API_KEY', // pragma: allowlist secret
|
2026-05-10 23:12:26 +02:00
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'databases'] });
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
databases: [
|
|
|
|
|
{ connectionId: 'warehouse', ready: true },
|
|
|
|
|
{ connectionId: 'analytics', ready: false },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports selected databases as ready only after the database setup step is complete', async () => {
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project'] });
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
databases: [{ connectionId: 'warehouse', ready: false }],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'databases'] });
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
databases: [{ connectionId: 'warehouse', ready: true }],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports source status from configured source connections', async () => {
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids: []',
|
|
|
|
|
'connections:',
|
|
|
|
|
' docs:',
|
|
|
|
|
' driver: notion',
|
|
|
|
|
' auth_token_ref: env:NOTION_TOKEN',
|
|
|
|
|
' crawl_mode: all_accessible',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'sources'] });
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
sources: [{ connectionId: 'docs', type: 'notion', ready: true }],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports agent status from the install manifest', async () => {
|
2026-05-10 23:51:24 +02:00
|
|
|
await mkdir(join(tempDir, '.ktx', 'agents'), { recursive: true });
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, '.ktx/agents/install-manifest.json'),
|
2026-05-10 23:12:26 +02:00
|
|
|
JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
version: 1,
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
installedAt: '2026-05-07T00:00:00.000Z',
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [
|
|
|
|
|
{ target: 'codex', scope: 'project', mode: 'mcp' },
|
|
|
|
|
{ target: 'codex', scope: 'project', mode: 'mcp-cli' },
|
|
|
|
|
],
|
2026-05-10 23:12:26 +02:00
|
|
|
entries: [],
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: [{ target: 'codex', scope: 'project', ready: true }],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('reports setup-managed context build status and commands', async () => {
|
|
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
' backend: anthropic',
|
|
|
|
|
' models:',
|
|
|
|
|
' default: claude-sonnet-4-6',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
|
|
|
|
' backend: openai',
|
|
|
|
|
' model: text-embedding-3-small',
|
|
|
|
|
' dimensions: 1536',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, {
|
|
|
|
|
completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources'],
|
|
|
|
|
});
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeKtxSetupContextState(tempDir, {
|
2026-05-10 23:12:26 +02:00
|
|
|
runId: 'setup-context-local-abc123',
|
2026-05-15 07:09:58 -04:00
|
|
|
status: 'stale',
|
2026-05-10 23:12:26 +02:00
|
|
|
startedAt: '2026-05-09T10:00:00.000Z',
|
|
|
|
|
updatedAt: '2026-05-09T10:01:00.000Z',
|
|
|
|
|
primarySourceConnectionIds: ['warehouse'],
|
|
|
|
|
contextSourceConnectionIds: [],
|
|
|
|
|
reportIds: [],
|
|
|
|
|
artifactPaths: [],
|
|
|
|
|
retryableFailedTargets: [],
|
2026-05-21 02:38:18 +02:00
|
|
|
commands: contextBuildCommands(tempDir),
|
2026-05-15 07:09:58 -04:00
|
|
|
failureReason: 'Previous foreground context build did not finish. Rerun setup or ktx ingest.',
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readKtxSetupStatus(tempDir)).resolves.toMatchObject({
|
2026-05-10 23:12:26 +02:00
|
|
|
context: {
|
|
|
|
|
ready: false,
|
2026-05-14 01:43:06 +02:00
|
|
|
status: 'stale',
|
2026-05-10 23:12:26 +02:00
|
|
|
runId: 'setup-context-local-abc123',
|
2026-05-13 00:38:26 +02:00
|
|
|
statusCommand: `ktx status --project-dir ${tempDir}`,
|
2026-05-14 01:43:06 +02:00
|
|
|
detail: 'Previous foreground context build did not finish. Rerun setup or ktx ingest.',
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-12 01:07:47 +02:00
|
|
|
it('reports Vertex LLM and context ready after a successful Metabase ingest report', async () => {
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
' metabase:',
|
|
|
|
|
' driver: metabase',
|
2026-05-15 00:08:11 +02:00
|
|
|
' api_url: https://metabase.example.test',
|
2026-05-12 01:07:47 +02:00
|
|
|
' api_key_ref: env:METABASE_API_KEY',
|
|
|
|
|
' warehouse_connection_id: warehouse',
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
' backend: vertex',
|
|
|
|
|
' vertex:',
|
|
|
|
|
' project: kaelio-dev',
|
|
|
|
|
' location: us-east5',
|
|
|
|
|
' models:',
|
|
|
|
|
' default: claude-sonnet-4-6',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
2026-05-19 16:40:01 +02:00
|
|
|
' backend: none',
|
2026-05-12 01:07:47 +02:00
|
|
|
' dimensions: 8',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'databases', 'sources'] });
|
2026-05-12 01:07:47 +02:00
|
|
|
await persistLocalBundleReport(
|
|
|
|
|
tempDir,
|
|
|
|
|
localFakeBundleReport('metabase-job-1', {
|
|
|
|
|
connectionId: 'warehouse',
|
|
|
|
|
sourceKey: 'metabase',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const status = await readKtxSetupStatus(tempDir);
|
2026-05-13 00:38:26 +02:00
|
|
|
const rendered = formatKtxSetupStatus(status);
|
2026-05-12 01:07:47 +02:00
|
|
|
|
|
|
|
|
expect(status.llm).toMatchObject({ backend: 'vertex', ready: true, model: 'claude-sonnet-4-6' });
|
|
|
|
|
expect(status.context).toMatchObject({ ready: true, status: 'completed' });
|
2026-05-13 00:38:26 +02:00
|
|
|
expect(rendered).toContain('LLM ready: yes (claude-sonnet-4-6)');
|
|
|
|
|
expect(rendered).toContain('KTX context built: yes');
|
2026-05-12 01:07:47 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-30 00:42:59 +02:00
|
|
|
it('reports context ready after a partial ingest report saved memory', async () => {
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DATABASE_URL',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
|
|
|
|
' backend: none',
|
|
|
|
|
' dimensions: 8',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'databases'] });
|
|
|
|
|
await persistLocalBundleReport(
|
|
|
|
|
tempDir,
|
|
|
|
|
localFakeBundleReport('warehouse-job-partial', {
|
|
|
|
|
connectionId: 'warehouse',
|
|
|
|
|
sourceKey: 'fake',
|
|
|
|
|
body: {
|
|
|
|
|
failedWorkUnits: ['orders-bad'],
|
|
|
|
|
workUnits: [
|
|
|
|
|
{
|
|
|
|
|
unitKey: 'orders-ok',
|
|
|
|
|
rawFiles: ['orders/orders.json'],
|
|
|
|
|
status: 'success',
|
|
|
|
|
actions: [{ target: 'wiki', type: 'created', key: 'wiki/orders.md', detail: 'orders' }],
|
|
|
|
|
touchedSlSources: [],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
unitKey: 'orders-bad',
|
|
|
|
|
rawFiles: ['orders/bad.json'],
|
|
|
|
|
status: 'failed',
|
|
|
|
|
reason: 'writer tool failed',
|
|
|
|
|
actions: [],
|
|
|
|
|
touchedSlSources: [],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const status = await readKtxSetupStatus(tempDir);
|
|
|
|
|
|
|
|
|
|
expect(status.context).toMatchObject({ ready: true, status: 'completed' });
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
it('formats plain and JSON setup status payloads', async () => {
|
|
|
|
|
const status = await readKtxSetupStatus(tempDir);
|
|
|
|
|
const rendered = formatKtxSetupStatus(status);
|
|
|
|
|
|
|
|
|
|
expect(rendered).toContain(`No KTX project found at ${tempDir}.`);
|
|
|
|
|
expect(rendered).toContain('Check another project: ktx --project-dir <folder> status');
|
|
|
|
|
expect(rendered).toContain('Or from that folder: ktx status');
|
|
|
|
|
expect(rendered).toContain('Create a new KTX project here: ktx setup');
|
|
|
|
|
expect(rendered).not.toContain('Project ready: no');
|
|
|
|
|
expect(JSON.parse(JSON.stringify(status))).toMatchObject({ project: { path: tempDir, ready: false } });
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prints the readiness checklist for an existing project', async () => {
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
const rendered = formatKtxSetupStatus(await readKtxSetupStatus(tempDir));
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
expect(rendered).toContain(`KTX project: ${tempDir}`);
|
|
|
|
|
expect(rendered).toContain('Project ready: yes');
|
|
|
|
|
expect(rendered).toContain('LLM ready: no');
|
2026-05-14 01:43:06 +02:00
|
|
|
expect(rendered).toContain('Databases configured: no');
|
|
|
|
|
expect(rendered).not.toContain(['Primary sources', 'configured'].join(' '));
|
2026-05-13 00:38:26 +02:00
|
|
|
expect(rendered).toContain('KTX context built: no');
|
|
|
|
|
expect(rendered).not.toContain('No KTX project found.');
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-18 18:54:20 -04:00
|
|
|
it('formats a concise ready summary for completed agent setup', () => {
|
|
|
|
|
const rendered = formatKtxSetupCompletionSummary(
|
|
|
|
|
{
|
|
|
|
|
project: { path: tempDir, ready: true },
|
|
|
|
|
llm: { ready: true, model: 'sonnet' },
|
|
|
|
|
embeddings: { ready: true, model: 'text-embedding-3-small' },
|
|
|
|
|
databases: [{ connectionId: 'postgres-warehouse', ready: true }],
|
|
|
|
|
sources: [{ connectionId: 'dbt-main', type: 'dbt', ready: true }],
|
|
|
|
|
runtime: { required: true, ready: true, features: ['core'] },
|
|
|
|
|
context: { ready: true, status: 'completed' },
|
|
|
|
|
agents: [
|
|
|
|
|
{ target: 'claude-code', scope: 'project', ready: true },
|
|
|
|
|
{ target: 'claude-desktop', scope: 'global', ready: true },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
agentNextActions: [
|
|
|
|
|
'1. Start MCP',
|
|
|
|
|
' Run this command before using Claude Code:',
|
|
|
|
|
'',
|
|
|
|
|
' RUN:',
|
|
|
|
|
` ktx mcp start --project-dir ${tempDir}`,
|
|
|
|
|
'',
|
|
|
|
|
' If you need to stop MCP later:',
|
|
|
|
|
` ktx mcp stop --project-dir ${tempDir}`,
|
|
|
|
|
'',
|
|
|
|
|
'2. Open Claude Code',
|
|
|
|
|
' Open Claude Code from the KTX project directory:',
|
|
|
|
|
'',
|
|
|
|
|
' RUN:',
|
|
|
|
|
` cd '${tempDir}'`,
|
|
|
|
|
' claude',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(rendered).toContain(`Project\n ${tempDir}`);
|
|
|
|
|
expect(rendered).toContain('Context\n built');
|
|
|
|
|
expect(rendered).toContain('Agents configured\n Claude Code, Claude Desktop');
|
|
|
|
|
expect(rendered).toContain('REQUIRED BEFORE USING AGENTS\n\n 1. Start MCP');
|
|
|
|
|
expect(rendered).toContain(' Run this command before using Claude Code:');
|
|
|
|
|
expect(rendered).toContain(' RUN:');
|
|
|
|
|
expect(rendered).toContain(' If you need to stop MCP later:');
|
|
|
|
|
expect(rendered).toContain(`ktx mcp stop --project-dir ${tempDir}`);
|
|
|
|
|
expect(rendered).toContain('After that, try\n Ask your agent: "Use KTX to show me the available tables."');
|
|
|
|
|
expect(rendered).not.toContain('Verify');
|
|
|
|
|
expect(rendered).not.toContain('Project ready: yes');
|
|
|
|
|
expect(rendered).not.toContain('What you can do next');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('prints agent next actions inside the final ready summary during full setup', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-18 18:54:20 -04:00
|
|
|
agents: false,
|
|
|
|
|
target: 'claude-code',
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
runtime: async () => runtimeReady(tempDir),
|
|
|
|
|
context: async () => {
|
|
|
|
|
await writeKtxSetupContextState(tempDir, {
|
|
|
|
|
runId: 'setup-context-local-test',
|
|
|
|
|
status: 'completed',
|
|
|
|
|
primarySourceConnectionIds: [],
|
|
|
|
|
contextSourceConnectionIds: [],
|
|
|
|
|
reportIds: [],
|
|
|
|
|
artifactPaths: [],
|
|
|
|
|
retryableFailedTargets: [],
|
2026-05-21 02:38:18 +02:00
|
|
|
commands: contextBuildCommands(tempDir),
|
2026-05-18 18:54:20 -04:00
|
|
|
});
|
|
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'context'] });
|
|
|
|
|
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
const output = testIo.stdout();
|
2026-05-19 15:21:49 +02:00
|
|
|
expect(output).toContain('Claude Code · Project scope');
|
|
|
|
|
expect(output).toContain(join(tempDir, '.mcp.json'));
|
|
|
|
|
expect(output).toContain('Requires MCP to be started.');
|
|
|
|
|
expect(output).toContain('Analytics skill installed.');
|
|
|
|
|
expect(output).not.toContain('Agent integration complete');
|
2026-05-18 18:54:20 -04:00
|
|
|
expect(output).toContain('Finish KTX agent setup');
|
|
|
|
|
expect(output).not.toContain('KTX project ready');
|
|
|
|
|
expect(output).toContain('REQUIRED BEFORE USING AGENTS');
|
|
|
|
|
expect(output).toContain('Run this command before using Claude Code:');
|
|
|
|
|
expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
|
|
|
|
expect(output).not.toContain('Finish agent setup');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-22 18:18:47 +02:00
|
|
|
it('emits debug telemetry for setup steps without project paths', async () => {
|
|
|
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
|
|
|
vi.stubEnv('CI', '');
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
testIo.io.stdout.isTTY = true;
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
runtime: async () => runtimeReady(tempDir),
|
|
|
|
|
context: async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(testIo.stderr()).toContain('"event":"setup_step"');
|
|
|
|
|
expect(testIo.stderr()).toContain('"step":"project"');
|
|
|
|
|
expect(testIo.stderr()).toContain('"step":"models"');
|
|
|
|
|
expect(testIo.stderr()).not.toContain(tempDir);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('prints the setup shell intro for auto-created run mode', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
expect(testIo.stdout()).toContain('KTX setup');
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(testIo.stdout()).toContain(`Project: ${tempDir}`);
|
|
|
|
|
expect(testIo.stdout()).toContain('Project ready: yes');
|
|
|
|
|
expect(testIo.stdout()).toContain('What you can do next:');
|
|
|
|
|
expect(testIo.stdout()).toContain('Connect data, then build context.');
|
2026-05-10 23:51:24 +02:00
|
|
|
expect(testIo.stdout()).toContain('ktx setup');
|
|
|
|
|
expect(testIo.stdout()).not.toContain('ktx agent context --json');
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(testIo.stdout()).not.toContain('Optional MCP:');
|
|
|
|
|
expect(testIo.stderr()).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
it('preserves a newly created missing project directory when a later setup step fails', async () => {
|
2026-05-19 18:18:38 +02:00
|
|
|
const projectDir = join(tempDir, 'missing-project');
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
databases: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
runtime: async () => ({ status: 'failed', projectDir, requirements: { features: ['core'], requirements: [] } }),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
await expect(stat(projectDir)).resolves.toBeDefined();
|
|
|
|
|
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
|
|
|
await expect(stat(join(projectDir, '.ktx'))).resolves.toBeDefined();
|
2026-05-19 18:18:38 +02:00
|
|
|
});
|
|
|
|
|
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
it('preserves KTX scaffold files in an initially empty project directory when setup fails', async () => {
|
2026-05-19 18:18:38 +02:00
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
databases: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
runtime: async () => ({ status: 'failed', projectDir: tempDir, requirements: { features: ['core'], requirements: [] } }),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
fix(cli): preserve project artifacts when ktx setup steps fail (#229)
ktx setup wiped ktx.yaml, .ktx/setup/state.json, wiki/, semantic-layer/,
raw-sources/, and .git/ — or removed the entire project dir — whenever any
single source in the context-build step failed, destroying hours of ingest
work and the persisted resume state. The cleanup hint was designed for an
"early abort, leave no trace" semantic but was applied indiscriminately to
every later step failure, in direct conflict with the .ktx/setup/state.json
resume mechanism.
Drop the cleanup mechanism entirely (KtxSetupCreatedProjectCleanup,
cleanupForFolderState, createProjectWithCleanup, cleanupCreatedProjectScaffold,
and the createdProjectCleanup plumbing through KtxSetupProjectResult). Step
failures now return non-zero without touching the filesystem, so re-running
ktx setup continues from completed steps and only re-attempts failed sources.
Rewrites the two tests that documented the wipe behavior to assert
preservation, and adds a regression test that simulates partial context-build
artifacts (state.json, wiki/, semantic-layer/) and verifies all survive a
failed context step.
Refs KLO-719
2026-05-28 15:17:06 +02:00
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
|
|
|
await expect(stat(join(tempDir, '.ktx'))).resolves.toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('preserves partial context-build artifacts and resume state when the context step fails', async () => {
|
|
|
|
|
const projectDir = join(tempDir, 'partial-context');
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
databases: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir }),
|
|
|
|
|
runtime: async () => runtimeReady(projectDir),
|
|
|
|
|
context: async () => {
|
|
|
|
|
await mkdir(join(projectDir, '.ktx', 'setup'), { recursive: true });
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(projectDir, '.ktx', 'setup', 'state.json'),
|
|
|
|
|
JSON.stringify({ status: 'failed', retryableFailedTargets: [{ source: 'metabase' }] }),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
await mkdir(join(projectDir, 'wiki'), { recursive: true });
|
|
|
|
|
await writeFile(join(projectDir, 'wiki', 'postgres-warehouse.md'), '# warehouse\n', 'utf-8');
|
|
|
|
|
await mkdir(join(projectDir, 'semantic-layer'), { recursive: true });
|
|
|
|
|
await writeFile(join(projectDir, 'semantic-layer', 'orders.yaml'), 'name: orders\n', 'utf-8');
|
|
|
|
|
return { status: 'failed', projectDir };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
|
|
|
await expect(readFile(join(projectDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toContain('"status":"failed"');
|
|
|
|
|
await expect(readFile(join(projectDir, 'wiki', 'postgres-warehouse.md'), 'utf-8')).resolves.toContain('warehouse');
|
|
|
|
|
await expect(readFile(join(projectDir, 'semantic-layer', 'orders.yaml'), 'utf-8')).resolves.toContain('orders');
|
2026-05-19 18:18:38 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('preserves a pre-existing non-empty project directory when runtime setup fails', async () => {
|
|
|
|
|
await writeFile(join(tempDir, 'notes.txt'), 'keep me\n', 'utf-8');
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
databases: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
runtime: async () => ({ status: 'failed', projectDir: tempDir, requirements: { features: ['core'], requirements: [] } }),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
await expect(readFile(join(tempDir, 'notes.txt'), 'utf-8')).resolves.toBe('keep me\n');
|
|
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('shows demo near the bottom of the first setup intent menu before project creation', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const select = vi.fn(async (options: { options: Array<{ value: string; label: string }> }) => {
|
|
|
|
|
const labels = options.options.map((option) => option.label);
|
|
|
|
|
expect(labels).toEqual([
|
2026-05-10 23:51:24 +02:00
|
|
|
'Set up KTX for my data',
|
2026-05-10 23:12:26 +02:00
|
|
|
'Check setup status',
|
2026-05-11 16:13:30 -07:00
|
|
|
'Explore a pre-built KTX project',
|
2026-05-10 23:12:26 +02:00
|
|
|
'Exit',
|
|
|
|
|
]);
|
2026-05-11 16:13:30 -07:00
|
|
|
expect(labels.indexOf('Explore a pre-built KTX project')).toBe(labels.length - 2);
|
2026-05-10 23:12:26 +02:00
|
|
|
return 'exit';
|
|
|
|
|
});
|
|
|
|
|
const cancel = vi.fn();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ entryMenuDeps: { prompts: { select, cancel } } },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(select).toHaveBeenCalledWith(expect.objectContaining({ message: 'What do you want to do?' }));
|
|
|
|
|
expect(cancel).toHaveBeenCalledWith('Setup cancelled.');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows agent connection only when the selected setup project exists', async () => {
|
|
|
|
|
const missingIo = makeIo();
|
|
|
|
|
const existingIo = makeIo();
|
|
|
|
|
const missingSelect = vi.fn(async (options: { options: Array<{ value: string; label: string }> }) => {
|
2026-05-10 23:51:24 +02:00
|
|
|
expect(options.options.map((option) => option.label)).not.toContain('Connect a coding agent to KTX');
|
2026-05-10 23:12:26 +02:00
|
|
|
return 'exit';
|
|
|
|
|
});
|
|
|
|
|
const existingSelect = vi.fn(async (options: { options: Array<{ value: string; label: string }> }) => {
|
|
|
|
|
const labels = options.options.map((option) => option.label);
|
|
|
|
|
expect(labels).toEqual([
|
|
|
|
|
'Resume or change an existing setup',
|
2026-05-10 23:51:24 +02:00
|
|
|
'Create a new KTX project',
|
|
|
|
|
'Connect a coding agent to KTX',
|
2026-05-10 23:12:26 +02:00
|
|
|
'Check setup status',
|
2026-05-11 16:13:30 -07:00
|
|
|
'Explore a pre-built KTX project',
|
2026-05-10 23:12:26 +02:00
|
|
|
'Exit',
|
|
|
|
|
]);
|
|
|
|
|
return 'exit';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
missingIo.io,
|
|
|
|
|
{ entryMenuDeps: { prompts: { select: missingSelect, cancel: vi.fn() } } },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
existingIo.io,
|
|
|
|
|
{ entryMenuDeps: { prompts: { select: existingSelect, cancel: vi.fn() } } },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(missingSelect).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(existingSelect).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lets Back from project selection return to the first setup intent menu', async () => {
|
|
|
|
|
const entryChoices = ['setup', 'exit'];
|
|
|
|
|
const entryPrompts = {
|
|
|
|
|
select: vi.fn(async () => entryChoices.shift() ?? 'exit'),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const projectPrompts = {
|
|
|
|
|
select: vi.fn(async () => 'back'),
|
|
|
|
|
text: vi.fn(),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
makeIo().io,
|
|
|
|
|
{
|
|
|
|
|
entryMenuDeps: { prompts: entryPrompts },
|
|
|
|
|
project: { prompts: projectPrompts },
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(projectPrompts.select).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2026-05-13 12:00:08 +02:00
|
|
|
message: 'Where should KTX create the project?',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: expect.arrayContaining([expect.objectContaining({ value: 'back', label: 'Back' })]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(projectPrompts.select).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2026-05-13 12:00:08 +02:00
|
|
|
message: 'Where should KTX create the project?',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: expect.not.arrayContaining([expect.objectContaining({ value: 'exit', label: 'Exit' })]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(entryPrompts.select).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(entryPrompts.cancel).toHaveBeenCalledWith('Setup cancelled.');
|
|
|
|
|
expect(projectPrompts.cancel).not.toHaveBeenCalled();
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).rejects.toThrow();
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lets Back from new project creation return to the first setup intent menu', async () => {
|
2026-05-14 17:39:31 +02:00
|
|
|
const existingConfig = 'connections: {}\n';
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), existingConfig, 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const entryChoices = ['new-project', 'exit'];
|
|
|
|
|
const entryPrompts = {
|
|
|
|
|
select: vi.fn(async () => entryChoices.shift() ?? 'exit'),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const projectPrompts = {
|
|
|
|
|
select: vi.fn(async () => 'back'),
|
|
|
|
|
text: vi.fn(),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
makeIo().io,
|
|
|
|
|
{
|
|
|
|
|
entryMenuDeps: { prompts: entryPrompts },
|
|
|
|
|
project: { prompts: projectPrompts },
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(projectPrompts.select).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2026-05-10 23:51:24 +02:00
|
|
|
message: 'Where should KTX create the project?',
|
2026-05-10 23:12:26 +02:00
|
|
|
options: expect.arrayContaining([expect.objectContaining({ value: 'back', label: 'Back' })]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(entryPrompts.select).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(entryPrompts.cancel).toHaveBeenCalledWith('Setup cancelled.');
|
|
|
|
|
expect(projectPrompts.cancel).not.toHaveBeenCalled();
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(existingConfig);
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates a separate project when the existing setup menu chooses new project', async () => {
|
|
|
|
|
const existingProjectDir = join(tempDir, 'existing');
|
|
|
|
|
const newProjectDir = join(tempDir, 'fresh');
|
|
|
|
|
await mkdir(existingProjectDir, { recursive: true });
|
2026-05-14 17:39:31 +02:00
|
|
|
const existingConfig = 'connections: {}\n';
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeFile(join(existingProjectDir, 'ktx.yaml'), existingConfig, 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const projectChoices = ['custom', 'create'];
|
|
|
|
|
const projectPrompts = {
|
|
|
|
|
select: vi.fn(async () => projectChoices.shift() ?? 'exit'),
|
|
|
|
|
text: vi.fn(async () => newProjectDir),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const model = vi.fn(async (args: { projectDir: string }) => ({
|
|
|
|
|
status: 'skipped' as const,
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
}));
|
|
|
|
|
const embeddings = vi.fn(async (args: { projectDir: string }) => ({
|
|
|
|
|
status: 'skipped' as const,
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
}));
|
|
|
|
|
const databases = vi.fn(async (args: { projectDir: string }) => ({
|
|
|
|
|
status: 'skipped' as const,
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
}));
|
|
|
|
|
const sources = vi.fn(async (args: { projectDir: string }) => ({
|
|
|
|
|
status: 'skipped' as const,
|
|
|
|
|
projectDir: args.projectDir,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: existingProjectDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
makeIo().io,
|
|
|
|
|
{
|
|
|
|
|
entryMenuDeps: { prompts: { select: vi.fn(async () => 'new-project'), cancel: vi.fn() } },
|
|
|
|
|
project: { prompts: projectPrompts },
|
|
|
|
|
model,
|
|
|
|
|
embeddings,
|
|
|
|
|
databases,
|
|
|
|
|
sources,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(projectPrompts.text).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
2026-05-12 16:58:00 -07:00
|
|
|
message: 'Project folder path\n│ Press Escape to go back.\n│',
|
2026-05-10 23:51:24 +02:00
|
|
|
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
2026-05-10 23:12:26 +02:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(projectPrompts.select).toHaveBeenCalledWith(
|
2026-05-10 23:51:24 +02:00
|
|
|
expect.objectContaining({ message: 'Where should KTX create the project?' }),
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(stat(join(newProjectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
|
|
|
|
await expect(readFile(join(existingProjectDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(existingConfig);
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(model).toHaveBeenCalledWith(expect.objectContaining({ projectDir: newProjectDir }), expect.anything());
|
|
|
|
|
expect(embeddings).toHaveBeenCalledWith(expect.objectContaining({ projectDir: newProjectDir }), expect.anything());
|
|
|
|
|
expect(databases).toHaveBeenCalledWith(expect.objectContaining({ projectDir: newProjectDir }), expect.anything());
|
|
|
|
|
expect(sources).toHaveBeenCalledWith(expect.objectContaining({ projectDir: newProjectDir }), expect.anything());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not print navigation instructions immediately after confirming new project creation', async () => {
|
|
|
|
|
const existingProjectDir = join(tempDir, 'existing');
|
|
|
|
|
const newProjectDir = join(tempDir, 'fresh');
|
|
|
|
|
await mkdir(existingProjectDir, { recursive: true });
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(existingProjectDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const projectChoices = ['custom', 'create'];
|
|
|
|
|
const projectPrompts = {
|
|
|
|
|
select: vi.fn(async () => projectChoices.shift() ?? 'exit'),
|
|
|
|
|
text: vi.fn(async () => newProjectDir),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const model = vi.fn(async (args: { projectDir: string; showPromptInstructions?: boolean }) => {
|
|
|
|
|
expect(args.showPromptInstructions).toBe(false);
|
|
|
|
|
return { status: 'skipped' as const, projectDir: args.projectDir };
|
|
|
|
|
});
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: existingProjectDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
entryMenuDeps: { prompts: { select: vi.fn(async () => 'new-project'), cancel: vi.fn() } },
|
|
|
|
|
project: { prompts: projectPrompts },
|
|
|
|
|
model,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(testIo.stdout()).toContain(`Project: ${newProjectDir}\n`);
|
|
|
|
|
expect(testIo.stdout()).not.toContain(
|
|
|
|
|
'Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-11 22:03:20 -07:00
|
|
|
it('runs the demo tour when the first setup intent menu chooses demo', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
2026-05-11 22:03:20 -07:00
|
|
|
{ entryMenuDeps: { prompts: { select: vi.fn(async () => 'demo'), cancel: vi.fn() } } },
|
2026-05-10 23:12:26 +02:00
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-11 22:03:20 -07:00
|
|
|
expect(runDemoTour).toHaveBeenCalledWith(
|
2026-05-22 18:18:47 +02:00
|
|
|
{ inputMode: 'auto', cliVersion: '0.2.0' },
|
2026-05-10 23:12:26 +02:00
|
|
|
testIo.io,
|
2026-05-11 22:03:20 -07:00
|
|
|
expect.objectContaining({}),
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 19:23:35 +02:00
|
|
|
it('creates a project through run mode when --yes is selected', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).resolves.toBeDefined();
|
2026-05-12 16:26:23 -07:00
|
|
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
|
|
|
|
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(
|
|
|
|
|
`${JSON.stringify({ completed_steps: ['project', 'sources'] }, null, 2)}\n`,
|
|
|
|
|
);
|
2026-05-10 23:51:24 +02:00
|
|
|
expect(testIo.stdout()).toContain('KTX setup');
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(testIo.stdout()).toContain(`Project: ${tempDir}`);
|
|
|
|
|
expect(testIo.stdout()).toContain('Project ready: yes');
|
|
|
|
|
expect(testIo.stderr()).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns nonzero when project selection is missing in no-input mode even when optional sections are skipped', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
expect(testIo.stderr()).toContain('Missing setup choice');
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).rejects.toThrow();
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns nonzero when project selection is missing in non-interactive setup', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
expect(testIo.stderr()).toContain('Missing setup choice');
|
2026-05-10 23:51:24 +02:00
|
|
|
await expect(stat(join(tempDir, 'ktx.yaml'))).rejects.toThrow();
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('runs the Anthropic model step after project selection succeeds', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-13 08:42:38 -04:00
|
|
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(model).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-13 08:42:38 -04:00
|
|
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-13 08:42:38 -04:00
|
|
|
skipLlm: false,
|
|
|
|
|
}),
|
|
|
|
|
testIo.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('passes Vertex AI model setup args after project selection succeeds', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-13 08:42:38 -04:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-13 08:42:38 -04:00
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
llmBackend: 'vertex',
|
|
|
|
|
vertexProject: 'local-gcp-project',
|
|
|
|
|
vertexLocation: 'us-east5',
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-13 08:42:38 -04:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(model).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
llmBackend: 'vertex',
|
|
|
|
|
vertexProject: 'local-gcp-project',
|
|
|
|
|
vertexLocation: 'us-east5',
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
}),
|
|
|
|
|
testIo.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('runs the embedding setup step after the model step succeeds', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-11 15:50:34 +02:00
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
2026-05-13 08:42:38 -04:00
|
|
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
embeddingBackend: 'openai',
|
2026-05-13 08:42:38 -04:00
|
|
|
embeddingApiKeyEnv: 'OPENAI_API_KEY', // pragma: allowlist secret
|
2026-05-10 23:12:26 +02:00
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model, embeddings },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(embeddings).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
runtimeInstallPolicy: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
embeddingBackend: 'openai',
|
2026-05-13 08:42:38 -04:00
|
|
|
embeddingApiKeyEnv: 'OPENAI_API_KEY', // pragma: allowlist secret
|
2026-05-10 23:12:26 +02:00
|
|
|
skipEmbeddings: false,
|
|
|
|
|
}),
|
|
|
|
|
testIo.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
it('passes no-input runtime policy to the embeddings step', async () => {
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));
|
2026-05-19 19:23:35 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-11 15:50:34 +02:00
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-11 15:50:34 +02:00
|
|
|
agents: false,
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: false,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{ embeddings },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
expect(embeddings).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
runtimeInstallPolicy: 'never',
|
|
|
|
|
}),
|
|
|
|
|
io.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
it('prompts before installing the managed runtime by default during setup', async () => {
|
2026-05-16 11:39:43 +02:00
|
|
|
const io = makeIo();
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
const context = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));
|
2026-05-19 19:23:35 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-16 11:39:43 +02:00
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-16 11:39:43 +02:00
|
|
|
agents: false,
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
embeddings,
|
|
|
|
|
context,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
|
|
|
|
expect(embeddings).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
cliVersion: '0.2.0',
|
2026-05-17 10:27:29 +02:00
|
|
|
runtimeInstallPolicy: 'prompt',
|
2026-05-16 11:39:43 +02:00
|
|
|
}),
|
|
|
|
|
io.io,
|
|
|
|
|
);
|
|
|
|
|
expect(context).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
cliVersion: '0.2.0',
|
2026-05-17 10:27:29 +02:00
|
|
|
runtimeInstallPolicy: 'prompt',
|
2026-05-16 11:39:43 +02:00
|
|
|
}),
|
|
|
|
|
io.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('lets Back from embedding setup return to the model step instead of exiting', async () => {
|
|
|
|
|
const testIo = makeIo();
|
2026-05-19 19:23:35 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
const modelResults = [
|
|
|
|
|
{ status: 'ready' as const, projectDir: tempDir },
|
|
|
|
|
{ status: 'back' as const, projectDir: tempDir },
|
|
|
|
|
];
|
|
|
|
|
const model = vi.fn(async () => modelResults.shift() ?? { status: 'back' as const, projectDir: tempDir });
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'back' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model, embeddings },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(model).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(model).toHaveBeenNthCalledWith(2, expect.objectContaining({ forcePrompt: true }), testIo.io);
|
|
|
|
|
expect(embeddings).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-14 14:35:58 +02:00
|
|
|
it('lets Back from database selection return to embedding setup', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const testIo = makeIo();
|
2026-05-19 19:23:35 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
const modelResults = [
|
|
|
|
|
{ status: 'ready' as const, projectDir: tempDir },
|
|
|
|
|
{ status: 'back' as const, projectDir: tempDir },
|
|
|
|
|
];
|
|
|
|
|
const model = vi.fn(async () => modelResults.shift() ?? { status: 'back' as const, projectDir: tempDir });
|
|
|
|
|
const embeddingResults = [
|
|
|
|
|
{ status: 'ready' as const, projectDir: tempDir },
|
|
|
|
|
{ status: 'back' as const, projectDir: tempDir },
|
|
|
|
|
];
|
|
|
|
|
const embeddings = vi.fn(async () => embeddingResults.shift() ?? { status: 'back' as const, projectDir: tempDir });
|
|
|
|
|
const databasePrompts = {
|
2026-05-14 14:35:58 +02:00
|
|
|
multiselect: vi.fn(async () => ['back']),
|
2026-05-22 14:22:11 +02:00
|
|
|
autocompleteMultiselect: vi.fn(async () => ['back']),
|
2026-05-10 23:12:26 +02:00
|
|
|
select: vi.fn(async () => 'back'),
|
|
|
|
|
text: vi.fn(),
|
|
|
|
|
password: vi.fn(),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
model,
|
|
|
|
|
embeddings,
|
|
|
|
|
databasesDeps: { prompts: databasePrompts },
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(databasePrompts.select).not.toHaveBeenCalled();
|
|
|
|
|
expect(embeddings).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(embeddings).toHaveBeenNthCalledWith(2, expect.objectContaining({ forcePrompt: true }), testIo.io);
|
2026-05-14 01:43:06 +02:00
|
|
|
expect(testIo.stderr()).not.toContain('No databases selected.');
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lets Back from the first setup step return to the entry menu instead of exiting', async () => {
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
const testIo = makeIo();
|
|
|
|
|
|
|
|
|
|
const entryChoices = ['setup', 'exit'];
|
|
|
|
|
const entryPrompts = {
|
|
|
|
|
select: vi.fn(async () => entryChoices.shift() ?? 'exit'),
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'back' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'auto',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
showEntryMenu: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{
|
|
|
|
|
entryMenuDeps: { prompts: entryPrompts },
|
|
|
|
|
model,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(entryPrompts.select).toHaveBeenCalledTimes(2);
|
|
|
|
|
expect(entryPrompts.cancel).toHaveBeenCalledWith('Setup cancelled.');
|
|
|
|
|
expect(model).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('runs database setup after embeddings succeed', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
const databases = vi.fn(async () => ({
|
|
|
|
|
status: 'ready' as const,
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
connectionIds: ['warehouse'],
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-13 08:42:38 -04:00
|
|
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
embeddingBackend: 'openai',
|
2026-05-13 08:42:38 -04:00
|
|
|
embeddingApiKeyEnv: 'OPENAI_API_KEY', // pragma: allowlist secret
|
2026-05-10 23:12:26 +02:00
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseDrivers: ['postgres'],
|
|
|
|
|
databaseConnectionId: 'warehouse',
|
|
|
|
|
databaseUrl: 'env:DATABASE_URL',
|
|
|
|
|
databaseSchemas: ['public'],
|
2026-05-14 01:43:06 +02:00
|
|
|
enableQueryHistory: true,
|
|
|
|
|
queryHistoryWindowDays: 30,
|
|
|
|
|
queryHistoryMinExecutions: 12,
|
|
|
|
|
queryHistoryServiceAccountPatterns: ['^svc_'],
|
|
|
|
|
queryHistoryRedactionPatterns: ['(?i)secret'],
|
2026-05-10 23:12:26 +02:00
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model, embeddings, databases },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(databases).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
databaseDrivers: ['postgres'],
|
|
|
|
|
databaseConnectionId: 'warehouse',
|
|
|
|
|
databaseUrl: 'env:DATABASE_URL',
|
|
|
|
|
databaseSchemas: ['public'],
|
2026-05-14 01:43:06 +02:00
|
|
|
enableQueryHistory: true,
|
|
|
|
|
queryHistoryWindowDays: 30,
|
|
|
|
|
queryHistoryMinExecutions: 12,
|
|
|
|
|
queryHistoryServiceAccountPatterns: ['^svc_'],
|
|
|
|
|
queryHistoryRedactionPatterns: ['(?i)secret'],
|
2026-05-10 23:12:26 +02:00
|
|
|
skipDatabases: false,
|
|
|
|
|
}),
|
|
|
|
|
testIo.io,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('runs sources after database setup', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => {
|
|
|
|
|
calls.push('model');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async () => {
|
|
|
|
|
calls.push('embeddings');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async () => {
|
|
|
|
|
calls.push('databases');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async (args) => {
|
|
|
|
|
expect(args.runInitialSourceIngest).toBe(false);
|
|
|
|
|
calls.push('sources');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-24 19:29:37 +02:00
|
|
|
it('passes context-source skip selection from database setup into the sources step', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => {
|
|
|
|
|
calls.push('model');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async () => {
|
|
|
|
|
calls.push('embeddings');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async () => {
|
|
|
|
|
calls.push('databases');
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
connectionIds: ['warehouse'],
|
|
|
|
|
skipSources: true,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
sources: async (args) => {
|
|
|
|
|
expect(args.skipSources).toBe(true);
|
|
|
|
|
calls.push('sources');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
runtime: async () => {
|
|
|
|
|
calls.push('runtime');
|
|
|
|
|
return runtimeReady(tempDir);
|
|
|
|
|
},
|
|
|
|
|
context: async () => {
|
|
|
|
|
calls.push('context');
|
|
|
|
|
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'runtime', 'context']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-12 01:05:28 +02:00
|
|
|
it.each([
|
|
|
|
|
{
|
|
|
|
|
backend: 'vertex',
|
|
|
|
|
providerLines: [' backend: vertex', ' vertex:', ' project: kaelio-dev', ' location: us-east5'],
|
|
|
|
|
model: 'claude-sonnet-4-6',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
backend: 'gateway',
|
|
|
|
|
providerLines: [' backend: gateway', ' gateway:', ' api_key: env:AI_GATEWAY_API_KEY'],
|
|
|
|
|
model: 'anthropic/claude-sonnet-4-6',
|
|
|
|
|
},
|
|
|
|
|
])('adds a dbt source in non-interactive setup with existing $backend llm config', async (fixture) => {
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids:',
|
|
|
|
|
' - warehouse',
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:WAREHOUSE_URL',
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
...fixture.providerLines,
|
|
|
|
|
' models:',
|
|
|
|
|
` default: ${fixture.model}`,
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, { completed_steps: ['project', 'databases'] });
|
2026-05-12 01:05:28 +02:00
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-12 01:05:28 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
source: 'dbt',
|
|
|
|
|
sourceConnectionId: 'dbt-main',
|
|
|
|
|
sourceGitUrl: 'https://github.com/Kaelio/klo-dbt-demo',
|
|
|
|
|
sourceBranch: 'main',
|
|
|
|
|
sourceProjectName: 'orbit_analytics',
|
|
|
|
|
sourceWarehouseConnectionId: 'warehouse',
|
|
|
|
|
skipSources: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
sourcesDeps: { validateDbt: vi.fn(async () => ({ ok: true as const, detail: 'dbt project valid' })) },
|
|
|
|
|
context: vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir, runId: 'setup-context-test' })),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(io.stderr()).not.toContain('Anthropic');
|
|
|
|
|
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('dbt-main:');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-11 22:35:07 +02:00
|
|
|
it('does not fail context build when prerequisites were explicitly skipped and agents are skipped', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: postgres',
|
|
|
|
|
' url: env:DEMO_DATABASE_URL',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-11 22:35:07 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => {
|
|
|
|
|
calls.push('model');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async () => {
|
|
|
|
|
calls.push('embeddings');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async () => {
|
|
|
|
|
calls.push('databases');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async () => {
|
|
|
|
|
calls.push('sources');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources']);
|
|
|
|
|
expect(io.stderr()).not.toContain('KTX cannot build agent-ready context yet.');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('runs context after sources and before agents in full setup', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => {
|
|
|
|
|
calls.push('model');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async () => {
|
|
|
|
|
calls.push('embeddings');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async () => {
|
|
|
|
|
calls.push('databases');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async () => {
|
|
|
|
|
calls.push('sources');
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: async () => {
|
|
|
|
|
calls.push('runtime');
|
|
|
|
|
return runtimeReady(tempDir);
|
|
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
context: async () => {
|
|
|
|
|
calls.push('context');
|
|
|
|
|
return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
|
|
|
|
|
},
|
|
|
|
|
agents: async () => {
|
|
|
|
|
calls.push('agents');
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
2026-05-10 23:12:26 +02:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'runtime', 'context', 'agents']);
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-12 16:56:58 -04:00
|
|
|
it('commits setup config changes written by later setup steps', async () => {
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-12 16:56:58 -04:00
|
|
|
agents: false,
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
databases: async () => {
|
|
|
|
|
const configPath = join(tempDir, 'ktx.yaml');
|
|
|
|
|
const current = await readFile(configPath, 'utf-8');
|
|
|
|
|
await writeFile(
|
|
|
|
|
configPath,
|
|
|
|
|
current.replace(
|
|
|
|
|
'connections: {}',
|
|
|
|
|
['connections:', ' warehouse:', ' driver: postgres', ' url: env:DATABASE_URL'].join('\n'),
|
|
|
|
|
),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: async () => runtimeReady(tempDir),
|
2026-05-12 16:56:58 -04:00
|
|
|
context: async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }),
|
|
|
|
|
agents: async () => ({
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
2026-05-12 16:56:58 -04:00
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
const { stdout } = await execFileAsync('git', ['-C', tempDir, 'status', '--short', '--', 'ktx.yaml']);
|
|
|
|
|
expect(stdout).toBe('');
|
|
|
|
|
const committedConfig = await execFileAsync('git', ['-C', tempDir, 'show', 'HEAD:ktx.yaml']);
|
|
|
|
|
expect(committedConfig.stdout).toContain('warehouse:');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
it('runs agent setup without runtime or context in --agents mode', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: true,
|
|
|
|
|
target: 'codex',
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
databases: async () => ({ status: 'skipped', projectDir: tempDir }),
|
|
|
|
|
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: async () => {
|
|
|
|
|
calls.push('runtime');
|
2026-05-19 12:18:52 +02:00
|
|
|
throw new Error('runtime should not run');
|
2026-05-17 10:27:29 +02:00
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
context: async () => {
|
|
|
|
|
calls.push('context');
|
2026-05-19 12:18:52 +02:00
|
|
|
throw new Error('context should not run');
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
agents: async () => {
|
|
|
|
|
calls.push('agents');
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
2026-05-10 23:12:26 +02:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
expect(calls).toEqual(['agents']);
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
it('installs agents when non-interactive --agents finds context incomplete', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const io = makeIo();
|
2026-05-19 12:18:52 +02:00
|
|
|
const runtime = vi.fn(async () => runtimeReady(tempDir));
|
|
|
|
|
const context = vi.fn(async () => ({ status: 'skipped' as const, projectDir: tempDir }));
|
2026-05-10 23:12:26 +02:00
|
|
|
const agents = vi.fn(async () => ({
|
|
|
|
|
status: 'ready' as const,
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex' as const, scope: 'project' as const, mode: 'mcp-cli' as const }],
|
2026-05-10 23:12:26 +02:00
|
|
|
}));
|
2026-05-14 17:39:31 +02:00
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: true,
|
|
|
|
|
target: 'codex',
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: true,
|
|
|
|
|
skipEmbeddings: true,
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
skipSources: true,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
2026-05-19 12:18:52 +02:00
|
|
|
runtime,
|
|
|
|
|
context,
|
2026-05-10 23:12:26 +02:00
|
|
|
agents,
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-05-19 12:18:52 +02:00
|
|
|
).resolves.toBe(0);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
expect(runtime).not.toHaveBeenCalled();
|
|
|
|
|
expect(context).not.toHaveBeenCalled();
|
|
|
|
|
expect(agents).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-19 19:23:35 +02:00
|
|
|
it('runs non-TTY --agents with a target without requiring --no-input or --yes', async () => {
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
const agents = vi.fn(async () => ({
|
|
|
|
|
status: 'ready' as const,
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
installs: [{ target: 'claude-code' as const, scope: 'project' as const, mode: 'mcp' as const }],
|
|
|
|
|
}));
|
|
|
|
|
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
mode: 'auto',
|
|
|
|
|
agents: true,
|
|
|
|
|
target: 'claude-code',
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{ agents },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(agents).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: false,
|
|
|
|
|
agents: true,
|
|
|
|
|
target: 'claude-code',
|
|
|
|
|
scope: 'project',
|
|
|
|
|
mode: 'mcp',
|
|
|
|
|
}),
|
|
|
|
|
io.io,
|
|
|
|
|
);
|
|
|
|
|
expect(io.stderr()).not.toContain('Interactive setup requires a terminal');
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
it('routes a ready project menu selection to agent setup', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
2026-05-10 23:51:24 +02:00
|
|
|
await mkdir(join(tempDir, '.ktx', 'agents'), { recursive: true });
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids: []',
|
|
|
|
|
'connections: {}',
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
' backend: anthropic',
|
|
|
|
|
' models:',
|
|
|
|
|
' default: claude-sonnet-4-6',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
|
|
|
|
' backend: openai',
|
|
|
|
|
' model: text-embedding-3-small',
|
|
|
|
|
' dimensions: 1536',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, {
|
2026-05-17 10:27:29 +02:00
|
|
|
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'runtime', 'context', 'agents'],
|
2026-05-13 13:55:21 +02:00
|
|
|
});
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(tempDir, '.ktx/agents/install-manifest.json'),
|
2026-05-10 23:12:26 +02:00
|
|
|
JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
version: 1,
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
installedAt: '2026-05-07T00:00:00.000Z',
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
2026-05-10 23:12:26 +02:00
|
|
|
entries: [],
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-10 23:51:24 +02:00
|
|
|
await writeKtxSetupContextState(tempDir, {
|
2026-05-10 23:12:26 +02:00
|
|
|
runId: 'setup-context-local-ready',
|
|
|
|
|
status: 'completed',
|
|
|
|
|
startedAt: '2026-05-09T10:00:00.000Z',
|
|
|
|
|
updatedAt: '2026-05-09T10:02:00.000Z',
|
|
|
|
|
completedAt: '2026-05-09T10:02:00.000Z',
|
|
|
|
|
primarySourceConnectionIds: [],
|
|
|
|
|
contextSourceConnectionIds: [],
|
|
|
|
|
reportIds: [],
|
|
|
|
|
artifactPaths: [],
|
|
|
|
|
retryableFailedTargets: [],
|
2026-05-21 02:38:18 +02:00
|
|
|
commands: contextBuildCommands(tempDir),
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-17 10:27:29 +02:00
|
|
|
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
|
|
|
|
|
process.env.KTX_RUNTIME_ROOT = await writeReadyRuntime(tempDir);
|
|
|
|
|
try {
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-17 10:27:29 +02:00
|
|
|
agents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
|
|
|
|
cliVersion: '0.2.0',
|
|
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
2026-05-17 10:27:29 +02:00
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
|
|
|
|
|
model: async (args) => {
|
|
|
|
|
expect(args.skipLlm).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async (args) => {
|
|
|
|
|
expect(args.skipEmbeddings).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async (args) => {
|
|
|
|
|
expect(args.skipDatabases).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async (args) => {
|
|
|
|
|
expect(args.skipSources).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
runtime: async () => {
|
|
|
|
|
calls.push('runtime');
|
|
|
|
|
return runtimeReady(tempDir);
|
|
|
|
|
},
|
|
|
|
|
agents: async () => {
|
|
|
|
|
calls.push('agents');
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
|
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
2026-05-17 10:27:29 +02:00
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
} finally {
|
|
|
|
|
if (previousRuntimeRoot === undefined) {
|
|
|
|
|
delete process.env.KTX_RUNTIME_ROOT;
|
|
|
|
|
} else {
|
|
|
|
|
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
expect(calls).toEqual(['agents']);
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-10 23:13:17 -07:00
|
|
|
it('skips to agent setup when context is ready but agents are not configured', async () => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
const io = makeIo();
|
|
|
|
|
await writeFile(
|
|
|
|
|
join(tempDir, 'ktx.yaml'),
|
|
|
|
|
[
|
|
|
|
|
'setup:',
|
|
|
|
|
' database_connection_ids: []',
|
|
|
|
|
'connections: {}',
|
|
|
|
|
'llm:',
|
|
|
|
|
' provider:',
|
|
|
|
|
' backend: anthropic',
|
|
|
|
|
' models:',
|
|
|
|
|
' default: claude-sonnet-4-6',
|
|
|
|
|
'ingest:',
|
|
|
|
|
' embeddings:',
|
|
|
|
|
' backend: openai',
|
|
|
|
|
' model: text-embedding-3-small',
|
|
|
|
|
' dimensions: 1536',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
2026-05-13 13:55:21 +02:00
|
|
|
await writeKtxSetupState(tempDir, {
|
|
|
|
|
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'context'],
|
|
|
|
|
});
|
2026-05-10 23:13:17 -07:00
|
|
|
await writeKtxSetupContextState(tempDir, {
|
|
|
|
|
runId: 'setup-context-local-ready',
|
|
|
|
|
status: 'completed',
|
|
|
|
|
startedAt: '2026-05-09T10:00:00.000Z',
|
|
|
|
|
updatedAt: '2026-05-09T10:02:00.000Z',
|
|
|
|
|
completedAt: '2026-05-09T10:02:00.000Z',
|
|
|
|
|
primarySourceConnectionIds: [],
|
|
|
|
|
contextSourceConnectionIds: [],
|
|
|
|
|
reportIds: [],
|
|
|
|
|
artifactPaths: [],
|
|
|
|
|
retryableFailedTargets: [],
|
2026-05-21 02:38:18 +02:00
|
|
|
commands: contextBuildCommands(tempDir),
|
2026-05-10 23:13:17 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const readyMenuSelect = vi.fn();
|
|
|
|
|
await expect(
|
|
|
|
|
runKtxSetup(
|
|
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:13:17 -07:00
|
|
|
agents: false,
|
|
|
|
|
inputMode: 'auto',
|
|
|
|
|
yes: false,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:13:17 -07:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
readyMenuDeps: { prompts: { select: readyMenuSelect, cancel: vi.fn() } },
|
|
|
|
|
model: async (args) => {
|
|
|
|
|
expect(args.skipLlm).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
embeddings: async (args) => {
|
|
|
|
|
expect(args.skipEmbeddings).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
databases: async (args) => {
|
|
|
|
|
expect(args.skipDatabases).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
|
|
|
|
sources: async (args) => {
|
|
|
|
|
expect(args.skipSources).toBe(true);
|
|
|
|
|
return { status: 'skipped', projectDir: tempDir };
|
|
|
|
|
},
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime: async () => {
|
|
|
|
|
calls.push('runtime');
|
|
|
|
|
return runtimeReady(tempDir);
|
|
|
|
|
},
|
2026-05-10 23:13:17 -07:00
|
|
|
agents: async () => {
|
|
|
|
|
calls.push('agents');
|
|
|
|
|
return {
|
|
|
|
|
status: 'ready',
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
|
2026-05-10 23:13:17 -07:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
|
|
|
|
expect(readyMenuSelect).not.toHaveBeenCalled();
|
2026-05-19 12:18:52 +02:00
|
|
|
expect(calls).toEqual(['agents']);
|
2026-05-10 23:13:17 -07:00
|
|
|
});
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
it('runs only project resolution and agent setup in --agents mode', async () => {
|
2026-05-10 23:12:26 +02:00
|
|
|
const io = makeIo();
|
2026-05-17 10:27:29 +02:00
|
|
|
const runtime = vi.fn(async () => runtimeReady(tempDir));
|
2026-05-10 23:12:26 +02:00
|
|
|
const context = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir, runId: 'setup-context-local-test' }));
|
|
|
|
|
const agents = vi.fn(async () => ({
|
|
|
|
|
status: 'ready' as const,
|
|
|
|
|
projectDir: tempDir,
|
feat(setup): add Claude Desktop target and MCP-first agent setup (#114)
* feat(setup): add Claude Desktop target and MCP-first agent setup
Adds `ktx mcp stdio` and a `claude-desktop` setup target that generates a
local plugin ZIP wiring the analytics skill and a stdio MCP config. Replaces
the CLI-only agent install mode with MCP+analytics (default) and an optional
admin CLI skill, renames the research skill to analytics, and lets interactive
setup pick project vs global scope when every target supports it. Extracts a
shared MCP server factory used by both HTTP and stdio entrypoints.
* Add MCP agent client setup support
* Polish setup output formatting
* Add MCP tool polish design spec
Design for slimming the MCP-registered surface from 25 to 11 tools,
introducing memory_ingest, applying the per-tool polish kit (annotations,
outputSchema, .describe(), in-band error wrapping, union-drift fixes,
type-narrowed jsonToolResult), emitting progress notifications on
sql_execution + sl_query, and refining the ktx-analytics SKILL.md to
match.
* Refine MCP tool polish design spec after adversarial review iteration 1
* Refine MCP tool polish design spec after adversarial review iteration 2
* Refine MCP tool polish design spec after adversarial review iteration 3
* refactor(context): rename memory capture service to ingest
* feat(mcp): slim research tool surface
* refactor(mcp): remove admin ports from server factory
* refactor(cli): rename text ingest memory port
* docs: update analytics skill for memory ingest
* chore: verify mcp surface rename
* Add MCP tool polish v1 surface change plan
* feat(context): polish mcp tool metadata
* fix(context): enforce resolved semantic layer compute sources
* feat(context): emit mcp query progress stages
* fix(context): keep mcp progress event internal
* Add MCP tool polish v1 metadata & progress plan
* Fix CI snapshot and docs checks
2026-05-16 11:39:55 +02:00
|
|
|
installs: [{ target: 'universal' as const, scope: 'project' as const, mode: 'mcp-cli' as const }],
|
2026-05-10 23:12:26 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: true,
|
|
|
|
|
target: 'universal',
|
|
|
|
|
agentScope: 'project',
|
|
|
|
|
inputMode: 'disabled',
|
|
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
skipDatabases: false,
|
|
|
|
|
skipSources: false,
|
|
|
|
|
skipAgents: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
},
|
|
|
|
|
io.io,
|
|
|
|
|
{
|
|
|
|
|
model: async () => {
|
|
|
|
|
throw new Error('model should not run');
|
|
|
|
|
},
|
2026-05-17 10:27:29 +02:00
|
|
|
runtime,
|
2026-05-10 23:12:26 +02:00
|
|
|
context,
|
|
|
|
|
agents,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(0);
|
|
|
|
|
|
2026-05-19 12:18:52 +02:00
|
|
|
expect(runtime).not.toHaveBeenCalled();
|
|
|
|
|
expect(context).not.toHaveBeenCalled();
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(agents).toHaveBeenCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not run embedding setup when the model step fails', async () => {
|
|
|
|
|
const testIo = makeIo();
|
|
|
|
|
const model = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));
|
|
|
|
|
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
|
|
|
|
|
|
|
|
|
await expect(
|
2026-05-10 23:51:24 +02:00
|
|
|
runKtxSetup(
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
|
|
|
|
command: 'run',
|
|
|
|
|
projectDir: tempDir,
|
2026-05-19 19:23:35 +02:00
|
|
|
mode: 'auto',
|
2026-05-10 23:12:26 +02:00
|
|
|
agents: false,
|
|
|
|
|
skipAgents: true,
|
|
|
|
|
inputMode: 'disabled',
|
2026-05-19 19:23:35 +02:00
|
|
|
yes: true,
|
2026-05-11 15:50:34 +02:00
|
|
|
cliVersion: '0.2.0',
|
2026-05-13 08:42:38 -04:00
|
|
|
anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
|
2026-05-19 19:23:35 +02:00
|
|
|
llmModel: 'claude-sonnet-4-6',
|
2026-05-10 23:12:26 +02:00
|
|
|
skipLlm: false,
|
|
|
|
|
skipEmbeddings: false,
|
|
|
|
|
databaseSchemas: [],
|
|
|
|
|
skipDatabases: true,
|
|
|
|
|
},
|
|
|
|
|
testIo.io,
|
|
|
|
|
{ model, embeddings },
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBe(1);
|
|
|
|
|
|
2026-05-19 19:23:35 +02:00
|
|
|
expect(model).toHaveBeenCalledTimes(1);
|
2026-05-10 23:12:26 +02:00
|
|
|
expect(embeddings).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|