mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
refactor: enforce ktx naming and AGENTS.md compliance sweep (#289)
Align the tree with AGENTS.md/CLAUDE.md conventions: - Rewrite user-facing strings, docs, and tests to lowercase `ktx` (no bare uppercase `KTX` tokens remain outside literal identifiers). - Drop the legacy `historicSql` migration path and its now-unused helpers, per the no-backward-compat rule. - Remove `as unknown as` / `any` casts: narrow `BaseTool` generics to `z.ZodObject`, add a typed `createLookerClient`, and delete the dead `getParametersSchema`/`toAnthropicFormat` pre-AI-SDK helpers. - Use `InvalidArgumentError` for Commander parse failures. - Finish the adapter→connector prose conversion in the `ktx.yaml` docs while keeping the literal `adapters` config key.
This commit is contained in:
parent
005c5fc860
commit
00cdf2de90
237 changed files with 844 additions and 974 deletions
|
|
@ -82,7 +82,7 @@ describe('admin Commander tree', () => {
|
|||
try {
|
||||
await expect(runKtxCli(['admin', 'init', projectDir], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
expect(testIo.stdout()).toContain(`Initialized ktx project at ${projectDir}`);
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.not.toContain('project:');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
|
|
@ -103,14 +103,14 @@ describe('admin Commander tree', () => {
|
|||
runKtxCli(['--project-dir', projectDir, 'admin', 'init'], testIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
expect(testIo.stdout()).toContain(`Initialized ktx project at ${projectDir}`);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('prints config schema without requiring a KTX project directory', async () => {
|
||||
it('prints config schema without requiring a ktx project directory', async () => {
|
||||
const { mkdtemp, rm } = await import('node:fs/promises');
|
||||
const { tmpdir } = await import('node:os');
|
||||
const { join } = await import('node:path');
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ describe('walkCommandTree', () => {
|
|||
|
||||
it('captures required, optional, and variadic arguments', () => {
|
||||
const command = new Command('scan')
|
||||
.argument('<connectionId>', 'KTX connection id')
|
||||
.argument('<connectionId>', 'ktx connection id')
|
||||
.argument('[schemas...]', 'Schemas');
|
||||
|
||||
expect(walkCommandTree(command).arguments).toEqual(['<connectionId>', '[schemas...]']);
|
||||
|
|
@ -56,7 +56,7 @@ describe('walkCommandTree', () => {
|
|||
it('walks registered commands without applying hidden-command policy', () => {
|
||||
const root = new Command('ktx');
|
||||
root.command('scan', { hidden: true }).description('Run a standalone connection scan');
|
||||
const ingest = root.command('ingest').description('Build or inspect KTX context');
|
||||
const ingest = root.command('ingest').description('Build or inspect ktx context');
|
||||
ingest.command('run', { hidden: true }).description('Run local ingest by adapter');
|
||||
ingest.command('watch', { hidden: true }).description('Open a stored visual report');
|
||||
ingest.command('status').description('Print status');
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe('registerMcpCommands', () => {
|
|||
registerMcpCommands(program, context);
|
||||
|
||||
await expect(program.parseAsync(['mcp', 'start', '--host', '0.0.0.0'], { from: 'user' })).rejects.toThrow(
|
||||
'Binding KTX MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN',
|
||||
'Binding ktx MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN',
|
||||
);
|
||||
expect(startDaemon).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -80,11 +80,11 @@ describe('registerMcpCommands', () => {
|
|||
expect(startDaemon).toHaveBeenCalledTimes(1);
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
[
|
||||
'KTX MCP daemon already running: http://127.0.0.1:7878/mcp',
|
||||
'ktx MCP daemon already running: http://127.0.0.1:7878/mcp',
|
||||
'',
|
||||
'KTX is ready for configured agents.',
|
||||
'Open your agent for this KTX project and ask a data question, for example:',
|
||||
' "Use KTX to show me the available tables and metrics."',
|
||||
'ktx is ready for configured agents.',
|
||||
'Open your agent for this ktx project and ask a data question, for example:',
|
||||
' "Use ktx to show me the available tables and metrics."',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
|
@ -112,10 +112,10 @@ describe('registerMcpCommands', () => {
|
|||
await program.parseAsync(['--project-dir', '/tmp/ktx-started', 'mcp', 'start'], { from: 'user' });
|
||||
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('KTX MCP daemon started: http://127.0.0.1:7878/mcp\n\nKTX is ready for configured agents.'),
|
||||
expect.stringContaining('ktx MCP daemon started: http://127.0.0.1:7878/mcp\n\nktx is ready for configured agents.'),
|
||||
);
|
||||
expect(context.io.stdout.write).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"Use KTX to show me the available tables and metrics."'),
|
||||
expect.stringContaining('"Use ktx to show me the available tables and metrics."'),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('KtxClickHouseDialect', () => {
|
|||
expect(dialect.formatTableName({ catalog: null, db: null, name: 'events' })).toBe('`events`');
|
||||
});
|
||||
|
||||
it('maps nullable and low-cardinality ClickHouse types to KTX dimension types', () => {
|
||||
it('maps nullable and low-cardinality ClickHouse types to ktx dimension types', () => {
|
||||
expect(dialect.mapToDimensionType('Nullable(DateTime64(3))')).toBe('time');
|
||||
expect(dialect.mapToDimensionType('LowCardinality(Nullable(String))')).toBe('string');
|
||||
expect(dialect.mapToDimensionType('UInt64')).toBe('number');
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('KtxMysqlDialect', () => {
|
|||
expect(dialect.formatTableName({ catalog: null, db: null, name: 'orders' })).toBe('`orders`');
|
||||
});
|
||||
|
||||
it('maps native MySQL types to KTX dimension types', () => {
|
||||
it('maps native MySQL types to ktx dimension types', () => {
|
||||
expect(dialect.mapToDimensionType('tinyint(1)')).toBe('boolean');
|
||||
expect(dialect.mapToDimensionType('int')).toBe('number');
|
||||
expect(dialect.mapToDimensionType('decimal(10,2)')).toBe('number');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('KtxPostgresDialect', () => {
|
|||
expect(dialect.formatTableName({ catalog: null, db: null, name: 'orders' })).toBe('"orders"');
|
||||
});
|
||||
|
||||
it('maps native PostgreSQL types to KTX dimension types', () => {
|
||||
it('maps native PostgreSQL types to ktx dimension types', () => {
|
||||
expect(dialect.mapToDimensionType('timestamp with time zone')).toBe('time');
|
||||
expect(dialect.mapToDimensionType('numeric(12,2)')).toBe('number');
|
||||
expect(dialect.mapToDimensionType('uuid')).toBe('string');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('KtxSqliteDialect', () => {
|
|||
expect(dialect.formatTableName({ catalog: 'ignored', db: 'ignored', name: 'orders' })).toBe('"orders"');
|
||||
});
|
||||
|
||||
it('maps native SQLite types to KTX dimension types', () => {
|
||||
it('maps native SQLite types to ktx dimension types', () => {
|
||||
expect(dialect.mapToDimensionType('INTEGER')).toBe('number');
|
||||
expect(dialect.mapToDimensionType('numeric(10,2)')).toBe('number');
|
||||
expect(dialect.mapToDimensionType('timestamp')).toBe('time');
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ describe('KtxSqlServerDialect', () => {
|
|||
expect(dialect.formatTableName({ catalog: null, db: null, name: 'events' })).toBe('[events]');
|
||||
});
|
||||
|
||||
it('maps SQL Server types to KTX dimension types', () => {
|
||||
it('maps SQL Server types to ktx dimension types', () => {
|
||||
expect(dialect.mapToDimensionType('datetime2')).toBe('time');
|
||||
expect(dialect.mapToDimensionType('decimal(18, 2)')).toBe('number');
|
||||
expect(dialect.mapToDimensionType('bigint')).toBe('number');
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ describe('extractProgressMessage', () => {
|
|||
});
|
||||
|
||||
it('returns null for non-progress output', () => {
|
||||
expect(extractProgressMessage('KTX scan completed\n')).toBeNull();
|
||||
expect(extractProgressMessage('ktx scan completed\n')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ describe('renderContextBuildView', () => {
|
|||
]);
|
||||
|
||||
const output = renderContextBuildView(state, { styled: false });
|
||||
expect(output).toContain('Building KTX context');
|
||||
expect(output).toContain('Building ktx context');
|
||||
expect(output).toContain('(0/2)');
|
||||
expect(output).toContain('○');
|
||||
expect(output).toContain('Databases:');
|
||||
|
|
@ -271,7 +271,7 @@ describe('renderContextBuildView', () => {
|
|||
|
||||
const output = renderContextBuildView(state, { styled: false });
|
||||
const lines = output.split('\n');
|
||||
const headerLine = lines.find((l) => l.includes('Building KTX context'))!;
|
||||
const headerLine = lines.find((l) => l.includes('Building ktx context'))!;
|
||||
const separatorLine = lines.find((l) => /^─+$/.test(l))!;
|
||||
expect(separatorLine.length).toBeGreaterThanOrEqual(headerLine.length);
|
||||
});
|
||||
|
|
@ -394,11 +394,11 @@ describe('renderContextBuildView', () => {
|
|||
{ connectionId: 'warehouse', driver: 'postgres', operation: 'database-ingest', debugCommand: '', steps: ['database-schema'] },
|
||||
]);
|
||||
state.primarySources[0].status = 'failed';
|
||||
state.primarySources[0].failureText = 'KTX lost its connection to PostgreSQL while reading schema for warehouse.';
|
||||
state.primarySources[0].failureText = 'ktx lost its connection to PostgreSQL while reading schema for warehouse.';
|
||||
|
||||
const output = renderContextBuildView(state, { styled: false });
|
||||
expect(output).toContain('✗');
|
||||
expect(output).toContain('KTX lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
expect(output).toContain('ktx lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
});
|
||||
|
||||
it('omits empty groups', () => {
|
||||
|
|
@ -897,7 +897,7 @@ describe('runContextBuild', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({ exitCode: 1 });
|
||||
expect(io.stdout()).toContain('KTX lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
expect(io.stdout()).toContain('ktx lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
expect(io.stdout()).toContain('network address unavailable (EADDRNOTAVAIL)');
|
||||
expect(io.stdout()).toContain('Retry: ktx setup --project-dir /tmp/project');
|
||||
expect(io.stdout()).not.toContain('BoundPool');
|
||||
|
|
@ -931,11 +931,11 @@ describe('runContextBuild', () => {
|
|||
|
||||
expect(result).toEqual({ exitCode: 1 });
|
||||
expect(io.stdout()).toContain(
|
||||
'KTX could not reach the local SQL analysis runtime while processing query history for warehouse.',
|
||||
'ktx could not reach the local SQL analysis runtime while processing query history for warehouse.',
|
||||
);
|
||||
expect(io.stdout()).toContain('connection refused (ECONNREFUSED)');
|
||||
expect(io.stdout()).toContain('Retry: ktx setup --project-dir /tmp/project');
|
||||
expect(io.stdout()).not.toContain('KTX lost its connection to PostgreSQL');
|
||||
expect(io.stdout()).not.toContain('ktx lost its connection to PostgreSQL');
|
||||
});
|
||||
|
||||
it('uses captured query-history stderr instead of generic failed-at detail', async () => {
|
||||
|
|
@ -944,11 +944,11 @@ describe('runContextBuild', () => {
|
|||
warehouse: { driver: 'postgres', context: { queryHistory: { enabled: true } } },
|
||||
});
|
||||
const executeTarget = vi.fn(async (target, _args, targetIo) => {
|
||||
targetIo.stdout.write('KTX scan completed\n');
|
||||
targetIo.stdout.write('ktx scan completed\n');
|
||||
targetIo.stdout.write('Mode: enriched\n');
|
||||
targetIo.stderr.write('Missing bundled Python runtime manifest: /tmp/assets/python/manifest.json\n');
|
||||
targetIo.stderr.write('In a source checkout, build the local runtime assets with: pnpm run artifacts:build\n');
|
||||
targetIo.stderr.write('Then retry the runtime-backed KTX command.\n');
|
||||
targetIo.stderr.write('Then retry the runtime-backed ktx command.\n');
|
||||
return {
|
||||
connectionId: target.connectionId,
|
||||
driver: target.driver,
|
||||
|
|
@ -976,7 +976,7 @@ describe('runContextBuild', () => {
|
|||
expect(result).toEqual({ exitCode: 1 });
|
||||
expect(io.stdout()).toContain('Missing bundled Python runtime manifest: /tmp/assets/python/manifest.json.');
|
||||
expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project --query-history');
|
||||
expect(io.stdout()).not.toContain('Then retry the runtime-backed KTX command');
|
||||
expect(io.stdout()).not.toContain('Then retry the runtime-backed ktx command');
|
||||
expect(io.stdout()).not.toContain('warehouse failed at query-history');
|
||||
expect(io.stdout().match(/Retry: /g)).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -999,7 +999,7 @@ describe('runContextBuild', () => {
|
|||
);
|
||||
|
||||
expect(result).toEqual({ exitCode: 1 });
|
||||
expect(io.stdout()).toContain('KTX lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
expect(io.stdout()).toContain('ktx lost its connection to PostgreSQL while reading schema for warehouse.');
|
||||
expect(io.stdout()).toContain('connection reset (ECONNRESET)');
|
||||
});
|
||||
|
||||
|
|
@ -1149,7 +1149,7 @@ describe('runContextBuild', () => {
|
|||
);
|
||||
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Building KTX context');
|
||||
expect(output).toContain('Building ktx context');
|
||||
expect(output).toContain('Project: /tmp/project');
|
||||
expect(output).toContain('Databases:');
|
||||
expect(output).toContain('warehouse');
|
||||
|
|
@ -1397,7 +1397,7 @@ describe('viewStateFromSourceProgress', () => {
|
|||
);
|
||||
|
||||
const output = renderContextBuildView(state, { styled: false });
|
||||
expect(output).toContain('Building KTX context');
|
||||
expect(output).toContain('Building ktx context');
|
||||
expect(output).toContain('Databases:');
|
||||
expect(output).toContain('warehouse');
|
||||
expect(output).toContain('42 tables');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { resolveKtxConfigReference, resolveKtxHomePath } from '../../../src/context/core/config-reference.js';
|
||||
|
||||
describe('KTX config references', () => {
|
||||
describe('ktx config references', () => {
|
||||
it('resolves env references without returning empty values', () => {
|
||||
expect(resolveKtxConfigReference('env:AI_GATEWAY_API_KEY', { AI_GATEWAY_API_KEY: ' gateway-key ' })).toBe(
|
||||
'gateway-key',
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ async function writeHistoricSqlProject(project: KtxLocalProject): Promise<KtxLoc
|
|||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' author: KTX Test <system@ktx.local>',
|
||||
' author: ktx Test <system@ktx.local>',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -193,7 +193,7 @@ async function writeHistoricSqlProject(project: KtxLocalProject): Promise<KtxLoc
|
|||
},
|
||||
},
|
||||
}),
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
'Seed schema shard',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function sdk(): LookerSdkPort {
|
|||
}
|
||||
|
||||
describe('DefaultLookerConnectionClientFactory', () => {
|
||||
it('resolves credentials by Looker connection id and creates a KTX Looker client', async () => {
|
||||
it('resolves credentials by Looker connection id and creates a ktx Looker client', async () => {
|
||||
const fakeSdk = sdk();
|
||||
const resolver: LookerCredentialResolver = {
|
||||
resolve: vi.fn().mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ describe('discoverLookerConnections', () => {
|
|||
});
|
||||
|
||||
describe('looker dialect and target validation helpers', () => {
|
||||
it('maps Looker dialect names to KTX connection types', () => {
|
||||
it('maps Looker dialect names to ktx connection types', () => {
|
||||
expect(lookerDialectToConnectionType('bigquery_standard_sql')).toBe('BIGQUERY');
|
||||
expect(lookerDialectToConnectionType('postgres')).toBe('POSTGRESQL');
|
||||
expect(lookerDialectToConnectionType('mssql')).toBeNull();
|
||||
|
|
@ -224,7 +224,7 @@ describe('computeLookerMappingDrift and validateLookerMappings', () => {
|
|||
).toEqual({
|
||||
ok: false,
|
||||
errors: [
|
||||
{ key: 'b2b_sandbox_bq', reason: 'KTX connection missing does not exist' },
|
||||
{ key: 'b2b_sandbox_bq', reason: 'ktx connection missing does not exist' },
|
||||
{
|
||||
key: 'pg_runtime',
|
||||
reason: 'Connection type LOOKER cannot be used as a Looker warehouse mapping target',
|
||||
|
|
@ -259,7 +259,7 @@ describe('collectExploreParseItems and projectParsedIdentifier', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('projects successful and failed parser rows into KTX parsed target tables', () => {
|
||||
it('projects successful and failed parser rows into ktx parsed target tables', () => {
|
||||
expect(
|
||||
projectParsedIdentifier({
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ describe('Looker staged runtime schemas', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('accepts slug-shaped connection ids inside KTX Looker runtime schemas', () => {
|
||||
it('accepts slug-shaped connection ids inside ktx Looker runtime schemas', () => {
|
||||
const parsedTargetTable = {
|
||||
ok: true as const,
|
||||
catalog: 'proj',
|
||||
|
|
@ -313,7 +313,7 @@ describe('Looker staged runtime schemas', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('rejects unsafe KTX Looker connection ids', () => {
|
||||
it('rejects unsafe ktx Looker connection ids', () => {
|
||||
expect(() =>
|
||||
parseLookerPullConfig({
|
||||
lookerConnectionId: '../prod-looker',
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ async function readMetabaseFile(name: string): Promise<string> {
|
|||
return readFile(join(metabaseDir, name), 'utf-8');
|
||||
}
|
||||
|
||||
describe('KTX Metabase client boundary', () => {
|
||||
it('keeps NestJS, server data-source base classes, and server-relative imports out of the KTX client', async () => {
|
||||
describe('ktx Metabase client boundary', () => {
|
||||
it('keeps NestJS, server data-source base classes, and server-relative imports out of the ktx client', async () => {
|
||||
const client = await readMetabaseFile('client.ts');
|
||||
expect(client).not.toContain(`@${'nestjs'}`);
|
||||
expect(client).not.toContain(`DataSource${'Client'}`);
|
||||
|
|
@ -19,7 +19,7 @@ describe('KTX Metabase client boundary', () => {
|
|||
expect(client).not.toContain('../../types/brand');
|
||||
});
|
||||
|
||||
it('keeps proxy implementation code out of the KTX v1 client', async () => {
|
||||
it('keeps proxy implementation code out of the ktx v1 client', async () => {
|
||||
const client = await readMetabaseFile('client.ts');
|
||||
expect(client).not.toContain(`network-${'proxy'}`);
|
||||
expect(client).not.toContain(`ssh${'2'}`);
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ describe('MetabaseClient admin auth helpers', () => {
|
|||
);
|
||||
|
||||
await expect(client.getPermissionGroups()).resolves.toEqual([{ id: 2, name: 'Administrators' }]);
|
||||
await expect(client.createApiKey({ name: 'KTX CLI test', groupId: 2 })).resolves.toBe(mintedMetabaseCredential);
|
||||
await expect(client.createApiKey({ name: 'ktx CLI test', groupId: 2 })).resolves.toBe(mintedMetabaseCredential);
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
|
|
@ -247,7 +247,7 @@ describe('MetabaseClient admin auth helpers', () => {
|
|||
'https://metabase.example.test/api/api-key',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'KTX CLI test', group_id: 2 }),
|
||||
body: JSON.stringify({ name: 'ktx CLI test', group_id: 2 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ describe('metabaseRuntimeConfigFromLocalConnection', () => {
|
|||
};
|
||||
|
||||
expect(() => metabaseRuntimeConfigFromLocalConnection('prod-metabase', connection)).toThrow(
|
||||
'Standalone KTX does not support proxy-bearing Metabase connections yet',
|
||||
'Standalone ktx does not support proxy-bearing Metabase connections yet',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ describe('validateMetabaseMappings', () => {
|
|||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
errors: [{ key: '2', reason: 'KTX connection missing-target does not exist' }],
|
||||
errors: [{ key: '2', reason: 'ktx connection missing-target does not exist' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -149,7 +149,7 @@ describe('validateMappingPhysicalMatch', () => {
|
|||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for unknown engines because KTX cannot validate them', () => {
|
||||
it('returns null for unknown engines because ktx cannot validate them', () => {
|
||||
expect(
|
||||
validateMappingPhysicalMatch(
|
||||
{ metabaseEngine: 'unknown-engine', metabaseDbName: 'X', metabaseHost: 'host' },
|
||||
|
|
@ -177,7 +177,7 @@ describe('computeMetabaseMappingPhysicalMismatches', () => {
|
|||
).toEqual([
|
||||
{
|
||||
mappingId: 'mapping-bad',
|
||||
reason: "Metabase database 'app' does not match KTX connection database 'other_app'",
|
||||
reason: "Metabase database 'app' does not match ktx connection database 'other_app'",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -216,7 +216,7 @@ describe('refreshMetabaseMapping', () => {
|
|||
physicalMismatches: [
|
||||
{
|
||||
mappingId: '2',
|
||||
reason: "Metabase database 'analytics' does not match KTX connection database 'wrong_database'",
|
||||
reason: "Metabase database 'analytics' does not match ktx connection database 'wrong_database'",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -282,7 +282,7 @@ describe('findBestMatch', () => {
|
|||
});
|
||||
|
||||
describe('METABASE_ENGINE_TO_CONNECTION_TYPE', () => {
|
||||
it('keeps the server-supported Metabase engine table in KTX', () => {
|
||||
it('keeps the server-supported Metabase engine table in ktx', () => {
|
||||
expect(METABASE_ENGINE_TO_CONNECTION_TYPE).toMatchObject({
|
||||
postgres: 'POSTGRESQL',
|
||||
bigquery: 'BIGQUERY',
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ function makeDeps(
|
|||
lockingService: { withLock: vi.fn(async (_key, fn) => fn()) },
|
||||
storage: {
|
||||
homeDir: join(runtime.configDir, '.ktx'),
|
||||
systemGitAuthor: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
systemGitAuthor: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
resolveUploadDir: (id) => join(runtime.homeDir, 'upload', id),
|
||||
resolvePullDir: (id) => join(runtime.homeDir, 'pull', id),
|
||||
resolveTranscriptDir: (id) => join(runtime.configDir, '.ktx/ingest-transcripts', id),
|
||||
|
|
@ -308,7 +308,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/custom-isolated.md'],
|
||||
'custom wiki',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -395,7 +395,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/legacy-isolated.md'],
|
||||
'legacy isolated wiki',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -486,7 +486,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/good.yaml'],
|
||||
'test: add good source',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -504,7 +504,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/bad.yaml'],
|
||||
'test: add bad source',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -606,7 +606,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
[`wiki/global/${sourceKey}-isolated.md`],
|
||||
`${sourceKey} wiki`,
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -682,7 +682,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
'---\nsummary: Account segments\nusage_mode: auto\nsl_refs:\n - mart_account_segments\n---\n\nARR is `mart_account_segments.total_contract_arr_cents`.\n',
|
||||
);
|
||||
currentSession.actions.push({ target: 'wiki', type: 'created', key: 'account-segments', detail: 'Account segments' });
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'wu wiki', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'wu wiki', 'ktx Test', 'system@ktx.local');
|
||||
}
|
||||
if (params.telemetryTags.unitKey === 'card-source') {
|
||||
await writeFile(
|
||||
|
|
@ -697,7 +697,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
detail: 'Dollar measure',
|
||||
targetConnectionId: 'warehouse',
|
||||
});
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/mart_account_segments.yaml'], 'wu source', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/mart_account_segments.yaml'], 'wu source', 'ktx Test', 'system@ktx.local');
|
||||
}
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
|
|
@ -740,7 +740,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await runtime.git.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml', 'wiki/global/account-segments.md'],
|
||||
'seed existing wiki body ref',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
const preRunHead = await runtime.git.revParseHead();
|
||||
|
|
@ -773,7 +773,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
'wu source rename',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -903,7 +903,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await mkdir(join(root, 'wiki/global'), { recursive: true });
|
||||
await writeFile(join(root, `wiki/global/${unitKey}.md`), `---\nsummary: ${unitKey}\nusage_mode: auto\n---\n\n${unitKey}\n`);
|
||||
currentSession.actions.push({ target: 'wiki', type: 'created', key: unitKey, detail: unitKey });
|
||||
await currentSession.gitService.commitFiles([`wiki/global/${unitKey}.md`], `wu ${unitKey}`, 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles([`wiki/global/${unitKey}.md`], `wu ${unitKey}`, 'ktx Test', 'system@ktx.local');
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
const runner = new IngestBundleRunner(deps);
|
||||
|
|
@ -950,7 +950,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
);
|
||||
addTouchedSlSource(currentSession.touchedSlSources, 'warehouse', 'orders');
|
||||
currentSession.actions.push({ target: 'sl', type: 'updated', key: 'orders', detail: suffix, targetConnectionId: 'warehouse' });
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/orders.yaml'], `wu ${suffix}`, 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/orders.yaml'], `wu ${suffix}`, 'ktx Test', 'system@ktx.local');
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
const runner = new IngestBundleRunner(deps);
|
||||
|
|
@ -1006,7 +1006,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
'---\nsummary: Projected orders\nusage_mode: auto\nsl_refs:\n - mart_account_segments\n---\n\nARR `mart_account_segments.total_contract_arr`.\n',
|
||||
);
|
||||
currentSession.actions.push({ target: 'wiki', type: 'created', key: 'projected-orders', detail: 'Projected orders' });
|
||||
await currentSession.gitService.commitFiles(['wiki/global/projected-orders.md'], 'wu projected wiki', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/projected-orders.md'], 'wu projected wiki', 'ktx Test', 'system@ktx.local');
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
const runner = new IngestBundleRunner(deps);
|
||||
|
|
@ -1042,7 +1042,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await mkdir(join(root, 'wiki/global'), { recursive: true });
|
||||
await writeFile(join(root, 'wiki/global/notion-page.md'), '---\nsummary: Notion page\nusage_mode: auto\nsl_refs:\n - missing_source\n---\n\nBody\n');
|
||||
currentSession.actions.push({ target: 'wiki', type: 'created', key: 'notion-page', detail: 'Notion page' });
|
||||
await currentSession.gitService.commitFiles(['wiki/global/notion-page.md'], 'wu notion', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/notion-page.md'], 'wu notion', 'ktx Test', 'system@ktx.local');
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
const runner = new IngestBundleRunner(deps);
|
||||
|
|
@ -1088,7 +1088,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
'wu source',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
} else {
|
||||
|
|
@ -1104,7 +1104,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
detail: 'Stale reconcile wiki page',
|
||||
rawPaths: ['cards/source.json'],
|
||||
});
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'reconcile wiki', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'reconcile wiki', 'ktx Test', 'system@ktx.local');
|
||||
}
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
|
|
@ -1167,7 +1167,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
detail: 'Account segments',
|
||||
rawPaths: ['cards/wiki.json'],
|
||||
});
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'wu wiki', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/account-segments.md'], 'wu wiki', 'ktx Test', 'system@ktx.local');
|
||||
}
|
||||
if (params.telemetryTags.unitKey === 'card-source') {
|
||||
await mkdir(join(root, 'semantic-layer/warehouse'), { recursive: true });
|
||||
|
|
@ -1187,7 +1187,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
'wu source',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -1302,7 +1302,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml', 'wiki/global/account-segments.md'],
|
||||
'valid artifacts with invalid provenance',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -1444,7 +1444,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
'name: orders\ngrain: [id]\ncolumns: [{name: id, type: string}]\njoins: []\nmeasures: []\n',
|
||||
);
|
||||
currentSession.actions.push({ target: 'sl', type: 'created', key: 'orders', detail: 'forbidden', targetConnectionId: 'warehouse' });
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/orders.yaml'], 'forbidden sl', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['semantic-layer/warehouse/orders.yaml'], 'forbidden sl', 'ktx Test', 'system@ktx.local');
|
||||
return { stopReason: 'natural' };
|
||||
}) as never;
|
||||
const runner = new IngestBundleRunner(deps);
|
||||
|
|
@ -1469,7 +1469,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
join(runtime.configDir, 'wiki/global/source-page.md'),
|
||||
'---\nsummary: Source page\nusage_mode: auto\n---\n\nSource page\n',
|
||||
);
|
||||
await runtime.git.commitFiles(['wiki/global/source-page.md'], 'seed source page', 'KTX Test', 'system@ktx.local');
|
||||
await runtime.git.commitFiles(['wiki/global/source-page.md'], 'seed source page', 'ktx Test', 'system@ktx.local');
|
||||
const preRunHead = await runtime.git.revParseHead();
|
||||
const { deps, adapter } = makeDeps(runtime);
|
||||
adapter.chunk.mockResolvedValue({
|
||||
|
|
@ -1501,7 +1501,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/account-segments.md'],
|
||||
'wu page ref',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -1517,7 +1517,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/source-page.md'],
|
||||
'wu delete source page',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -1582,7 +1582,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await runtime.git.commitFiles(
|
||||
['wiki/global/source-page.md', 'wiki/global/account-segments.md'],
|
||||
'seed inbound wiki refs',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
const preRunHead = await runtime.git.revParseHead();
|
||||
|
|
@ -1613,7 +1613,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/source-page.md'],
|
||||
'wu delete target page',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -1751,7 +1751,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/finance/orders.yaml'],
|
||||
'wu unauthorized target',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -1822,7 +1822,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
detail: 'Valid page',
|
||||
rawPaths: ['pages/source.json'],
|
||||
});
|
||||
await currentSession.gitService.commitFiles(['wiki/global/valid-page.md'], 'wu valid page', 'KTX Test', 'system@ktx.local');
|
||||
await currentSession.gitService.commitFiles(['wiki/global/valid-page.md'], 'wu valid page', 'ktx Test', 'system@ktx.local');
|
||||
} else {
|
||||
await mkdir(join(root, 'semantic-layer/finance'), { recursive: true });
|
||||
await writeFile(
|
||||
|
|
@ -1841,7 +1841,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/finance/reconcile_orders.yaml'],
|
||||
'reconcile unauthorized target',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
}
|
||||
|
|
@ -1959,7 +1959,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
`wu ${params.telemetryTags.unitKey}`,
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' };
|
||||
|
|
@ -2025,7 +2025,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await runtime.git.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml', 'wiki/global/account-segments.md'],
|
||||
'seed stale wiki body ref',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
|
||||
|
|
@ -2073,7 +2073,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
'wu source rename',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' as const };
|
||||
|
|
@ -2128,7 +2128,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await runtime.git.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml', 'wiki/global/account-segments.md'],
|
||||
'seed stale wiki body ref',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
const preRunHead = await runtime.git.revParseHead();
|
||||
|
|
@ -2168,7 +2168,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['semantic-layer/warehouse/mart_account_segments.yaml'],
|
||||
'wu source rename',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' as const };
|
||||
|
|
@ -2302,7 +2302,7 @@ describe('IngestBundleRunner isolated diff path', () => {
|
|||
await currentSession.gitService.commitFiles(
|
||||
['wiki/global/orders.md'],
|
||||
'wu orders',
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
);
|
||||
return { stopReason: 'natural' as const };
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ const buildRunner = (deps: ReturnType<typeof makeDeps> = makeDeps(), overrides:
|
|||
lockingService: deps.lockingService as any,
|
||||
storage: {
|
||||
homeDir: '/tmp/ktx-test',
|
||||
systemGitAuthor: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
systemGitAuthor: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
resolveUploadDir: (uploadId) => `/tmp/ktx-test/ingest-uploads/${uploadId}`,
|
||||
resolvePullDir: (jobId) => `/tmp/ktx-test/ingest-pulls/${jobId}`,
|
||||
resolveTranscriptDir: (jobId) => `/tmp/ktx-test/run/wu-transcripts/${jobId}`,
|
||||
|
|
@ -1518,7 +1518,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
...(buildRunner(deps) as any).deps,
|
||||
storage: {
|
||||
homeDir: tempRoot,
|
||||
systemGitAuthor: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
systemGitAuthor: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
resolveUploadDir: (uploadId: string) => join(tempRoot, 'ingest-uploads', uploadId),
|
||||
resolvePullDir: (jobId: string) => join(tempRoot, 'ingest-pulls', jobId),
|
||||
resolveTranscriptDir: (jobId: string) => join(tempRoot, 'run', 'wu-transcripts', jobId),
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ describe('ingest prompt assets', () => {
|
|||
expect(prompt).toContain('Do not create a duplicate contested artifact');
|
||||
});
|
||||
|
||||
it('uses product-neutral KTX runtime wording', async () => {
|
||||
it('uses product-neutral ktx runtime wording', async () => {
|
||||
const prompt = await readFile(
|
||||
new URL('../../../src/prompts/memory_agent_bundle_ingest_work_unit.md', import.meta.url),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
expect(prompt).toContain('KTX semantic-layer sources and/or knowledge wiki pages');
|
||||
expect(prompt).toContain('maps cleanly to KTX');
|
||||
expect(prompt).toContain('ktx semantic-layer sources and/or knowledge wiki pages');
|
||||
expect(prompt).toContain('maps cleanly to ktx');
|
||||
expect(prompt).not.toMatch(forbiddenProductPattern());
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function forbiddenProductPattern() {
|
|||
}
|
||||
|
||||
describe('ingest runtime assets', () => {
|
||||
it('resolves every reusable ingest skill from packaged KTX assets without server fallback', async () => {
|
||||
it('resolves every reusable ingest skill from packaged ktx assets without server fallback', async () => {
|
||||
const registry = new SkillsRegistryService({ skillsDir });
|
||||
const expected = [...new Set([...adapterSkillNames, ...adapterReconcileSkillNames])].sort();
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ describe('ingest runtime assets', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('loads page-triage and light-extraction prompts from packaged KTX prompt assets', async () => {
|
||||
it('loads page-triage and light-extraction prompts from packaged ktx prompt assets', async () => {
|
||||
const prompts = new PromptService({ promptsDir, partials: [] });
|
||||
|
||||
for (const promptName of pageTriagePromptNames) {
|
||||
|
|
@ -61,7 +61,7 @@ describe('ingest runtime assets', () => {
|
|||
await expect(prompts.loadPrompt('skills/light_extraction')).resolves.toContain('# Light Context Extraction');
|
||||
});
|
||||
|
||||
it('packages historic-SQL table digest guidance from KTX assets', async () => {
|
||||
it('packages historic-SQL table digest guidance from ktx assets', async () => {
|
||||
const registry = new SkillsRegistryService({ skillsDir });
|
||||
const skills = await registry.listSkills(['historic_sql_table_digest'], 'memory_agent');
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ describe('ingest runtime assets', () => {
|
|||
expect(body).not.toMatch(forbiddenProductPattern());
|
||||
});
|
||||
|
||||
it('packages historic-SQL patterns guidance from KTX assets', async () => {
|
||||
it('packages historic-SQL patterns guidance from ktx assets', async () => {
|
||||
const registry = new SkillsRegistryService({ skillsDir });
|
||||
const skills = await registry.listSkills(['historic_sql_patterns'], 'memory_agent');
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree: vi.fn().mockResolvedValue(undefined),
|
||||
slDisallowed: false,
|
||||
allowedTargetConnectionIds: new Set(['c1']),
|
||||
|
|
@ -82,7 +82,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree: vi.fn().mockRejectedValue(new Error('final artifact gates failed')),
|
||||
slDisallowed: false,
|
||||
allowedTargetConnectionIds: new Set(['c1']),
|
||||
|
|
@ -117,7 +117,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree: vi.fn().mockResolvedValue(undefined),
|
||||
slDisallowed: true,
|
||||
allowedTargetConnectionIds: new Set(['c1']),
|
||||
|
|
@ -158,7 +158,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree: vi.fn().mockResolvedValue(undefined),
|
||||
slDisallowed: false,
|
||||
allowedTargetConnectionIds: new Set(['warehouse']),
|
||||
|
|
@ -325,7 +325,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree,
|
||||
slDisallowed: false,
|
||||
allowedTargetConnectionIds: new Set(['c1']),
|
||||
|
|
@ -380,7 +380,7 @@ describe('integrateWorkUnitPatch', () => {
|
|||
patchPath,
|
||||
integrationGit: git,
|
||||
trace,
|
||||
author: { name: 'KTX Test', email: 'system@ktx.local' },
|
||||
author: { name: 'ktx Test', email: 'system@ktx.local' },
|
||||
validateAppliedTree: vi.fn().mockRejectedValue(new Error('final artifact gates failed')),
|
||||
slDisallowed: false,
|
||||
allowedTargetConnectionIds: new Set(['c1']),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ describe('runIsolatedWorkUnit', () => {
|
|||
run: async (child) => {
|
||||
await mkdir(join(child.workdir, 'wiki/global'), { recursive: true });
|
||||
await writeFile(join(child.workdir, 'wiki/global/a.md'), '---\nsummary: A\nusage_mode: auto\n---\n\nBody\n');
|
||||
await child.git.commitFiles(['wiki/global/a.md'], 'test: write wiki', 'KTX Test', 'system@ktx.local');
|
||||
await child.git.commitFiles(['wiki/global/a.md'], 'test: write wiki', 'ktx Test', 'system@ktx.local');
|
||||
return {
|
||||
unitKey: 'wu-1',
|
||||
status: 'success',
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ describe('canonical local ingest', () => {
|
|||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' author: KTX Test <system@ktx.local>',
|
||||
' author: ktx Test <system@ktx.local>',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -530,7 +530,7 @@ describe('canonical local ingest', () => {
|
|||
await historicProject.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/_schema/public.yaml',
|
||||
YAML.stringify({ tables: { orders: { table: 'public.orders', columns: [{ name: 'id', type: 'string' }] } } }),
|
||||
'KTX Test',
|
||||
'ktx Test',
|
||||
'system@ktx.local',
|
||||
'Seed schema shard',
|
||||
);
|
||||
|
|
@ -682,7 +682,7 @@ describe('canonical local ingest', () => {
|
|||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' author: KTX Test <system@ktx.local>',
|
||||
' author: ktx Test <system@ktx.local>',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -765,7 +765,7 @@ describe('canonical local ingest', () => {
|
|||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' author: KTX Test <system@ktx.local>',
|
||||
' author: ktx Test <system@ktx.local>',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -808,7 +808,7 @@ describe('canonical local ingest', () => {
|
|||
' state: sqlite',
|
||||
' search: sqlite-fts5',
|
||||
' git:',
|
||||
' author: KTX Test <system@ktx.local>',
|
||||
' author: ktx Test <system@ktx.local>',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
|
|||
|
|
@ -596,7 +596,7 @@ describe('local ingest', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('includes upload-capable KTX adapters in default local ingest adapters', () => {
|
||||
it('includes upload-capable ktx adapters in default local ingest adapters', () => {
|
||||
expect(createDefaultLocalIngestAdapters(project).map((adapter) => adapter.source)).toEqual(
|
||||
expect.arrayContaining(['dbt', 'metricflow', 'notion']),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('memory-flow acceptance scenarios', () => {
|
|||
it('renders a completed replay with a clear saved-memory completion line', () => {
|
||||
const output = renderScenario(successfulReplayScenario());
|
||||
|
||||
expect(output).toContain('KTX memory flow warehouse/metricflow done');
|
||||
expect(output).toContain('ktx memory flow warehouse/metricflow done');
|
||||
expect(output).toContain('Saved 3 memories from 4 raw files: 2 wiki pages, 1 SL updates.');
|
||||
expect(output).toContain('Commit: abc12345 Run: run-success Report: ingest-report.json');
|
||||
});
|
||||
|
|
@ -48,7 +48,7 @@ describe('memory-flow acceptance scenarios', () => {
|
|||
it('renders no ANSI color codes in the text fallback for terminals without color support', () => {
|
||||
const output = renderScenario(successfulReplayScenario(), 80);
|
||||
|
||||
expect(output).toContain('KTX memory flow warehouse/metricflow done');
|
||||
expect(output).toContain('ktx memory flow warehouse/metricflow done');
|
||||
expect(output).not.toMatch(/\u001b\[[0-9;]*m/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import type { MemoryFlowInteractionState, MemoryFlowViewModel } from '../../../.
|
|||
|
||||
function view(): MemoryFlowViewModel {
|
||||
return {
|
||||
title: 'KTX memory flow warehouse/metricflow running',
|
||||
title: 'ktx memory flow warehouse/metricflow running',
|
||||
subtitle: 'Run run-1 Sync sync-1',
|
||||
status: 'running',
|
||||
activeLine: 'active: WorkUnit orders step 2/4',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import type { MemoryFlowViewModel } from '../../../../src/context/ingest/memory-
|
|||
|
||||
function view(): MemoryFlowViewModel {
|
||||
return {
|
||||
title: 'KTX memory flow warehouse/metricflow done',
|
||||
title: 'ktx memory flow warehouse/metricflow done',
|
||||
subtitle: 'Run run-1 Sync sync-1',
|
||||
status: 'done',
|
||||
activeLine: 'active: complete',
|
||||
|
|
@ -128,7 +128,7 @@ describe('renderMemoryFlowInteractive', () => {
|
|||
|
||||
const output = renderMemoryFlowInteractive(view(), state, { terminalWidth: 140 });
|
||||
|
||||
expect(output).toContain('KTX memory flow warehouse/metricflow done');
|
||||
expect(output).toContain('ktx memory flow warehouse/metricflow done');
|
||||
expect(output).toContain('OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED');
|
||||
expect(output).toContain('[WORKUNITS]');
|
||||
expect(output).toContain('> orders');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { renderMemoryFlowReplay } from '../../../../src/context/ingest/memory-fl
|
|||
|
||||
function view(): MemoryFlowViewModel {
|
||||
return {
|
||||
title: 'KTX memory flow warehouse/metricflow done',
|
||||
title: 'ktx memory flow warehouse/metricflow done',
|
||||
subtitle: 'Run run-1 Sync sync-1',
|
||||
status: 'done',
|
||||
activeLine: 'active: complete',
|
||||
|
|
@ -79,7 +79,7 @@ describe('renderMemoryFlowReplay', () => {
|
|||
'OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED',
|
||||
);
|
||||
expect(renderMemoryFlowReplay(view(), { terminalWidth: 140 })).toMatchInlineSnapshot(`
|
||||
"KTX memory flow warehouse/metricflow done
|
||||
"ktx memory flow warehouse/metricflow done
|
||||
active: complete
|
||||
Run run-1 Sync sync-1
|
||||
OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
it('builds six readable columns from replay events', () => {
|
||||
const view = buildMemoryFlowViewModel(replayInput());
|
||||
|
||||
expect(view.title).toBe('KTX memory flow warehouse/metricflow done');
|
||||
expect(view.title).toBe('ktx memory flow warehouse/metricflow done');
|
||||
expect(view.activeLine).toBe('active: complete');
|
||||
expect(view.columns.map((column) => column.id)).toEqual([
|
||||
'source',
|
||||
|
|
@ -179,7 +179,7 @@ describe('buildMemoryFlowViewModel', () => {
|
|||
details: { actions: [], provenance: [], transcripts: [] },
|
||||
});
|
||||
|
||||
expect(view.title).toBe('KTX memory flow Warehouse + dbt + BI + Docs done');
|
||||
expect(view.title).toBe('ktx memory flow Warehouse + dbt + BI + Docs done');
|
||||
expect(view.columns.find((column) => column.id === 'source')?.counters[0]).toBe('Warehouse, dbt, BI, Docs');
|
||||
expect(view.completionLine).toContain('Saved 16 memories from 29 raw files');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function viewWithStatuses(statuses: Array<'waiting' | 'active' | 'complete' | 'w
|
|||
const ids = ['source', 'chunks', 'workUnits', 'actions', 'gates', 'saved'] as const;
|
||||
|
||||
return {
|
||||
title: 'KTX memory flow warehouse/metricflow running',
|
||||
title: 'ktx memory flow warehouse/metricflow running',
|
||||
subtitle: 'Run run-1 Sync sync-1',
|
||||
status: 'running',
|
||||
activeLine: 'active: WorkUnit orders',
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ describe('PageTriageService', () => {
|
|||
'Reusable outbound sequence:',
|
||||
'',
|
||||
'- Ask about current customer success expansion workflow.',
|
||||
'- Position KTX as AI search visibility for CS teams.',
|
||||
'- Position ktx as AI search visibility for CS teams.',
|
||||
'- Close with a discovery call request.',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
|
|
@ -233,7 +233,7 @@ describe('PageTriageService', () => {
|
|||
{
|
||||
candidateKey: 'cold-call-script',
|
||||
topic: 'Cold Call Script',
|
||||
assertion: 'Cold call outreach should position KTX around AI search visibility for CS teams.',
|
||||
assertion: 'Cold call outreach should position ktx around AI search visibility for CS teams.',
|
||||
rationale: 'The script gives a reusable outbound call sequence and positioning language.',
|
||||
evidenceChunkIds: ['00000000-0000-0000-0000-000000000101'],
|
||||
suggestedPageKey: 'cold-call-script',
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ describe('repo-fetch', () => {
|
|||
|
||||
const cacheGit = createSimpleGit(cacheDir);
|
||||
await cacheGit.addConfig('user.email', 'test@ktx.local');
|
||||
await cacheGit.addConfig('user.name', 'KTX Test');
|
||||
await cacheGit.addConfig('user.name', 'ktx Test');
|
||||
await writeFile(join(cacheDir, 'local-only.txt'), 'local commit\n', 'utf-8');
|
||||
await cacheGit.add('.');
|
||||
await cacheGit.commit('local-only divergent commit');
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('registers only exact KTX MCP tool ids and denies non-KTX tools', async () => {
|
||||
it('registers only exact ktx MCP tool ids and denies non-ktx tools', async () => {
|
||||
const query = vi.fn((_input: any) =>
|
||||
stream([
|
||||
initMessage({ tools: ['mcp__ktx__load_skill'], mcp_servers: [{ name: 'ktx', status: 'connected' }] }),
|
||||
|
|
@ -487,7 +487,7 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('allows host-discovered context during agent loops while requiring exact KTX MCP tools and servers', async () => {
|
||||
it('allows host-discovered context during agent loops while requiring exact ktx MCP tools and servers', async () => {
|
||||
const query = vi.fn((_input: any) =>
|
||||
stream([
|
||||
initMessage({
|
||||
|
|
@ -553,7 +553,7 @@ describe('ClaudeCodeKtxLlmRuntime', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('still rejects unexpected tools, missing KTX tools, plugins, and non-KTX MCP servers from init messages', async () => {
|
||||
it('still rejects unexpected tools, missing ktx tools, plugins, and non-ktx MCP servers from init messages', async () => {
|
||||
const query = vi.fn((_input: any) =>
|
||||
stream([
|
||||
initMessage({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { KtxIngestEmbeddingPortAdapter, KtxScanEmbeddingPortAdapter } from '../../../src/context/llm/embedding-port.js';
|
||||
|
||||
describe('KTX embedding port adapters', () => {
|
||||
describe('ktx embedding port adapters', () => {
|
||||
it('adapts LLM modules embeddings to ingest embedding port shape', async () => {
|
||||
const provider = {
|
||||
dimensions: 3,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
resolveLocalKtxLlmConfig,
|
||||
} from '../../../src/context/llm/local-config.js';
|
||||
|
||||
describe('local KTX LLM config', () => {
|
||||
describe('local ktx LLM config', () => {
|
||||
it('resolves env and file references into a KtxLlmConfig', () => {
|
||||
const config: KtxProjectLlmConfig = {
|
||||
provider: {
|
||||
|
|
@ -190,7 +190,7 @@ describe('local KTX LLM config', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('local KTX embedding config', () => {
|
||||
describe('local ktx embedding config', () => {
|
||||
it('resolves sentence-transformers config', () => {
|
||||
const config: KtxProjectEmbeddingConfig = {
|
||||
backend: 'sentence-transformers',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createLocalKtxLlmProviderFromConfig, createLocalKtxLlmRuntimeFromConfig } from '../../../src/context/llm/local-config.js';
|
||||
|
||||
describe('local KTX LLM runtime config', () => {
|
||||
describe('local ktx LLM runtime config', () => {
|
||||
it('creates a Claude Code runtime for claude-code backend without creating an AI SDK provider', () => {
|
||||
const runtime = createLocalKtxLlmRuntimeFromConfig(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
{
|
||||
"name": "connection_list",
|
||||
"title": "Connection List",
|
||||
"description": "List configured read-only data connections available to this KTX project. Use this before connection-scoped tools when the project may have multiple warehouses.",
|
||||
"description": "List configured read-only data connections available to this ktx project. Use this before connection-scoped tools when the project may have multiple warehouses.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
{
|
||||
"name": "wiki_search",
|
||||
"title": "Wiki Search",
|
||||
"description": "Search KTX wiki pages for reusable business context. Example: wiki_search({ query: \"revenue recognition\", limit: 5 }).",
|
||||
"description": "Search ktx wiki pages for reusable business context. Example: wiki_search({ query: \"revenue recognition\", limit: 5 }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
{
|
||||
"name": "wiki_read",
|
||||
"title": "Wiki Read",
|
||||
"description": "Read a KTX wiki page by key returned from wiki_search. Example: wiki_read({ key: \"global/revenue\" }).",
|
||||
"description": "Read a ktx wiki page by key returned from wiki_search. Example: wiki_read({ key: \"global/revenue\" }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1227,7 +1227,7 @@
|
|||
{
|
||||
"name": "discover_data",
|
||||
"title": "Discover Data",
|
||||
"description": "Search across KTX wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: \"monthly orders by customer\", connectionId: \"warehouse\", kinds: [\"sl_source\", \"table\"] }).",
|
||||
"description": "Search across ktx wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: \"monthly orders by customer\", connectionId: \"warehouse\", kinds: [\"sl_source\", \"table\"] }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1398,7 +1398,7 @@
|
|||
{
|
||||
"name": "sql_execution",
|
||||
"title": "SQL Execution",
|
||||
"description": "Execute one parser-validated read-only SQL query against a configured KTX connection. Example: sql_execution({ connectionId: \"warehouse\", sql: \"select count(*) from public.orders\", maxRows: 100 }).",
|
||||
"description": "Execute one parser-validated read-only SQL query against a configured ktx connection. Example: sql_execution({ connectionId: \"warehouse\", sql: \"select count(*) from public.orders\", maxRows: 100 }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -1472,7 +1472,7 @@
|
|||
{
|
||||
"name": "memory_ingest",
|
||||
"title": "Memory Ingest",
|
||||
"description": "Ingest free-form markdown knowledge into durable KTX memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: \"warehouse\", content: \"ARR is reported in cents in this warehouse.\" }).",
|
||||
"description": "Ingest free-form markdown knowledge into durable ktx memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: \"warehouse\", content: \"ARR is reported in cents in this warehouse.\" }).",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -987,7 +987,7 @@ describe('createKtxMcpServer', () => {
|
|||
expect(ingest.ingest).toHaveBeenCalledWith({
|
||||
userId: 'mcp-user',
|
||||
chatId: expect.stringMatching(/^mcp-/),
|
||||
userMessage: 'Ingest external knowledge into KTX memory.',
|
||||
userMessage: 'Ingest external knowledge into ktx memory.',
|
||||
assistantMessage: content,
|
||||
connectionId: '00000000-0000-4000-8000-000000000001',
|
||||
sourceType: 'external_ingest',
|
||||
|
|
@ -996,7 +996,7 @@ describe('createKtxMcpServer', () => {
|
|||
const cliEquivalentInput: MemoryAgentInput = {
|
||||
userId: 'mcp-user',
|
||||
chatId: 'cli-text-ingest-test-1',
|
||||
userMessage: 'Ingest external text artifact "orders lookml" into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "orders lookml" into ktx memory.',
|
||||
assistantMessage: content,
|
||||
connectionId: '00000000-0000-4000-8000-000000000001',
|
||||
sourceType: 'external_ingest',
|
||||
|
|
@ -1101,7 +1101,7 @@ describe('createKtxMcpServer', () => {
|
|||
expect(ingestSpy).toHaveBeenCalledWith({
|
||||
userId: 'local',
|
||||
chatId: expect.stringMatching(/^mcp-/),
|
||||
userMessage: 'Ingest external knowledge into KTX memory.',
|
||||
userMessage: 'Ingest external knowledge into ktx memory.',
|
||||
assistantMessage: 'Revenue means paid order value.',
|
||||
connectionId: 'warehouse',
|
||||
sourceType: 'external_ingest',
|
||||
|
|
@ -1127,7 +1127,7 @@ describe('createKtxMcpServer', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('registers KTX context MCP tools when context ports are supplied', async () => {
|
||||
it('registers ktx context MCP tools when context ports are supplied', async () => {
|
||||
const fake = makeFakeServer();
|
||||
const contextTools: KtxMcpContextPorts = {
|
||||
connections: {
|
||||
|
|
@ -1298,7 +1298,7 @@ describe('createKtxMcpServer', () => {
|
|||
expect(jsonToolResult({ ok: true }).structuredContent).toEqual({ ok: true });
|
||||
|
||||
if (false) {
|
||||
// @ts-expect-error bare arrays are not valid MCP structuredContent objects in KTX
|
||||
// @ts-expect-error bare arrays are not valid MCP structuredContent objects in ktx
|
||||
jsonToolResult([]);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ const expectedAdapterSkillHeadings: Record<string, string> = {
|
|||
historic_sql_table_digest: '# Historic SQL Table Digest',
|
||||
live_database_ingest: '# Live Database Ingest',
|
||||
looker_ingest: '# Looker Runtime Ingest',
|
||||
lookml_ingest: '# LookML to KTX Semantic Layer',
|
||||
metabase_ingest: '# Metabase to KTX Semantic Layer',
|
||||
metricflow_ingest: '# MetricFlow to KTX Semantic Layer',
|
||||
lookml_ingest: '# LookML to ktx Semantic Layer',
|
||||
metabase_ingest: '# Metabase to ktx Semantic Layer',
|
||||
metricflow_ingest: '# MetricFlow to ktx Semantic Layer',
|
||||
};
|
||||
const verificationWriterSkills = [
|
||||
'notion_synthesize',
|
||||
|
|
@ -125,7 +125,7 @@ describe('memory runtime assets', () => {
|
|||
it('ships Metabase guidance that avoids invalid joins for SQL-only card outputs', async () => {
|
||||
const body = await readFile(join(skillsDir, 'metabase_ingest', 'SKILL.md'), 'utf-8');
|
||||
|
||||
expect(body).toContain('Do not declare a KTX join just because the card SQL joins that table internally');
|
||||
expect(body).toContain('Do not declare a ktx join just because the card SQL joins that table internally');
|
||||
expect(body).toContain('only when the card output exposes a local key that matches the target source grain');
|
||||
expect(body).toContain('If `sl_discover` resolves the table, it is not outside the manifest');
|
||||
expect(body).toContain('reason: "parse_error"');
|
||||
|
|
@ -167,7 +167,7 @@ describe('memory runtime assets', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('ships only the KTX connectionId sql_execution call shape in writer guidance', async () => {
|
||||
it('ships only the ktx connectionId sql_execution call shape in writer guidance', async () => {
|
||||
const shared = await readFile(join(skillsDir, '_shared', 'identifier-verification.md'), 'utf-8');
|
||||
const bodies = [{ name: '_shared/identifier-verification.md', body: shared }];
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
|
||||
const removedAutoCommitKey = ['auto', 'commit'].join('_');
|
||||
|
||||
describe('KTX project config', () => {
|
||||
describe('ktx project config', () => {
|
||||
it.each(['status', 'replay', 'run', 'watch'])('accepts former ingest subcommand name "%s" as a connection id', (connectionId) => {
|
||||
expect(
|
||||
parseKtxProjectConfig(`
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { initKtxProject, loadKtxProject } from '../../../src/context/project/project.js';
|
||||
|
||||
describe('KTX local project runtime', () => {
|
||||
describe('ktx local project runtime', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
setKtxSetupDatabaseConnectionIds,
|
||||
} from '../../../src/context/project/setup-config.js';
|
||||
|
||||
describe('KTX setup config helpers', () => {
|
||||
describe('ktx setup config helpers', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '../../../src/context/scan/credentials.js';
|
||||
import type { KtxCredentialEnvelope, KtxScanReport, KtxScanWarning } from '../../../src/context/scan/types.js';
|
||||
|
||||
describe('KTX scan credential redaction', () => {
|
||||
describe('ktx scan credential redaction', () => {
|
||||
it('keeps credential references inspectable', () => {
|
||||
const envReference: KtxCredentialEnvelope = { kind: 'env', name: 'DATABASE_URL' };
|
||||
const fileReference: KtxCredentialEnvelope = { kind: 'file', path: '~/.config/ktx/warehouse' };
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
|
||||
const defaultPatterns = defaultKtxDataDictionarySettings.excludePatterns;
|
||||
|
||||
describe('KTX scan data dictionary policy', () => {
|
||||
describe('ktx scan data dictionary policy', () => {
|
||||
it('includes text-like and boolean categorical types', () => {
|
||||
expect(isKtxDataDictionaryCandidate('varchar(50)', 'status', defaultPatterns)).toBe(true);
|
||||
expect(isKtxDataDictionaryCandidate('VARCHAR', 'category', defaultPatterns)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ function createConnector(): KtxScanConnector {
|
|||
};
|
||||
}
|
||||
|
||||
describe('KTX description prompt builders', () => {
|
||||
describe('ktx description prompt builders', () => {
|
||||
it('builds column prompts with sample values, source descriptions, and nested BigQuery guidance', () => {
|
||||
const { system, user } = buildKtxColumnDescriptionPrompt({
|
||||
columnName: 'payload',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { buildKtxColumnEmbeddingText } from '../../../src/context/scan/embedding-text.js';
|
||||
|
||||
describe('KTX scan embedding text', () => {
|
||||
describe('ktx scan embedding text', () => {
|
||||
it('builds column embedding text with table, description, FK, and sample-value context', () => {
|
||||
expect(
|
||||
buildKtxColumnEmbeddingText({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
skippedKtxScanEnrichmentSummary,
|
||||
} from '../../../src/context/scan/enrichment-summary.js';
|
||||
|
||||
describe('KTX scan enrichment summaries', () => {
|
||||
describe('ktx scan enrichment summaries', () => {
|
||||
it('keeps structural scans skipped when no enrichment was requested', () => {
|
||||
expect(failedKtxScanEnrichmentSummary('structural', false)).toEqual(skippedKtxScanEnrichmentSummary);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
KtxStructuralSyncPlan,
|
||||
} from '../../../src/context/scan/enrichment-types.js';
|
||||
|
||||
describe('KTX scan enrichment contracts', () => {
|
||||
describe('ktx scan enrichment contracts', () => {
|
||||
it('models an enriched schema with reusable table, column, and relationship metadata', () => {
|
||||
const schema: KtxEnrichedSchema = {
|
||||
connectionId: 'warehouse',
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ describe('local scan enrichment', () => {
|
|||
expect(result.summary.statisticalValidation).toBe('skipped');
|
||||
expect(result.warnings).toContainEqual({
|
||||
code: 'relationship_validation_failed',
|
||||
message: 'KTX scan connector advertises readOnlySql but does not expose executeReadOnly',
|
||||
message: 'ktx scan connector advertises readOnlySql but does not expose executeReadOnly',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1644,7 +1644,7 @@ describe('local scan', () => {
|
|||
expect(result.report.warnings).toEqual([
|
||||
{
|
||||
code: 'enrichment_failed',
|
||||
message: 'KTX scan enrichment failed after structural scan completed: embedding service timed out',
|
||||
message: 'ktx scan enrichment failed after structural scan completed: embedding service timed out',
|
||||
recoverable: true,
|
||||
metadata: {
|
||||
mode: 'enriched',
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ describe('relationship benchmark report', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(markdown).toContain('# KTX Relationship Discovery Benchmark Evidence');
|
||||
expect(markdown).toContain('# ktx Relationship Discovery Benchmark Evidence');
|
||||
expect(markdown).toContain(
|
||||
'| demo_b2b_no_declared_constraints | smoke | declared_pks_and_declared_fks_removed | run | no | 0.500 | 0.000 | 0.000 | 0 |',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ describe('relationship diagnostics artifacts', () => {
|
|||
warnings: [
|
||||
{
|
||||
code: 'connector_capability_missing',
|
||||
message: 'KTX scan connector cannot run standalone statistical relationship validation',
|
||||
message: 'ktx scan connector cannot run standalone statistical relationship validation',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -449,7 +449,7 @@ describe('production relationship discovery', () => {
|
|||
});
|
||||
expect(result.warnings).toContainEqual({
|
||||
code: 'connector_capability_missing',
|
||||
message: 'KTX scan connector cannot run read-only SQL relationship validation',
|
||||
message: 'ktx scan connector cannot run read-only SQL relationship validation',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -146,12 +146,12 @@ describe('relationship LLM proposals', () => {
|
|||
expect(runtime.generateObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
role: 'candidateExtraction',
|
||||
system: expect.stringContaining('You are helping KTX review possible SQL relationships'),
|
||||
system: expect.stringContaining('You are helping ktx review possible SQL relationships'),
|
||||
prompt: expect.stringContaining('"tables"'),
|
||||
}),
|
||||
);
|
||||
const call = vi.mocked(runtime.generateObject).mock.calls[0]?.[0];
|
||||
expect(call?.prompt).not.toContain('You are helping KTX review possible SQL relationships');
|
||||
expect(call?.prompt).not.toContain('You are helping ktx review possible SQL relationships');
|
||||
});
|
||||
|
||||
it('skips when no runtime is configured', async () => {
|
||||
|
|
@ -207,7 +207,7 @@ describe('relationship LLM proposals', () => {
|
|||
expect(failed).toMatchObject({ candidates: [], llmCalls: 1, summary: 'failed' });
|
||||
expect(failed.warnings[0]).toMatchObject({
|
||||
code: 'relationship_llm_proposal_failed',
|
||||
message: 'KTX relationship LLM proposal failed: model unavailable',
|
||||
message: 'ktx relationship LLM proposal failed: model unavailable',
|
||||
recoverable: true,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { inferKtxDimensionType, ktxColumnTypeMappingFromNative, normalizeKtxNativeType } from '../../../src/context/scan/type-normalization.js';
|
||||
|
||||
describe('KTX scan type normalization', () => {
|
||||
describe('ktx scan type normalization', () => {
|
||||
it('normalizes native database type strings', () => {
|
||||
expect(normalizeKtxNativeType(' NUMERIC(12, 2) ')).toBe('numeric');
|
||||
expect(normalizeKtxNativeType('TIMESTAMP WITH TIME ZONE')).toBe('timestamp with time zone');
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
type KtxSchemaSnapshot,
|
||||
} from '../../../src/context/scan/types.js';
|
||||
|
||||
describe('KTX scan contract types', () => {
|
||||
describe('ktx scan contract types', () => {
|
||||
it('defaults to structural-only connector capabilities', () => {
|
||||
expect(createKtxConnectorCapabilities()).toEqual({
|
||||
structuralIntrospection: true,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const baseTable: SemanticLayerSource = {
|
|||
};
|
||||
|
||||
describe('listConnectionIdsWithNames', () => {
|
||||
it('discovers local KTX connection ids from semantic-layer directories', async () => {
|
||||
it('discovers local ktx connection ids from semantic-layer directories', async () => {
|
||||
const configService = {
|
||||
listFiles: vi.fn().mockResolvedValue({
|
||||
files: [
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export async function makeLocalGitRepo(fixtureDir: string, destRoot: string): Pr
|
|||
await git.init();
|
||||
await git.raw(['checkout', '-B', 'main']);
|
||||
await git.addConfig('user.email', 'test@ktx.local');
|
||||
await git.addConfig('user.name', 'KTX Test');
|
||||
await git.addConfig('user.name', 'ktx Test');
|
||||
await git.add('.');
|
||||
await git.commit('initial');
|
||||
const commit = async (message: string): Promise<string> => {
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ describe('formatDoctorReport', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const output = formatDoctorReport({ title: 'KTX status', checks });
|
||||
expect(output).toContain('KTX status');
|
||||
const output = formatDoctorReport({ title: 'ktx status', checks });
|
||||
expect(output).toContain('ktx status');
|
||||
expect(output).toContain('✗ Environment');
|
||||
expect(output).toContain('1 of 2 need attention');
|
||||
expect(output).toContain('✗ Native SQLite: Cannot load better-sqlite3');
|
||||
|
|
@ -83,7 +83,7 @@ describe('formatDoctorReport', () => {
|
|||
{ id: 'pnpm', label: 'pnpm 10.20+', status: 'pass', detail: '10.28.0', group: 'toolchain' },
|
||||
];
|
||||
|
||||
const output = formatDoctorReport({ title: 'KTX status', checks });
|
||||
const output = formatDoctorReport({ title: 'ktx status', checks });
|
||||
expect(output).toContain('✓ Environment');
|
||||
expect(output).toContain('Node 22+ · pnpm 10.20+');
|
||||
expect(output).not.toContain('v22.16.0');
|
||||
|
|
@ -106,7 +106,7 @@ describe('formatDoctorReport', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const output = formatDoctorReport({ title: 'KTX status', checks });
|
||||
const output = formatDoctorReport({ title: 'ktx status', checks });
|
||||
expect(output).toContain('✓ Semantic search');
|
||||
expect(output).toContain('openai/text-embedding-3-small (1536d) probe succeeded');
|
||||
});
|
||||
|
|
@ -116,7 +116,7 @@ describe('formatDoctorReport', () => {
|
|||
{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0', group: 'toolchain' },
|
||||
];
|
||||
|
||||
const output = formatDoctorReport({ title: 'KTX status', checks }, { verbose: true });
|
||||
const output = formatDoctorReport({ title: 'ktx status', checks }, { verbose: true });
|
||||
expect(output).toContain('✓ Node 22+: v22.16.0');
|
||||
});
|
||||
});
|
||||
|
|
@ -249,7 +249,7 @@ describe('runKtxDoctor', () => {
|
|||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('KTX status');
|
||||
expect(testIo.stdout()).toContain('ktx status');
|
||||
expect(testIo.stdout()).toContain('No project here yet.');
|
||||
expect(testIo.stdout()).toContain('Before you can run');
|
||||
expect(testIo.stdout()).toContain('✗ TypeScript package build: Missing packages/cli/dist/bin.js');
|
||||
|
|
@ -304,7 +304,7 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(JSON.parse(testIo.stdout())).toEqual({
|
||||
title: 'KTX status',
|
||||
title: 'ktx status',
|
||||
checks: [{ id: 'node', label: 'Node 22+', status: 'pass', detail: 'v22.16.0 ABI 127' }],
|
||||
});
|
||||
});
|
||||
|
|
@ -323,8 +323,8 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
const out = testIo.stdout();
|
||||
expect(out).toContain('KTX status');
|
||||
expect(out).toContain('No KTX project here yet.');
|
||||
expect(out).toContain('ktx status');
|
||||
expect(out).toContain('No ktx project here yet.');
|
||||
expect(out).toContain('ktx setup');
|
||||
expect(out).toContain('KTX_PROJECT_DIR');
|
||||
expect(out).not.toContain('ENOENT');
|
||||
|
|
@ -377,7 +377,7 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
const out = testIo.stdout();
|
||||
expect(out).toContain('KTX status');
|
||||
expect(out).toContain('ktx status');
|
||||
expect(out).toContain('Config');
|
||||
expect(out).toContain('Unsupported storrage: unknown field');
|
||||
expect(out).toContain('Unsupported ingest.llm: unknown field');
|
||||
|
|
@ -479,7 +479,7 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
const out = testIo.stdout();
|
||||
expect(out).toContain('KTX status');
|
||||
expect(out).toContain('ktx status');
|
||||
expect(out).toContain(`· ${basename(tempDir)}`);
|
||||
expect(out).toContain('Connections (1)');
|
||||
expect(out).toContain('LLM');
|
||||
|
|
@ -759,7 +759,7 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
const out = testIo.stdout();
|
||||
expect(out).toContain('KTX status');
|
||||
expect(out).toContain('ktx status');
|
||||
expect(out).toContain('Config');
|
||||
expect(out).toContain('ktx.yaml schema valid');
|
||||
expect(out).not.toContain('LLM');
|
||||
|
|
@ -855,7 +855,7 @@ describe('runKtxDoctor', () => {
|
|||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('No KTX project here yet.');
|
||||
expect(testIo.stdout()).toContain('No ktx project here yet.');
|
||||
});
|
||||
|
||||
it('does not invoke the Postgres query-history probe in validate mode', async () => {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ describe('runKtxCli', () => {
|
|||
await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
|
||||
expect(testIo.stdout()).toContain('KTX data agent context layer CLI');
|
||||
expect(testIo.stdout()).toContain('ktx data agent context layer CLI');
|
||||
for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'admin']) {
|
||||
expect(testIo.stdout()).toContain(`${command}`);
|
||||
}
|
||||
|
|
@ -407,7 +407,7 @@ describe('runKtxCli', () => {
|
|||
await expect(runKtxCli(['admin', 'runtime', 'stop', '--help'], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('--all');
|
||||
expect(testIo.stdout()).toContain('Stop all KTX daemon processes recorded or discoverable');
|
||||
expect(testIo.stdout()).toContain('Stop all ktx daemon processes recorded or discoverable');
|
||||
expect(testIo.stdout()).toContain('on this machine');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -913,7 +913,7 @@ describe('runKtxCli', () => {
|
|||
await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest })).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('Usage: ktx ingest');
|
||||
expect(testIo.stdout()).toContain('Build or inspect KTX context');
|
||||
expect(testIo.stdout()).toContain('Build or inspect ktx context');
|
||||
expect(testIo.stdout()).toContain('--all');
|
||||
expect(testIo.stdout()).toContain('--query-history');
|
||||
expect(testIo.stdout()).toContain('--no-query-history');
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.objectContaining({ memoryFlow: expect.any(Object) }));
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
expect((io.stdout().match(/KTX memory flow/g) ?? []).length).toBeGreaterThan(1);
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect((io.stdout().match(/ktx memory flow/g) ?? []).length).toBeGreaterThan(1);
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('fake-orders');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -145,7 +145,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
expect(liveSession.update).toHaveBeenCalled();
|
||||
expect(liveSession.close).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -303,7 +303,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(startLiveMemoryFlow).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('falls back to text live rendering when TUI startup fails with a redacted warning', async () => {
|
||||
|
|
@ -345,7 +345,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stderr()).toContain('TUI visualization unavailable: Failed [redacted-url] [redacted]');
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('\u001b[2J\u001b[H');
|
||||
});
|
||||
|
||||
|
|
@ -384,7 +384,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(startLiveMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(runLocal).toHaveBeenCalledWith(expect.not.objectContaining({ memoryFlow: expect.anything() }));
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('attaches a plain progress memory-flow sink for interactive plain run output', async () => {
|
||||
|
|
@ -416,7 +416,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
expect(io.stderr()).toContain('[5%] Fetching source files for warehouse/fake');
|
||||
expect(io.stdout()).toContain('Job: plain-run');
|
||||
expect(io.stdout()).not.toContain('[5%]');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
});
|
||||
|
||||
it('falls back to plain run output for run --viz when stdout is not interactive', async () => {
|
||||
|
|
@ -447,7 +447,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('Job: non-tty-viz-run');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -493,7 +493,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
expect(io.stderr()).toContain('[5%] Fetching source files for warehouse/fake');
|
||||
expect(io.stdout()).toContain('Job: raw-missing-viz-run');
|
||||
expect(io.stdout()).not.toContain('[5%]');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -547,7 +547,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
runKtxIngest({ command: 'watch', projectDir, outputMode: 'viz', inputMode: 'disabled' }, io.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('Run: run-watch-latest');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -572,7 +572,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/metabase done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/metabase done');
|
||||
expect(io.stdout()).toContain('Saved 2 memories from 2 raw files');
|
||||
expect(io.stdout()).toContain('Commit: abc12345 Run: run-1 Report: report-1');
|
||||
expect(io.stdout()).toContain('SOURCE');
|
||||
|
|
@ -667,7 +667,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('SOURCE');
|
||||
expect(io.stdout()).toContain('CHUNKS');
|
||||
expect(io.stdout()).toContain('WORKUNITS');
|
||||
|
|
@ -736,7 +736,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(renderStoredMemoryFlow).toHaveBeenCalledTimes(1);
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('does not use TUI for stored --viz when input is disabled', async () => {
|
||||
|
|
@ -766,7 +766,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
});
|
||||
|
||||
it('falls back to plain status for stored --viz when stdin raw mode is unavailable', async () => {
|
||||
|
|
@ -797,7 +797,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
expect(renderStoredMemoryFlow).not.toHaveBeenCalled();
|
||||
expect(io.stdout()).toContain('Run: run-raw-missing-stored-viz-run');
|
||||
expect(io.stdout()).toContain('Job: raw-missing-stored-viz-run');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdin raw mode is unavailable; printing plain output.',
|
||||
);
|
||||
|
|
@ -826,7 +826,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -854,7 +854,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX memory flow warehouse/fake done');
|
||||
expect(io.stdout()).toContain('ktx memory flow warehouse/fake done');
|
||||
expect(io.stdout()).not.toContain('\u001b[2J\u001b[H');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
|
@ -884,7 +884,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Run: run-redirected-no-input-viz-run');
|
||||
expect(io.stdout()).toContain('Job: redirected-no-input-viz-run');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
@ -910,7 +910,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Run: run-dumb-terminal-viz-run');
|
||||
expect(io.stdout()).toContain('Job: dumb-terminal-viz-run');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but TERM=dumb does not support the visual renderer; printing plain output.',
|
||||
);
|
||||
|
|
@ -932,7 +932,7 @@ describe('runKtxIngest viz and replay', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Run: run-viz-run-2');
|
||||
expect(io.stdout()).toContain('Job: viz-run-2');
|
||||
expect(io.stdout()).not.toContain('KTX memory flow');
|
||||
expect(io.stdout()).not.toContain('ktx memory flow');
|
||||
expect(io.stderr()).toContain(
|
||||
'Visualization requested but stdout is not an interactive terminal; printing plain output.',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1507,7 +1507,7 @@ describe('runKtxIngest', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('passes KTX daemon options to adapters and pull-config options when no explicit daemon URL is set', async () => {
|
||||
it('passes ktx daemon options to adapters and pull-config options when no explicit daemon URL is set', async () => {
|
||||
const projectDir = join(tempDir, 'managed-daemon-ingest-project');
|
||||
await initKtxProject({ projectDir });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { runKtxEmbeddingHealthCheck } from '../../src/llm/embedding-health.js';
|
||||
|
||||
describe('KTX embedding health check', () => {
|
||||
describe('ktx embedding health check', () => {
|
||||
it('runs a one-shot OpenAI embedding check through the configured provider', async () => {
|
||||
const createOpenAIClient = vi.fn(() => ({
|
||||
embeddings: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe('createKtxEmbeddingProvider', () => {
|
|||
}),
|
||||
) as KtxEmbeddingConfig;
|
||||
|
||||
expect(() => createKtxEmbeddingProvider(config)).toThrow('Unsupported KTX embedding backend: deterministic');
|
||||
expect(() => createKtxEmbeddingProvider(config)).toThrow('Unsupported ktx embedding backend: deterministic');
|
||||
});
|
||||
|
||||
it('rejects gateway embeddings', () => {
|
||||
|
|
@ -25,7 +25,7 @@ describe('createKtxEmbeddingProvider', () => {
|
|||
}),
|
||||
) as KtxEmbeddingConfig;
|
||||
|
||||
expect(() => createKtxEmbeddingProvider(config)).toThrow('Unsupported KTX embedding backend: gateway');
|
||||
expect(() => createKtxEmbeddingProvider(config)).toThrow('Unsupported ktx embedding backend: gateway');
|
||||
});
|
||||
|
||||
it('uses OpenAI embeddings with configured dimensions', async () => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { runKtxLlmHealthCheck } from '../../src/llm/model-health.js';
|
|||
|
||||
const anthropicModel = { modelId: 'claude-sonnet-4-6' } as never;
|
||||
|
||||
describe('KTX LLM health check', () => {
|
||||
describe('ktx LLM health check', () => {
|
||||
it('runs a minimal non-streaming model call through the configured provider', async () => {
|
||||
const generateText = vi.fn(async () => ({ text: 'ok' }));
|
||||
const createAnthropic = vi.fn(() => vi.fn(() => anthropicModel));
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ function daemonResult(status: 'started' | 'reused' = 'reused'): ManagedPythonDae
|
|||
}
|
||||
|
||||
describe('managedLocalEmbeddingHealthConfig', () => {
|
||||
it('uses the active KTX daemon URL for the immediate health check', () => {
|
||||
it('uses the active ktx daemon URL for the immediate health check', () => {
|
||||
expect(
|
||||
managedLocalEmbeddingHealthConfig({
|
||||
baseUrl: 'http://127.0.0.1:61234',
|
||||
|
|
@ -112,7 +112,7 @@ describe('managedLocalEmbeddingHealthConfig', () => {
|
|||
});
|
||||
|
||||
describe('ensureManagedLocalEmbeddingsDaemon', () => {
|
||||
it('ensures the local-embeddings feature and starts the KTX daemon', async () => {
|
||||
it('ensures the local-embeddings feature and starts the ktx daemon', async () => {
|
||||
const io = makeIo();
|
||||
const ensureRuntime = vi.fn(async () => runtime());
|
||||
const startDaemon = vi.fn(async () => daemonResult('started'));
|
||||
|
|
@ -144,7 +144,7 @@ describe('ensureManagedLocalEmbeddingsDaemon', () => {
|
|||
features: ['local-embeddings'],
|
||||
force: false,
|
||||
});
|
||||
expect(io.stderr()).toContain('Started KTX daemon: http://127.0.0.1:61234');
|
||||
expect(io.stderr()).toContain('Started ktx daemon: http://127.0.0.1:61234');
|
||||
});
|
||||
|
||||
it('reuses an already running daemon without reporting a new start', async () => {
|
||||
|
|
@ -159,7 +159,7 @@ describe('ensureManagedLocalEmbeddingsDaemon', () => {
|
|||
startDaemon: vi.fn(async () => daemonResult('reused')),
|
||||
});
|
||||
|
||||
expect(io.stderr()).toContain('Using KTX daemon: http://127.0.0.1:61234');
|
||||
expect(io.stderr()).toContain('Using ktx daemon: http://127.0.0.1:61234');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -157,9 +157,9 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
layout: { versionDir: '/runtime/0.2.0' },
|
||||
});
|
||||
|
||||
expect(io.stderr()).toContain('Installing KTX Python runtime (local-embeddings) with uv...');
|
||||
expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
|
||||
expect(io.stderr().match(/Installing KTX Python runtime/g)).toHaveLength(1);
|
||||
expect(io.stderr()).toContain('Installing ktx Python runtime (local-embeddings) with uv...');
|
||||
expect(io.stderr()).toContain('ktx Python runtime ready: /runtime/0.2.0');
|
||||
expect(io.stderr().match(/Installing ktx Python runtime/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows runtime installation progress with the CLI spinner', async () => {
|
||||
|
|
@ -181,8 +181,8 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
});
|
||||
|
||||
expect(events).toEqual([
|
||||
'start:Installing KTX Python runtime (local-embeddings) with uv...',
|
||||
'stop:KTX Python runtime ready: /runtime/0.2.0',
|
||||
'start:Installing ktx Python runtime (local-embeddings) with uv...',
|
||||
'stop:ktx Python runtime ready: /runtime/0.2.0',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
readStatus: vi.fn(async () => missingStatus()),
|
||||
installRuntime,
|
||||
}),
|
||||
).rejects.toThrow('KTX Python runtime is required for this command. Run: ktx admin runtime install --yes');
|
||||
).rejects.toThrow('ktx Python runtime is required for this command. Run: ktx admin runtime install --yes');
|
||||
|
||||
expect(installRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -251,8 +251,8 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
force: false,
|
||||
});
|
||||
expect(events).toEqual([
|
||||
'start:Installing KTX Python runtime (core) with uv...',
|
||||
'stop:KTX Python runtime ready: /runtime/0.2.0',
|
||||
'start:Installing ktx Python runtime (core) with uv...',
|
||||
'stop:ktx Python runtime ready: /runtime/0.2.0',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -274,7 +274,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
});
|
||||
|
||||
expect(confirmInstall).toHaveBeenCalledWith(
|
||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
'ktx needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
io.io,
|
||||
);
|
||||
expect(installRuntime).toHaveBeenCalledWith({
|
||||
|
|
@ -282,7 +282,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
features: ['core'],
|
||||
force: false,
|
||||
});
|
||||
expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
|
||||
expect(events).toContainEqual('start:Installing ktx Python runtime (core) with uv...');
|
||||
});
|
||||
|
||||
it('uses injected runtime confirmation instead of reading process TTY directly', async () => {
|
||||
|
|
@ -306,10 +306,10 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
).resolves.toBe(compute);
|
||||
|
||||
expect(confirmInstall).toHaveBeenCalledWith(
|
||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
'ktx needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
io.io,
|
||||
);
|
||||
expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
|
||||
expect(events).toContainEqual('start:Installing ktx Python runtime (core) with uv...');
|
||||
});
|
||||
|
||||
it('can decide default runtime prompting from injected io capabilities', async () => {
|
||||
|
|
@ -325,6 +325,6 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
installRuntime: vi.fn(),
|
||||
createPythonCompute: () => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() }),
|
||||
}),
|
||||
).rejects.toThrow('KTX Python runtime installation was cancelled');
|
||||
).rejects.toThrow('ktx Python runtime installation was cancelled');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ function daemonOptionsBase(root: string) {
|
|||
} as const;
|
||||
}
|
||||
|
||||
describe('KTX daemon lifecycle', () => {
|
||||
describe('ktx daemon lifecycle', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ describe('createManagedPythonDaemonBaseUrlResolver', () => {
|
|||
features: ['core'],
|
||||
force: false,
|
||||
});
|
||||
expect(testIo.stderr()).toContain('Started KTX daemon: http://127.0.0.1:61234');
|
||||
expect(testIo.stderr()).toContain('Started ktx daemon: http://127.0.0.1:61234');
|
||||
});
|
||||
|
||||
it('reports daemon reuse without reinstalling after the first resolved URL', async () => {
|
||||
|
|
@ -86,7 +86,7 @@ describe('createManagedPythonDaemonBaseUrlResolver', () => {
|
|||
|
||||
expect(ensureRuntime).toHaveBeenCalledTimes(1);
|
||||
expect(startDaemon).toHaveBeenCalledTimes(1);
|
||||
expect(testIo.stderr()).toContain('Using existing KTX daemon: http://127.0.0.1:61234');
|
||||
expect(testIo.stderr()).toContain('Using existing ktx daemon: http://127.0.0.1:61234');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -104,8 +104,8 @@ describe('createManagedDaemonHttpJsonRunner', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('KTX daemon ingest ports', () => {
|
||||
it('creates a Looker table parser backed by the KTX daemon runner', async () => {
|
||||
describe('ktx daemon ingest ports', () => {
|
||||
it('creates a Looker table parser backed by the ktx daemon runner', async () => {
|
||||
const requestJson = vi.fn(async () => ({
|
||||
results: {
|
||||
'model.explore': {
|
||||
|
|
@ -135,7 +135,7 @@ describe('KTX daemon ingest ports', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('creates a SQL analysis port backed by the KTX daemon runner', async () => {
|
||||
it('creates a SQL analysis port backed by the ktx daemon runner', async () => {
|
||||
const requestJson = vi.fn(async () => ({
|
||||
fingerprint: 'select-orders',
|
||||
normalized_sql: 'SELECT * FROM public.orders WHERE id = ?',
|
||||
|
|
@ -157,7 +157,7 @@ describe('KTX daemon ingest ports', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('routes SQL batch analysis through the KTX daemon runner', async () => {
|
||||
it('routes SQL batch analysis through the ktx daemon runner', async () => {
|
||||
const requestJson = vi.fn(async () => ({
|
||||
results: {
|
||||
orders: {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ describe('buildMcpSecurityConfig', () => {
|
|||
allowedHosts: [],
|
||||
allowedOrigins: [],
|
||||
}),
|
||||
).toThrow('Binding KTX MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN');
|
||||
).toThrow('Binding ktx MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN');
|
||||
});
|
||||
|
||||
it('validates allowed origins as full origins', () => {
|
||||
|
|
@ -88,7 +88,7 @@ describe('isMcpRequestAuthorized', () => {
|
|||
{ path: '/health', headers: { host: 'evil.example.test' } },
|
||||
config,
|
||||
),
|
||||
).toEqual({ ok: false, status: 403, message: 'Host header is not allowed for KTX MCP.' });
|
||||
).toEqual({ ok: false, status: 403, message: 'Host header is not allowed for ktx MCP.' });
|
||||
});
|
||||
|
||||
it('rejects browser origins unless explicitly allowed', () => {
|
||||
|
|
@ -100,7 +100,7 @@ describe('isMcpRequestAuthorized', () => {
|
|||
},
|
||||
config,
|
||||
),
|
||||
).toEqual({ ok: false, status: 403, message: 'Origin header is not allowed for KTX MCP.' });
|
||||
).toEqual({ ok: false, status: 403, message: 'Origin header is not allowed for ktx MCP.' });
|
||||
});
|
||||
|
||||
it('requires bearer auth on /mcp when token auth is enabled', () => {
|
||||
|
|
@ -109,7 +109,7 @@ describe('isMcpRequestAuthorized', () => {
|
|||
{ path: '/mcp', headers: { host: 'mcp.example.test', authorization: 'Bearer wrong' } },
|
||||
config,
|
||||
),
|
||||
).toEqual({ ok: false, status: 401, message: 'Missing or invalid KTX MCP bearer token.' });
|
||||
).toEqual({ ok: false, status: 401, message: 'Missing or invalid ktx MCP bearer token.' });
|
||||
});
|
||||
|
||||
it('does not require bearer auth on /health', () => {
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ describe('createKtxMcpServerFactory', () => {
|
|||
factory();
|
||||
|
||||
expect(io.stderr.write).toHaveBeenCalledWith(
|
||||
'KTX MCP memory_ingest disabled: missing local memory prerequisites\n',
|
||||
'ktx MCP memory_ingest disabled: missing local memory prerequisites\n',
|
||||
);
|
||||
expect(createDefaultKtxMcpServer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ describe('sanitizeMemoryFlowTuiError', () => {
|
|||
});
|
||||
|
||||
describe('MemoryFlowTuiApp', () => {
|
||||
it('always shows the KTX logo', () => {
|
||||
it('always shows the ktx logo', () => {
|
||||
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={replayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
|
||||
expect(lastFrame()).toContain('╚███╔╝');
|
||||
});
|
||||
|
|
@ -197,7 +197,7 @@ describe('MemoryFlowTuiApp', () => {
|
|||
expect(frame).toContain('Created so far:');
|
||||
expect(frame).toContain('order lifecycle');
|
||||
expect(frame).toContain('customer metrics');
|
||||
expect(frame).toContain('KTX finished ingesting your data');
|
||||
expect(frame).toContain('ktx finished ingesting your data');
|
||||
expect(frame).toContain('ktx sl');
|
||||
expect(frame).toContain('ktx wiki');
|
||||
expect(frame).not.toContain('ktx serve --mcp stdio --user-id local');
|
||||
|
|
@ -253,7 +253,7 @@ describe('MemoryFlowTuiApp', () => {
|
|||
|
||||
it('hides completion while running', () => {
|
||||
const { lastFrame } = renderInkTest(<MemoryFlowTuiApp input={runningReplayInput()} terminalWidth={120} onExit={vi.fn()} showBoot={false} />);
|
||||
expect(lastFrame()).not.toContain('KTX finished ingesting');
|
||||
expect(lastFrame()).not.toContain('ktx finished ingesting');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
formatSetupNextStepLines,
|
||||
} from '../src/next-steps.js';
|
||||
|
||||
describe('KTX demo next steps', () => {
|
||||
describe('ktx demo next steps', () => {
|
||||
it('uses supported context-build commands before agent usage', () => {
|
||||
expect(KTX_CONTEXT_BUILD_COMMANDS).toEqual([
|
||||
{
|
||||
|
|
@ -48,8 +48,8 @@ describe('KTX demo next steps', () => {
|
|||
it('explains what the next-step commands are for', () => {
|
||||
const rendered = formatNextStepLines().join('\n');
|
||||
|
||||
expect(rendered).toContain('KTX context is ready for agents.');
|
||||
expect(rendered).toContain('KTX project directory');
|
||||
expect(rendered).toContain('ktx context is ready for agents.');
|
||||
expect(rendered).toContain('ktx project directory');
|
||||
expect(rendered).toContain('ask a data question');
|
||||
expect(rendered).toContain('Verify with:');
|
||||
expect(rendered).not.toContain('this directory');
|
||||
|
|
@ -82,7 +82,7 @@ describe('KTX demo next steps', () => {
|
|||
agentIntegrationReady: true,
|
||||
}).join('\n');
|
||||
|
||||
expect(rendered).toContain('KTX context is ready for agents.');
|
||||
expect(rendered).toContain('ktx context is ready for agents.');
|
||||
expect(rendered).toContain('ktx status --json');
|
||||
expect(rendered).not.toContain('ktx agent');
|
||||
expect(rendered).not.toContain('ktx serve --mcp stdio --user-id local');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe('renderKtxCommandTree', () => {
|
|||
expect(output).not.toContain('__complete');
|
||||
|
||||
expect(output).toContain('│ └── test [connectionId]');
|
||||
expect(output).toContain('│ ├── status Show KTX MCP daemon status');
|
||||
expect(output).toContain('│ ├── status Show ktx MCP daemon status');
|
||||
expect(output).not.toContain('│ ├── add');
|
||||
expect(output).not.toContain('│ ├── remove');
|
||||
expect(output).not.toContain('│ ├── map');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ describe('prompt navigation helpers', () => {
|
|||
});
|
||||
|
||||
it('adds a blank separator between multiline menu copy and the option list', () => {
|
||||
expect(withMenuOptionSpacing('Which embedding option should KTX use?\n\nKTX uses embeddings for search.')).toBe(
|
||||
'Which embedding option should KTX use?\n\nKTX uses embeddings for search.\n',
|
||||
expect(withMenuOptionSpacing('Which embedding option should ktx use?\n\nktx uses embeddings for search.')).toBe(
|
||||
'Which embedding option should ktx use?\n\nktx uses embeddings for search.\n',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -25,10 +25,10 @@ describe('prompt navigation helpers', () => {
|
|||
it('adds a blank separator between text input helper copy and the editable value', () => {
|
||||
expect(
|
||||
withTextInputNavigation(
|
||||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.',
|
||||
'Name this PostgreSQL connection\nktx will use this short name in commands and config. You can rename it now.',
|
||||
),
|
||||
).toBe(
|
||||
'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
'Name this PostgreSQL connection\n│\n│ ktx will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ describe('prompt navigation helpers', () => {
|
|||
it('normalizes already hinted text input prompts without duplicating the hint', () => {
|
||||
expect(
|
||||
withTextInputNavigation(
|
||||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.',
|
||||
'Name this PostgreSQL connection\nktx will use this short name in commands and config. You can rename it now.\nPress Escape to go back.',
|
||||
),
|
||||
).toBe(
|
||||
'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
'Name this PostgreSQL connection\n│\n│ ktx will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe('prompt navigation helpers', () => {
|
|||
|
||||
it('is idempotent when text input navigation with body is applied twice', () => {
|
||||
const once = withTextInputNavigation(
|
||||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config.',
|
||||
'Name this PostgreSQL connection\nktx will use this short name in commands and config.',
|
||||
);
|
||||
expect(withTextInputNavigation(once)).toBe(once);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ describe('public ingest copy sanitizers', () => {
|
|||
it('maps database scan failure text into public database ingest wording', () => {
|
||||
expect(
|
||||
publicDatabaseIngestMessage(
|
||||
'KTX scan enrichment failed after structural scan completed: embedding service timed out',
|
||||
'ktx scan enrichment failed after structural scan completed: embedding service timed out',
|
||||
),
|
||||
).toBe('Database enrichment failed after schema context completed: embedding service timed out');
|
||||
expect(publicDatabaseIngestMessage('structural scan wrote partial artifacts')).toBe(
|
||||
|
|
@ -42,7 +42,7 @@ describe('public ingest copy sanitizers', () => {
|
|||
it('sanitizes captured public output lines across database and query-history internals', () => {
|
||||
expect(
|
||||
publicIngestOutputLine(
|
||||
'KTX scan enrichment failed after structural scan completed in raw-sources/warehouse/live-database/sync-1',
|
||||
'ktx scan enrichment failed after structural scan completed in raw-sources/warehouse/live-database/sync-1',
|
||||
),
|
||||
).toBe('Database enrichment failed after schema context completed in raw-sources/warehouse/database schema/sync-1');
|
||||
expect(publicIngestOutputLine('Historic SQL local ingest requires a configured reader')).toBe(
|
||||
|
|
|
|||
|
|
@ -837,7 +837,7 @@ describe('runKtxPublicIngest', () => {
|
|||
const io = makeIo();
|
||||
const project = deepReadyProject({ warehouse: { driver: 'postgres' } });
|
||||
const runScan = vi.fn(async (_args, scanIo) => {
|
||||
scanIo.stdout.write('KTX scan completed\n');
|
||||
scanIo.stdout.write('ktx scan completed\n');
|
||||
scanIo.stdout.write('Mode: structural\n');
|
||||
scanIo.stdout.write('Report: raw-sources/warehouse/live-database/sync-1/scan-report.json\n');
|
||||
scanIo.stdout.write('Raw sources: raw-sources/warehouse/live-database/sync-1\n');
|
||||
|
|
@ -861,7 +861,7 @@ describe('runKtxPublicIngest', () => {
|
|||
|
||||
expect(io.stdout()).toContain('Ingest finished\n');
|
||||
expect(io.stdout()).toContain('warehouse');
|
||||
expect(io.stdout()).not.toContain('KTX scan completed');
|
||||
expect(io.stdout()).not.toContain('ktx scan completed');
|
||||
expect(io.stdout()).not.toContain('Mode: structural');
|
||||
expect(io.stdout()).not.toContain('Report: raw-sources');
|
||||
expect(io.stdout()).not.toContain('live-database');
|
||||
|
|
@ -871,7 +871,7 @@ describe('runKtxPublicIngest', () => {
|
|||
const io = makeIo();
|
||||
const project = deepReadyProject({ warehouse: { driver: 'postgres' } });
|
||||
const runScan = vi.fn(async (_args, scanIo) => {
|
||||
scanIo.stdout.write('KTX scan enrichment failed after structural scan completed: embedding service timed out\n');
|
||||
scanIo.stdout.write('ktx scan enrichment failed after structural scan completed: embedding service timed out\n');
|
||||
return 1;
|
||||
});
|
||||
|
||||
|
|
@ -894,7 +894,7 @@ describe('runKtxPublicIngest', () => {
|
|||
'warehouse failed: Database enrichment failed after schema context completed: embedding service timed out.',
|
||||
);
|
||||
expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project');
|
||||
expect(io.stdout()).not.toContain('KTX scan enrichment failed');
|
||||
expect(io.stdout()).not.toContain('ktx scan enrichment failed');
|
||||
expect(io.stdout()).not.toContain('structural scan');
|
||||
});
|
||||
|
||||
|
|
@ -988,7 +988,7 @@ describe('runKtxPublicIngest', () => {
|
|||
docs: { driver: 'notion' },
|
||||
});
|
||||
const runScan = vi.fn<NonNullable<KtxPublicIngestDeps['runScan']>>(async (_args, scanIo, deps) => {
|
||||
scanIo.stdout.write('KTX scan completed\n');
|
||||
scanIo.stdout.write('ktx scan completed\n');
|
||||
scanIo.stdout.write('Report: raw-sources/warehouse/live-database/sync-1/scan-report.json\n');
|
||||
await deps?.progress?.update(0.12, 'Inspecting database schema');
|
||||
const enrichmentProgress = deps?.progress?.startPhase(0.5);
|
||||
|
|
@ -1030,7 +1030,7 @@ describe('runKtxPublicIngest', () => {
|
|||
expect(io.stdout()).toContain('Ingest finished');
|
||||
expect(io.stdout()).toContain('warehouse');
|
||||
expect(io.stdout()).toContain('docs');
|
||||
expect(io.stdout()).not.toContain('KTX scan completed');
|
||||
expect(io.stdout()).not.toContain('ktx scan completed');
|
||||
expect(io.stdout()).not.toContain('Report:');
|
||||
expect(io.stdout()).not.toContain('Adapter:');
|
||||
expect(io.stderr()).toContain('[1/2] warehouse · database schema\n');
|
||||
|
|
@ -1080,7 +1080,7 @@ describe('runKtxPublicIngest', () => {
|
|||
const project = deepReadyProject({ warehouse: { driver: 'postgres' } });
|
||||
const runScan = vi.fn<NonNullable<KtxPublicIngestDeps['runScan']>>(async (_args, scanIo, deps) => {
|
||||
await deps?.progress?.update(0.42, 'Enriching schema metadata');
|
||||
scanIo.stdout.write('KTX scan enrichment failed after structural scan completed: embedding service timed out\n');
|
||||
scanIo.stdout.write('ktx scan enrichment failed after structural scan completed: embedding service timed out\n');
|
||||
return 1;
|
||||
});
|
||||
|
||||
|
|
@ -1105,7 +1105,7 @@ describe('runKtxPublicIngest', () => {
|
|||
expect(io.stdout()).toContain(
|
||||
'warehouse failed: Database enrichment failed after schema context completed: embedding service timed out.',
|
||||
);
|
||||
expect(io.stdout()).not.toContain('KTX scan enrichment failed');
|
||||
expect(io.stdout()).not.toContain('ktx scan enrichment failed');
|
||||
expect(io.stdout()).not.toContain('structural scan');
|
||||
});
|
||||
|
||||
|
|
@ -1477,7 +1477,7 @@ describe('runKtxPublicIngest', () => {
|
|||
scanIo.stdout.write('Run: scan-run-1\n');
|
||||
scanIo.stdout.write('Mode: enriched\n');
|
||||
scanIo.stdout.write('Dry run: no\n');
|
||||
scanIo.stdout.write('KTX scan completed\n');
|
||||
scanIo.stdout.write('ktx scan completed\n');
|
||||
return 0;
|
||||
});
|
||||
const runIngest = vi.fn(async (_args, ingestIo) => {
|
||||
|
|
@ -1516,7 +1516,7 @@ describe('runKtxPublicIngest', () => {
|
|||
const runIngest = vi.fn(async (_args, ingestIo) => {
|
||||
ingestIo.stderr.write('Missing bundled Python runtime manifest: /repo/packages/cli/assets/python/manifest.json\n');
|
||||
ingestIo.stderr.write('In a source checkout, build the local runtime assets with: pnpm run artifacts:build\n');
|
||||
ingestIo.stderr.write('Then retry the runtime-backed KTX command.\n');
|
||||
ingestIo.stderr.write('Then retry the runtime-backed ktx command.\n');
|
||||
return 1;
|
||||
});
|
||||
|
||||
|
|
@ -1542,7 +1542,7 @@ describe('runKtxPublicIngest', () => {
|
|||
'In a source checkout, build the local runtime assets with: pnpm run artifacts:build',
|
||||
);
|
||||
expect(io.stdout()).toContain('Retry: ktx ingest warehouse --project-dir /tmp/project --query-history');
|
||||
expect(io.stdout()).not.toContain('Then retry the runtime-backed KTX command');
|
||||
expect(io.stdout()).not.toContain('Then retry the runtime-backed ktx command');
|
||||
});
|
||||
|
||||
it('fails enrichment-readiness targets before work starts while continuing independent --all targets', async () => {
|
||||
|
|
|
|||
|
|
@ -103,13 +103,13 @@ describe('runKtxRuntime', () => {
|
|||
features: ['local-embeddings'],
|
||||
force: true,
|
||||
});
|
||||
expect(io.stdout()).toContain('Installed KTX Python runtime');
|
||||
expect(io.stdout()).toContain('Installed ktx Python runtime');
|
||||
expect(io.stdout()).toContain('features: core, local-embeddings');
|
||||
expect(io.stdout()).toContain('manifest: /runtime/0.2.0/manifest.json');
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('starts the KTX daemon and prints the base URL', async () => {
|
||||
it('starts the ktx daemon and prints the base URL', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
startDaemon: vi.fn(async (): Promise<ManagedPythonDaemonStartResult> => ({
|
||||
|
|
@ -160,14 +160,14 @@ describe('runKtxRuntime', () => {
|
|||
features: ['local-embeddings'],
|
||||
force: true,
|
||||
});
|
||||
expect(io.stdout()).toContain('Started KTX daemon');
|
||||
expect(io.stdout()).toContain('Started ktx daemon');
|
||||
expect(io.stdout()).toContain('url: http://127.0.0.1:61234');
|
||||
expect(io.stdout()).toContain('pid: 4242');
|
||||
expect(io.stdout()).toContain('features: core, local-embeddings');
|
||||
expect(io.stdout()).toContain('stderr: /work/proj/.ktx/runtime/daemon.stderr.log');
|
||||
});
|
||||
|
||||
it('stops the KTX daemon', async () => {
|
||||
it('stops the ktx daemon', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
stopDaemon: vi.fn(async (): Promise<ManagedPythonDaemonStopResult> => ({
|
||||
|
|
@ -208,11 +208,11 @@ describe('runKtxRuntime', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(deps.stopDaemon).toHaveBeenCalledWith({ cliVersion: '0.2.0', projectDir: '/work/proj' });
|
||||
expect(io.stdout()).toContain('Stopped KTX daemon');
|
||||
expect(io.stdout()).toContain('Stopped ktx daemon');
|
||||
expect(io.stdout()).toContain('pid: 4242');
|
||||
});
|
||||
|
||||
it('stops all discovered KTX daemons and reports the summary', async () => {
|
||||
it('stops all discovered ktx daemons and reports the summary', async () => {
|
||||
const io = makeIo();
|
||||
const deps: KtxRuntimeDeps = {
|
||||
stopAllDaemons: vi.fn(async (): Promise<ManagedPythonDaemonStopAllResult> => ({
|
||||
|
|
@ -231,7 +231,7 @@ describe('runKtxRuntime', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(deps.stopAllDaemons).toHaveBeenCalledWith({ cliVersion: '0.2.0', projectDir: '/work/proj' });
|
||||
expect(io.stdout()).toContain('Stopped 2 KTX daemons');
|
||||
expect(io.stdout()).toContain('Stopped 2 ktx daemons');
|
||||
expect(io.stdout()).toContain('pid: 4242 source: state url: http://127.0.0.1:61234');
|
||||
expect(io.stdout()).toContain('pid: 5252 source: process url: http://127.0.0.1:8765');
|
||||
});
|
||||
|
|
@ -259,7 +259,7 @@ describe('runKtxRuntime', () => {
|
|||
runKtxRuntime({ command: 'stop', cliVersion: '0.2.0', projectDir: '/work/proj', all: true }, io.io, deps),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(io.stderr()).toContain('Stopped 0 KTX daemons; failed 1');
|
||||
expect(io.stderr()).toContain('Stopped 0 ktx daemons; failed 1');
|
||||
expect(io.stderr()).toContain('pid: 4242 source: state url: http://127.0.0.1:61234');
|
||||
expect(io.stderr()).toContain('process scan: ps failed');
|
||||
});
|
||||
|
|
@ -362,9 +362,9 @@ describe('runKtxRuntime', () => {
|
|||
|
||||
await expect(runKtxRuntime({ command: 'status', cliVersion: '0.2.0', json: false }, io.io, deps)).resolves.toBe(0);
|
||||
|
||||
expect(io.stdout()).toContain('KTX Python runtime');
|
||||
expect(io.stdout()).toContain('ktx Python runtime');
|
||||
expect(io.stdout()).toContain('status: ready');
|
||||
expect(io.stdout()).toContain('KTX Python runtime checks');
|
||||
expect(io.stdout()).toContain('ktx Python runtime checks');
|
||||
expect(io.stdout()).toContain('PASS uv: uv 0.9.5');
|
||||
expect(io.stdout()).toContain('PASS Managed Python runtime: Runtime ready at /runtime/0.2.0');
|
||||
expect(io.stderr()).toBe('');
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ const reportWithAttention: KtxScanReport = {
|
|||
warnings: [
|
||||
{
|
||||
code: 'connector_capability_missing',
|
||||
message: 'KTX scan connector is missing optional capability: columnStats',
|
||||
message: 'ktx scan connector is missing optional capability: columnStats',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'columnStats' },
|
||||
},
|
||||
|
|
@ -372,7 +372,7 @@ describe('runKtxScan', () => {
|
|||
connector: undefined,
|
||||
}),
|
||||
);
|
||||
expect(io.stdout()).toContain('KTX scan completed\n');
|
||||
expect(io.stdout()).toContain('ktx scan completed\n');
|
||||
expect(io.stdout()).toContain('Run: scan-run-1');
|
||||
expect(io.stdout()).toContain('Mode: structural');
|
||||
expect(io.stdout()).toContain('What changed\n');
|
||||
|
|
@ -494,7 +494,7 @@ describe('runKtxScan', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('passes KTX daemon options to local ingest adapters when no explicit daemon URL is set', async () => {
|
||||
it('passes ktx daemon options to local ingest adapters when no explicit daemon URL is set', async () => {
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const createLocalIngestAdapters = vi.fn(() => []);
|
||||
const runLocalScan = vi.fn(
|
||||
|
|
@ -649,7 +649,7 @@ describe('runKtxScan', () => {
|
|||
warnings: [
|
||||
{
|
||||
code: 'connector_capability_missing',
|
||||
message: 'KTX scan connector cannot run read-only SQL relationship validation',
|
||||
message: 'ktx scan connector cannot run read-only SQL relationship validation',
|
||||
recoverable: true,
|
||||
metadata: { capability: 'readOnlySql' },
|
||||
},
|
||||
|
|
@ -690,7 +690,7 @@ describe('runKtxScan', () => {
|
|||
expect(io.stdout()).toContain('Review: 12');
|
||||
expect(io.stdout()).toContain('Rejected: 44');
|
||||
expect(io.stdout()).toContain(
|
||||
'connector_capability_missing: KTX scan connector cannot run read-only SQL relationship validation',
|
||||
'connector_capability_missing: ktx scan connector cannot run read-only SQL relationship validation',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -972,7 +972,7 @@ describe('runKtxScan', () => {
|
|||
}
|
||||
|
||||
expect(io.stdout()).toContain('✓');
|
||||
expect(io.stdout()).toContain('KTX scan completed');
|
||||
expect(io.stdout()).toContain('ktx scan completed');
|
||||
expect(io.stdout()).toContain('\u001b[');
|
||||
});
|
||||
|
||||
|
|
@ -1017,7 +1017,7 @@ describe('runKtxScan', () => {
|
|||
}
|
||||
}
|
||||
|
||||
expect(io.stdout()).toContain('KTX scan completed');
|
||||
expect(io.stdout()).toContain('ktx scan completed');
|
||||
expect(io.stdout()).not.toContain('\u001b[');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -406,16 +406,16 @@ describe('setup agents', () => {
|
|||
});
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'What should agents be allowed to do with this KTX project?',
|
||||
message: 'What should agents be allowed to do with this ktx project?',
|
||||
options: [
|
||||
{
|
||||
value: 'mcp',
|
||||
label: 'Ask data questions with KTX MCP',
|
||||
label: 'Ask data questions with ktx MCP',
|
||||
hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
|
||||
},
|
||||
{
|
||||
value: 'mcp-cli',
|
||||
label: 'Ask data questions + manage KTX with CLI commands',
|
||||
label: 'Ask data questions + manage ktx with CLI commands',
|
||||
hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
|
||||
},
|
||||
{
|
||||
|
|
@ -459,16 +459,16 @@ describe('setup agents', () => {
|
|||
).resolves.toMatchObject({ status: 'skipped', projectDir: tempDir });
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'What should agents be allowed to do with this KTX project?',
|
||||
message: 'What should agents be allowed to do with this ktx project?',
|
||||
options: [
|
||||
{
|
||||
value: 'mcp',
|
||||
label: 'Ask data questions with KTX MCP',
|
||||
label: 'Ask data questions with ktx MCP',
|
||||
hint: 'Installs the MCP connection and analytics workflow skill. Best for normal use.',
|
||||
},
|
||||
{
|
||||
value: 'mcp-cli',
|
||||
label: 'Ask data questions + manage KTX with CLI commands',
|
||||
label: 'Ask data questions + manage ktx with CLI commands',
|
||||
hint: 'Adds an admin CLI skill so agents can run ktx status, sl, wiki, and setup commands.',
|
||||
},
|
||||
{
|
||||
|
|
@ -518,17 +518,17 @@ describe('setup agents', () => {
|
|||
});
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: `Where should KTX install supported agent config?\n\nKTX project: ${tempDir}`,
|
||||
message: `Where should ktx install supported agent config?\n\nktx project: ${tempDir}`,
|
||||
options: [
|
||||
{
|
||||
value: 'project',
|
||||
label: 'Project scope (KTX project directory)',
|
||||
hint: 'Only agents opened from this KTX project path load the project-scoped config.',
|
||||
label: 'Project scope (ktx project directory)',
|
||||
hint: 'Only agents opened from this ktx project path load the project-scoped config.',
|
||||
},
|
||||
{
|
||||
value: 'global',
|
||||
label: 'Global scope (user config)',
|
||||
hint: 'Agents can load this KTX project from any working directory.',
|
||||
hint: 'Agents can load this ktx project from any working directory.',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -584,7 +584,7 @@ describe('setup agents', () => {
|
|||
args: [expect.stringContaining('bin.js'), '--project-dir', tempDir, 'mcp', 'stdio'],
|
||||
});
|
||||
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('ktx Analytics Workflow');
|
||||
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(analyticsSkillPath, '.claude-plugin/plugin.json')).rejects.toThrow('Missing zip entry');
|
||||
await expect(readZipText(analyticsSkillPath, 'skills/ktx-analytics/SKILL.md')).rejects.toThrow(
|
||||
|
|
@ -597,11 +597,11 @@ describe('setup agents', () => {
|
|||
expect(io.stdout()).toContain('claude_desktop_config.json');
|
||||
expect(io.stdout()).toContain('Required before using agents');
|
||||
expect(io.stdout()).toContain('1. Restart Claude Desktop');
|
||||
expect(io.stdout()).toContain('Claude Desktop loads KTX MCP after restart.');
|
||||
expect(io.stdout()).toContain('Claude Desktop loads ktx MCP after restart.');
|
||||
expect(io.stdout()).toContain('2. Upload Claude Desktop skills');
|
||||
expect(io.stdout()).toContain('Customize > Skills > + > Create skill > Upload a skill');
|
||||
expect(io.stdout()).toContain('Upload this file:');
|
||||
expect(io.stdout()).toContain('Toggle the uploaded KTX skills on.');
|
||||
expect(io.stdout()).toContain('Toggle the uploaded ktx skills on.');
|
||||
expect(io.stdout()).not.toContain('Run `ktx mcp start`');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
|
|
@ -686,7 +686,7 @@ describe('setup agents', () => {
|
|||
|
||||
const analyticsSkillPath = join(tempDir, '.ktx/agents/claude/ktx-analytics.zip');
|
||||
const adminSkillPath = join(tempDir, '.ktx/agents/claude/ktx.zip');
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('KTX Analytics Workflow');
|
||||
expect(await readZipText(analyticsSkillPath, 'ktx-analytics/SKILL.md')).toContain('ktx Analytics Workflow');
|
||||
await expect(readZipText(analyticsSkillPath, 'ktx/SKILL.md')).rejects.toThrow('Missing zip entry');
|
||||
const adminSkill = await readZipText(adminSkillPath, 'ktx/SKILL.md');
|
||||
expect(adminSkill).toContain(`--project-dir ${tempDir}`);
|
||||
|
|
@ -1026,7 +1026,7 @@ describe('setup agents', () => {
|
|||
expect(io.stdout().match(/Space to select/g)).toHaveLength(1);
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Which agent targets should KTX install?',
|
||||
message: 'Which agent targets should ktx install?',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -1055,7 +1055,7 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('Analytics skill installed.');
|
||||
expect(output).toContain('Admin CLI skill installed.');
|
||||
expect(output).not.toContain('Agent integration complete');
|
||||
expect(output).not.toContain(`KTX project\n ${tempDir}`);
|
||||
expect(output).not.toContain(`ktx project\n ${tempDir}`);
|
||||
expect(output).not.toContain('Installed agents');
|
||||
expect(output).not.toContain('.claude/skills/ktx-analytics/SKILL.md');
|
||||
expect(output).not.toContain('.claude/skills/ktx/SKILL.md');
|
||||
|
|
@ -1167,11 +1167,11 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('Run this command before using Claude Code:');
|
||||
expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
|
||||
expect(output).toContain(`ktx mcp stop --project-dir ${tempDir}\n\n2. Open Claude Code`);
|
||||
expect(output).toContain('Open Claude Code from the KTX project directory');
|
||||
expect(output).toContain('Open Claude Code from the ktx project directory');
|
||||
expect(output).toContain('RUN:');
|
||||
expect(output).toContain(`cd '${tempDir}'`);
|
||||
expect(output).toContain('3. Restart Claude Desktop');
|
||||
expect(output).toContain('Claude Desktop loads KTX MCP after restart.');
|
||||
expect(output).toContain('Claude Desktop loads ktx MCP after restart.');
|
||||
expect(output).toContain('4. Upload Claude Desktop skills');
|
||||
expect(output).toContain('Customize > Skills > + > Create skill > Upload a skill');
|
||||
expect(output).toContain(join(tempDir, '.ktx/agents/claude/ktx-analytics.zip'));
|
||||
|
|
@ -1179,7 +1179,7 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('Upload this file:');
|
||||
expect(output).toContain('All set.');
|
||||
expect(output).not.toContain('Finish Claude Desktop setup');
|
||||
expect(output).not.toContain('Run `ktx mcp start` to enable the configured KTX MCP server.');
|
||||
expect(output).not.toContain('Run `ktx mcp start` to enable the configured ktx MCP server.');
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
|
|
@ -1216,7 +1216,7 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('2. Open Claude Code');
|
||||
expect(output).toContain('RUN:');
|
||||
expect(output).toContain('claude');
|
||||
expect(output).not.toContain('Open Claude Code from the KTX project directory');
|
||||
expect(output).not.toContain('Open Claude Code from the ktx project directory');
|
||||
expect(output).not.toContain(`cd '${tempDir}'`);
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
|
|
@ -1263,7 +1263,7 @@ describe('setup agents', () => {
|
|||
expect(output).toContain('3. Configure unsupported MCP clients');
|
||||
expect(output).toContain('4. Start MCP');
|
||||
expect(output).toContain('Run this command before using Codex, Cursor, OpenCode, and Universal .agents:');
|
||||
expect(output).toContain('Open Cursor from the KTX project directory');
|
||||
expect(output).toContain('Open Cursor from the ktx project directory');
|
||||
expect(output).toContain('Open ~/.codex/config.toml, then paste this block:\n\n PASTE:\n [mcp_servers.ktx]');
|
||||
expect(output).toContain('Open opencode.json, then paste this block:');
|
||||
expect(output).toContain('Use this endpoint when setting up unsupported MCP clients:');
|
||||
|
|
@ -1298,9 +1298,9 @@ describe('setup agents', () => {
|
|||
expect(heading).toContain('Upload Claude Desktop skills');
|
||||
expect(heading).not.toMatch(/^2\. /);
|
||||
|
||||
const sub = format(' Toggle the uploaded KTX skills on.');
|
||||
const sub = format(' Toggle the uploaded ktx skills on.');
|
||||
expect(sub).toMatch(/^ {3}/);
|
||||
expect(sub).toContain('Toggle the uploaded KTX skills on.');
|
||||
expect(sub).toContain('Toggle the uploaded ktx skills on.');
|
||||
});
|
||||
|
||||
it('renders skill bundle .zip paths as bullets and shortens HOME to ~', () => {
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ describe('setup context build state', () => {
|
|||
reportIds: ['report-docs-1'],
|
||||
artifactPaths: ['raw-sources/warehouse/live-database/sync-1/scan-report.json'],
|
||||
});
|
||||
expect(io.stdout()).toContain('KTX context is ready for agents.');
|
||||
expect(io.stdout()).toContain('ktx context is ready for agents.');
|
||||
expect(io.stdout()).toContain('Databases:');
|
||||
expect(io.stdout()).not.toContain(['Primary sources', ':'].join(''));
|
||||
});
|
||||
|
|
@ -398,7 +398,7 @@ describe('setup context build state', () => {
|
|||
completedAt: '2026-05-09T10:00:00.000Z',
|
||||
contextSourceConnectionIds: ['docs'],
|
||||
});
|
||||
expect(io.stdout()).toContain('KTX context is ready for agents.');
|
||||
expect(io.stdout()).toContain('ktx context is ready for agents.');
|
||||
expect(io.stdout()).not.toContain(['Primary sources', ':'].join(''));
|
||||
});
|
||||
|
||||
|
|
@ -523,7 +523,7 @@ describe('setup context build state', () => {
|
|||
),
|
||||
).resolves.toEqual({ status: 'failed', projectDir: tempDir });
|
||||
|
||||
expect(io.stderr()).toContain('No databases or context sources are configured for a KTX context build.');
|
||||
expect(io.stderr()).toContain('No databases or context sources are configured for a ktx context build.');
|
||||
});
|
||||
|
||||
it('starts a fresh foreground build when stale state is found', async () => {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ function makePromptAdapter(options: {
|
|||
if (message.startsWith('Enable all tables in ') && message.includes(', or refine tables?')) {
|
||||
return 'save';
|
||||
}
|
||||
if (message.includes('How much database context should KTX build?')) {
|
||||
if (message.includes('How much database context should ktx build?')) {
|
||||
const nextValue = selectValues[0];
|
||||
return nextValue === 'fast' || nextValue === 'deep' || nextValue === 'back'
|
||||
? (selectValues.shift() ?? 'fast')
|
||||
|
|
@ -126,7 +126,7 @@ function makePromptAdapter(options: {
|
|||
}
|
||||
|
||||
function connectionNamePrompt(label: string): string {
|
||||
return `Name this ${label} connection\nKTX will use this short name in commands and config. You can rename it now.`;
|
||||
return `Name this ${label} connection\nktx will use this short name in commands and config. You can rename it now.`;
|
||||
}
|
||||
|
||||
function textInputPrompt(message: string): string {
|
||||
|
|
@ -234,7 +234,7 @@ describe('setup databases step', () => {
|
|||
expect(result.status).toBe('back');
|
||||
expect(prompts.multiselect).toHaveBeenCalledWith({
|
||||
message:
|
||||
'Which databases should KTX connect to?\n' +
|
||||
'Which databases should ktx connect to?\n' +
|
||||
'Use Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||
options: [
|
||||
{ value: 'postgres', label: 'PostgreSQL' },
|
||||
|
|
@ -272,7 +272,7 @@ describe('setup databases step', () => {
|
|||
});
|
||||
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(prompts.multiselect).mock.calls[1]?.[0].message).toBe(
|
||||
'Which databases should KTX connect to?\n' +
|
||||
'Which databases should ktx connect to?\n' +
|
||||
'Use Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||
);
|
||||
});
|
||||
|
|
@ -926,7 +926,7 @@ describe('setup databases step', () => {
|
|||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(io.stdout()).not.toContain('KTX cannot work without at least one database');
|
||||
expect(io.stdout()).not.toContain('ktx cannot work without at least one database');
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(3, {
|
||||
message: 'Databases configured: postgres-warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
|
|
@ -971,7 +971,7 @@ describe('setup databases step', () => {
|
|||
initialValues: ['postgres'],
|
||||
required: true,
|
||||
}));
|
||||
expect(io.stdout()).not.toContain('KTX cannot work without at least one database');
|
||||
expect(io.stdout()).not.toContain('ktx cannot work without at least one database');
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(2, {
|
||||
message: 'Databases configured: warehouse\nWhat would you like to do?',
|
||||
options: [
|
||||
|
|
@ -1813,7 +1813,7 @@ describe('setup databases step', () => {
|
|||
commandIo.stdout.write('[55%] Semantic layer comparison found 2 changes across 2 tables\n');
|
||||
commandIo.stdout.write('[70%] Writing schema artifacts\n');
|
||||
commandIo.stdout.write('[100%] Scan completed\n');
|
||||
commandIo.stdout.write('✓ KTX scan completed\n');
|
||||
commandIo.stdout.write('✓ ktx scan completed\n');
|
||||
commandIo.stdout.write('Status: done\n');
|
||||
commandIo.stdout.write('Run: local-moywh3ky\n');
|
||||
commandIo.stdout.write('Connection: postgres-warehouse\n');
|
||||
|
|
@ -3176,7 +3176,7 @@ describe('setup databases step', () => {
|
|||
});
|
||||
vi.mocked(prompts.select).mockImplementation(async ({ message, options }) => {
|
||||
if (message.startsWith('Enable query-history ingest')) return 'yes';
|
||||
if (message.includes('How much database context should KTX build?')) return 'fast';
|
||||
if (message.includes('How much database context should ktx build?')) return 'fast';
|
||||
if (message.startsWith('Connection setup failed for analytics')) {
|
||||
failurePromptCount += 1;
|
||||
failurePromptOptions.push(options);
|
||||
|
|
@ -3300,89 +3300,6 @@ describe('setup databases step', () => {
|
|||
expect(config.connections.warehouse.historicSql).toBeUndefined();
|
||||
});
|
||||
|
||||
it('migrates legacy historicSql to context.queryHistory during database setup', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' readonly: true',
|
||||
' schemas:',
|
||||
" - 'public'",
|
||||
' historicSql:',
|
||||
' enabled: true',
|
||||
' dialect: postgres',
|
||||
' windowDays: 45',
|
||||
' minExecutions: 9',
|
||||
' concurrency: 3',
|
||||
' staleArchiveAfterDays: 120',
|
||||
' filters:',
|
||||
' dropTrivialProbes: true',
|
||||
' serviceAccounts:',
|
||||
' mode: exclude',
|
||||
' patterns:',
|
||||
" - '^svc_'",
|
||||
' orchestrators:',
|
||||
' mode: exclude',
|
||||
' patterns:',
|
||||
' - airflow',
|
||||
' dropFailedBelow: 2',
|
||||
' redactionPatterns:',
|
||||
" - '(?i)secret'",
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxSetupDatabasesStep(
|
||||
{
|
||||
projectDir: tempDir,
|
||||
inputMode: 'disabled',
|
||||
databaseConnectionIds: ['warehouse'],
|
||||
databaseSchemas: [],
|
||||
skipDatabases: false,
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
testConnection: vi.fn(async () => 0),
|
||||
scanConnection: vi.fn(async () => 0),
|
||||
historicSqlReadinessProbe: vi.fn(async () => {
|
||||
const runner = fakeHistoricSqlRunner('postgres', 'pg_stat_statements');
|
||||
return {
|
||||
ok: true as const,
|
||||
dialect: 'postgres' as const,
|
||||
runner,
|
||||
result: { pgServerVersion: 'PostgreSQL 16.4', warnings: [], info: [] },
|
||||
};
|
||||
}),
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({ status: 'ready' });
|
||||
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.connections.warehouse.historicSql).toBeUndefined();
|
||||
expect(config.connections.warehouse.context).toMatchObject({
|
||||
queryHistory: {
|
||||
enabled: true,
|
||||
windowDays: 45,
|
||||
minExecutions: 9,
|
||||
concurrency: 3,
|
||||
staleArchiveAfterDays: 120,
|
||||
filters: {
|
||||
dropTrivialProbes: true,
|
||||
serviceAccounts: { mode: 'exclude', patterns: ['^svc_'] },
|
||||
orchestrators: { mode: 'exclude', patterns: ['airflow'] },
|
||||
dropFailedBelow: 2,
|
||||
},
|
||||
redactionPatterns: ['(?i)secret'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('prints a non-blocking Postgres query history probe failure after connection test succeeds', async () => {
|
||||
const io = makeIo();
|
||||
const runner = {
|
||||
|
|
@ -3594,7 +3511,7 @@ describe('setup databases step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('skipped');
|
||||
expect(io.stdout()).toContain('KTX cannot work until you add a database.');
|
||||
expect(io.stdout()).toContain('ktx cannot work until you add a database.');
|
||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ describe('renderDemoCompletionSummary', () => {
|
|||
|
||||
it('includes star headline', () => {
|
||||
const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true));
|
||||
expect(plain).toContain('★ KTX demo is ready');
|
||||
expect(plain).toContain('★ ktx demo is ready');
|
||||
});
|
||||
|
||||
it('shows manual instructions when agent not installed', () => {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import { ManagedPythonDaemonStartError } from '../src/managed-python-daemon.js';
|
|||
import { type KtxSetupEmbeddingsPromptAdapter, runKtxSetupEmbeddingsStep } from '../src/setup-embeddings.js';
|
||||
|
||||
const EMBEDDING_OPTION_PROMPT_MESSAGE = [
|
||||
'Which embedding option should KTX use?',
|
||||
'Which embedding option should ktx use?',
|
||||
'',
|
||||
'KTX uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
|
||||
'ktx uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
|
||||
'and relationship evidence.',
|
||||
].join('\n');
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ describe('setup embeddings step', () => {
|
|||
});
|
||||
expect(vi.mocked(prompts.select).mock.calls.map((call) => call[0].message)).toEqual([
|
||||
EMBEDDING_OPTION_PROMPT_MESSAGE,
|
||||
'How should KTX find your OpenAI embedding API key?',
|
||||
'How should ktx find your OpenAI embedding API key?',
|
||||
EMBEDDING_OPTION_PROMPT_MESSAGE,
|
||||
]);
|
||||
});
|
||||
|
|
@ -286,7 +286,7 @@ describe('setup embeddings step', () => {
|
|||
const io = makeIo();
|
||||
const ensureLocalEmbeddings = vi.fn(async () => {
|
||||
throw new Error(
|
||||
'KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes',
|
||||
'ktx Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -304,7 +304,7 @@ describe('setup embeddings step', () => {
|
|||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain(
|
||||
'KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes',
|
||||
'ktx Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -361,7 +361,7 @@ describe('setup embeddings step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('Recent KTX daemon stderr:');
|
||||
expect(io.stderr()).toContain('Recent ktx daemon stderr:');
|
||||
expect(io.stderr()).toContain('daemon traceback line 6');
|
||||
expect(io.stderr()).toContain('daemon traceback line 45');
|
||||
expect(io.stderr()).not.toContain('daemon traceback line 5');
|
||||
|
|
@ -395,7 +395,7 @@ describe('setup embeddings step', () => {
|
|||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).toContain('Local embedding health check failed: fetch failed: connect ECONNREFUSED');
|
||||
expect(io.stderr()).toContain('Recent KTX daemon stderr:');
|
||||
expect(io.stderr()).toContain('Recent ktx daemon stderr:');
|
||||
expect(io.stderr()).toContain('daemon startup traceback 6');
|
||||
expect(io.stderr()).toContain('daemon startup traceback 45');
|
||||
expect(io.stderr()).not.toContain('daemon startup traceback 5');
|
||||
|
|
@ -425,7 +425,7 @@ describe('setup embeddings step', () => {
|
|||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(io.stderr()).not.toContain('Recent KTX daemon stderr:');
|
||||
expect(io.stderr()).not.toContain('Recent ktx daemon stderr:');
|
||||
});
|
||||
|
||||
it('uses fixed OpenAI defaults and only asks for credentials when OpenAI is selected', async () => {
|
||||
|
|
@ -506,7 +506,7 @@ describe('setup embeddings step', () => {
|
|||
});
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Local embeddings are not reachable. Start the local KTX daemon, then retry.',
|
||||
message: 'Local embeddings are not reachable. Start the local ktx daemon, then retry.',
|
||||
options: expect.arrayContaining([expect.objectContaining({ value: 'openai' })]),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('back');
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
||||
message: expect.stringContaining('Which LLM provider should ktx use?'),
|
||||
options: [
|
||||
{ value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
|
||||
{ value: 'codex', label: 'Codex subscription' },
|
||||
|
|
@ -199,12 +199,12 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
||||
message: expect.stringContaining('Which LLM provider should ktx use?'),
|
||||
}),
|
||||
);
|
||||
expect(prompts.select).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which Claude Code model should KTX use?'),
|
||||
message: expect.stringContaining('Which Claude Code model should ktx use?'),
|
||||
}),
|
||||
);
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
|
|
@ -299,7 +299,7 @@ describe('setup Anthropic model step', () => {
|
|||
|
||||
expect(result.status).toBe('ready');
|
||||
expect(io.stderr()).toContain('claude-code ignores llm.promptCaching.systemTtl');
|
||||
expect(io.stderr()).toContain('Claude Agent SDK does not expose KTX prompt-cache TTL, tool, or history markers');
|
||||
expect(io.stderr()).toContain('Claude Agent SDK does not expose ktx prompt-cache TTL, tool, or history markers');
|
||||
});
|
||||
|
||||
it('returns from Anthropic credential Back to provider selection', async () => {
|
||||
|
|
@ -315,7 +315,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
||||
message: expect.stringContaining('Which LLM provider should ktx use?'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -495,7 +495,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(prompts.select).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('How should KTX authenticate with Google Vertex AI?'),
|
||||
message: expect.stringContaining('How should ktx authenticate with Google Vertex AI?'),
|
||||
}),
|
||||
);
|
||||
expect(readGcloudProject).toHaveBeenCalled();
|
||||
|
|
@ -503,7 +503,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
expect(prompts.autocomplete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which Google Cloud project should KTX use for Vertex AI?'),
|
||||
message: expect.stringContaining('Which Google Cloud project should ktx use for Vertex AI?'),
|
||||
options: [
|
||||
{ value: 'local-gcp-project', label: 'local-gcp-project - Local project (current gcloud project)' },
|
||||
{ value: 'other-gcp-project', label: 'other-gcp-project - Other project' },
|
||||
|
|
@ -545,12 +545,12 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(prompts.select).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('How should KTX authenticate with Google Vertex AI?'),
|
||||
message: expect.stringContaining('How should ktx authenticate with Google Vertex AI?'),
|
||||
}),
|
||||
);
|
||||
expect(prompts.autocomplete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which Google Cloud project should KTX use for Vertex AI?'),
|
||||
message: expect.stringContaining('Which Google Cloud project should ktx use for Vertex AI?'),
|
||||
}),
|
||||
);
|
||||
expect(healthCheck).toHaveBeenCalledWith(
|
||||
|
|
@ -615,7 +615,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(prompts.autocomplete).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which Google Cloud project should KTX use for Vertex AI?'),
|
||||
message: expect.stringContaining('Which Google Cloud project should ktx use for Vertex AI?'),
|
||||
options: [
|
||||
{ value: 'manual', label: 'Enter a project ID manually' },
|
||||
{ value: 'back', label: 'Back' },
|
||||
|
|
@ -710,7 +710,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which LLM provider should KTX use?'),
|
||||
message: expect.stringContaining('Which LLM provider should ktx use?'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
@ -899,20 +899,20 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('back');
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('How should KTX find your Anthropic API key?'),
|
||||
message: expect.stringContaining('How should ktx find your Anthropic API key?'),
|
||||
options: expect.not.arrayContaining([expect.objectContaining({ value: 'skip' })]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('explains why KTX asks for an Anthropic API key', async () => {
|
||||
it('explains why ktx asks for an Anthropic API key', async () => {
|
||||
const io = makeIo();
|
||||
const prompts = makePromptAdapter({ credentialChoice: 'back' });
|
||||
const expectedPromptMessage = [
|
||||
'How should KTX find your Anthropic API key?',
|
||||
'How should ktx find your Anthropic API key?',
|
||||
'',
|
||||
[
|
||||
'KTX uses the key to verify Anthropic model access now and to run ingest agents that turn schemas, SQL,',
|
||||
'ktx uses the key to verify Anthropic model access now and to run ingest agents that turn schemas, SQL,',
|
||||
'BI metadata, and docs into semantic-layer sources and wiki context. ktx.yaml stores an env: or file:',
|
||||
'reference, not the raw key.',
|
||||
].join(' '),
|
||||
|
|
@ -930,7 +930,7 @@ describe('setup Anthropic model step', () => {
|
|||
message: expectedPromptMessage,
|
||||
}),
|
||||
);
|
||||
expect(io.stdout()).not.toContain('KTX uses the key');
|
||||
expect(io.stdout()).not.toContain('ktx uses the key');
|
||||
});
|
||||
|
||||
it('does not persist llm completion when the health check fails', async () => {
|
||||
|
|
@ -980,7 +980,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(prompts.select).toHaveBeenCalledTimes(3);
|
||||
expect(prompts.autocomplete).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining('Which Anthropic model should KTX use?'),
|
||||
message: expect.stringContaining('Which Anthropic model should ktx use?'),
|
||||
}),
|
||||
);
|
||||
expect(io.stderr()).toContain('Anthropic model health check failed: model not found');
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ describe('setup project step', () => {
|
|||
expect(result.projectDir).toBe(projectDir);
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
message: 'Where should ktx create the project?',
|
||||
options: [
|
||||
expect.objectContaining({ value: 'current', label: `Current directory (${projectDir})` }),
|
||||
expect.objectContaining({
|
||||
|
|
@ -165,7 +165,7 @@ describe('setup project step', () => {
|
|||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
message: 'Where should ktx create the project?',
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: 'new-default',
|
||||
|
|
@ -176,11 +176,11 @@ describe('setup project step', () => {
|
|||
);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ message: `Create KTX project at ${projectDir}?` }),
|
||||
expect.objectContaining({ message: `Create ktx project at ${projectDir}?` }),
|
||||
);
|
||||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
expect(result.status === 'ready' ? result.project.configPath : '').toBe(join(projectDir, 'ktx.yaml'));
|
||||
expect(testIo.stdout()).toContain(`│ KTX will create:\n│ ${projectDir}`);
|
||||
expect(testIo.stdout()).toContain(`│ ktx will create:\n│ ${projectDir}`);
|
||||
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ describe('setup project step', () => {
|
|||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
message: `Create KTX project at ${customProjectDir}?`,
|
||||
message: `Create ktx project at ${customProjectDir}?`,
|
||||
options: [
|
||||
expect.objectContaining({ value: 'create', label: 'Create project' }),
|
||||
expect.objectContaining({ value: 'choose-another', label: 'Choose another folder' }),
|
||||
|
|
@ -253,7 +253,7 @@ describe('setup project step', () => {
|
|||
);
|
||||
expect(prompts.select).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({ message: 'Where should KTX create the project?' }),
|
||||
expect.objectContaining({ message: 'Where should ktx create the project?' }),
|
||||
);
|
||||
await expect(stat(join(customProjectDir, 'ktx.yaml'))).rejects.toThrow();
|
||||
});
|
||||
|
|
@ -280,7 +280,7 @@ describe('setup project step', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('confirms before creating KTX files inside an existing non-empty folder', async () => {
|
||||
it('confirms before creating ktx files inside an existing non-empty folder', async () => {
|
||||
const startDir = join(tempDir, 'start');
|
||||
const projectDir = join(startDir, 'analytics-ktx');
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
|
|
@ -300,7 +300,7 @@ describe('setup project step', () => {
|
|||
expect.objectContaining({
|
||||
message: `That folder already exists and is not empty: ${projectDir}`,
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'use-existing', label: 'Yes, create KTX files there' }),
|
||||
expect.objectContaining({ value: 'use-existing', label: 'Yes, create ktx files there' }),
|
||||
expect.objectContaining({ value: 'choose-another', label: 'Choose another folder' }),
|
||||
]),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -73,14 +73,14 @@ describe('setup prompt adapter', () => {
|
|||
|
||||
await expect(
|
||||
adapter.select({
|
||||
message: 'Which embedding option should KTX use?\n\nKTX uses embeddings for search.',
|
||||
message: 'Which embedding option should ktx use?\n\nktx uses embeddings for search.',
|
||||
options,
|
||||
}),
|
||||
).resolves.toBe('openai');
|
||||
|
||||
expect(mocks.withSetupInterruptConfirmation).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.select).toHaveBeenCalledWith({
|
||||
message: 'Which embedding option should KTX use?\n\nKTX uses embeddings for search.\n',
|
||||
message: 'Which embedding option should ktx use?\n\nktx uses embeddings for search.\n',
|
||||
options,
|
||||
});
|
||||
});
|
||||
|
|
@ -229,10 +229,10 @@ describe('setup prompt adapter', () => {
|
|||
};
|
||||
|
||||
const ui = createKtxSetupUiAdapter();
|
||||
ui.intro('KTX setup', io);
|
||||
ui.intro('ktx setup', io);
|
||||
ui.note(' $ ktx status', 'What you can do next', io);
|
||||
|
||||
expect(chunks.join('')).toBe('KTX setup\n\nWhat you can do next:\n $ ktx status\n');
|
||||
expect(chunks.join('')).toBe('ktx setup\n\nWhat you can do next:\n $ ktx status\n');
|
||||
expect(mocks.intro).not.toHaveBeenCalled();
|
||||
expect(mocks.note).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -251,13 +251,13 @@ describe('setup prompt adapter', () => {
|
|||
};
|
||||
|
||||
const ui = createKtxSetupUiAdapter();
|
||||
ui.intro('KTX setup', io);
|
||||
ui.intro('ktx setup', io);
|
||||
ui.note(' $ ktx status', 'What you can do next', io);
|
||||
|
||||
const bannerWrite = output.write.mock.calls.map((call) => String(call[0])).join('');
|
||||
expect(bannerWrite).toContain('██');
|
||||
expect(bannerWrite).toContain('context layer for data agents');
|
||||
expect(mocks.intro).toHaveBeenCalledWith('KTX setup', { output });
|
||||
expect(mocks.intro).toHaveBeenCalledWith('ktx setup', { output });
|
||||
expect(mocks.note).toHaveBeenCalledWith(' $ ktx status', 'What you can do next', { output });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ describe('runKtxSetupReadyMenu', () => {
|
|||
{ value: 'embeddings', label: 'Embeddings' },
|
||||
{ value: 'databases', label: 'Databases' },
|
||||
{ value: 'sources', label: 'Context sources' },
|
||||
{ value: 'context', label: 'Rebuild KTX context' },
|
||||
{ value: 'context', label: 'Rebuild ktx context' },
|
||||
{ value: 'agents', label: 'Agent integration' },
|
||||
{ value: 'exit', label: 'Exit' },
|
||||
],
|
||||
|
|
@ -95,7 +95,7 @@ describe('runKtxSetupReadyChangeMenu', () => {
|
|||
{ value: 'embeddings', label: 'Embeddings' },
|
||||
{ value: 'databases', label: 'Databases' },
|
||||
{ value: 'sources', label: 'Context sources' },
|
||||
{ value: 'context', label: 'Rebuild KTX context' },
|
||||
{ value: 'context', label: 'Rebuild ktx context' },
|
||||
{ value: 'agents', label: 'Agent integration' },
|
||||
{ value: 'exit', label: 'Exit' },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('runKtxSetupRuntimeStep', () => {
|
|||
it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
|
||||
const io = makeIo();
|
||||
const ensureRuntime = vi.fn(async () => {
|
||||
throw new Error('KTX Python runtime is required for this command. Run: ktx admin runtime install --yes');
|
||||
throw new Error('ktx Python runtime is required for this command. Run: ktx admin runtime install --yes');
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
|
@ -97,7 +97,7 @@ describe('runKtxSetupRuntimeStep', () => {
|
|||
expect(io.stderr()).toContain('ktx admin runtime install --yes');
|
||||
});
|
||||
|
||||
it('starts the KTX daemon for configured sentence-transformers embeddings', async () => {
|
||||
it('starts the ktx daemon for configured sentence-transformers embeddings', async () => {
|
||||
const io = makeIo();
|
||||
const ensureLocalEmbeddings = vi.fn(async () => ({
|
||||
baseUrl: 'http://127.0.0.1:61234',
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function prompts(values: {
|
|||
}
|
||||
|
||||
function connectionNamePrompt(label: string): string {
|
||||
return `Name this ${label} connection\nKTX will use this short name in commands and config. You can rename it now.`;
|
||||
return `Name this ${label} connection\nktx will use this short name in commands and config. You can rename it now.`;
|
||||
}
|
||||
|
||||
function textInputPrompt(message: string): string {
|
||||
|
|
@ -445,7 +445,7 @@ describe('setup sources step', () => {
|
|||
|
||||
expect(pickNotionRootPages).toHaveBeenCalledOnce();
|
||||
expect(testPrompts.select).toHaveBeenCalledWith({
|
||||
message: 'Which Notion pages should KTX ingest?',
|
||||
message: 'Which Notion pages should ktx ingest?',
|
||||
options: [
|
||||
{ value: 'all_accessible', label: 'All pages the integration can access' },
|
||||
{ value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
|
||||
|
|
@ -673,7 +673,7 @@ describe('setup sources step', () => {
|
|||
it('lets visible Metabase mapping surface refresh and validation failures', async () => {
|
||||
await addPrimarySource();
|
||||
const runMapping = vi.fn(async (_projectDir: string, _connectionId: string, io: KtxCliIo) => {
|
||||
io.stderr.write('1: Metabase database does not match KTX connection database\n');
|
||||
io.stderr.write('1: Metabase database does not match ktx connection database\n');
|
||||
return 1;
|
||||
});
|
||||
const io = makeIo();
|
||||
|
|
@ -704,7 +704,7 @@ describe('setup sources step', () => {
|
|||
stderr: expect.objectContaining({ write: expect.any(Function) }),
|
||||
}),
|
||||
);
|
||||
expect(io.stderr()).toContain('1: Metabase database does not match KTX connection database');
|
||||
expect(io.stderr()).toContain('1: Metabase database does not match ktx connection database');
|
||||
expect(io.stderr()).not.toContain('Metabase mapping validation failed');
|
||||
expect(testPrompts.log).toHaveBeenCalledWith('Validating Metabase mapping...');
|
||||
expect(testPrompts.select).toHaveBeenCalledWith(
|
||||
|
|
@ -761,7 +761,7 @@ describe('setup sources step', () => {
|
|||
expect(testPrompts.multiselect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message:
|
||||
'Which context sources should KTX ingest?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||
'Which context sources should ktx ingest?\nUse Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||
}),
|
||||
);
|
||||
const options = vi.mocked(testPrompts.multiselect).mock.calls[0]?.[0].options ?? [];
|
||||
|
|
@ -1404,7 +1404,7 @@ describe('setup sources step', () => {
|
|||
).resolves.toEqual({ status: 'ready', projectDir, connectionIds: ['notion-main'] });
|
||||
|
||||
expect(testPrompts.select).toHaveBeenCalledWith({
|
||||
message: 'How should KTX find your Notion integration token?',
|
||||
message: 'How should ktx find your Notion integration token?',
|
||||
options: [
|
||||
{ value: 'keep', label: 'Keep existing credential' },
|
||||
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
|
||||
|
|
@ -1473,7 +1473,7 @@ describe('setup sources step', () => {
|
|||
initialValue: 'https://metabase-old.example.com',
|
||||
});
|
||||
expect(testPrompts.select).toHaveBeenCalledWith({
|
||||
message: 'How should KTX find your Metabase API key?',
|
||||
message: 'How should ktx find your Metabase API key?',
|
||||
options: [
|
||||
{ value: 'keep', label: 'Keep existing credential' },
|
||||
{ value: 'paste', label: 'Paste a key and save it as a local secret file' },
|
||||
|
|
@ -1817,7 +1817,7 @@ describe('setup sources step', () => {
|
|||
select: ['env', 'back', 'env', 'all_accessible'],
|
||||
text: ['notion-main'],
|
||||
deps: { validateNotion: vi.fn(async () => ({ ok: true as const, detail: 'roots=0' })) },
|
||||
repeatedSelectMessage: 'How should KTX find your Notion integration token?',
|
||||
repeatedSelectMessage: 'How should ktx find your Notion integration token?',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -1961,7 +1961,7 @@ describe('setup sources step', () => {
|
|||
|
||||
expect(testPrompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Multiple dbt projects found — which one should KTX use?',
|
||||
message: 'Multiple dbt projects found — which one should ktx use?',
|
||||
}),
|
||||
);
|
||||
expect(testPrompts.text).toHaveBeenCalledTimes(2);
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ describe('setup status', () => {
|
|||
expect(status.llm).toMatchObject({ backend: 'vertex', ready: true, model: 'claude-sonnet-4-6' });
|
||||
expect(status.context).toMatchObject({ ready: true, status: 'completed' });
|
||||
expect(rendered).toContain('LLM ready: yes (claude-sonnet-4-6)');
|
||||
expect(rendered).toContain('KTX context built: yes');
|
||||
expect(rendered).toContain('ktx context built: yes');
|
||||
});
|
||||
|
||||
it('reports context ready after a partial ingest report saved memory', async () => {
|
||||
|
|
@ -462,10 +462,10 @@ describe('setup status', () => {
|
|||
const status = await readKtxSetupStatus(tempDir);
|
||||
const rendered = formatKtxSetupStatus(status);
|
||||
|
||||
expect(rendered).toContain(`No KTX project found at ${tempDir}.`);
|
||||
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).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 } });
|
||||
});
|
||||
|
|
@ -475,13 +475,13 @@ describe('setup status', () => {
|
|||
|
||||
const rendered = formatKtxSetupStatus(await readKtxSetupStatus(tempDir));
|
||||
|
||||
expect(rendered).toContain(`KTX project: ${tempDir}`);
|
||||
expect(rendered).toContain(`ktx project: ${tempDir}`);
|
||||
expect(rendered).toContain('Project ready: yes');
|
||||
expect(rendered).toContain('LLM ready: no');
|
||||
expect(rendered).toContain('Databases configured: no');
|
||||
expect(rendered).not.toContain(['Primary sources', 'configured'].join(' '));
|
||||
expect(rendered).toContain('KTX context built: no');
|
||||
expect(rendered).not.toContain('No KTX project found.');
|
||||
expect(rendered).toContain('ktx context built: no');
|
||||
expect(rendered).not.toContain('No ktx project found.');
|
||||
});
|
||||
|
||||
it('formats a concise ready summary for completed agent setup', () => {
|
||||
|
|
@ -511,7 +511,7 @@ describe('setup status', () => {
|
|||
` ktx mcp stop --project-dir ${tempDir}`,
|
||||
'',
|
||||
'2. Open Claude Code',
|
||||
' Open Claude Code from the KTX project directory:',
|
||||
' Open Claude Code from the ktx project directory:',
|
||||
'',
|
||||
' RUN:',
|
||||
` cd '${tempDir}'`,
|
||||
|
|
@ -528,7 +528,7 @@ describe('setup status', () => {
|
|||
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).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');
|
||||
|
|
@ -582,8 +582,8 @@ describe('setup status', () => {
|
|||
expect(output).toContain('Requires MCP to be started.');
|
||||
expect(output).toContain('Analytics skill installed.');
|
||||
expect(output).not.toContain('Agent integration complete');
|
||||
expect(output).toContain('Finish KTX agent setup');
|
||||
expect(output).not.toContain('KTX project ready');
|
||||
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}`);
|
||||
|
|
@ -651,7 +651,7 @@ describe('setup status', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain('KTX setup');
|
||||
expect(testIo.stdout()).toContain('ktx setup');
|
||||
expect(testIo.stdout()).toContain(`Project: ${tempDir}`);
|
||||
expect(testIo.stdout()).toContain('Project ready: yes');
|
||||
expect(testIo.stdout()).toContain('What you can do next:');
|
||||
|
|
@ -733,7 +733,7 @@ describe('setup status', () => {
|
|||
await expect(stat(join(projectDir, '.ktx'))).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('preserves KTX scaffold files in an initially empty project directory when setup fails', async () => {
|
||||
it('preserves ktx scaffold files in an initially empty project directory when setup fails', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(
|
||||
|
|
@ -860,12 +860,12 @@ describe('setup status', () => {
|
|||
const select = vi.fn(async (options: { options: Array<{ value: string; label: string }> }) => {
|
||||
const labels = options.options.map((option) => option.label);
|
||||
expect(labels).toEqual([
|
||||
'Set up KTX for my data',
|
||||
'Set up ktx for my data',
|
||||
'Check setup status',
|
||||
'Explore a pre-built KTX project',
|
||||
'Explore a pre-built ktx project',
|
||||
'Exit',
|
||||
]);
|
||||
expect(labels.indexOf('Explore a pre-built KTX project')).toBe(labels.length - 2);
|
||||
expect(labels.indexOf('Explore a pre-built ktx project')).toBe(labels.length - 2);
|
||||
return 'exit';
|
||||
});
|
||||
const cancel = vi.fn();
|
||||
|
|
@ -901,17 +901,17 @@ describe('setup status', () => {
|
|||
const missingIo = makeIo();
|
||||
const existingIo = makeIo();
|
||||
const missingSelect = vi.fn(async (options: { options: Array<{ value: string; label: string }> }) => {
|
||||
expect(options.options.map((option) => option.label)).not.toContain('Connect a coding agent to KTX');
|
||||
expect(options.options.map((option) => option.label)).not.toContain('Connect a coding agent to ktx');
|
||||
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',
|
||||
'Create a new KTX project',
|
||||
'Connect a coding agent to KTX',
|
||||
'Create a new ktx project',
|
||||
'Connect a coding agent to ktx',
|
||||
'Check setup status',
|
||||
'Explore a pre-built KTX project',
|
||||
'Explore a pre-built ktx project',
|
||||
'Exit',
|
||||
]);
|
||||
return 'exit';
|
||||
|
|
@ -1009,13 +1009,13 @@ describe('setup status', () => {
|
|||
|
||||
expect(projectPrompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
message: 'Where should ktx create the project?',
|
||||
options: expect.arrayContaining([expect.objectContaining({ value: 'back', label: 'Back' })]),
|
||||
}),
|
||||
);
|
||||
expect(projectPrompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
message: 'Where should ktx create the project?',
|
||||
options: expect.not.arrayContaining([expect.objectContaining({ value: 'exit', label: 'Exit' })]),
|
||||
}),
|
||||
);
|
||||
|
|
@ -1068,7 +1068,7 @@ describe('setup status', () => {
|
|||
|
||||
expect(projectPrompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Where should KTX create the project?',
|
||||
message: 'Where should ktx create the project?',
|
||||
options: expect.arrayContaining([expect.objectContaining({ value: 'back', label: 'Back' })]),
|
||||
}),
|
||||
);
|
||||
|
|
@ -1145,7 +1145,7 @@ describe('setup status', () => {
|
|||
}),
|
||||
);
|
||||
expect(projectPrompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: 'Where should KTX create the project?' }),
|
||||
expect.objectContaining({ message: 'Where should ktx create the project?' }),
|
||||
);
|
||||
await expect(stat(join(newProjectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
||||
await expect(readFile(join(existingProjectDir, 'ktx.yaml'), 'utf-8')).resolves.toBe(existingConfig);
|
||||
|
|
@ -1268,7 +1268,7 @@ describe('setup status', () => {
|
|||
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(
|
||||
`${JSON.stringify({ completed_steps: ['project', 'sources'] }, null, 2)}\n`,
|
||||
);
|
||||
expect(testIo.stdout()).toContain('KTX setup');
|
||||
expect(testIo.stdout()).toContain('ktx setup');
|
||||
expect(testIo.stdout()).toContain(`Project: ${tempDir}`);
|
||||
expect(testIo.stdout()).toContain('Project ready: yes');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
|
|
@ -1969,7 +1969,7 @@ describe('setup status', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources']);
|
||||
expect(io.stderr()).not.toContain('KTX cannot build agent-ready context yet.');
|
||||
expect(io.stderr()).not.toContain('ktx cannot build agent-ready context yet.');
|
||||
});
|
||||
|
||||
it('runs context after sources and before agents in full setup', async () => {
|
||||
|
|
@ -2185,7 +2185,7 @@ describe('setup status', () => {
|
|||
expect(runtime).not.toHaveBeenCalled();
|
||||
expect(context).not.toHaveBeenCalled();
|
||||
expect(agents).toHaveBeenCalledTimes(1);
|
||||
expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
|
||||
expect(io.stderr()).not.toContain('ktx context is not ready for agents.');
|
||||
});
|
||||
|
||||
it('runs non-TTY --agents with a target without requiring --no-input or --yes', async () => {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
it('runs status setup checks through the built binary', async () => {
|
||||
const result = await runBuiltCli(['status', '--verbose', '--no-input'], { cwd: tempDir });
|
||||
|
||||
expect(result.stdout).toMatch(/KTX status/);
|
||||
expect(result.stdout).toMatch(/ktx status/);
|
||||
if (result.stdout.includes('No project here yet.')) {
|
||||
expect(result.stdout).toContain('ktx setup');
|
||||
} else {
|
||||
|
|
@ -203,7 +203,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const ingest = await runBuiltCli(['ingest', 'warehouse', '--project-dir', projectDir, '--no-input']);
|
||||
expect(ingest.code).toBe(1);
|
||||
expect(ingest.stdout).toContain('warehouse cannot be ingested: enrichment is not configured');
|
||||
expect(ingest.stdout).not.toContain('KTX scan completed');
|
||||
expect(ingest.stdout).not.toContain('ktx scan completed');
|
||||
}, 30_000);
|
||||
|
||||
it('parses gateway LLM config and OpenAI enrichment embeddings used by standalone scans without network calls', async () => {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ describe('runKtxTextIngest', () => {
|
|||
expect.objectContaining({
|
||||
userId: 'local-cli',
|
||||
chatId: 'cli-text-ingest-1700000000000-1',
|
||||
userMessage: 'Ingest external text artifact "Revenue means gross receipts." into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "Revenue means gross receipts." into ktx memory.',
|
||||
assistantMessage: 'Revenue means gross receipts.',
|
||||
sourceType: 'external_ingest',
|
||||
}),
|
||||
|
|
@ -120,7 +120,7 @@ describe('runKtxTextIngest', () => {
|
|||
2,
|
||||
expect.objectContaining({
|
||||
chatId: 'cli-text-ingest-1700000000000-2',
|
||||
userMessage: 'Ingest external text artifact "Orders are completed purchases." into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "Orders are completed purchases." into ktx memory.',
|
||||
assistantMessage: 'Orders are completed purchases.',
|
||||
}),
|
||||
);
|
||||
|
|
@ -176,7 +176,7 @@ describe('runKtxTextIngest', () => {
|
|||
expect.objectContaining({
|
||||
connectionId: 'warehouse',
|
||||
userId: 'agent',
|
||||
userMessage: 'Ingest external text artifact "revenue.md" into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "revenue.md" into ktx memory.',
|
||||
assistantMessage: 'file:/tmp/docs/revenue.md',
|
||||
}),
|
||||
);
|
||||
|
|
@ -184,7 +184,7 @@ describe('runKtxTextIngest', () => {
|
|||
2,
|
||||
expect.objectContaining({
|
||||
connectionId: 'warehouse',
|
||||
userMessage: 'Ingest external text artifact "stdin" into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "stdin" into ktx memory.',
|
||||
assistantMessage: 'stdin content',
|
||||
}),
|
||||
);
|
||||
|
|
@ -228,19 +228,19 @@ describe('runKtxTextIngest', () => {
|
|||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "remember to call me Andrey" into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "remember to call me Andrey" into ktx memory.',
|
||||
}),
|
||||
);
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "first line second line" into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "first line second line" into ktx memory.',
|
||||
}),
|
||||
);
|
||||
expect(ingest.ingest).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.objectContaining({
|
||||
userMessage: 'Ingest external text artifact "This inline note is intentionally long xxxxxxxx..." into KTX memory.',
|
||||
userMessage: 'Ingest external text artifact "This inline note is intentionally long xxxxxxxx..." into ktx memory.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue