ktx/packages/cli/test/mongo-query.test.ts
Kevin Messiaen 0e2e6c7d90
refactor(mongo-query): consolidate query types and drop unsafe cast
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
2026-07-11 21:19:55 +07:00

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"');
});
});