Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/mcp/mcpController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { createMCPConnectionErrorHandler } from './mcpConnectionErrorHandler';
import { getMCPConfigFromVSCodeSettings } from './mcpConfig';
import { DEFAULT_TELEMETRY_APP_NAME } from '../connectionController';
import formatError from '../utils/formatError';
import { TelemetryService } from '../telemetry';
import type { TelemetryService } from '../telemetry';

export type MCPServerStartupConfig =
| 'prompt'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { expect } from 'chai';

import {
gracefullyDeserializeEjson,
toDisplayString,
InvalidBSONValue,
} from '../../../../views/data-browsing-app/gracefulEjsonDeserialize';

describe('gracefulEjsonDeserialize', function () {
describe('gracefullyDeserializeEjson', function () {
it('should deserialize a valid EJSON document', function () {
const ejson = {
_id: { $oid: '507f1f77bcf86cd799439011' },
count: { $numberLong: '42' },
name: 'hello',
};
const result = gracefullyDeserializeEjson(ejson);

expect(result._id).to.have.property('_bsontype', 'ObjectId');
expect(result.count).to.have.property('_bsontype', 'Long');
expect(result.name).to.equal('hello');
});

it('should substitute InvalidBSONValue for an invalid $numberLong', function () {
const ejson = {
good: { $numberLong: '42' },
bad: { $numberLong: 'pineapple' },
};
const result = gracefullyDeserializeEjson(ejson);

expect(result.good).to.have.property('_bsontype', 'Long');
expect(result.bad).to.be.instanceOf(InvalidBSONValue);
expect((result.bad as InvalidBSONValue).typeName).to.equal('NumberLong');
});

it('should substitute InvalidBSONValue for an invalid $date', function () {
const ejson = {
good: { $date: '2024-01-01T00:00:00Z' },
bad: { $date: 'pineapple' },
};
const result = gracefullyDeserializeEjson(ejson);

expect(result.good).to.be.instanceOf(Date);
expect(result.bad).to.be.instanceOf(InvalidBSONValue);
expect((result.bad as InvalidBSONValue).typeName).to.equal('Date');
});

it('should substitute InvalidBSONValue for an invalid $oid', function () {
const ejson = { bad: { $oid: 'not-an-objectid' } };
const result = gracefullyDeserializeEjson(ejson);

expect(result.bad).to.be.instanceOf(InvalidBSONValue);
expect((result.bad as InvalidBSONValue).typeName).to.equal('Oid');
});

it('should substitute InvalidBSONValue for an invalid $numberDecimal', function () {
const ejson = {
good: { $numberDecimal: '3.14' },
bad: { $numberDecimal: 'not-a-number' },
};
const result = gracefullyDeserializeEjson(ejson);

expect(result.good).to.have.property('_bsontype', 'Decimal128');
expect(result.bad).to.be.instanceOf(InvalidBSONValue);
expect((result.bad as InvalidBSONValue).typeName).to.equal(
'NumberDecimal',
);
});

it('should pass through regular objects with $-prefixed keys', function () {
const ejson = {
query: { $set: { x: 1 } },
};
const result = gracefullyDeserializeEjson(ejson);

// $set is not an EJSON type wrapper, so it should be passed through
// as a regular object.
expect(result.query).to.not.be.instanceOf(InvalidBSONValue);
expect(result.query).to.have.property('$set');
});

it('should not treat a subdocument with mixed $-prefixed and plain keys as an EJSON wrapper', function () {
const ejson = {
item: { $date: '2024-01-01T00:00:00Z', extra: 'field' },
};
const result = gracefullyDeserializeEjson(ejson);

// Has a non-$-prefixed key alongside $date, so it's a plain object,
// not an EJSON type wrapper.
expect(result.item).to.not.be.instanceOf(InvalidBSONValue);
const item = result.item as Record<string, unknown>;
expect(item.extra).to.equal('field');
// The $date field inside should be deserialized individually.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The comment says “The $date field inside should be deserialized individually”, but the assertion expects the raw string value and the implementation does not deserialize $date when it’s just a normal field in a mixed-key subdocument. Update the comment to reflect the actual behavior (i.e., $date is treated as a regular field here).

Suggested change
// The $date field inside should be deserialized individually.
// The $date field inside is treated as a regular field and not deserialized.

Copilot uses AI. Check for mistakes.
expect(item.$date).to.equal('2024-01-01T00:00:00Z');
});

it('should handle nested documents with mixed valid/invalid values', function () {
const ejson = {
_id: { $oid: '507f1f77bcf86cd799439011' },
nested: {
ok: { $numberLong: '42' },
bad: { $numberLong: 'NaN' },
deep: {
name: 'test',
also_bad: { $date: 'garbage' },
},
},
};
const result = gracefullyDeserializeEjson(ejson);

expect(result._id).to.have.property('_bsontype', 'ObjectId');
const nested = result.nested as Record<string, unknown>;
expect(nested.ok).to.have.property('_bsontype', 'Long');
expect(nested.bad).to.be.instanceOf(InvalidBSONValue);
const deep = nested.deep as Record<string, unknown>;
expect(deep.name).to.equal('test');
expect(deep.also_bad).to.be.instanceOf(InvalidBSONValue);
});

it('should handle arrays with mixed valid/invalid values', function () {
const ejson = {
items: [{ $numberLong: '1' }, { $numberLong: 'bad' }, 'plain string'],
};
const result = gracefullyDeserializeEjson(ejson);

const items = result.items as unknown[];
expect(items[0]).to.have.property('_bsontype', 'Long');
expect(items[1]).to.be.instanceOf(InvalidBSONValue);
expect(items[2]).to.equal('plain string');
});

it('should handle primitives and nulls', function () {
const ejson = {
str: 'hello',
num: 42,
bool: true,
nil: null,
};
const result = gracefullyDeserializeEjson(ejson);

expect(result.str).to.equal('hello');
expect(result.num).to.equal(42);
expect(result.bool).to.equal(true);
expect(result.nil).to.equal(null);
});
});

describe('toDisplayString', function () {
it('should render InvalidBSONValue as unquoted text', function () {
const doc = gracefullyDeserializeEjson({
bad: { $numberLong: 'pineapple' },
});
const output = toDisplayString(doc);

// Should contain "Invalid NumberLong" without quotes around it
expect(output).to.include('Invalid NumberLong');
// Should NOT be quoted like a string
expect(output).to.not.include("'Invalid NumberLong'");
expect(output).to.not.include('"Invalid NumberLong"');
});

it('should render valid BSON types in shell syntax', function () {
const doc = gracefullyDeserializeEjson({
_id: { $oid: '507f1f77bcf86cd799439011' },
count: { $numberLong: '42' },
});
const output = toDisplayString(doc);

expect(output).to.include("ObjectId('507f1f77bcf86cd799439011')");
expect(output).to.include("NumberLong('42')");
});

it('should render a mix of valid, invalid, and primitive values', function () {
const doc = gracefullyDeserializeEjson({
_id: { $oid: '507f1f77bcf86cd799439011' },
bad: { $numberLong: 'NaN' },
name: 'hello',
});
const output = toDisplayString(doc);

expect(output).to.include("ObjectId('507f1f77bcf86cd799439011')");
expect(output).to.include('Invalid NumberLong');
expect(output).to.include("'hello'");
});

it('should render nested objects with proper indentation', function () {
const doc = gracefullyDeserializeEjson({
nested: {
ok: { $numberLong: '42' },
bad: { $date: 'garbage' },
},
});
const output = toDisplayString(doc);

expect(output).to.include('nested: {');
expect(output).to.include("NumberLong('42')");
expect(output).to.include('Invalid Date');
});

it('should render arrays', function () {
const doc = gracefullyDeserializeEjson({
items: [{ $numberLong: '1' }, { $numberLong: 'bad' }, 'plain'],
});
const output = toDisplayString(doc);

expect(output).to.include("NumberLong('1')");
expect(output).to.include('Invalid NumberLong');
expect(output).to.include("'plain'");
});

it('should quote keys that are not valid JS identifiers', function () {
const doc = gracefullyDeserializeEjson({
valid_key: 'a',
'with space': 'b',
'123start': 'c',
'with-hyphen': 'd',
"it's": 'e',
});
const output = toDisplayString(doc);

expect(output).to.include("valid_key: 'a'");
expect(output).to.include("'with space': 'b'");
expect(output).to.include("'123start': 'c'");
expect(output).to.include("'with-hyphen': 'd'");
expect(output).to.include("'it\\'s': 'e'");
});

it('should render empty objects and arrays', function () {
const doc = gracefullyDeserializeEjson({
emptyObj: {},
emptyArr: [],
});
const output = toDisplayString(doc);

// The formatter might represent these differently, but they should
// not cause errors.
expect(output).to.be.a('string');
expect(output.length).to.be.greaterThan(0);
});
});
});
Loading
Loading