mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-25 12:01:03 +02:00
Route the Mongo read path through one canonical request/result shape and remove the `as unknown as` narrowing flagged in review. - Add optional executeQuery to KtxScanConnector and narrow by capability in the runner, replacing the double cast with a driver + method guard. - Move KtxMongoQueryInput/KtxMongoQueryResult into context/scan/types.ts; delete the duplicate runner request type, the inline MCP-port input, and the KtxMongoQueryResponse alias. KtxMongoQueryArgs now extends the base. - Inline the pass-through executeMongoQuery wrapper. - Correct the --output default in the mongo-query and sql CLI docs: it is TTY/CI/KTX_OUTPUT-derived, not unconditionally pretty. - Add tests: database override, empty result, parseLimitOption bounds, bigint/plural-row/empty-header rendering, and mongo_query_completed telemetry fields on both outcomes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ga2tvQ2YCEphmY8HM5Guzq
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { runKtxMongoQuery } from '../src/mongo-query.js';
|
|
import { KtxMongoDbScanConnector } from '../src/connectors/mongodb/connector.js';
|
|
import type { KtxCliIo } from '../src/cli-runtime.js';
|
|
import type { KtxLocalProject } from '../src/context/project/project.js';
|
|
|
|
function captureIo(): { io: KtxCliIo; out: () => string; err: () => string } {
|
|
let out = '';
|
|
let err = '';
|
|
const io = {
|
|
stdout: { write: (s: string) => { out += s; return true; }, isTTY: false },
|
|
stderr: { write: (s: string) => { err += s; return true; } },
|
|
} as unknown as KtxCliIo;
|
|
return { io, out: () => out, err: () => err };
|
|
}
|
|
|
|
function mongoConnector(): KtxMongoDbScanConnector {
|
|
return new KtxMongoDbScanConnector({
|
|
connectionId: 'mongo',
|
|
connection: { driver: 'mongodb', url: 'mongodb://localhost:27017/app', databases: ['app'] },
|
|
clientFactory: {
|
|
create: () => ({
|
|
listCollections: vi.fn(async () => []),
|
|
estimatedDocumentCount: vi.fn(async () => 0),
|
|
find: vi.fn(async () => []),
|
|
aggregate: vi.fn(async () => [{ _id: 'a1', city: 'Indianapolis' }, { _id: 'a2', city: 'Indianapolis' }]),
|
|
ping: vi.fn(async () => undefined),
|
|
close: vi.fn(async () => undefined),
|
|
}),
|
|
},
|
|
});
|
|
}
|
|
|
|
const project = {
|
|
projectDir: '/tmp/ktx',
|
|
config: { connections: { mongo: { driver: 'mongodb', url: 'mongodb://localhost:27017/app', databases: ['app'] } } },
|
|
} as unknown as KtxLocalProject;
|
|
|
|
const baseArgs = {
|
|
projectDir: '/tmp/ktx',
|
|
connectionId: 'mongo',
|
|
collection: 'business',
|
|
pipeline: [{ $match: { city: 'Indianapolis' } }],
|
|
limit: 100,
|
|
cliVersion: '0.0.0-test',
|
|
};
|
|
|
|
describe('runKtxMongoQuery', () => {
|
|
it('prints rows in plain mode and returns exit code 0', async () => {
|
|
const { io, out } = captureIo();
|
|
const code = await runKtxMongoQuery({ ...baseArgs, output: 'plain' }, io, {
|
|
loadProject: async () => project,
|
|
createScanConnector: (async () => mongoConnector()) as never,
|
|
});
|
|
expect(code).toBe(0);
|
|
expect(out()).toContain('_id\tcity');
|
|
expect(out()).toContain('a1\tIndianapolis');
|
|
});
|
|
|
|
it('returns exit code 1 and writes the error message for an unconfigured connection', async () => {
|
|
const { io, err } = captureIo();
|
|
const code = await runKtxMongoQuery({ ...baseArgs, connectionId: 'nope' }, io, {
|
|
loadProject: async () => project,
|
|
createScanConnector: (async () => mongoConnector()) as never,
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(err().length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('runKtxMongoQuery telemetry', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('emits mongo_query_completed with outcome ok and the pipeline stage count on success', async () => {
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
vi.stubEnv('CI', '');
|
|
const { io, err } = captureIo();
|
|
const code = await runKtxMongoQuery({ ...baseArgs, output: 'plain' }, io, {
|
|
loadProject: async () => project,
|
|
createScanConnector: (async () => mongoConnector()) as never,
|
|
});
|
|
expect(code).toBe(0);
|
|
expect(err()).toContain('"event":"mongo_query_completed"');
|
|
expect(err()).toContain('"outcome":"ok"');
|
|
expect(err()).toContain('"stageCount":1');
|
|
});
|
|
|
|
it('emits mongo_query_completed with outcome error on failure', async () => {
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
vi.stubEnv('CI', '');
|
|
const { io, err } = captureIo();
|
|
const code = await runKtxMongoQuery({ ...baseArgs, connectionId: 'nope' }, io, {
|
|
loadProject: async () => project,
|
|
createScanConnector: (async () => mongoConnector()) as never,
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(err()).toContain('"event":"mongo_query_completed"');
|
|
expect(err()).toContain('"outcome":"error"');
|
|
});
|
|
});
|