Skip to content

chore: test that autocomplete works without the fallback, re-order the tests #559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/mongodb-ts-autocomplete/scripts/extract-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ const deps: Record<string, string[]> = {
'assert.js', // exists only
],
buffer: ['package.json', 'index.d.ts'],
events: ['package.json'],
events: [
'package.json',
'events.js', // exists only (also only on windows)
],
punycode: [
'package.json',
'punycode.js', // exists only
Expand Down
85 changes: 54 additions & 31 deletions packages/mongodb-ts-autocomplete/src/autocompleter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MongoDBAutocompleter } from './index';
import type { AutocompletionContext } from './autocompletion-context';
import { analyzeDocuments } from 'mongodb-schema';
import { expect } from 'chai';
import { relativeNodePath } from '@mongodb-js/ts-autocomplete';

/*
This is intended as deliberately diabolical database and collection names to
Expand All @@ -25,6 +26,7 @@ describe('MongoDBAutocompleter', function () {
let fallbackServiceHost: ts.LanguageServiceHost;
let autocompleterContext: AutocompletionContext;
let autocompleter: MongoDBAutocompleter;
let autocompleterWithFallback: MongoDBAutocompleter;
let encounteredPaths: EncounteredPaths;

beforeEach(function () {
Expand All @@ -51,19 +53,19 @@ describe('MongoDBAutocompleter', function () {
ts.sys.readFile(fileName) || '',
);

encounteredPaths.getScriptSnapshot.push(fileName);
encounteredPaths.getScriptSnapshot.push(relativeNodePath(fileName));
return result;
},
fileExists: (fileName: string) => {
const result = ts.sys.fileExists(fileName);
if (result) {
encounteredPaths.fileExists.push(fileName);
encounteredPaths.fileExists.push(relativeNodePath(fileName));
}
return result;
},
readFile: (fileName: string) => {
const result = ts.sys.readFile(fileName);
encounteredPaths.readFile.push(fileName);
encounteredPaths.readFile.push(relativeNodePath(fileName));
return result;
},
readDirectory: (...args) => {
Expand Down Expand Up @@ -126,11 +128,32 @@ describe('MongoDBAutocompleter', function () {

autocompleter = new MongoDBAutocompleter({
context: autocompleterContext,
});

autocompleterWithFallback = new MongoDBAutocompleter({
context: autocompleterContext,
fallbackServiceHost,
});
});

afterEach(function () {
/*
This test can be used to recreate the list of deps in extract-types.ts.

ie. if you comment out the deps structure so it is an empty object, run
extract-types (so it is just everything except the node types and Javascript
lib) and then run this test, then it will essentially print what that structure
needs to be.

The other tests would fail at the same time because they don't use the fallback
service host, so typescript wouldn't load all the dependencies.
*/
it('autocompletes', async function () {
Copy link
Collaborator Author

@lerouxb lerouxb Jul 4, 2025

Choose a reason for hiding this comment

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

The best way to fully test/prove this automatically (ie. that it can recreate the list) is do comment out deps in extract-types.ts to make it just an empty structure, run extract-types so none of the @types/node and lib files are in there, then run this test. It then finds the entire list.

I tried modifying extract-types to calculate the list automatically by running autocomplete, but it becomes a bit code-surgery with things passed down just for testing and a chicken and egg problem of extract-types using autocomplete which imports the extracted types which aren't generated yet... I tried to fix it with dynamic import, but then TS doesn't want to import the code from the script and it just becomes a rabbit hole.

I think this test that lists what's missing even if you start over with an empty list and the rest of the tests fail because they don't use the fallback is probably good enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(I have now added a version of this as a comment next to the test.)

await autocompleterWithFallback.autocomplete('db.foo.find({ fo');

encounteredPaths.fileExists.sort();
encounteredPaths.getScriptSnapshot.sort();
encounteredPaths.readFile.sort();

// this is what tells us what we're missing in extract-types.ts
expect(encounteredPaths).to.deep.equal({
fileExists: [],
Expand All @@ -139,33 +162,6 @@ describe('MongoDBAutocompleter', function () {
});
});

it('deals with no connection', async function () {
// The body of tests are all wrapped in loops so that we exercise the
// caching logic in the autocompleter.
for (let i = 0; i < 2; i++) {
autocompleterContext.currentDatabaseAndConnection = () => {
return undefined;
};

const completions = await autocompleter.autocomplete('db.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the bson package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('bson.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the ShellAPI package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('ShellAPI.');
expect(completions).to.deep.equal([]);
}
});

it('completes a bson expression', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('Ob');
Expand Down Expand Up @@ -285,6 +281,33 @@ describe('MongoDBAutocompleter', function () {
]);
});

it('deals with no connection', async function () {
// The body of tests are all wrapped in loops so that we exercise the
// caching logic in the autocompleter.
for (let i = 0; i < 2; i++) {
autocompleterContext.currentDatabaseAndConnection = () => {
return undefined;
};

const completions = await autocompleter.autocomplete('db.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the bson package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('bson.');
expect(completions).to.deep.equal([]);
}
});

it('does not leak the ShellAPI package', async function () {
for (let i = 0; i < 2; i++) {
const completions = await autocompleter.autocomplete('ShellAPI.');
expect(completions).to.deep.equal([]);
}
});

describe('getConnectionSchemaCode', function () {
it('generates code for a connection', async function () {
const docs = [
Expand Down
23 changes: 12 additions & 11 deletions packages/ts-autocomplete/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type UpdateDefinitionFunction = (
newDef: Record<TypeFilename, string | boolean>,
) => void;

function relativeNodePath(fileName: string): string {
export function relativeNodePath(fileName: string): string {
const parts = fileName.split(/\/node_modules\//g);
if (parts.length === 1 && fileName.endsWith('package.json')) {
// special case: when it looks up this package itself it isn't going to find
Expand Down Expand Up @@ -65,13 +65,14 @@ function getVirtualLanguageService(
return (versions[fileName] ?? 1).toString();
},
getScriptSnapshot: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was actually a bug - I was passing the relative filename to the fallback resolver and that needs the original absolute filename so it can actually load it from disk.


if (relativeFileName in codeHolder) {
// if its a boolean rather than code, just return a blank string if for
// some reason we ever get here.
const code =
typeof codeHolder[fileName] === 'string'
? (codeHolder[fileName] as string)
typeof codeHolder[relativeFileName] === 'string'
? (codeHolder[relativeFileName] as string)
: '';
return ts.ScriptSnapshot.fromString(code);
}
Expand All @@ -86,8 +87,8 @@ function getVirtualLanguageService(
return ts.getDefaultLibFilePath(options);
},
fileExists: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
if (relativeFileName in codeHolder) {
return true;
}

Expand All @@ -98,13 +99,13 @@ function getVirtualLanguageService(
return false;
},
readFile: (fileName) => {
fileName = relativeNodePath(fileName);
if (fileName in codeHolder) {
const relativeFileName = relativeNodePath(fileName);
if (relativeFileName in codeHolder) {
// if its a boolean rather than code, just return a blank string if for
// some reason we ever get here.
const code =
typeof codeHolder[fileName] === 'string'
? (codeHolder[fileName] as string)
typeof codeHolder[relativeFileName] === 'string'
? (codeHolder[relativeFileName] as string)
: undefined;
return code;
}
Expand Down