File size: 5,240 Bytes
15c3607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { describe, expect, it, vi } from 'vitest';
import { parseMcpServerSettings } from '$lib/utils/mcp';
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';

/**
 * Tests for the mcpServers settings parser.
 *
 * The branch seeds the MCP servers setting with a default value of
 * `JSON.stringify(RECOMMENDED_MCP_SERVERS)`, so the parser has to be
 * resilient to anything that may live in the user's localStorage: malformed
 * JSON, wrong shapes, missing fields, falsy-but-not-zero numbers, and entry
 * arrays that have been mutated by the user via the settings form.
 */
describe('parseMcpServerSettings', () => {
	it('returns an empty array for falsy or whitespace-only input', () => {
		expect(parseMcpServerSettings(null)).toEqual([]);
		expect(parseMcpServerSettings(undefined)).toEqual([]);
		expect(parseMcpServerSettings('')).toEqual([]);
		expect(parseMcpServerSettings('   ')).toEqual([]);
	});

	it('returns an empty array and logs a warning for invalid JSON strings', () => {
		const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});

		expect(parseMcpServerSettings('{not-json')).toEqual([]);
		expect(warn).toHaveBeenCalled();

		warn.mockRestore();
	});

	it('returns an empty array for valid JSON that is not an array', () => {
		expect(parseMcpServerSettings('"plain-string"')).toEqual([]);
		expect(parseMcpServerSettings('{"id":"foo"}')).toEqual([]);
		expect(parseMcpServerSettings('42')).toEqual([]);
		expect(parseMcpServerSettings('null')).toEqual([]);
	});

	it('drops entries with no parseable id and substitutes a stable fallback', () => {
		const parsed = parseMcpServerSettings(
			JSON.stringify([{ url: 'https://a.test', enabled: true }, { url: 'https://b.test' }])
		);

		expect(parsed).toHaveLength(2);
		expect(parsed[0]?.id).toBe(`${MCP_SERVER_ID_PREFIX}-1`);
		expect(parsed[1]?.id).toBe(`${MCP_SERVER_ID_PREFIX}-2`);
	});

	it('reuses the first id when it is present and falls back only for missing ones', () => {
		const parsed = parseMcpServerSettings(
			JSON.stringify([
				{ id: 'custom-1', url: 'https://a.test' },
				{ url: 'https://b.test' },
				{ id: 'custom-3', url: 'https://c.test' }
			])
		);

		expect(parsed[0]?.id).toBe('custom-1');
		expect(parsed[1]?.id).toBe(`${MCP_SERVER_ID_PREFIX}-2`);
		expect(parsed[2]?.id).toBe('custom-3');
	});

	it('falls back to the configured default requestTimeoutSeconds only for nullish values', () => {
		const fallback = DEFAULT_MCP_CONFIG.requestTimeoutSeconds;

		const parsed = parseMcpServerSettings(
			JSON.stringify([
				{ id: 'a', url: 'https://a.test' },
				{ id: 'b', url: 'https://b.test', requestTimeoutSeconds: undefined },
				{ id: 'c', url: 'https://c.test', requestTimeoutSeconds: 0 },
				{ id: 'd', url: 'https://d.test', requestTimeoutSeconds: 45 }
			])
		);

		// The parser uses ?? for timeout fallback, which only triggers on
		// null/undefined. Explicit 0 is preserved at face value.
		expect(parsed[0]?.requestTimeoutSeconds).toBe(fallback);
		expect(parsed[1]?.requestTimeoutSeconds).toBe(fallback);
		expect(parsed[2]?.requestTimeoutSeconds).toBe(0);
		expect(parsed[3]?.requestTimeoutSeconds).toBe(45);
	});

	it('treats whitespace-only headers strings as undefined', () => {
		const parsed = parseMcpServerSettings(
			JSON.stringify([
				{ id: 'a', url: 'https://a.test', headers: '   ' },
				{ id: 'b', url: 'https://b.test', headers: '{"X-Foo":"bar"}' }
			])
		);

		// The parser trims headers and coerces empty/whitespace to undefined.
		expect(parsed[0]?.headers).toBeUndefined();
		expect(parsed[1]?.headers).toBe('{"X-Foo":"bar"}');
	});

	it('defaults coercion for booleans (undefined -> false, true -> true)', () => {
		const parsed = parseMcpServerSettings(
			JSON.stringify([
				{ id: 'a', url: 'https://a.test' },
				{ id: 'b', url: 'https://b.test', enabled: true },
				{ id: 'c', url: 'https://c.test', enabled: false },
				{ id: 'd', url: 'https://d.test', useProxy: true }
			])
		);

		expect(parsed[0]?.enabled).toBe(false);
		expect(parsed[1]?.enabled).toBe(true);
		expect(parsed[2]?.enabled).toBe(false);
		expect(parsed[0]?.useProxy).toBe(false);
		expect(parsed[3]?.useProxy).toBe(true);
	});

	it('preserves input order when mapping entries', () => {
		const source = [
			{ id: 'gamma', url: 'https://c.test' },
			{ id: 'alpha', url: 'https://a.test' },
			{ id: 'beta', url: 'https://b.test' }
		];

		const parsed = parseMcpServerSettings(JSON.stringify(source));

		expect(parsed.map((entry) => entry.id)).toEqual(['gamma', 'alpha', 'beta']);
	});

	it('passes non-string raw input through the JSON-equality path', () => {
		const parsed = parseMcpServerSettings([
			{ id: 'a', url: 'https://a.test' },
			{ id: 'b', url: 'https://b.test', enabled: true }
		]);

		expect(parsed).toHaveLength(2);
		expect(parsed[0]?.id).toBe('a');
		expect(parsed[1]?.enabled).toBe(true);
	});

	it('coerces non-string url values to an empty string rather than throwing', () => {
		const parsed = parseMcpServerSettings(
			JSON.stringify([{ id: 'a', url: 42 }, { id: 'b' }, { id: 'c', url: 'https://c.test' }])
		);

		expect(parsed[0]?.url).toBe('');
		expect(parsed[1]?.url).toBe('');
		expect(parsed[2]?.url).toBe('https://c.test');
	});
});