@cosmocoder/mcp-web-docs 2.0.20 → 2.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/config.d.ts +0 -19
- package/build/config.js +0 -50
- package/build/config.js.map +1 -1
- package/build/config.test.js +1 -31
- package/build/config.test.js.map +1 -1
- package/build/crawler/base.d.ts +1 -16
- package/build/crawler/base.js +4 -120
- package/build/crawler/base.js.map +1 -1
- package/build/crawler/base.test.js +2 -184
- package/build/crawler/base.test.js.map +1 -1
- package/build/crawler/crawlee-crawler.js +0 -1
- package/build/crawler/crawlee-crawler.js.map +1 -1
- package/build/crawler/crawlee-crawler.test.js +13 -0
- package/build/crawler/crawlee-crawler.test.js.map +1 -1
- package/build/crawler/docs-crawler.d.ts +4 -7
- package/build/crawler/docs-crawler.js +5 -12
- package/build/crawler/docs-crawler.js.map +1 -1
- package/build/crawler/docs-crawler.test.js +13 -51
- package/build/crawler/docs-crawler.test.js.map +1 -1
- package/build/crawler/github.d.ts +1 -1
- package/build/crawler/github.js +7 -7
- package/build/crawler/github.js.map +1 -1
- package/build/crawler/github.test.js +9 -39
- package/build/crawler/github.test.js.map +1 -1
- package/build/crawler/llms-txt.js +5 -3
- package/build/crawler/llms-txt.js.map +1 -1
- package/build/crawler/llms-txt.test.js +2 -0
- package/build/crawler/llms-txt.test.js.map +1 -1
- package/build/index.js +26 -1664
- package/build/index.js.map +1 -1
- package/build/index.test.js +267 -433
- package/build/index.test.js.map +1 -1
- package/build/indexing/queue-manager.d.ts +2 -0
- package/build/indexing/queue-manager.js +13 -18
- package/build/indexing/queue-manager.js.map +1 -1
- package/build/indexing/queue-manager.test.js +5 -12
- package/build/indexing/queue-manager.test.js.map +1 -1
- package/build/indexing/workflow.d.ts +38 -0
- package/build/indexing/workflow.js +223 -0
- package/build/indexing/workflow.js.map +1 -0
- package/build/indexing/workflow.test.d.ts +1 -0
- package/build/indexing/workflow.test.js +218 -0
- package/build/indexing/workflow.test.js.map +1 -0
- package/build/server.d.ts +88 -0
- package/build/server.js +1460 -0
- package/build/server.js.map +1 -0
- package/build/server.test.d.ts +1 -0
- package/build/server.test.js +27 -0
- package/build/server.test.js.map +1 -0
- package/build/storage/storage.js +1 -0
- package/build/storage/storage.js.map +1 -1
- package/build/storage/storage.test.js +25 -8
- package/build/storage/storage.test.js.map +1 -1
- package/build/types.d.ts +0 -15
- package/build/util/docs.js +1 -2
- package/build/util/docs.js.map +1 -1
- package/build/util/docs.test.js +8 -2
- package/build/util/docs.test.js.map +1 -1
- package/build/util/security.d.ts +1 -0
- package/build/util/security.js +6 -5
- package/build/util/security.js.map +1 -1
- package/build/util/security.test.js +7 -1
- package/build/util/security.test.js.map +1 -1
- package/package.json +1 -1
package/build/index.test.js
CHANGED
|
@@ -1,30 +1,48 @@
|
|
|
1
1
|
import { setImmediate as nextTurn } from 'node:timers/promises';
|
|
2
|
-
import {
|
|
3
|
-
import { validateToolArgs, AddDocumentationArgsSchema, SearchDocumentationArgsSchema, SetTagsArgsSchema, detectPromptInjection, wrapExternalContent, addInjectionWarnings, sanitizeErrorMessage, SessionExpiredError, } from './util/security.js';
|
|
4
|
-
import { generateDocId } from './util/docs.js';
|
|
2
|
+
import { SessionExpiredError } from './util/security.js';
|
|
5
3
|
import { IndexingStatusTracker } from './indexing/status.js';
|
|
6
|
-
const { mockCrawlerAbort, mockCrawlerCrawl, mockCrawlerSetPathPrefix, mockClearSession, mockDatasetOpen, mockFetchFavicon, mockNotification, mockProcessorProcess, mockRunLatest, mockStoreAddDocument, mockStoreDeleteDocument, mockStoreGetCollectionUrls, mockStoreGetDocument, mockStoreSearchByText, requestHandlers, } = vi.hoisted(() => ({
|
|
4
|
+
const { mockCrawlerAbort, mockCrawlerCrawl, mockCrawlerSetPathPrefix, mockAuthCleanup, mockAuthHasSession, mockAuthInitialize, mockAuthPerformInteractiveLogin, mockAuthValidateSession, mockClearSession, mockCloseOutboundProxy, mockDatasetOpen, mockFetchFavicon, mockIsValidPublicUrl, mockLoadConfig, mockNotification, mockProcessorProcess, mockQueueCancelAll, mockRunLatest, mockServerClose, mockServerConnect, mockStoreAddDocument, mockStoreClose, mockStoreDeleteDocument, mockStoreGetCollectionUrls, mockStoreGetDocument, mockStoreSearchByText, requestHandlers, testConfig, } = vi.hoisted(() => ({
|
|
7
5
|
mockCrawlerAbort: vi.fn(),
|
|
8
6
|
mockCrawlerCrawl: vi.fn().mockImplementation(async function* () {
|
|
9
7
|
yield { url: 'https://example.com', path: '/', content: 'Test', contentFormat: 'text', title: 'Test' };
|
|
10
8
|
}),
|
|
11
9
|
mockCrawlerSetPathPrefix: vi.fn(),
|
|
10
|
+
mockAuthCleanup: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
mockAuthHasSession: vi.fn().mockResolvedValue(false),
|
|
12
|
+
mockAuthInitialize: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
mockAuthPerformInteractiveLogin: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
mockAuthValidateSession: vi.fn().mockResolvedValue({ isValid: true }),
|
|
12
15
|
mockClearSession: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
mockCloseOutboundProxy: vi.fn().mockResolvedValue(undefined),
|
|
13
17
|
mockDatasetOpen: vi.fn().mockResolvedValue({ drop: vi.fn().mockResolvedValue(undefined) }),
|
|
14
18
|
mockFetchFavicon: vi.fn().mockResolvedValue('https://example.com/favicon.ico'),
|
|
19
|
+
mockIsValidPublicUrl: vi.fn().mockReturnValue(true),
|
|
20
|
+
mockLoadConfig: vi.fn(),
|
|
15
21
|
mockNotification: vi.fn().mockResolvedValue(undefined),
|
|
16
22
|
mockProcessorProcess: vi.fn().mockResolvedValue({
|
|
17
23
|
metadata: { url: 'https://example.com', title: 'Test', lastIndexed: new Date() },
|
|
18
24
|
chunks: [],
|
|
19
25
|
}),
|
|
26
|
+
mockQueueCancelAll: vi.fn().mockResolvedValue(undefined),
|
|
20
27
|
mockRunLatest: vi.fn(),
|
|
28
|
+
mockServerClose: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
mockServerConnect: vi.fn().mockResolvedValue(undefined),
|
|
21
30
|
mockStoreAddDocument: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
mockStoreClose: vi.fn().mockResolvedValue(undefined),
|
|
22
32
|
mockStoreDeleteDocument: vi.fn().mockResolvedValue(undefined),
|
|
23
33
|
mockStoreGetCollectionUrls: vi.fn().mockResolvedValue([]),
|
|
24
34
|
mockStoreGetDocument: vi.fn().mockResolvedValue(null),
|
|
25
35
|
mockStoreSearchByText: vi.fn().mockResolvedValue([]),
|
|
26
36
|
requestHandlers: [],
|
|
37
|
+
testConfig: {
|
|
38
|
+
maxChunkSize: 1000,
|
|
39
|
+
cacheSize: 100,
|
|
40
|
+
dataDir: '/tmp/test',
|
|
41
|
+
dbPath: '/tmp/test/docs.db',
|
|
42
|
+
vectorDbPath: '/tmp/test/vectors',
|
|
43
|
+
},
|
|
27
44
|
}));
|
|
45
|
+
mockLoadConfig.mockResolvedValue(testConfig);
|
|
28
46
|
mockRunLatest.mockImplementation(async (_url, operation) => {
|
|
29
47
|
const completion = Promise.resolve().then(() => operation(new AbortController().signal));
|
|
30
48
|
return { completion, replacedExisting: false };
|
|
@@ -39,7 +57,8 @@ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
|
|
39
57
|
notification: mockNotification,
|
|
40
58
|
onerror: null,
|
|
41
59
|
},
|
|
42
|
-
connect:
|
|
60
|
+
connect: mockServerConnect,
|
|
61
|
+
close: mockServerClose,
|
|
43
62
|
};
|
|
44
63
|
}),
|
|
45
64
|
}));
|
|
@@ -50,6 +69,7 @@ vi.mock('./storage/storage.js', () => ({
|
|
|
50
69
|
DocumentStore: vi.fn().mockImplementation(function () {
|
|
51
70
|
return {
|
|
52
71
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
72
|
+
close: mockStoreClose,
|
|
53
73
|
listDocuments: vi.fn().mockResolvedValue([]),
|
|
54
74
|
getDocument: mockStoreGetDocument,
|
|
55
75
|
searchByText: mockStoreSearchByText,
|
|
@@ -64,8 +84,13 @@ vi.mock('./storage/storage.js', () => ({
|
|
|
64
84
|
}));
|
|
65
85
|
vi.mock('./indexing/queue-manager.js', () => ({
|
|
66
86
|
IndexingQueueManager: function () {
|
|
87
|
+
let closed = false;
|
|
67
88
|
return {
|
|
68
|
-
runLatest: mockRunLatest,
|
|
89
|
+
runLatest: (...args) => closed ? Promise.reject(new Error('Indexing queue is closed')) : mockRunLatest(...args),
|
|
90
|
+
cancelAll: () => {
|
|
91
|
+
closed = true;
|
|
92
|
+
return mockQueueCancelAll();
|
|
93
|
+
},
|
|
69
94
|
};
|
|
70
95
|
},
|
|
71
96
|
}));
|
|
@@ -97,31 +122,27 @@ vi.mock('./crawler/docs-crawler.js', () => ({
|
|
|
97
122
|
vi.mock('./crawler/auth.js', () => ({
|
|
98
123
|
AuthManager: vi.fn().mockImplementation(function () {
|
|
99
124
|
return {
|
|
100
|
-
initialize:
|
|
101
|
-
|
|
125
|
+
initialize: mockAuthInitialize,
|
|
126
|
+
cleanup: mockAuthCleanup,
|
|
127
|
+
hasSession: mockAuthHasSession,
|
|
102
128
|
loadSession: vi.fn().mockResolvedValue(null),
|
|
103
129
|
clearSession: mockClearSession,
|
|
104
|
-
performInteractiveLogin:
|
|
105
|
-
validateSession:
|
|
130
|
+
performInteractiveLogin: mockAuthPerformInteractiveLogin,
|
|
131
|
+
validateSession: mockAuthValidateSession,
|
|
106
132
|
};
|
|
107
133
|
}),
|
|
108
134
|
}));
|
|
109
135
|
vi.mock('./config.js', () => ({
|
|
110
|
-
loadConfig:
|
|
111
|
-
|
|
112
|
-
maxRequestsPerCrawl: 100,
|
|
113
|
-
maxChunkSize: 1000,
|
|
114
|
-
cacheSize: 100,
|
|
115
|
-
dataDir: '/tmp/test',
|
|
116
|
-
dbPath: '/tmp/test/docs.db',
|
|
117
|
-
vectorDbPath: '/tmp/test/vectors',
|
|
118
|
-
}),
|
|
119
|
-
isValidPublicUrl: vi.fn().mockReturnValue(true),
|
|
136
|
+
loadConfig: mockLoadConfig,
|
|
137
|
+
isValidPublicUrl: mockIsValidPublicUrl,
|
|
120
138
|
normalizeUrl: vi.fn().mockImplementation((url) => url.replace(/\/$/, '')),
|
|
121
139
|
}));
|
|
122
140
|
vi.mock('./util/favicon.js', () => ({
|
|
123
141
|
fetchFavicon: mockFetchFavicon,
|
|
124
142
|
}));
|
|
143
|
+
vi.mock('./util/outbound-request.js', () => ({
|
|
144
|
+
closeOutboundProxy: mockCloseOutboundProxy,
|
|
145
|
+
}));
|
|
125
146
|
vi.mock('./util/docs.js', () => ({
|
|
126
147
|
generateDocId: vi.fn().mockImplementation((url, title) => {
|
|
127
148
|
if (title.includes('/')) {
|
|
@@ -155,6 +176,11 @@ const processedPageWithChunk = {
|
|
|
155
176
|
describe('WebDocsServer', () => {
|
|
156
177
|
beforeEach(() => {
|
|
157
178
|
vi.clearAllMocks();
|
|
179
|
+
mockAuthHasSession.mockResolvedValue(false);
|
|
180
|
+
mockAuthPerformInteractiveLogin.mockResolvedValue(undefined);
|
|
181
|
+
mockAuthValidateSession.mockResolvedValue({ isValid: true });
|
|
182
|
+
mockClearSession.mockResolvedValue(undefined);
|
|
183
|
+
mockStoreClose.mockResolvedValue(undefined);
|
|
158
184
|
});
|
|
159
185
|
afterEach(() => {
|
|
160
186
|
vi.restoreAllMocks();
|
|
@@ -553,6 +579,58 @@ describe('WebDocsServer', () => {
|
|
|
553
579
|
await Promise.allSettled(completion ? [completion] : []);
|
|
554
580
|
}
|
|
555
581
|
});
|
|
582
|
+
it.each(['missing', 'valid', 'expired'])('handles a $state auth session when adding documentation', async (state) => {
|
|
583
|
+
mockAuthHasSession.mockResolvedValue(state !== 'missing');
|
|
584
|
+
mockAuthValidateSession.mockResolvedValue({ isValid: state !== 'expired', reason: 'expired cookie' });
|
|
585
|
+
if (state !== 'expired') {
|
|
586
|
+
mockProcessorProcess.mockResolvedValueOnce(processedPageWithChunk);
|
|
587
|
+
}
|
|
588
|
+
const call = toolHandler({
|
|
589
|
+
params: {
|
|
590
|
+
name: 'add_documentation',
|
|
591
|
+
arguments: { url: 'https://auth.example.com', auth: { requiresAuth: true } },
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (state === 'expired') {
|
|
595
|
+
await expect(call).rejects.toThrow('Authentication session has expired (expired cookie)');
|
|
596
|
+
expect(mockClearSession).toHaveBeenCalledWith('https://auth.example.com');
|
|
597
|
+
expect(mockRunLatest).not.toHaveBeenCalled();
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
await call;
|
|
601
|
+
await vi.waitFor(() => expect(mockStoreAddDocument).toHaveBeenCalledOnce());
|
|
602
|
+
expect(mockRunLatest).toHaveBeenCalledOnce();
|
|
603
|
+
}
|
|
604
|
+
expect(mockAuthPerformInteractiveLogin).toHaveBeenCalledTimes(state === 'missing' ? 1 : 0);
|
|
605
|
+
expect(mockAuthValidateSession).toHaveBeenCalledTimes(state === 'missing' ? 0 : 1);
|
|
606
|
+
});
|
|
607
|
+
it.each(['missing', 'valid', 'expired'])('handles a $state auth session when reindexing documentation', async (state) => {
|
|
608
|
+
const url = 'https://auth.example.com';
|
|
609
|
+
mockStoreGetDocument.mockResolvedValueOnce({
|
|
610
|
+
url,
|
|
611
|
+
title: 'Authenticated Docs',
|
|
612
|
+
lastIndexed: new Date(),
|
|
613
|
+
requiresAuth: true,
|
|
614
|
+
authDomain: 'auth.example.com',
|
|
615
|
+
});
|
|
616
|
+
mockAuthHasSession.mockResolvedValue(state !== 'missing');
|
|
617
|
+
mockAuthValidateSession.mockResolvedValue({ isValid: state !== 'expired', reason: 'expired cookie' });
|
|
618
|
+
if (state === 'valid') {
|
|
619
|
+
mockProcessorProcess.mockResolvedValueOnce(processedPageWithChunk);
|
|
620
|
+
}
|
|
621
|
+
const call = toolHandler({ params: { name: 'reindex_documentation', arguments: { url } } });
|
|
622
|
+
if (state === 'valid') {
|
|
623
|
+
await call;
|
|
624
|
+
await vi.waitFor(() => expect(mockStoreAddDocument).toHaveBeenCalledOnce());
|
|
625
|
+
expect(mockRunLatest).toHaveBeenCalledOnce();
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
await expect(call).rejects.toThrow(state === 'missing' ? 'no session was found' : 'Authentication session has expired');
|
|
629
|
+
expect(mockRunLatest).not.toHaveBeenCalled();
|
|
630
|
+
}
|
|
631
|
+
expect(mockAuthValidateSession).toHaveBeenCalledTimes(state === 'missing' ? 0 : 1);
|
|
632
|
+
expect(mockClearSession).toHaveBeenCalledTimes(state === 'expired' ? 1 : 0);
|
|
633
|
+
});
|
|
556
634
|
it('starts reindex status inside runLatest and preserves the replacement message', async () => {
|
|
557
635
|
const url = 'https://example.com';
|
|
558
636
|
mockStoreGetDocument.mockResolvedValueOnce({
|
|
@@ -583,6 +661,35 @@ describe('WebDocsServer', () => {
|
|
|
583
661
|
expect(mockCrawlerSetPathPrefix).toHaveBeenCalledWith('/api/v2');
|
|
584
662
|
expect(mockStoreAddDocument).toHaveBeenCalledWith(expect.objectContaining({ metadata: expect.objectContaining({ pathPrefix: '/api/v2' }) }), expect.objectContaining({ tags: ['docs'] }));
|
|
585
663
|
});
|
|
664
|
+
it.each([
|
|
665
|
+
{ name: 'overrides the stored prefix', stored: '/old', override: '/new', expected: '/new' },
|
|
666
|
+
{ name: 'clears the stored prefix', stored: '/old', override: null, expected: undefined },
|
|
667
|
+
{ name: 'normalizes a missing legacy prefix', stored: undefined, override: undefined, expected: undefined },
|
|
668
|
+
])('$name when reindexing', async ({ stored, override, expected }) => {
|
|
669
|
+
const url = 'https://prefix.example.com';
|
|
670
|
+
mockStoreGetDocument.mockResolvedValueOnce({
|
|
671
|
+
url,
|
|
672
|
+
title: 'Prefix Docs',
|
|
673
|
+
lastIndexed: new Date(),
|
|
674
|
+
requiresAuth: false,
|
|
675
|
+
pathPrefix: stored,
|
|
676
|
+
});
|
|
677
|
+
mockProcessorProcess.mockResolvedValueOnce(processedPageWithChunk);
|
|
678
|
+
await toolHandler({
|
|
679
|
+
params: {
|
|
680
|
+
name: 'reindex_documentation',
|
|
681
|
+
arguments: { url, ...(override !== undefined && { pathPrefix: override }) },
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
await vi.waitFor(() => expect(mockStoreAddDocument).toHaveBeenCalledOnce());
|
|
685
|
+
if (expected) {
|
|
686
|
+
expect(mockCrawlerSetPathPrefix).toHaveBeenCalledWith(expected);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
expect(mockCrawlerSetPathPrefix).not.toHaveBeenCalled();
|
|
690
|
+
}
|
|
691
|
+
expect(mockStoreAddDocument).toHaveBeenCalledWith(expect.objectContaining({ metadata: expect.objectContaining({ pathPrefix: expected }) }), expect.objectContaining({ tags: [] }));
|
|
692
|
+
});
|
|
586
693
|
it('deletes current and historical crawl datasets independently', async () => {
|
|
587
694
|
const legacyDrop = vi.fn().mockResolvedValue(undefined);
|
|
588
695
|
mockStoreGetDocument.mockResolvedValueOnce({
|
|
@@ -641,420 +748,147 @@ describe('WebDocsServer', () => {
|
|
|
641
748
|
expect(mockStoreAddDocument).not.toHaveBeenCalled();
|
|
642
749
|
});
|
|
643
750
|
});
|
|
644
|
-
describe('
|
|
645
|
-
|
|
646
|
-
const
|
|
647
|
-
|
|
751
|
+
describe('process shutdown', () => {
|
|
752
|
+
const captureProcess = () => {
|
|
753
|
+
const handlers = new Map();
|
|
754
|
+
vi.spyOn(process, 'once').mockImplementation(((event, listener) => {
|
|
755
|
+
handlers.set(event, listener);
|
|
756
|
+
return process;
|
|
757
|
+
}));
|
|
758
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
759
|
+
return { handlers, exit };
|
|
760
|
+
};
|
|
761
|
+
beforeEach(() => {
|
|
762
|
+
vi.useFakeTimers();
|
|
763
|
+
mockServerConnect.mockResolvedValue(undefined);
|
|
764
|
+
mockServerClose.mockResolvedValue(undefined);
|
|
765
|
+
mockQueueCancelAll.mockResolvedValue(undefined);
|
|
766
|
+
mockAuthInitialize.mockResolvedValue(undefined);
|
|
767
|
+
mockAuthCleanup.mockResolvedValue(undefined);
|
|
768
|
+
mockAuthHasSession.mockResolvedValue(false);
|
|
769
|
+
mockAuthPerformInteractiveLogin.mockResolvedValue(undefined);
|
|
770
|
+
mockAuthValidateSession.mockResolvedValue({ isValid: true });
|
|
771
|
+
mockCloseOutboundProxy.mockResolvedValue(undefined);
|
|
648
772
|
mockIsValidPublicUrl.mockReturnValue(true);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
};
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
expect(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
expect(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
expect(
|
|
678
|
-
expect(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
expect(
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
expect(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
expect(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
expect(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const content = 'Normal content';
|
|
769
|
-
const detectionResult = {
|
|
770
|
-
hasInjection: true,
|
|
771
|
-
maxSeverity: 'high',
|
|
772
|
-
detections: [{ severity: 'high', description: 'Test', match: 'test' }],
|
|
773
|
-
};
|
|
774
|
-
const result = addInjectionWarnings(content, detectionResult);
|
|
775
|
-
expect(result).toContain('⚠️ HIGH RISK');
|
|
776
|
-
expect(result).toContain('POTENTIAL PROMPT INJECTION DETECTED');
|
|
777
|
-
});
|
|
778
|
-
it('should not modify content without injections', () => {
|
|
779
|
-
const content = 'Normal documentation content';
|
|
780
|
-
const detectionResult = {
|
|
781
|
-
hasInjection: false,
|
|
782
|
-
maxSeverity: 'none',
|
|
783
|
-
detections: [],
|
|
784
|
-
};
|
|
785
|
-
const result = addInjectionWarnings(content, detectionResult);
|
|
786
|
-
expect(result).toBe(content);
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
describe('Document ID Generation', () => {
|
|
790
|
-
it('should generate IDs from URLs', () => {
|
|
791
|
-
const mockGenerateDocId = generateDocId;
|
|
792
|
-
mockGenerateDocId.mockReturnValue('example-com');
|
|
793
|
-
const id = generateDocId('https://example.com/docs', 'Example Docs');
|
|
794
|
-
expect(id).toBe('example-com');
|
|
795
|
-
});
|
|
796
|
-
});
|
|
797
|
-
describe('Error Handling', () => {
|
|
798
|
-
it('should sanitize error messages', () => {
|
|
799
|
-
const errorWithPassword = new Error('Connection failed: password=secret123');
|
|
800
|
-
const sanitized = sanitizeErrorMessage(errorWithPassword);
|
|
801
|
-
expect(sanitized).toContain('[REDACTED]');
|
|
802
|
-
expect(sanitized).not.toContain('secret123');
|
|
803
|
-
});
|
|
804
|
-
it('should handle unknown error types', () => {
|
|
805
|
-
const result = sanitizeErrorMessage(null);
|
|
806
|
-
expect(result).toBe('An unexpected error occurred');
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
describe('Mock Store Operations', () => {
|
|
810
|
-
it('should define expected store interface', () => {
|
|
811
|
-
// Verify the expected store interface
|
|
812
|
-
const storeInterface = {
|
|
813
|
-
initialize: vi.fn(),
|
|
814
|
-
listDocuments: vi.fn(),
|
|
815
|
-
getDocument: vi.fn(),
|
|
816
|
-
searchByText: vi.fn(),
|
|
817
|
-
addDocument: vi.fn(),
|
|
818
|
-
deleteDocument: vi.fn(),
|
|
819
|
-
};
|
|
820
|
-
expect(typeof storeInterface.initialize).toBe('function');
|
|
821
|
-
expect(typeof storeInterface.listDocuments).toBe('function');
|
|
822
|
-
expect(typeof storeInterface.getDocument).toBe('function');
|
|
823
|
-
expect(typeof storeInterface.searchByText).toBe('function');
|
|
824
|
-
});
|
|
825
|
-
it('should mock listDocuments return value', async () => {
|
|
826
|
-
const mockDocs = [{ url: 'https://example.com', title: 'Example', lastIndexed: new Date() }];
|
|
827
|
-
const mockListDocuments = vi.fn().mockResolvedValue(mockDocs);
|
|
828
|
-
const result = await mockListDocuments();
|
|
829
|
-
expect(result).toHaveLength(1);
|
|
830
|
-
expect(result[0].url).toBe('https://example.com');
|
|
831
|
-
});
|
|
832
|
-
it('should mock searchByText return value', async () => {
|
|
833
|
-
const mockResults = [
|
|
834
|
-
{
|
|
835
|
-
id: '1',
|
|
836
|
-
url: 'https://example.com/docs',
|
|
837
|
-
title: 'Docs',
|
|
838
|
-
content: 'Test content',
|
|
839
|
-
score: 0.9,
|
|
840
|
-
metadata: {
|
|
841
|
-
type: 'overview',
|
|
842
|
-
path: '/docs',
|
|
843
|
-
lastUpdated: new Date(),
|
|
844
|
-
},
|
|
845
|
-
},
|
|
846
|
-
];
|
|
847
|
-
const mockSearchByText = vi.fn().mockResolvedValue(mockResults);
|
|
848
|
-
const results = await mockSearchByText('test query');
|
|
849
|
-
expect(results).toHaveLength(1);
|
|
850
|
-
expect(results[0].score).toBe(0.9);
|
|
851
|
-
});
|
|
852
|
-
it('should mock getDocument returning null', async () => {
|
|
853
|
-
const mockGetDocument = vi.fn().mockResolvedValue(null);
|
|
854
|
-
const doc = await mockGetDocument('https://nonexistent.com');
|
|
855
|
-
expect(doc).toBeNull();
|
|
856
|
-
});
|
|
857
|
-
it('should mock setTags', async () => {
|
|
858
|
-
const mockSetTags = vi.fn().mockResolvedValue(undefined);
|
|
859
|
-
await mockSetTags('https://example.com', ['frontend', 'react']);
|
|
860
|
-
expect(mockSetTags).toHaveBeenCalledWith('https://example.com', ['frontend', 'react']);
|
|
861
|
-
});
|
|
862
|
-
it('should mock listAllTags return value', async () => {
|
|
863
|
-
const mockTags = [
|
|
864
|
-
{ tag: 'frontend', count: 5 },
|
|
865
|
-
{ tag: 'backend', count: 3 },
|
|
866
|
-
{ tag: 'api', count: 2 },
|
|
867
|
-
];
|
|
868
|
-
const mockListAllTags = vi.fn().mockResolvedValue(mockTags);
|
|
869
|
-
const tags = await mockListAllTags();
|
|
870
|
-
expect(tags).toHaveLength(3);
|
|
871
|
-
expect(tags[0].tag).toBe('frontend');
|
|
872
|
-
expect(tags[0].count).toBe(5);
|
|
873
|
-
});
|
|
874
|
-
it('should mock listDocuments with tags', async () => {
|
|
875
|
-
const mockDocs = [
|
|
876
|
-
{
|
|
877
|
-
url: 'https://example.com',
|
|
878
|
-
title: 'Example',
|
|
879
|
-
lastIndexed: new Date(),
|
|
880
|
-
tags: ['frontend', 'react'],
|
|
881
|
-
},
|
|
882
|
-
];
|
|
883
|
-
const mockListDocuments = vi.fn().mockResolvedValue(mockDocs);
|
|
884
|
-
const result = await mockListDocuments();
|
|
885
|
-
expect(result).toHaveLength(1);
|
|
886
|
-
expect(result[0].tags).toEqual(['frontend', 'react']);
|
|
887
|
-
});
|
|
888
|
-
it('should mock getDocument with tags', async () => {
|
|
889
|
-
const mockDoc = {
|
|
890
|
-
url: 'https://example.com',
|
|
891
|
-
title: 'Example',
|
|
892
|
-
lastIndexed: new Date(),
|
|
893
|
-
tags: ['frontend', 'mycompany'],
|
|
894
|
-
};
|
|
895
|
-
const mockGetDocument = vi.fn().mockResolvedValue(mockDoc);
|
|
896
|
-
const doc = await mockGetDocument('https://example.com');
|
|
897
|
-
expect(doc.tags).toEqual(['frontend', 'mycompany']);
|
|
898
|
-
});
|
|
899
|
-
});
|
|
900
|
-
describe('Auth Manager Operations', () => {
|
|
901
|
-
it('should mock hasSession', async () => {
|
|
902
|
-
const mockHasSession = vi.fn().mockResolvedValue(true);
|
|
903
|
-
const hasSession = await mockHasSession('https://example.com');
|
|
904
|
-
expect(hasSession).toBe(true);
|
|
905
|
-
});
|
|
906
|
-
it('should mock validateSession with valid session', async () => {
|
|
907
|
-
const mockValidateSession = vi.fn().mockResolvedValue({ isValid: true });
|
|
908
|
-
const validation = await mockValidateSession('https://example.com');
|
|
909
|
-
expect(validation.isValid).toBe(true);
|
|
910
|
-
});
|
|
911
|
-
it('should mock validateSession with expired session', async () => {
|
|
912
|
-
const mockValidateSession = vi.fn().mockResolvedValue({
|
|
913
|
-
isValid: false,
|
|
914
|
-
reason: 'Session cookie expired',
|
|
915
|
-
});
|
|
916
|
-
const validation = await mockValidateSession('https://example.com');
|
|
917
|
-
expect(validation.isValid).toBe(false);
|
|
918
|
-
expect(validation.reason).toBe('Session cookie expired');
|
|
919
|
-
});
|
|
920
|
-
it('should mock clearSession', async () => {
|
|
921
|
-
const mockClearSession = vi.fn().mockResolvedValue(undefined);
|
|
922
|
-
await mockClearSession('https://example.com');
|
|
923
|
-
expect(mockClearSession).toHaveBeenCalledWith('https://example.com');
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
describe('Authentication Detection Logic', () => {
|
|
927
|
-
describe('add_documentation with existing session', () => {
|
|
928
|
-
it('should auto-detect auth requirement when session exists', async () => {
|
|
929
|
-
// Simulate the logic in handleAddDocumentation:
|
|
930
|
-
// If hasSession returns true (session exists), even without auth.requiresAuth,
|
|
931
|
-
// the document should be marked as requiresAuth=true
|
|
932
|
-
const mockHasSession = vi.fn().mockResolvedValue(true);
|
|
933
|
-
const explicitAuthRequired = false; // No explicit auth option provided
|
|
934
|
-
const hasExistingSession = await mockHasSession('https://private.example.com');
|
|
935
|
-
const requiresAuth = explicitAuthRequired || hasExistingSession;
|
|
936
|
-
expect(hasExistingSession).toBe(true);
|
|
937
|
-
expect(requiresAuth).toBe(true);
|
|
938
|
-
});
|
|
939
|
-
it('should not mark auth required if no session and no auth option', async () => {
|
|
940
|
-
const mockHasSession = vi.fn().mockResolvedValue(false);
|
|
941
|
-
const explicitAuthRequired = false; // No explicit auth option provided
|
|
942
|
-
const hasExistingSession = await mockHasSession('https://public.example.com');
|
|
943
|
-
const requiresAuth = explicitAuthRequired || hasExistingSession;
|
|
944
|
-
expect(hasExistingSession).toBe(false);
|
|
945
|
-
expect(requiresAuth).toBe(false);
|
|
946
|
-
});
|
|
947
|
-
it('should respect explicit auth.requiresAuth=true', async () => {
|
|
948
|
-
const mockHasSession = vi.fn().mockResolvedValue(false);
|
|
949
|
-
const authOptions = { requiresAuth: true };
|
|
950
|
-
const hasExistingSession = await mockHasSession('https://private.example.com');
|
|
951
|
-
const requiresAuth = authOptions?.requiresAuth || hasExistingSession;
|
|
952
|
-
expect(requiresAuth).toBe(true);
|
|
953
|
-
});
|
|
954
|
-
it('should generate correct authDomain from URL', () => {
|
|
955
|
-
const url = 'https://private.example.com/docs/page';
|
|
956
|
-
const authDomain = new URL(url).hostname;
|
|
957
|
-
expect(authDomain).toBe('private.example.com');
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
describe('reindex_documentation with auth requirement', () => {
|
|
961
|
-
it('should require session validation when doc.requiresAuth is true', async () => {
|
|
962
|
-
// Simulate the logic in handleReindexDocumentation
|
|
963
|
-
const mockGetDocument = vi.fn().mockResolvedValue({
|
|
964
|
-
url: 'https://private.example.com',
|
|
965
|
-
title: 'Private Docs',
|
|
966
|
-
lastIndexed: new Date(),
|
|
967
|
-
requiresAuth: true,
|
|
968
|
-
authDomain: 'private.example.com',
|
|
969
|
-
});
|
|
970
|
-
const doc = await mockGetDocument('https://private.example.com');
|
|
971
|
-
expect(doc.requiresAuth).toBe(true);
|
|
972
|
-
// If doc.requiresAuth is true, we must validate session
|
|
973
|
-
const mustValidateSession = doc.requiresAuth === true;
|
|
974
|
-
expect(mustValidateSession).toBe(true);
|
|
975
|
-
});
|
|
976
|
-
it('should skip session validation when doc.requiresAuth is false', async () => {
|
|
977
|
-
const mockGetDocument = vi.fn().mockResolvedValue({
|
|
978
|
-
url: 'https://public.example.com',
|
|
979
|
-
title: 'Public Docs',
|
|
980
|
-
lastIndexed: new Date(),
|
|
981
|
-
requiresAuth: false,
|
|
982
|
-
});
|
|
983
|
-
const doc = await mockGetDocument('https://public.example.com');
|
|
984
|
-
expect(doc.requiresAuth).toBe(false);
|
|
985
|
-
const mustValidateSession = doc.requiresAuth === true;
|
|
986
|
-
expect(mustValidateSession).toBe(false);
|
|
987
|
-
});
|
|
988
|
-
it('should throw error when requiresAuth but no session exists', async () => {
|
|
989
|
-
const mockHasSession = vi.fn().mockResolvedValue(false);
|
|
990
|
-
const doc = {
|
|
991
|
-
requiresAuth: true,
|
|
992
|
-
authDomain: 'private.example.com',
|
|
993
|
-
};
|
|
994
|
-
const hasSession = await mockHasSession(doc.authDomain);
|
|
995
|
-
if (doc.requiresAuth && !hasSession) {
|
|
996
|
-
const error = new Error(`This documentation site requires authentication but no session was found. Please use the 'authenticate' tool to log in before re-indexing.`);
|
|
997
|
-
expect(error.message).toContain('requires authentication');
|
|
998
|
-
expect(error.message).toContain('no session was found');
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
it('should throw error when session is expired', async () => {
|
|
1002
|
-
const mockValidateSession = vi.fn().mockResolvedValue({
|
|
1003
|
-
isValid: false,
|
|
1004
|
-
reason: 'Cookie expired',
|
|
1005
|
-
});
|
|
1006
|
-
const validation = await mockValidateSession('https://private.example.com');
|
|
1007
|
-
if (!validation.isValid) {
|
|
1008
|
-
const error = new Error(`Authentication session has expired (${validation.reason}). Please use the 'authenticate' tool to log in again before re-indexing.`);
|
|
1009
|
-
expect(error.message).toContain('expired');
|
|
1010
|
-
expect(error.message).toContain('Cookie expired');
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1013
|
-
it('should proceed when session is valid', async () => {
|
|
1014
|
-
const mockValidateSession = vi.fn().mockResolvedValue({ isValid: true });
|
|
1015
|
-
const validation = await mockValidateSession('https://private.example.com');
|
|
1016
|
-
expect(validation.isValid).toBe(true);
|
|
1017
|
-
});
|
|
1018
|
-
it('should use authDomain for session lookup when available', async () => {
|
|
1019
|
-
const mockHasSession = vi.fn().mockResolvedValue(true);
|
|
1020
|
-
const doc = {
|
|
1021
|
-
url: 'https://shiny-adventure.pages.github.io',
|
|
1022
|
-
requiresAuth: true,
|
|
1023
|
-
authDomain: 'github.com', // Auth was done at github.com
|
|
1024
|
-
};
|
|
1025
|
-
// Should use authDomain, not the doc URL
|
|
1026
|
-
const sessionUrl = doc.authDomain || new URL(doc.url).hostname;
|
|
1027
|
-
expect(sessionUrl).toBe('github.com');
|
|
1028
|
-
await mockHasSession(sessionUrl);
|
|
1029
|
-
expect(mockHasSession).toHaveBeenCalledWith('github.com');
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
describe('authInfo preservation', () => {
|
|
1033
|
-
it('should pass authInfo to indexAndAdd when auth required', () => {
|
|
1034
|
-
const requiresAuth = true;
|
|
1035
|
-
const normalizedUrl = 'https://private.example.com';
|
|
1036
|
-
const authInfo = requiresAuth
|
|
1037
|
-
? {
|
|
1038
|
-
requiresAuth: true,
|
|
1039
|
-
authDomain: new URL(normalizedUrl).hostname,
|
|
1040
|
-
}
|
|
1041
|
-
: undefined;
|
|
1042
|
-
expect(authInfo).toEqual({
|
|
1043
|
-
requiresAuth: true,
|
|
1044
|
-
authDomain: 'private.example.com',
|
|
1045
|
-
});
|
|
1046
|
-
});
|
|
1047
|
-
it('should not pass authInfo when auth not required', () => {
|
|
1048
|
-
const requiresAuth = false;
|
|
1049
|
-
const normalizedUrl = 'https://public.example.com';
|
|
1050
|
-
const authInfo = requiresAuth
|
|
1051
|
-
? {
|
|
1052
|
-
requiresAuth: true,
|
|
1053
|
-
authDomain: new URL(normalizedUrl).hostname,
|
|
1054
|
-
}
|
|
1055
|
-
: undefined;
|
|
1056
|
-
expect(authInfo).toBeUndefined();
|
|
1057
|
-
});
|
|
773
|
+
mockLoadConfig.mockResolvedValue(testConfig);
|
|
774
|
+
mockStoreClose.mockResolvedValue(undefined);
|
|
775
|
+
});
|
|
776
|
+
afterEach(() => {
|
|
777
|
+
vi.useRealTimers();
|
|
778
|
+
});
|
|
779
|
+
it('deduplicates mixed signals and starts every cleanup while indexing settles', async () => {
|
|
780
|
+
const cancellation = Promise.withResolvers();
|
|
781
|
+
mockQueueCancelAll.mockReturnValue(cancellation.promise);
|
|
782
|
+
const { handlers, exit } = captureProcess();
|
|
783
|
+
vi.resetModules();
|
|
784
|
+
const { IndexingStatusTracker: StatusTracker } = await import('./indexing/status.js');
|
|
785
|
+
const stop = vi.spyOn(StatusTracker.prototype, 'stop');
|
|
786
|
+
await import('./index.js');
|
|
787
|
+
await vi.waitFor(() => expect(mockServerConnect).toHaveBeenCalledOnce());
|
|
788
|
+
handlers.get('SIGINT')?.('SIGINT');
|
|
789
|
+
handlers.get('SIGTERM')?.('SIGTERM');
|
|
790
|
+
await vi.waitFor(() => expect(mockQueueCancelAll).toHaveBeenCalledOnce());
|
|
791
|
+
expect(mockServerClose).toHaveBeenCalledOnce();
|
|
792
|
+
expect(mockServerClose.mock.invocationCallOrder[0]).toBeLessThan(mockQueueCancelAll.mock.invocationCallOrder[0]);
|
|
793
|
+
expect(stop).toHaveBeenCalledOnce();
|
|
794
|
+
expect(mockAuthCleanup).toHaveBeenCalledOnce();
|
|
795
|
+
expect(mockCloseOutboundProxy).toHaveBeenCalledOnce();
|
|
796
|
+
expect(mockStoreClose).not.toHaveBeenCalled();
|
|
797
|
+
expect(exit).not.toHaveBeenCalled();
|
|
798
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
799
|
+
cancellation.resolve();
|
|
800
|
+
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0));
|
|
801
|
+
expect(mockStoreClose).toHaveBeenCalledOnce();
|
|
802
|
+
expect(exit).toHaveBeenCalledOnce();
|
|
803
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
804
|
+
});
|
|
805
|
+
it('does not connect when shutdown starts during initialization', async () => {
|
|
806
|
+
const config = Promise.withResolvers();
|
|
807
|
+
mockLoadConfig.mockReturnValue(config.promise);
|
|
808
|
+
const { handlers, exit } = captureProcess();
|
|
809
|
+
vi.resetModules();
|
|
810
|
+
await import('./index.js');
|
|
811
|
+
handlers.get('SIGTERM')?.('SIGTERM');
|
|
812
|
+
await Promise.resolve();
|
|
813
|
+
config.resolve(testConfig);
|
|
814
|
+
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0));
|
|
815
|
+
expect(mockServerConnect).not.toHaveBeenCalled();
|
|
816
|
+
expect(mockAuthCleanup).toHaveBeenCalledOnce();
|
|
817
|
+
});
|
|
818
|
+
it('cleans up safely when initialization fails before auth exists', async () => {
|
|
819
|
+
const config = Promise.withResolvers();
|
|
820
|
+
mockLoadConfig.mockReturnValue(config.promise);
|
|
821
|
+
const { handlers, exit } = captureProcess();
|
|
822
|
+
vi.resetModules();
|
|
823
|
+
await import('./index.js');
|
|
824
|
+
handlers.get('SIGINT')?.('SIGINT');
|
|
825
|
+
config.reject(new Error('configuration unavailable'));
|
|
826
|
+
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0));
|
|
827
|
+
expect(mockAuthCleanup).not.toHaveBeenCalled();
|
|
828
|
+
});
|
|
829
|
+
it('drains a tool call accepted while the transport is closing', async () => {
|
|
830
|
+
const preflight = Promise.withResolvers();
|
|
831
|
+
const serverClose = Promise.withResolvers();
|
|
832
|
+
mockAuthHasSession.mockReturnValue(preflight.promise);
|
|
833
|
+
mockServerClose.mockReturnValue(serverClose.promise);
|
|
834
|
+
const { handlers, exit } = captureProcess();
|
|
835
|
+
vi.resetModules();
|
|
836
|
+
await import('./index.js');
|
|
837
|
+
await vi.waitFor(() => expect(mockServerConnect).toHaveBeenCalledOnce());
|
|
838
|
+
handlers.get('SIGTERM')?.('SIGTERM');
|
|
839
|
+
await vi.waitFor(() => expect(mockQueueCancelAll).toHaveBeenCalledOnce());
|
|
840
|
+
const toolHandler = requestHandlers.at(-1);
|
|
841
|
+
const toolCall = toolHandler({
|
|
842
|
+
params: { name: 'add_documentation', arguments: { url: 'https://example.com' } },
|
|
843
|
+
});
|
|
844
|
+
const rejection = expect(toolCall).rejects.toThrow('Indexing queue is closed');
|
|
845
|
+
serverClose.resolve();
|
|
846
|
+
await Promise.resolve();
|
|
847
|
+
expect(exit).not.toHaveBeenCalled();
|
|
848
|
+
preflight.resolve(false);
|
|
849
|
+
await rejection;
|
|
850
|
+
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0));
|
|
851
|
+
});
|
|
852
|
+
it('settles pending authentication before closing storage and exiting', async () => {
|
|
853
|
+
const login = Promise.withResolvers();
|
|
854
|
+
mockAuthPerformInteractiveLogin.mockReturnValue(login.promise);
|
|
855
|
+
mockAuthCleanup.mockImplementationOnce(async () => login.reject(new Error('browser closed')));
|
|
856
|
+
const { handlers, exit } = captureProcess();
|
|
857
|
+
vi.resetModules();
|
|
858
|
+
await import('./index.js');
|
|
859
|
+
await vi.waitFor(() => expect(mockServerConnect).toHaveBeenCalledOnce());
|
|
860
|
+
const toolHandler = requestHandlers.at(-1);
|
|
861
|
+
let authenticationSettled = false;
|
|
862
|
+
const authentication = toolHandler({ params: { name: 'authenticate', arguments: { url: 'https://auth.example.com' } } }).then((response) => {
|
|
863
|
+
authenticationSettled = true;
|
|
864
|
+
return response;
|
|
865
|
+
});
|
|
866
|
+
mockStoreClose.mockImplementationOnce(async () => expect(authenticationSettled).toBe(true));
|
|
867
|
+
await vi.waitFor(() => expect(mockAuthPerformInteractiveLogin).toHaveBeenCalledOnce());
|
|
868
|
+
handlers.get('SIGTERM')?.('SIGTERM');
|
|
869
|
+
await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(0));
|
|
870
|
+
const response = (await authentication);
|
|
871
|
+
expect(JSON.parse(response.content[0].text)).toEqual(expect.objectContaining({ status: 'failed' }));
|
|
872
|
+
expect(mockAuthCleanup.mock.invocationCallOrder[0]).toBeLessThan(mockStoreClose.mock.invocationCallOrder[0]);
|
|
873
|
+
expect(mockStoreClose).toHaveBeenCalledOnce();
|
|
874
|
+
});
|
|
875
|
+
it('exits once with failure when graceful shutdown times out', async () => {
|
|
876
|
+
const cancellation = Promise.withResolvers();
|
|
877
|
+
mockQueueCancelAll.mockReturnValue(cancellation.promise);
|
|
878
|
+
const { handlers, exit } = captureProcess();
|
|
879
|
+
vi.resetModules();
|
|
880
|
+
await import('./index.js');
|
|
881
|
+
await vi.waitFor(() => expect(mockServerConnect).toHaveBeenCalledOnce());
|
|
882
|
+
handlers.get('SIGINT')?.('SIGINT');
|
|
883
|
+
handlers.get('SIGTERM')?.('SIGTERM');
|
|
884
|
+
await Promise.resolve();
|
|
885
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
886
|
+
expect(exit).not.toHaveBeenCalled();
|
|
887
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
888
|
+
expect(exit).toHaveBeenCalledWith(1);
|
|
889
|
+
cancellation.resolve();
|
|
890
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
891
|
+
expect(exit).toHaveBeenCalledOnce();
|
|
1058
892
|
});
|
|
1059
893
|
});
|
|
1060
894
|
});
|