Skip to content
130 changes: 97 additions & 33 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2442,15 +2442,52 @@ describe('CliRepl', function () {
hasCollectionNames: boolean;
hasDatabaseNames: boolean;
}): void {
let wantVersion = true;
let wantQueryOperators = true;

if (process.env.USE_NEW_AUTOCOMPLETE && !testServer) {
// mongodb-ts-autocomplete does not support noDb mode. It wouldn't be able
// to list collections anyway, and since the collections don't exist it
// wouldn't autocomplete methods on those collections.
wantVersion = false;
wantQueryOperators = false;
wantWatch = false;
wantShardDistribution = false;
hasCollectionNames = false;
hasDatabaseNames = false;
}

if (process.env.USE_NEW_AUTOCOMPLETE && testServer) {
if ((testServer as any)?._opts.args?.includes('--auth')) {
// mongodb-ts-autocomplete does not take into account the server version
// or capabilities, so it always completes db.watch
wantWatch = true;
// db.collection.getShardDistribution won't be autocompleted because we
// can't list the collections due to to auth being required
wantShardDistribution = false;
// we can't get the document schema because auth is required
wantQueryOperators = false;
} else {
// mongodb-ts-autocomplete does not take into account the server version
// or capabilities, so it always completes db.watch and
// db.collection.getShardDistribution assuming collection exists and can
// be listed
wantWatch = true;
wantShardDistribution = true;

// TODO: we need MQL support in mongodb-ts-autocomplete in order for it
// to autocomplete collection field names
wantQueryOperators = false;
}
}

describe('autocompletion', function () {
let cliRepl: CliRepl;
const tab = async () => {

const tabCompletion = async () => {
await tick();
input.write('\u0009');
};
const tabtab = async () => {
await tab();
await tab();
await waitCompletion(cliRepl.bus);
};

beforeEach(async function () {
Expand All @@ -2463,6 +2500,13 @@ describe('CliRepl', function () {
testServer ? await testServer.connectionString() : '',
{} as any
);

if (!(testServer as any)?._opts.args?.includes('--auth')) {
// make sure there are some collections we can autocomplete on below
input.write('db.coll.insertOne({})\n');
input.write('db.movies.insertOne({})\n');
await waitEval(cliRepl.bus);
}
});

afterEach(async function () {
Expand All @@ -2479,26 +2523,33 @@ describe('CliRepl', function () {
if (process.env.MONGOSH_TEST_FORCE_API_STRICT) {
return this.skip();
}

output = '';
input.write('db.wat');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
if (wantWatch) {
expect(output).to.include('db.watch');
} else {
expect(output).not.to.include('db.watch');
}
});

it('completes the version method', async function () {
it(`${
wantVersion ? 'completes' : 'does not complete'
} the version method`, async function () {
if (process.env.MONGOSH_TEST_FORCE_API_STRICT) {
return this.skip();
}
output = '';
input.write('db.vers');
await tabtab();
await waitCompletion(cliRepl.bus);
expect(output).to.include('db.version');
await tabCompletion();
await tabCompletion();
if (wantVersion) {
expect(output).to.include('db.version');
} else {
expect(output).to.not.include('db.version');
}
});

it('does not complete legacy JS get/set definitions', async function () {
Expand All @@ -2507,8 +2558,8 @@ describe('CliRepl', function () {
}
output = '';
input.write('JSON.');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
expect(output).to.include('JSON.__proto__');
expect(output).not.to.include('JSON.__defineGetter__');
expect(output).not.to.include('JSON.__defineSetter__');
Expand All @@ -2524,8 +2575,8 @@ describe('CliRepl', function () {
}
output = '';
input.write('db.coll.getShardDis');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
if (wantShardDistribution) {
expect(output).to.include('db.coll.getShardDistribution');
} else {
Expand All @@ -2543,8 +2594,8 @@ describe('CliRepl', function () {

output = '';
input.write('db.testcoll');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
expect(output).to.include(collname);

input.write(`db.${collname}.drop()\n`);
Expand All @@ -2553,50 +2604,63 @@ describe('CliRepl', function () {

it('completes JS value properties properly (incomplete, double tab)', async function () {
input.write('JSON.');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
expect(output).to.include('JSON.parse');
expect(output).to.include('JSON.stringify');
expect(output).not.to.include('rawValue');
});

it('completes JS value properties properly (complete, single tab)', async function () {
input.write('JSON.pa');
await tab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
expect(output).to.include('JSON.parse');
expect(output).not.to.include('JSON.stringify');
expect(output).not.to.include('rawValue');
});

it('completes shell commands', async function () {
if (process.env.USE_NEW_AUTOCOMPLETE) {
// TODO(MONGOSH-2035): not supported yet
this.skip();
}

input.write('const dSomeVariableStartingWithD = 10;\n');
await waitEval(cliRepl.bus);

output = '';
input.write('show d');
await tab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
expect(output).to.include('show databases');
expect(output).not.to.include('dSomeVariableStartingWithD');
});

it('completes use <db>', async function () {
if (process.env.USE_NEW_AUTOCOMPLETE) {
// TODO(MONGOSH-2035): not supported yet
this.skip();
}

if (!hasDatabaseNames) return this.skip();
input.write('db.getMongo()._listDatabases()\n'); // populate database cache
await waitEval(cliRepl.bus);

input.write('use adm');
await tab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
expect(output).to.include('use admin');
});

it('completes query operators', async function () {
it(`${
wantQueryOperators ? 'completes' : 'does not complete'
} query operators`, async function () {
input.write('db.movies.find({year: {$g');
await tabtab();
await waitCompletion(cliRepl.bus);
expect(output).to.include('db.movies.find({year: {$gte');
await tabCompletion();
await tabCompletion();
if (wantQueryOperators) {
expect(output).to.include('db.movies.find({year: {$gte');
} else {
expect(output).to.not.include('db.movies.find({year: {$gte');
}
});

it('completes properties of shell API result types', async function () {
Expand All @@ -2614,8 +2678,8 @@ describe('CliRepl', function () {
expect(output).to.include('DeleteResult');

input.write('res.a');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
expect(output).to.include('res.acknowledged');
});

Expand All @@ -2631,8 +2695,8 @@ describe('CliRepl', function () {

output = '';
input.write('db.actestc');
await tabtab();
await waitCompletion(cliRepl.bus);
await tabCompletion();
await tabCompletion();
expect(output).to.include('db.actestcoll1');
expect(output).to.not.include('db.actestcoll2');
});
Expand Down
21 changes: 12 additions & 9 deletions packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
fakeTTYProps,
tick,
useTmpdir,
waitCompletion,
waitEval,
waitMongoshCompletionResults,
} from '../test/repl-helpers';
import type { MongoshIOProvider, MongoshNodeReplOptions } from './mongosh-repl';
import MongoshNodeRepl from './mongosh-repl';
Expand Down Expand Up @@ -364,9 +364,6 @@ describe('MongoshNodeRepl', function () {
};
const tabtab = async () => {
await tab();
if (process.env.USE_NEW_AUTOCOMPLETE) {
await waitMongoshCompletionResults(bus);
}
await tab();
};

Expand Down Expand Up @@ -406,6 +403,7 @@ describe('MongoshNodeRepl', function () {
output = '';
input.write('db.');
await tabtab();
await waitCompletion(bus);
await tick();
input.write('version()\n');
input.write('\u0004'); // Ctrl+D
Expand Down Expand Up @@ -479,29 +477,30 @@ describe('MongoshNodeRepl', function () {
it('autocompletes collection methods', async function () {
input.write('db.coll.');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('db.coll.updateOne');
});
it('autocompletes collection schema fields', async function () {
if (!process.env.USE_NEW_AUTOCOMPLETE) {
// not supported in the old autocomplete
this.skip();
}
// this will eventually be supported in the new autocomplete
it.skip('autocompletes collection schema fields', async function () {
input.write('db.coll.find({');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('db.coll.find({foo');
});
it('autocompletes shell-api methods (once)', async function () {
input.write('vers');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('version');
expect(output, output).to.not.match(/version[ \t]+version/);
});
it('autocompletes async shell api methods', async function () {
input.write('db.coll.find().');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('db.coll.find().toArray');
});
Expand All @@ -511,19 +510,22 @@ describe('MongoshNodeRepl', function () {
output = '';
input.write('somelong');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('somelongvariable');
});
it('autocompletes partial repl commands', async function () {
input.write('.e');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('editor');
expect(output, output).to.include('exit');
});
it('autocompletes full repl commands', async function () {
input.write('.ed');
await tabtab();
await waitCompletion(bus);
await tick();
expect(output, output).to.include('.editor');
expect(output, output).not.to.include('exit');
Expand All @@ -535,6 +537,7 @@ describe('MongoshNodeRepl', function () {
expect((mongoshRepl.runtimeState().repl as any)._prompt).to.equal('');
input.write('db.');
await tabtab();
await waitCompletion(bus);
await tick();
input.write('foo\nbar\n');
expect((mongoshRepl.runtimeState().repl as any)._prompt).to.equal('');
Expand Down
5 changes: 5 additions & 0 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ class MongoshNodeRepl implements EvaluationListener {
});
}

// mongodb-ts-autocomplete requires a connection and a schema
if (this.shellCliOptions.nodb) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--nodb does not directly imply that db is inaccessible, e.g. you can do

$ mongosh --nodb
[...]
> db = connect(process.env.CLUSTER);

to basically get the same behavior as mongosh $CLUSTER

Copy link
Contributor Author

@lerouxb lerouxb May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have an idea of a better thing to check?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd have to check what the methods on getAutocompletionContext() return (/whether they return), I'd say

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the code from here. Will add it to mongodb-ts-autocomplete separately

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return [[], text];
}

const results = await newMongoshCompleter.autocomplete(text);
const transformed = transformAutocompleteResults(text, results);
return transformed;
Expand Down
25 changes: 4 additions & 21 deletions packages/cli-repl/test/repl-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,10 @@ async function waitEval(bus: MongoshBus) {
}

async function waitCompletion(bus: MongoshBus) {
await waitBus(bus, 'mongosh:autocompletion-complete');
await tick();
}

async function waitMongoshCompletionResults(bus: MongoshBus) {
// Waiting for the completion results can "time out" if an async action such
// as listing the databases or collections or loading the schema takes longer
// than 200ms (at the time of writing), but by the next try or at least
// eventually the action should complete and then the next autocomplete call
// will return the cached result.
let found = false;
while (!found) {
const [, mongoshResults] = await waitBus(
bus,
'mongosh:autocompletion-complete'
);
if (mongoshResults.length === 0) {
found = true;
}
}
await Promise.race([
waitBus(bus, 'mongosh:autocompletion-complete'),
new Promise((resolve) => setTimeout(resolve, 5000)?.unref?.()),
]);
await tick();
}

Expand Down Expand Up @@ -125,7 +109,6 @@ export {
waitBus,
waitEval,
waitCompletion,
waitMongoshCompletionResults,
fakeTTYProps,
readReplLogFile,
};
Loading