@marimo-team/frontend 0.23.16-dev35 → 0.23.16-dev37
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/dist/assets/{CellStatus-CmojVYzm.js → CellStatus-C6Cj3oJc.js} +1 -1
- package/dist/assets/{JsonOutput-BwwRpByA.js → JsonOutput-CyzvjIjx.js} +12 -12
- package/dist/assets/{MarimoErrorOutput-BWte8vjk.js → MarimoErrorOutput-CIuzXFFo.js} +4 -4
- package/dist/assets/{RenderHTML-DRltV71H.js → RenderHTML-Cd-PP5ZR.js} +1 -1
- package/dist/assets/{RunButton-DIegd2mN.js → RunButton-CnYWedW4.js} +1 -1
- package/dist/assets/{add-cell-with-ai-BBFD-M9V.js → add-cell-with-ai-ByOBQT99.js} +17 -17
- package/dist/assets/{add-connection-dialog-CECJ8507.js → add-connection-dialog-C97ylgk0.js} +1 -1
- package/dist/assets/{agent-panel-YaS0WXP1.js → agent-panel-Dxo9WUKV.js} +4 -4
- package/dist/assets/ai-model-dropdown-Bvqy98cI.js +5 -0
- package/dist/assets/{app-config-button-BW-w01aF.js → app-config-button-v1lG63C2.js} +1 -1
- package/dist/assets/{cell-editor-EV-1IccQ.js → cell-editor-BKrHbyX_.js} +16 -16
- package/dist/assets/cell-link-w5IbCQV4.js +1 -0
- package/dist/assets/cells-DzyDERZy.js +235 -0
- package/dist/assets/{chat-display-8BbC5B-D.js → chat-display-Ccgk4vbE.js} +1 -1
- package/dist/assets/{chat-panel-CMOZp5xW.js → chat-panel--53TIc3t.js} +4 -4
- package/dist/assets/{chat-ui-DWhJ2Ozy.js → chat-ui-B5rBxgsw.js} +4 -4
- package/dist/assets/{column-preview-DeXESYpM.js → column-preview-Cb8R9kfl.js} +1 -1
- package/dist/assets/{command-palette-Dx6AkiPd.js → command-palette-B1S9PjZi.js} +1 -1
- package/dist/assets/{common-XVELED_O.js → common-CJ3baa6D.js} +1 -1
- package/dist/assets/{components-BLHpmhXx.js → components-DpUIWGaj.js} +1 -1
- package/dist/assets/{datasource-CTqa8bf-.js → datasource-D_VcQMEn.js} +2 -2
- package/dist/assets/{dependency-graph-panel-C4Nzcddp.js → dependency-graph-panel-DY5lvPDx.js} +4 -4
- package/dist/assets/{documentation-panel-Bym4G7IS.js → documentation-panel-CGhwxsXK.js} +1 -1
- package/dist/assets/{download-HKgqJX4l.js → download-DpS57jyt.js} +2 -2
- package/dist/assets/{edit-page-Jtp6Qqjl.js → edit-page-DA765yQV.js} +6 -6
- package/dist/assets/error-entries-CTYMwfsh.js +8 -0
- package/dist/assets/{error-panel-CfSy7uUr.js → error-panel-DGAkG5kW.js} +1 -1
- package/dist/assets/{file-explorer-panel-Dd5Xn2TK.js → file-explorer-panel-BKUo-MDh.js} +4 -4
- package/dist/assets/{file-icons-CSjJ3JC6.js → file-icons-DfWENcb8.js} +1 -1
- package/dist/assets/{file-name-input-CrKtrXMl.js → file-name-input-CNVZtm5K.js} +1 -1
- package/dist/assets/{floating-outline-ByQ8vvif.js → floating-outline-LitP0wCD.js} +1 -1
- package/dist/assets/{focus-CVONl-Dy.js → focus-CNKbm-Xz.js} +1 -1
- package/dist/assets/{form-Ca3yd_Od.js → form-nHzLTU1m.js} +2 -2
- package/dist/assets/{home-page-FpAawj9s.js → home-page-DM9Z_IJ7.js} +2 -2
- package/dist/assets/{html-to-image-C-4ysbGe.js → html-to-image-JaxFFZcr.js} +2 -2
- package/dist/assets/{index-CiJmYQBT.js → index-C7_REKh1.js} +7 -7
- package/dist/assets/{index-xs-zSSrX.css → index-D6Dp_PFj.css} +1 -1
- package/dist/assets/{layout-Dtql_o_T.js → layout-B29hqiiK.js} +5 -5
- package/dist/assets/logs-panel-CQRPYS8j.js +1 -0
- package/dist/assets/{markdown-renderer-dABDqZDw.js → markdown-renderer-sEayY9-m.js} +2 -2
- package/dist/assets/{name-cell-input-Y4knmEIU.js → name-cell-input-B-aj1KH3.js} +1 -1
- package/dist/assets/{outline-panel-CQ_KWLip.js → outline-panel-BOryk60D.js} +1 -1
- package/dist/assets/{packages-panel-Ijt7d51P.js → packages-panel-DoYDLty3.js} +1 -1
- package/dist/assets/{panels-DfU1Oikj.js → panels-Bb4fIHgH.js} +2 -2
- package/dist/assets/process-output-d-LEMtGI.js +1 -0
- package/dist/assets/{radio-group-22eWJ9fv.js → radio-group-DfioQthW.js} +1 -1
- package/dist/assets/readonly-code-display-C9fu8tvK.js +1 -0
- package/dist/assets/{readonly-python-code-n_TrLmsR.js → readonly-python-code-DHxOnQGj.js} +1 -1
- package/dist/assets/{reveal-component-CCLTV1q6.js → reveal-component-5jf0W625.js} +2 -2
- package/dist/assets/{run-page-DbezK7Qs.js → run-page-BMKx9ld_.js} +1 -1
- package/dist/assets/scratchpad-panel-C8z8vzBX.js +1 -0
- package/dist/assets/{session-panel-BZGNcZmp.js → session-panel-BVVk-02h.js} +1 -1
- package/dist/assets/{skeleton-BzlKcq21.js → skeleton-Bubr9pIj.js} +1 -1
- package/dist/assets/{snippets-panel-DNaNtkR0.js → snippets-panel-U2-iok-c.js} +1 -1
- package/dist/assets/{state-DFPAfLw7.js → state-DtziTd1T.js} +3 -3
- package/dist/assets/{state-jCdh8Dcx.js → state-DxQhUsG9.js} +1 -1
- package/dist/assets/{textarea-DuEtSOCl.js → textarea-Bmzmbbpo.js} +1 -1
- package/dist/assets/{tracing-ttUd3J7U.js → tracing-B5XFvMmV.js} +1 -1
- package/dist/assets/{tracing-panel-rVAa3En0.js → tracing-panel-DnR2bYDe.js} +2 -2
- package/dist/assets/{tree-actions-UCr1tMHw.js → tree-actions-Dx9CDaxV.js} +1 -1
- package/dist/assets/useCellActionButton-DypR3yDt.js +1 -0
- package/dist/assets/{useDeleteCell-PrgejW8A.js → useDeleteCell-vMXAr3rY.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-5jc-_-hy.js → useDependencyPanelTab-CwdwKf7A.js} +1 -1
- package/dist/assets/{useNotebookActions-4vw_NiRs.js → useNotebookActions-jWIcmpv0.js} +3 -3
- package/dist/assets/useRunCells-C2dvU2jA.js +1 -0
- package/dist/assets/{useSplitCell-MZpER0Bh.js → useSplitCell-C1tubdNi.js} +1 -1
- package/dist/index.html +23 -23
- package/package.json +2 -3
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/rtc/state.ts +9 -0
- package/src/css/app/codemirror.css +16 -2
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/dist/assets/ai-model-dropdown-BpVBUZnz.js +0 -5
- package/dist/assets/cell-link-C5gpAn3R.js +0 -1
- package/dist/assets/cells-6yd8nOAu.js +0 -231
- package/dist/assets/error-entries-CxyvaxEM.js +0 -8
- package/dist/assets/logs-panel-5u1wUBn1.js +0 -1
- package/dist/assets/process-output-Dd11YI04.js +0 -1
- package/dist/assets/readonly-code-display-BjK-TEri.js +0 -1
- package/dist/assets/scratchpad-panel-C2wAEv5n.js +0 -1
- package/dist/assets/useCellActionButton-CJknR0XY.js +0 -1
- package/dist/assets/useRunCells-DHsd7HGL.js +0 -1
|
@@ -1,565 +1,153 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { WebSocketTransport } from "@open-rpc/client-js";
|
|
5
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
3
|
import { Mocks } from "@/__mocks__/common";
|
|
7
4
|
import { LazyWebsocketTransport } from "../transport";
|
|
8
5
|
|
|
9
|
-
// Mock the Logger
|
|
10
6
|
vi.mock("@/utils/Logger", () => ({
|
|
11
7
|
Logger: Mocks.logger(),
|
|
12
8
|
}));
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
class FakeWebSocket {
|
|
11
|
+
static readonly OPEN = 1;
|
|
12
|
+
static readonly CLOSED = 3;
|
|
13
|
+
static readonly instances: FakeWebSocket[] = [];
|
|
14
|
+
|
|
15
|
+
readonly sent: string[] = [];
|
|
16
|
+
readyState = 0;
|
|
17
|
+
readonly url: string;
|
|
18
|
+
private readonly listeners = new Map<
|
|
19
|
+
string,
|
|
20
|
+
Set<(event: Record<string, never>) => void>
|
|
21
|
+
>();
|
|
22
|
+
|
|
23
|
+
constructor(url: string) {
|
|
24
|
+
this.url = url;
|
|
25
|
+
FakeWebSocket.instances.push(this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addEventListener(
|
|
29
|
+
type: string,
|
|
30
|
+
listener: (event: Record<string, never>) => void,
|
|
31
|
+
) {
|
|
32
|
+
const listeners = this.listeners.get(type) ?? new Set();
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
this.listeners.set(type, listeners);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
send(frame: string) {
|
|
38
|
+
this.sent.push(frame);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
close() {
|
|
42
|
+
this.readyState = FakeWebSocket.CLOSED;
|
|
43
|
+
this.emit("close");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
open() {
|
|
47
|
+
this.readyState = FakeWebSocket.OPEN;
|
|
48
|
+
this.emit("open");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fail() {
|
|
52
|
+
this.emit("error");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private emit(type: string) {
|
|
56
|
+
for (const listener of this.listeners.get(type) ?? []) {
|
|
57
|
+
listener({});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getSocket(index: number): Promise<FakeWebSocket> {
|
|
63
|
+
await vi.waitFor(() => {
|
|
64
|
+
expect(FakeWebSocket.instances.length).toBeGreaterThan(index);
|
|
65
|
+
});
|
|
66
|
+
return FakeWebSocket.instances[index];
|
|
67
|
+
}
|
|
27
68
|
|
|
28
69
|
describe("LazyWebsocketTransport", () => {
|
|
29
70
|
const mockWsUrl = "ws://localhost:8080/copilot";
|
|
30
|
-
let mockGetWsUrl: ReturnType<typeof vi.fn>;
|
|
31
|
-
let mockWaitForReady: ReturnType<typeof vi.fn>;
|
|
32
|
-
let mockShowError: ReturnType<typeof vi.fn>;
|
|
33
71
|
|
|
34
72
|
beforeEach(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
mockGetWsUrl = vi.fn(() => mockWsUrl);
|
|
38
|
-
mockWaitForReady = vi.fn().mockResolvedValue(undefined);
|
|
39
|
-
mockShowError = vi.fn();
|
|
40
|
-
|
|
41
|
-
// Mock the WebSocketTransport constructor
|
|
42
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
43
|
-
this.connect = vi.fn().mockResolvedValue(undefined);
|
|
44
|
-
this.close = vi.fn();
|
|
45
|
-
this.sendData = vi.fn().mockResolvedValue({ result: "success" });
|
|
46
|
-
this.subscribe = vi.fn();
|
|
47
|
-
this.unsubscribe = vi.fn();
|
|
48
|
-
});
|
|
73
|
+
FakeWebSocket.instances.length = 0;
|
|
74
|
+
vi.stubGlobal("WebSocket", FakeWebSocket);
|
|
49
75
|
});
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const transport = new LazyWebsocketTransport({
|
|
54
|
-
getWsUrl: mockGetWsUrl,
|
|
55
|
-
waitForReady: mockWaitForReady,
|
|
56
|
-
showError: mockShowError,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(transport).toBeDefined();
|
|
60
|
-
expect(mockGetWsUrl).not.toHaveBeenCalled();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("should accept custom retry options", () => {
|
|
64
|
-
const transport = new LazyWebsocketTransport({
|
|
65
|
-
getWsUrl: mockGetWsUrl,
|
|
66
|
-
waitForReady: mockWaitForReady,
|
|
67
|
-
showError: mockShowError,
|
|
68
|
-
retries: 5,
|
|
69
|
-
retryDelayMs: 2000,
|
|
70
|
-
maxTimeoutMs: 10_000,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(transport).toBeDefined();
|
|
74
|
-
});
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
vi.unstubAllGlobals();
|
|
75
79
|
});
|
|
76
80
|
|
|
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
|
-
expect(delegate).toBeDefined();
|
|
103
|
-
expect(delegate.connect).toHaveBeenCalledTimes(1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should retry on connection failure", async () => {
|
|
107
|
-
let attemptCount = 0;
|
|
108
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
109
|
-
this.connect = vi.fn().mockImplementation(() => {
|
|
110
|
-
attemptCount++;
|
|
111
|
-
if (attemptCount < 2) {
|
|
112
|
-
return Promise.reject(new Error("Connection failed"));
|
|
113
|
-
}
|
|
114
|
-
return Promise.resolve(undefined);
|
|
115
|
-
});
|
|
116
|
-
this.close = vi.fn();
|
|
117
|
-
this.sendData = vi.fn();
|
|
118
|
-
this.subscribe = vi.fn();
|
|
119
|
-
this.unsubscribe = vi.fn();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const transport = new LazyWebsocketTransport({
|
|
123
|
-
getWsUrl: mockGetWsUrl,
|
|
124
|
-
waitForReady: mockWaitForReady,
|
|
125
|
-
showError: mockShowError,
|
|
126
|
-
retries: 3,
|
|
127
|
-
retryDelayMs: 10,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
await transport.connect();
|
|
131
|
-
|
|
132
|
-
expect(attemptCount).toBe(2);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should show error toast on final retry failure", async () => {
|
|
136
|
-
const connectionError = new Error("Connection failed");
|
|
137
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
138
|
-
this.connect = vi.fn().mockRejectedValue(connectionError);
|
|
139
|
-
this.close = vi.fn();
|
|
140
|
-
this.sendData = vi.fn();
|
|
141
|
-
this.subscribe = vi.fn();
|
|
142
|
-
this.unsubscribe = vi.fn();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const transport = new LazyWebsocketTransport({
|
|
146
|
-
getWsUrl: mockGetWsUrl,
|
|
147
|
-
waitForReady: mockWaitForReady,
|
|
148
|
-
showError: mockShowError,
|
|
149
|
-
retries: 2,
|
|
150
|
-
retryDelayMs: 10,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
await expect(transport.connect()).rejects.toThrow("Connection failed");
|
|
154
|
-
|
|
155
|
-
expect(mockShowError).toHaveBeenCalledTimes(1);
|
|
156
|
-
expect(mockShowError).toHaveBeenCalledWith(
|
|
157
|
-
"GitHub Copilot Connection Error",
|
|
158
|
-
"Failed to connect to GitHub Copilot. Please check your settings and try again.\n\nConnection failed",
|
|
159
|
-
);
|
|
160
|
-
expect((transport as any).delegate).toBeUndefined();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("should handle waitForReady failure", async () => {
|
|
164
|
-
const waitError = new Error("Prerequisites not ready");
|
|
165
|
-
mockWaitForReady.mockRejectedValue(waitError);
|
|
166
|
-
|
|
167
|
-
const transport = new LazyWebsocketTransport({
|
|
168
|
-
getWsUrl: mockGetWsUrl,
|
|
169
|
-
waitForReady: mockWaitForReady,
|
|
170
|
-
showError: mockShowError,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await expect(transport.connect()).rejects.toThrow(
|
|
174
|
-
"Prerequisites not ready",
|
|
175
|
-
);
|
|
176
|
-
expect(WebSocketTransport).not.toHaveBeenCalled();
|
|
177
|
-
});
|
|
81
|
+
it("waits for Copilot prerequisites before connecting", async () => {
|
|
82
|
+
let ready: (() => void) | undefined;
|
|
83
|
+
const waitForReady = vi.fn(
|
|
84
|
+
() =>
|
|
85
|
+
new Promise<void>((resolve) => {
|
|
86
|
+
ready = resolve;
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
const transport = new LazyWebsocketTransport({
|
|
90
|
+
getWsUrl: () => mockWsUrl,
|
|
91
|
+
waitForReady,
|
|
92
|
+
showError: vi.fn(),
|
|
93
|
+
retryDelayMs: 0,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const connection = transport.connect();
|
|
97
|
+
expect(waitForReady).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(FakeWebSocket.instances).toHaveLength(0);
|
|
99
|
+
|
|
100
|
+
ready?.();
|
|
101
|
+
const socket = await getSocket(0);
|
|
102
|
+
socket.open();
|
|
103
|
+
await connection;
|
|
104
|
+
|
|
105
|
+
expect(socket.url).toBe(mockWsUrl);
|
|
178
106
|
});
|
|
179
107
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
waitForReady: mockWaitForReady,
|
|
202
|
-
showError: mockShowError,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
await transport.connect();
|
|
206
|
-
|
|
207
|
-
const handler = vi.fn();
|
|
208
|
-
transport.subscribe("notification", handler);
|
|
209
|
-
|
|
210
|
-
const delegate = (transport as any).delegate;
|
|
211
|
-
expect(delegate.subscribe).toHaveBeenCalledWith("notification", handler);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("should register pending subscriptions when delegate is created", async () => {
|
|
215
|
-
const transport = new LazyWebsocketTransport({
|
|
216
|
-
getWsUrl: mockGetWsUrl,
|
|
217
|
-
waitForReady: mockWaitForReady,
|
|
218
|
-
showError: mockShowError,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const handler1 = vi.fn();
|
|
222
|
-
const handler2 = vi.fn();
|
|
223
|
-
transport.subscribe("notification", handler1);
|
|
224
|
-
transport.subscribe("response", handler2);
|
|
225
|
-
|
|
226
|
-
await transport.connect();
|
|
227
|
-
|
|
228
|
-
const delegate = (transport as any).delegate;
|
|
229
|
-
expect(delegate.subscribe).toHaveBeenCalledWith("notification", handler1);
|
|
230
|
-
expect(delegate.subscribe).toHaveBeenCalledWith("response", handler2);
|
|
231
|
-
});
|
|
108
|
+
it("shows a user-facing error after the final retry", async () => {
|
|
109
|
+
const showError = vi.fn();
|
|
110
|
+
const transport = new LazyWebsocketTransport({
|
|
111
|
+
getWsUrl: () => mockWsUrl,
|
|
112
|
+
waitForReady: vi.fn().mockResolvedValue(undefined),
|
|
113
|
+
showError,
|
|
114
|
+
retries: 2,
|
|
115
|
+
retryDelayMs: 0,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const connection = transport.connect();
|
|
119
|
+
(await getSocket(0)).fail();
|
|
120
|
+
(await getSocket(1)).fail();
|
|
121
|
+
|
|
122
|
+
await expect(connection).rejects.toThrow(
|
|
123
|
+
"WebSocket connection to ws://localhost:8080/copilot failed",
|
|
124
|
+
);
|
|
125
|
+
expect(showError).toHaveBeenCalledWith(
|
|
126
|
+
"GitHub Copilot Connection Error",
|
|
127
|
+
"Failed to connect to GitHub Copilot. Please check your settings and try again.\n\nWebSocket connection to ws://localhost:8080/copilot failed",
|
|
128
|
+
);
|
|
232
129
|
});
|
|
233
130
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("should remove handler from all events when event not specified", () => {
|
|
256
|
-
const transport = new LazyWebsocketTransport({
|
|
257
|
-
getWsUrl: mockGetWsUrl,
|
|
258
|
-
waitForReady: mockWaitForReady,
|
|
259
|
-
showError: mockShowError,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const handler = vi.fn();
|
|
263
|
-
transport.subscribe("notification", handler);
|
|
264
|
-
transport.subscribe("response", handler);
|
|
265
|
-
|
|
266
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(2);
|
|
267
|
-
|
|
268
|
-
transport.unsubscribe(undefined, handler);
|
|
269
|
-
|
|
270
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(0);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("should remove all handlers for event when handler not specified", () => {
|
|
274
|
-
const transport = new LazyWebsocketTransport({
|
|
275
|
-
getWsUrl: mockGetWsUrl,
|
|
276
|
-
waitForReady: mockWaitForReady,
|
|
277
|
-
showError: mockShowError,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const handler1 = vi.fn();
|
|
281
|
-
const handler2 = vi.fn();
|
|
282
|
-
transport.subscribe("notification", handler1);
|
|
283
|
-
transport.subscribe("notification", handler2);
|
|
284
|
-
transport.subscribe("response", handler1);
|
|
285
|
-
|
|
286
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(3);
|
|
287
|
-
|
|
288
|
-
transport.unsubscribe("notification");
|
|
289
|
-
|
|
290
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(1);
|
|
291
|
-
expect((transport as any).pendingSubscriptions[0].event).toBe("response");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it("should remove all subscriptions when neither event nor handler specified", () => {
|
|
295
|
-
const transport = new LazyWebsocketTransport({
|
|
296
|
-
getWsUrl: mockGetWsUrl,
|
|
297
|
-
waitForReady: mockWaitForReady,
|
|
298
|
-
showError: mockShowError,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
transport.subscribe("notification", vi.fn());
|
|
302
|
-
transport.subscribe("response", vi.fn());
|
|
303
|
-
|
|
304
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(2);
|
|
305
|
-
|
|
306
|
-
transport.unsubscribe();
|
|
307
|
-
|
|
308
|
-
expect((transport as any).pendingSubscriptions).toHaveLength(0);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("should unsubscribe from delegate if it exists", async () => {
|
|
312
|
-
const transport = new LazyWebsocketTransport({
|
|
313
|
-
getWsUrl: mockGetWsUrl,
|
|
314
|
-
waitForReady: mockWaitForReady,
|
|
315
|
-
showError: mockShowError,
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
await transport.connect();
|
|
319
|
-
|
|
320
|
-
const handler = vi.fn();
|
|
321
|
-
transport.subscribe("notification", handler);
|
|
322
|
-
transport.unsubscribe("notification", handler);
|
|
323
|
-
|
|
324
|
-
const delegate = (transport as any).delegate;
|
|
325
|
-
expect(delegate.unsubscribe).toHaveBeenCalledWith(
|
|
326
|
-
"notification",
|
|
327
|
-
handler,
|
|
328
|
-
);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe("sendData", () => {
|
|
333
|
-
it("should send data through delegate when connected", async () => {
|
|
334
|
-
const transport = new LazyWebsocketTransport({
|
|
335
|
-
getWsUrl: mockGetWsUrl,
|
|
336
|
-
waitForReady: mockWaitForReady,
|
|
337
|
-
showError: mockShowError,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
await transport.connect();
|
|
341
|
-
|
|
342
|
-
const data: any = { method: "test", params: [] };
|
|
343
|
-
const result = await transport.sendData(data, 5000);
|
|
344
|
-
|
|
345
|
-
const delegate = (transport as any).delegate;
|
|
346
|
-
expect(delegate.sendData).toHaveBeenCalledWith(data, 5000);
|
|
347
|
-
expect(result).toEqual({ result: "success" });
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("should reconnect if delegate is undefined", async () => {
|
|
351
|
-
const transport = new LazyWebsocketTransport({
|
|
352
|
-
getWsUrl: mockGetWsUrl,
|
|
353
|
-
waitForReady: mockWaitForReady,
|
|
354
|
-
showError: mockShowError,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
const data: any = { method: "test", params: [] };
|
|
358
|
-
await transport.sendData(data, 5000);
|
|
359
|
-
|
|
360
|
-
// sendData calls tryConnect directly, not connect, so wait functions aren't called
|
|
361
|
-
// But it should still create and connect the delegate
|
|
362
|
-
expect(WebSocketTransport).toHaveBeenCalled();
|
|
363
|
-
const delegate = (transport as any).delegate;
|
|
364
|
-
expect(delegate).toBeDefined();
|
|
365
|
-
expect(delegate.connect).toHaveBeenCalled();
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("should use maxTimeoutMs as default when no timeout is provided", async () => {
|
|
369
|
-
const transport = new LazyWebsocketTransport({
|
|
370
|
-
getWsUrl: mockGetWsUrl,
|
|
371
|
-
waitForReady: mockWaitForReady,
|
|
372
|
-
showError: mockShowError,
|
|
373
|
-
maxTimeoutMs: 5000,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await transport.connect();
|
|
377
|
-
|
|
378
|
-
const data: any = { method: "test", params: [] };
|
|
379
|
-
await transport.sendData(data, undefined);
|
|
380
|
-
|
|
381
|
-
const delegate = (transport as any).delegate;
|
|
382
|
-
expect(delegate.sendData).toHaveBeenCalledWith(data, 5000);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("should respect caller-provided timeout without clamping", async () => {
|
|
386
|
-
const transport = new LazyWebsocketTransport({
|
|
387
|
-
getWsUrl: mockGetWsUrl,
|
|
388
|
-
waitForReady: mockWaitForReady,
|
|
389
|
-
showError: mockShowError,
|
|
390
|
-
maxTimeoutMs: 5000,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
await transport.connect();
|
|
394
|
-
|
|
395
|
-
const data: any = { method: "test", params: [] };
|
|
396
|
-
await transport.sendData(data, 30_000);
|
|
397
|
-
|
|
398
|
-
const delegate = (transport as any).delegate;
|
|
399
|
-
expect(delegate.sendData).toHaveBeenCalledWith(data, 30_000);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("should throw error if reconnection fails", async () => {
|
|
403
|
-
const connectionError = new Error("Connection failed");
|
|
404
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
405
|
-
this.connect = vi.fn().mockRejectedValue(connectionError);
|
|
406
|
-
this.close = vi.fn();
|
|
407
|
-
this.sendData = vi.fn();
|
|
408
|
-
this.subscribe = vi.fn();
|
|
409
|
-
this.unsubscribe = vi.fn();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const transport = new LazyWebsocketTransport({
|
|
413
|
-
getWsUrl: mockGetWsUrl,
|
|
414
|
-
waitForReady: mockWaitForReady,
|
|
415
|
-
showError: mockShowError,
|
|
416
|
-
retries: 1,
|
|
417
|
-
retryDelayMs: 10,
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const data: any = { method: "test", params: [] };
|
|
421
|
-
await expect(transport.sendData(data, 5000)).rejects.toThrow(
|
|
422
|
-
"Unable to connect to GitHub Copilot",
|
|
423
|
-
);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
describe("onReconnect", () => {
|
|
428
|
-
it("should call onReconnect after close + sendData reconnection", async () => {
|
|
429
|
-
const transport = new LazyWebsocketTransport({
|
|
430
|
-
getWsUrl: mockGetWsUrl,
|
|
431
|
-
waitForReady: mockWaitForReady,
|
|
432
|
-
showError: mockShowError,
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const onReconnect = vi.fn().mockResolvedValue(undefined);
|
|
436
|
-
transport.onReconnect = onReconnect;
|
|
437
|
-
|
|
438
|
-
// Initial connect
|
|
439
|
-
await transport.connect();
|
|
440
|
-
expect(onReconnect).not.toHaveBeenCalled();
|
|
441
|
-
|
|
442
|
-
// Close the transport (simulates failure handling)
|
|
443
|
-
transport.close();
|
|
444
|
-
|
|
445
|
-
// sendData should reconnect and call onReconnect
|
|
446
|
-
const data: any = { method: "test", params: [] };
|
|
447
|
-
await transport.sendData(data, 5000);
|
|
448
|
-
|
|
131
|
+
it("inherits reconnect callbacks for Copilot re-initialization", async () => {
|
|
132
|
+
const transport = new LazyWebsocketTransport({
|
|
133
|
+
getWsUrl: () => mockWsUrl,
|
|
134
|
+
waitForReady: vi.fn().mockResolvedValue(undefined),
|
|
135
|
+
showError: vi.fn(),
|
|
136
|
+
retryDelayMs: 0,
|
|
137
|
+
});
|
|
138
|
+
const onReconnect = vi.fn().mockResolvedValue(undefined);
|
|
139
|
+
transport.onReconnect = onReconnect;
|
|
140
|
+
|
|
141
|
+
const initialConnection = transport.connect();
|
|
142
|
+
const firstSocket = await getSocket(0);
|
|
143
|
+
firstSocket.open();
|
|
144
|
+
await initialConnection;
|
|
145
|
+
firstSocket.close();
|
|
146
|
+
|
|
147
|
+
const secondSocket = await getSocket(1);
|
|
148
|
+
secondSocket.open();
|
|
149
|
+
await vi.waitFor(() => {
|
|
449
150
|
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
450
151
|
});
|
|
451
|
-
|
|
452
|
-
it("should NOT call onReconnect on initial sendData connection", async () => {
|
|
453
|
-
const transport = new LazyWebsocketTransport({
|
|
454
|
-
getWsUrl: mockGetWsUrl,
|
|
455
|
-
waitForReady: mockWaitForReady,
|
|
456
|
-
showError: mockShowError,
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
const onReconnect = vi.fn().mockResolvedValue(undefined);
|
|
460
|
-
transport.onReconnect = onReconnect;
|
|
461
|
-
|
|
462
|
-
// sendData without prior connect — this is the initial connection
|
|
463
|
-
const data: any = { method: "test", params: [] };
|
|
464
|
-
await transport.sendData(data, 5000);
|
|
465
|
-
|
|
466
|
-
expect(onReconnect).not.toHaveBeenCalled();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("should call onReconnect after tryConnect failure + retry via sendData", async () => {
|
|
470
|
-
let connectAttempt = 0;
|
|
471
|
-
(WebSocketTransport as any).mockImplementation(function (this: any) {
|
|
472
|
-
this.connect = vi.fn().mockImplementation(() => {
|
|
473
|
-
connectAttempt++;
|
|
474
|
-
// First 2 attempts fail (retries=2 exhausted), then succeed on next sendData
|
|
475
|
-
if (connectAttempt <= 2) {
|
|
476
|
-
return Promise.reject(new Error("Connection failed"));
|
|
477
|
-
}
|
|
478
|
-
return Promise.resolve(undefined);
|
|
479
|
-
});
|
|
480
|
-
this.close = vi.fn();
|
|
481
|
-
this.sendData = vi.fn().mockResolvedValue({ result: "success" });
|
|
482
|
-
this.subscribe = vi.fn();
|
|
483
|
-
this.unsubscribe = vi.fn();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const transport = new LazyWebsocketTransport({
|
|
487
|
-
getWsUrl: mockGetWsUrl,
|
|
488
|
-
waitForReady: mockWaitForReady,
|
|
489
|
-
showError: mockShowError,
|
|
490
|
-
retries: 2,
|
|
491
|
-
retryDelayMs: 10,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
const onReconnect = vi.fn().mockResolvedValue(undefined);
|
|
495
|
-
transport.onReconnect = onReconnect;
|
|
496
|
-
|
|
497
|
-
// Initial connect fails (all retries exhausted)
|
|
498
|
-
await expect(transport.connect()).rejects.toThrow("Connection failed");
|
|
499
|
-
expect(onReconnect).not.toHaveBeenCalled();
|
|
500
|
-
|
|
501
|
-
// Now sendData reconnects successfully
|
|
502
|
-
const data: any = { method: "test", params: [] };
|
|
503
|
-
await transport.sendData(data, 5000);
|
|
504
|
-
|
|
505
|
-
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it("should propagate onReconnect rejection and allow retry", async () => {
|
|
509
|
-
const transport = new LazyWebsocketTransport({
|
|
510
|
-
getWsUrl: mockGetWsUrl,
|
|
511
|
-
waitForReady: mockWaitForReady,
|
|
512
|
-
showError: mockShowError,
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const reconnectError = new Error("Re-initialization failed");
|
|
516
|
-
const onReconnect = vi.fn().mockRejectedValueOnce(reconnectError);
|
|
517
|
-
transport.onReconnect = onReconnect;
|
|
518
|
-
|
|
519
|
-
await transport.connect();
|
|
520
|
-
transport.close();
|
|
521
|
-
|
|
522
|
-
const data: any = { method: "test", params: [] };
|
|
523
|
-
await expect(transport.sendData(data, 5000)).rejects.toThrow(
|
|
524
|
-
"Re-initialization failed",
|
|
525
|
-
);
|
|
526
|
-
expect(onReconnect).toHaveBeenCalledTimes(1);
|
|
527
|
-
|
|
528
|
-
// needsReInitialization should still be true, so a subsequent retry
|
|
529
|
-
// will attempt onReconnect again
|
|
530
|
-
onReconnect.mockResolvedValueOnce(undefined);
|
|
531
|
-
await transport.sendData(data, 5000);
|
|
532
|
-
expect(onReconnect).toHaveBeenCalledTimes(2);
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
describe("close", () => {
|
|
537
|
-
it("should close delegate and clear it", async () => {
|
|
538
|
-
const transport = new LazyWebsocketTransport({
|
|
539
|
-
getWsUrl: mockGetWsUrl,
|
|
540
|
-
waitForReady: mockWaitForReady,
|
|
541
|
-
showError: mockShowError,
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
await transport.connect();
|
|
545
|
-
|
|
546
|
-
const delegate = (transport as any).delegate;
|
|
547
|
-
expect(delegate).toBeDefined();
|
|
548
|
-
|
|
549
|
-
transport.close();
|
|
550
|
-
|
|
551
|
-
expect(delegate.close).toHaveBeenCalledTimes(1);
|
|
552
|
-
expect((transport as any).delegate).toBeUndefined();
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it("should handle close when delegate is undefined", () => {
|
|
556
|
-
const transport = new LazyWebsocketTransport({
|
|
557
|
-
getWsUrl: mockGetWsUrl,
|
|
558
|
-
waitForReady: mockWaitForReady,
|
|
559
|
-
showError: mockShowError,
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
expect(() => transport.close()).not.toThrow();
|
|
563
|
-
});
|
|
564
152
|
});
|
|
565
153
|
});
|