@marimo-team/frontend 0.25.3-dev80 → 0.25.3-dev81
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-cD3S0wEA.js → CellStatus-DXroYLOP.js} +1 -1
- package/dist/assets/{ErrorBoundary-DJhsFCsX.js → ErrorBoundary-DaQ9THH_.js} +2 -2
- package/dist/assets/{JsonOutput-Pm6OCP3J.js → JsonOutput-VxzHyItt.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-DlEjLCZ-.js → MarimoErrorOutput-D-hLaDA-.js} +1 -1
- package/dist/assets/{RenderHTML-CfQ6i3QV.js → RenderHTML-DjOEZL25.js} +1 -1
- package/dist/assets/{RunButton-DvuvkgrG.js → RunButton-CuhNTo5_.js} +1 -1
- package/dist/assets/{add-cell-with-ai-By51zSlC.js → add-cell-with-ai-BbVntTh7.js} +1 -1
- package/dist/assets/{add-connection-dialog-D7EYSraq.js → add-connection-dialog-Qf63aKV6.js} +1 -1
- package/dist/assets/{agent-panel-aCWa3Yai.js → agent-panel-DMOfF3qF.js} +1 -1
- package/dist/assets/{ai-model-dropdown-bwTRyFn4.js → ai-model-dropdown-B15l0R62.js} +2 -2
- package/dist/assets/{app-config-button-DGhsM0xw.js → app-config-button-BIMB8qjD.js} +1 -1
- package/dist/assets/{blob-e6VlOmQP.js → blob-De9GR2s6.js} +1 -1
- package/dist/assets/{cell-editor-M8dEnDzk.js → cell-editor-DFNLDjfX.js} +3 -3
- package/dist/assets/{cell-link-DFqfW-Ym.js → cell-link-DR3W2HC7.js} +1 -1
- package/dist/assets/{cells-T9_Y4Jlv.js → cells-styV7IX8.js} +6 -6
- package/dist/assets/{chat-display-DXn2Yrjr.js → chat-display-BzdYdN39.js} +1 -1
- package/dist/assets/{chat-panel-D3iUyZId.js → chat-panel-De3YzkSF.js} +1 -1
- package/dist/assets/{chat-ui-YuJtv3UJ.js → chat-ui-NTNapdtP.js} +1 -1
- package/dist/assets/{code-visibility-CV3S4xuf.js → code-visibility-csc3tgA0.js} +1 -1
- package/dist/assets/{column-preview-C6uXRJry.js → column-preview-DkH-oD8f.js} +1 -1
- package/dist/assets/{command-palette-CGNUQdgW.js → command-palette-OOQHBTnr.js} +1 -1
- package/dist/assets/{common-DeeLgjWQ.js → common-D7VAS6rd.js} +1 -1
- package/dist/assets/{components-DyAF3_Nf.js → components-CNmvC781.js} +1 -1
- package/dist/assets/config-BQT0uYUo.js +1 -0
- package/dist/assets/{config-CjQ4WJnJ.js → config-BbRbT9Ww.js} +1 -1
- package/dist/assets/{datasource-gqFgG9Qd.js → datasource-C3yfeDa2.js} +1 -1
- package/dist/assets/{dependency-graph-panel-B3T9kSTs.js → dependency-graph-panel-CjoA6L0w.js} +1 -1
- package/dist/assets/{documentation-panel-BBpGbT-f.js → documentation-panel-DHWFOiG1.js} +1 -1
- package/dist/assets/{download-C8Gw-D0Y.js → download-BrCpRZKN.js} +1 -1
- package/dist/assets/{edit-page-CBftLPze.js → edit-page-DafDcIAI.js} +4 -4
- package/dist/assets/{error-entries-uyqOG5tg.js → error-entries-BkEAmmnD.js} +1 -1
- package/dist/assets/{error-panel-Cli5zPWi.js → error-panel-BhTfPA_i.js} +1 -1
- package/dist/assets/feedback-button-BAcClv7c.js +1 -0
- package/dist/assets/{feedback-button-CvrwBGr7.js → feedback-button-D9InK87G.js} +1 -1
- package/dist/assets/{file-explorer-panel-BSU5ultY.js → file-explorer-panel-BSd_ZbUC.js} +1 -1
- package/dist/assets/{file-name-input-CDrRlIiX.js → file-name-input-RHURglL_.js} +1 -1
- package/dist/assets/{floating-outline-GQD9km0W.js → floating-outline-Cm49rYYe.js} +1 -1
- package/dist/assets/{gallery-page-B-5H1fEt.js → gallery-page-DdMdOtdv.js} +1 -1
- package/dist/assets/{glide-data-editor-B1kpoetH.js → glide-data-editor-eiSlEUyR.js} +1 -1
- package/dist/assets/{home-page-3qsPTb4v.js → home-page-DwWvBB0R.js} +1 -1
- package/dist/assets/{hooks-BrFYiTJS.js → hooks-C4GjmtQ-.js} +1 -1
- package/dist/assets/{index-D0IWODMU.js → index-jR_KRDMC.js} +7 -7
- package/dist/assets/{layout-CGC8ajpe.js → layout-Bq7wRzdR.js} +3 -3
- package/dist/assets/{logs-panel-CdUc5vtA.js → logs-panel-CAAk72Gb.js} +1 -1
- package/dist/assets/{markdown-renderer-pKc_bFac.js → markdown-renderer-CAyCgogC.js} +1 -1
- package/dist/assets/{name-cell-input-csH3Cy54.js → name-cell-input-DA8hU9XT.js} +1 -1
- package/dist/assets/{outline-panel-DhsHYTuo.js → outline-panel-Cc5oQ4LL.js} +1 -1
- package/dist/assets/{packages-panel-BtkXVz3L.js → packages-panel-DTfKez6I.js} +1 -1
- package/dist/assets/{panel-accordion-state-Bco6nsUI.js → panel-accordion-state-BZHex38W.js} +1 -1
- package/dist/assets/panels-Cnu2Flfw.js +105 -0
- package/dist/assets/{process-output-B2-bT4cq.js → process-output-CrDc_O1D.js} +1 -1
- package/dist/assets/{readonly-code-display-Dsp6vWPh.js → readonly-code-display-BR-_jUj8.js} +1 -1
- package/dist/assets/{readonly-python-code-B81AjzXM.js → readonly-python-code-D7jcy9ds.js} +1 -1
- package/dist/assets/{reveal-component-Cxo3hLGA.js → reveal-component-BWu6kkxZ.js} +1 -1
- package/dist/assets/{run-page-CLP1s8uX.js → run-page-BJq9MULx.js} +1 -1
- package/dist/assets/{runtime-details-state-CznIe40L.js → runtime-details-state-hb7A1jgR.js} +1 -1
- package/dist/assets/{scratchpad-panel-yHIW63VQ.js → scratchpad-panel-OHQ7Z0RY.js} +1 -1
- package/dist/assets/{session-panel-C7-wmZhc.js → session-panel-DmDjaPsN.js} +1 -1
- package/dist/assets/{snippets-panel-BARpOPyS.js → snippets-panel-o9KL5jMi.js} +1 -1
- package/dist/assets/state-BkAqJoW7.js +3 -0
- package/dist/assets/{state-Bye6rCo4.js → state-Os7LSUUZ.js} +1 -1
- package/dist/assets/{terminal-Bhcn_y1N.js → terminal-BI85khrS.js} +1 -1
- package/dist/assets/{tracing-DDohxo24.js → tracing-AsNGuDsh.js} +1 -1
- package/dist/assets/{tracing-panel-DYhGDAo5.js → tracing-panel-CBcmea6L.js} +2 -2
- package/dist/assets/{tree-actions-Cb-nUK1F.js → tree-actions-Cx5EDpA6.js} +1 -1
- package/dist/assets/{useAddCell-Bielffe0.js → useAddCell-uJJYtgwa.js} +1 -1
- package/dist/assets/{useBoolean-CMgj9hnq.js → useBoolean-8wlrTeus.js} +1 -1
- package/dist/assets/{useCellActionButton-DAcQhse_.js → useCellActionButton-CmQ-OcPa.js} +1 -1
- package/dist/assets/{useDeleteCell-BV4VemCv.js → useDeleteCell-CXgOCDqY.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-PtXIVh53.js → useDependencyPanelTab-Ch6gm7uM.js} +1 -1
- package/dist/assets/{useInstallPackage-C7neMPwh.js → useInstallPackage-CYQoMdMQ.js} +1 -1
- package/dist/assets/{useNotebookActions-DZBB98fP.js → useNotebookActions-CqXa3U6u.js} +1 -1
- package/dist/assets/{useRunCells-BFeZMi7f.js → useRunCells-DAfDmOMC.js} +1 -1
- package/dist/assets/{useSplitCell-DllkNFke.js → useSplitCell-NLULgcq7.js} +1 -1
- package/dist/assets/{vega-component-D7JFthDD.js → vega-component-BKJU3uaB.js} +1 -1
- package/dist/index.html +24 -24
- package/package.json +1 -1
- package/src/core/host/__tests__/notebook-client.test.ts +368 -350
- package/src/core/host/notebook-client.ts +129 -186
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +133 -59
- package/src/core/websocket/__tests__/useNotebookSession.hook.test.tsx +152 -250
- package/src/core/websocket/transports/__tests__/notebook.test.ts +502 -0
- package/src/core/websocket/transports/notebook.ts +213 -0
- package/src/core/websocket/useNotebookClient.ts +28 -17
- package/dist/assets/config-DzdxZb17.js +0 -1
- package/dist/assets/feedback-button-DknbRCN_.js +0 -1
- package/dist/assets/panels-D1i80TGn.js +0 -107
- package/dist/assets/state-BRYi026f.js +0 -3
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
vi,
|
|
22
22
|
} from "vitest";
|
|
23
23
|
|
|
24
|
-
vi.mock("../transports/
|
|
25
|
-
|
|
24
|
+
vi.mock("../transports/notebook", () => ({
|
|
25
|
+
createNotebookTransport: vi.fn(),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
28
|
vi.mock("@/core/runtime/config", async () => {
|
|
@@ -36,7 +36,6 @@ vi.mock("@/core/runtime/config", async () => {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
import { MockNotebook } from "@/__mocks__/notebook";
|
|
39
|
-
import { Logger } from "@/utils/Logger";
|
|
40
39
|
import { Deferred } from "@/utils/Deferred";
|
|
41
40
|
import { cellId } from "@/__tests__/branded";
|
|
42
41
|
import { alertAtom, getPackageAlert } from "@/core/alerts/state";
|
|
@@ -44,7 +43,6 @@ import { notebookAtom } from "@/core/cells/cells";
|
|
|
44
43
|
import { AppConfigSchema } from "@/core/config/config-schema";
|
|
45
44
|
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
46
45
|
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
47
|
-
import type { RunEvent } from "@marimo-team/client/protocol";
|
|
48
46
|
import type { NotificationPayload } from "@/core/kernel/messages";
|
|
49
47
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
50
48
|
import { initialRunCompletedAtom } from "../../kernel/state";
|
|
@@ -52,54 +50,72 @@ import { connectionAtom, startupProgressAtom } from "../../network/connection";
|
|
|
52
50
|
import { generateSessionId } from "../../kernel/session";
|
|
53
51
|
import { RuntimeManager } from "../../runtime/runtime";
|
|
54
52
|
import { WebSocketClosedReason, ConnectionState } from "../types";
|
|
55
|
-
import
|
|
53
|
+
import { classifyCloseEvent } from "../connection-state";
|
|
54
|
+
import { createNotebookTransport } from "../transports/notebook";
|
|
56
55
|
import { useNotebookSession } from "../useNotebookSession";
|
|
57
|
-
import {
|
|
56
|
+
import { type NotebookTransport, StreamStopped } from "@marimo-team/client";
|
|
58
57
|
|
|
59
58
|
const SESSION_ID = generateSessionId();
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
removeEventListener: Mock<IConnectionTransport["removeEventListener"]>;
|
|
60
|
+
/** One connection the fake transport opened. */
|
|
61
|
+
interface Attempt {
|
|
62
|
+
open(): void;
|
|
63
|
+
receive(message: unknown): void;
|
|
64
|
+
drop(): void;
|
|
65
|
+
stop(error: StreamStopped): void;
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
/** A transport the test drives, one connection at a time. */
|
|
69
|
+
function makeTransport() {
|
|
70
|
+
const attempts: Attempt[] = [];
|
|
71
|
+
const transport: NotebookTransport = ({ signal }, handlers) =>
|
|
72
|
+
new Promise((resolve, reject) => {
|
|
73
|
+
attempts.push({
|
|
74
|
+
open: handlers.open,
|
|
75
|
+
receive: handlers.message,
|
|
76
|
+
drop: resolve,
|
|
77
|
+
stop: reject,
|
|
78
|
+
});
|
|
79
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
80
|
+
});
|
|
81
|
+
return { transport, attempts };
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
type FakeTransport = ReturnType<typeof makeTransport>;
|
|
85
|
+
|
|
86
|
+
/** Use a fake transport for the next session the hook follows. */
|
|
87
|
+
function withTransport(fake: FakeTransport = makeTransport()) {
|
|
88
|
+
vi.mocked(createNotebookTransport).mockReturnValue(fake.transport);
|
|
89
|
+
return fake;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Let the stream open its first connection. */
|
|
93
|
+
const started = () => act(() => Promise.resolve());
|
|
94
|
+
|
|
95
|
+
/** Drive the latest connection the fake transport opened. */
|
|
96
|
+
function connection(fake: FakeTransport) {
|
|
97
|
+
const current = () => {
|
|
98
|
+
const attempt = fake.attempts.at(-1);
|
|
99
|
+
if (attempt === undefined) {
|
|
100
|
+
throw new Error("No connection was opened");
|
|
101
|
+
}
|
|
102
|
+
return attempt;
|
|
103
|
+
};
|
|
98
104
|
return {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
open: () => current().open(),
|
|
106
|
+
receive: (message: unknown) => current().receive(message),
|
|
107
|
+
/** Deliver a notification as the session routes frame it. */
|
|
108
|
+
send: (notification: unknown) =>
|
|
109
|
+
current().receive({ event: "marimo", data: { marimo: notification } }),
|
|
110
|
+
/** End the connection so the stream reconnects. */
|
|
111
|
+
drop: () => current().drop(),
|
|
112
|
+
/** Close the connection with a reason the editor treats as final. */
|
|
113
|
+
close: (reason: string) =>
|
|
114
|
+
current().stop(
|
|
115
|
+
new StreamStopped(reason, {
|
|
116
|
+
cause: classifyCloseEvent({ reason }).status,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
103
119
|
};
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -141,12 +157,12 @@ function renderConnectionHook(store: ReturnType<typeof createStore>) {
|
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
describe("useNotebookSession.reconnect()", () => {
|
|
144
|
-
let
|
|
160
|
+
let fake: FakeTransport;
|
|
145
161
|
let isHealthy: Mock<() => Promise<boolean>>;
|
|
146
162
|
let store: ReturnType<typeof createStore>;
|
|
147
163
|
|
|
148
164
|
beforeEach(() => {
|
|
149
|
-
|
|
165
|
+
fake = withTransport();
|
|
150
166
|
isHealthy = vi.fn<() => Promise<boolean>>().mockResolvedValue(true);
|
|
151
167
|
store = createStore();
|
|
152
168
|
store.set(connectionAtom, {
|
|
@@ -154,55 +170,53 @@ describe("useNotebookSession.reconnect()", () => {
|
|
|
154
170
|
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
155
171
|
reason: "kernel not found",
|
|
156
172
|
});
|
|
157
|
-
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
158
173
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager(isHealthy));
|
|
159
174
|
});
|
|
160
175
|
|
|
161
|
-
function
|
|
162
|
-
|
|
176
|
+
async function renderStopped() {
|
|
177
|
+
const hook = renderConnectionHook(store);
|
|
178
|
+
await started();
|
|
179
|
+
await act(async () => connection(fake).close("MARIMO_SHUTDOWN"));
|
|
180
|
+
return hook;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
it("
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
expect(isHealthy).not.toHaveBeenCalled();
|
|
182
|
-
expect(transport.reconnect).not.toHaveBeenCalled();
|
|
183
|
-
});
|
|
183
|
+
it.each(["connecting", "connected"])(
|
|
184
|
+
"is a no-op while the stream is %s",
|
|
185
|
+
async (state) => {
|
|
186
|
+
const { result } = renderConnectionHook(store);
|
|
187
|
+
await started();
|
|
188
|
+
if (state === "connected") {
|
|
189
|
+
act(() => connection(fake).open());
|
|
190
|
+
}
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await result.current.reconnect();
|
|
193
|
+
});
|
|
194
|
+
expect(isHealthy).not.toHaveBeenCalled();
|
|
195
|
+
expect(fake.attempts).toHaveLength(1);
|
|
196
|
+
},
|
|
197
|
+
);
|
|
184
198
|
|
|
185
199
|
it("probes /health and reconnects when the runtime is healthy", async () => {
|
|
186
200
|
isHealthy.mockResolvedValue(true);
|
|
187
|
-
const { result } =
|
|
201
|
+
const { result } = await renderStopped();
|
|
188
202
|
await act(async () => {
|
|
189
203
|
await result.current.reconnect();
|
|
190
204
|
});
|
|
191
205
|
expect(isHealthy).toHaveBeenCalledOnce();
|
|
192
|
-
expect(
|
|
206
|
+
expect(fake.attempts).toHaveLength(2);
|
|
193
207
|
expect(store.get(connectionAtom)).toEqual({
|
|
194
208
|
state: ConnectionState.CONNECTING,
|
|
195
209
|
});
|
|
196
210
|
});
|
|
197
211
|
|
|
198
|
-
it("transitions to CLOSED and does not
|
|
212
|
+
it("transitions to CLOSED and does not reconnect when the probe fails", async () => {
|
|
199
213
|
isHealthy.mockResolvedValue(false);
|
|
200
|
-
const { result } =
|
|
214
|
+
const { result } = await renderStopped();
|
|
201
215
|
await act(async () => {
|
|
202
216
|
await result.current.reconnect();
|
|
203
217
|
});
|
|
204
218
|
expect(isHealthy).toHaveBeenCalledOnce();
|
|
205
|
-
expect(
|
|
219
|
+
expect(fake.attempts).toHaveLength(1);
|
|
206
220
|
expect(store.get(connectionAtom)).toEqual({
|
|
207
221
|
state: ConnectionState.CLOSED,
|
|
208
222
|
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
@@ -213,115 +227,30 @@ describe("useNotebookSession.reconnect()", () => {
|
|
|
213
227
|
|
|
214
228
|
describe("useNotebookSession messages", () => {
|
|
215
229
|
let store: ReturnType<typeof createStore>;
|
|
230
|
+
let fake: FakeTransport;
|
|
216
231
|
|
|
217
|
-
beforeEach(() => {
|
|
232
|
+
beforeEach(async () => {
|
|
218
233
|
store = createStore();
|
|
219
|
-
|
|
220
|
-
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
221
|
-
makeTransport(WebSocket.OPEN),
|
|
222
|
-
);
|
|
234
|
+
fake = withTransport();
|
|
223
235
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
224
236
|
renderConnectionHook(store);
|
|
237
|
+
await started();
|
|
225
238
|
});
|
|
226
239
|
|
|
227
240
|
function receive(payload: unknown) {
|
|
228
|
-
|
|
229
|
-
act(() => {
|
|
230
|
-
options.onMessage(
|
|
231
|
-
new MessageEvent("message", { data: JSON.stringify(payload) }),
|
|
232
|
-
);
|
|
233
|
-
});
|
|
241
|
+
act(() => connection(fake).receive(payload));
|
|
234
242
|
}
|
|
235
243
|
|
|
236
|
-
it
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
reason: "Failed to decode notebook message",
|
|
247
|
-
});
|
|
248
|
-
receive({
|
|
249
|
-
event: "marimo",
|
|
250
|
-
data: {
|
|
251
|
-
marimo: {
|
|
252
|
-
op: "completed-run",
|
|
253
|
-
run_id: "r1",
|
|
254
|
-
outcome: "succeeded",
|
|
255
|
-
error: null,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
260
|
-
},
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
it.each(["session", "host"])(
|
|
264
|
-
"records completion of the initial run from the %s stream",
|
|
265
|
-
(stream) => {
|
|
266
|
-
const notification = {
|
|
267
|
-
op: "completed-run",
|
|
268
|
-
run_id: null,
|
|
269
|
-
outcome: "succeeded",
|
|
270
|
-
error: null,
|
|
271
|
-
} satisfies NotificationPayload["data"];
|
|
272
|
-
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
273
|
-
receive(
|
|
274
|
-
stream === "host"
|
|
275
|
-
? { id: "42", event: "marimo", data: { marimo: notification } }
|
|
276
|
-
: { op: notification.op, data: notification },
|
|
277
|
-
);
|
|
278
|
-
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
it("ignores other host events before handling the next notification", () => {
|
|
283
|
-
const errors = vi.spyOn(Logger, "error").mockImplementation(vi.fn());
|
|
284
|
-
try {
|
|
285
|
-
receive({ id: "1", event: "ready", data: {} });
|
|
286
|
-
receive({
|
|
287
|
-
id: "2",
|
|
288
|
-
event: "run",
|
|
289
|
-
data: {
|
|
290
|
-
run: {
|
|
291
|
-
id: "r1",
|
|
292
|
-
notebook_id: "n1",
|
|
293
|
-
caused_by: null,
|
|
294
|
-
status: "completed",
|
|
295
|
-
outcome: "succeeded",
|
|
296
|
-
started_at: "2026-10-19T12:00:00Z",
|
|
297
|
-
completed_at: "2026-10-19T12:00:01Z",
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
} satisfies RunEvent);
|
|
301
|
-
receive({
|
|
302
|
-
id: "3",
|
|
303
|
-
event: "future-event",
|
|
304
|
-
data: { marimo: { op: "completed-run", run_id: "r1" } },
|
|
305
|
-
});
|
|
306
|
-
receive({ id: "4", event: "another-event", data: null });
|
|
307
|
-
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
308
|
-
receive({
|
|
309
|
-
id: "5",
|
|
310
|
-
event: "marimo",
|
|
311
|
-
data: {
|
|
312
|
-
marimo: {
|
|
313
|
-
op: "completed-run",
|
|
314
|
-
run_id: "r1",
|
|
315
|
-
outcome: "succeeded",
|
|
316
|
-
error: null,
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
321
|
-
expect(errors).not.toHaveBeenCalled();
|
|
322
|
-
} finally {
|
|
323
|
-
errors.mockRestore();
|
|
324
|
-
}
|
|
244
|
+
it("records completion of the initial run", () => {
|
|
245
|
+
const notification = {
|
|
246
|
+
op: "completed-run",
|
|
247
|
+
run_id: null,
|
|
248
|
+
outcome: "succeeded",
|
|
249
|
+
error: null,
|
|
250
|
+
} satisfies NotificationPayload["data"];
|
|
251
|
+
expect(store.get(initialRunCompletedAtom)).toBe(false);
|
|
252
|
+
receive({ id: "42", event: "marimo", data: { marimo: notification } });
|
|
253
|
+
expect(store.get(initialRunCompletedAtom)).toBe(true);
|
|
325
254
|
});
|
|
326
255
|
});
|
|
327
256
|
|
|
@@ -330,45 +259,34 @@ it.each(["kernel-ready", "reconnected"])(
|
|
|
330
259
|
async (op) => {
|
|
331
260
|
const store = createStore();
|
|
332
261
|
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
333
|
-
|
|
334
|
-
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
335
|
-
makeTransport(WebSocket.OPEN),
|
|
336
|
-
);
|
|
262
|
+
const fake = withTransport();
|
|
337
263
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
338
264
|
renderConnectionHook(store);
|
|
339
|
-
|
|
265
|
+
await started();
|
|
266
|
+
const conn = connection(fake);
|
|
340
267
|
|
|
341
|
-
await act(async () =>
|
|
342
|
-
await options.onOpen(new Event("open"));
|
|
343
|
-
});
|
|
268
|
+
await act(async () => conn.open());
|
|
344
269
|
expect(store.get(connectionAtom).state).toBe(ConnectionState.CONNECTING);
|
|
345
270
|
|
|
346
|
-
act(() =>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
auto_instantiated: false,
|
|
366
|
-
consumer_capabilities: { edit: true, interact: true },
|
|
367
|
-
},
|
|
368
|
-
}),
|
|
369
|
-
}),
|
|
370
|
-
);
|
|
371
|
-
});
|
|
271
|
+
act(() =>
|
|
272
|
+
conn.send({
|
|
273
|
+
op,
|
|
274
|
+
cell_ids: [],
|
|
275
|
+
codes: [],
|
|
276
|
+
names: [],
|
|
277
|
+
configs: [],
|
|
278
|
+
layout: null,
|
|
279
|
+
resumed: true,
|
|
280
|
+
ui_values: {},
|
|
281
|
+
last_executed_code: {},
|
|
282
|
+
last_execution_time: {},
|
|
283
|
+
app_config: { width: "normal" },
|
|
284
|
+
kiosk: false,
|
|
285
|
+
capabilities: { terminal: false },
|
|
286
|
+
auto_instantiated: false,
|
|
287
|
+
consumer_capabilities: { edit: true, interact: true },
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
372
290
|
expect(store.get(connectionAtom).state).toBe(ConnectionState.OPEN);
|
|
373
291
|
},
|
|
374
292
|
);
|
|
@@ -377,11 +295,10 @@ describe("connection notice", () => {
|
|
|
377
295
|
beforeEach(() => vi.useFakeTimers());
|
|
378
296
|
afterEach(() => vi.useRealTimers());
|
|
379
297
|
|
|
380
|
-
function renderNotice() {
|
|
298
|
+
async function renderNotice() {
|
|
381
299
|
const store = createStore();
|
|
382
300
|
store.set(connectionAtom, { state: ConnectionState.CONNECTING });
|
|
383
|
-
const
|
|
384
|
-
vi.mocked(createConnectionTransport).mockReturnValue(transport);
|
|
301
|
+
const fake = withTransport();
|
|
385
302
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
386
303
|
const Connection = () => {
|
|
387
304
|
const { reconnect } = useNotebookSession({
|
|
@@ -403,20 +320,17 @@ describe("connection notice", () => {
|
|
|
403
320
|
</ErrorBoundary>
|
|
404
321
|
</JotaiProvider>,
|
|
405
322
|
);
|
|
406
|
-
|
|
323
|
+
await started();
|
|
324
|
+
const conn = connection(fake);
|
|
325
|
+
// Messages arrive on an open connection.
|
|
326
|
+
act(() => conn.open());
|
|
407
327
|
const send = (data: NotificationPayload["data"]) =>
|
|
408
|
-
act(() =>
|
|
409
|
-
|
|
410
|
-
new MessageEvent("message", {
|
|
411
|
-
data: JSON.stringify({ op: data.op, data }),
|
|
412
|
-
}),
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
return { store, transport, options, send };
|
|
328
|
+
act(() => conn.send(data));
|
|
329
|
+
return { store, fake, conn, send };
|
|
416
330
|
}
|
|
417
331
|
|
|
418
|
-
it("keeps elapsed time across startup phases and leaves reconnection to the footer once cells are available", () => {
|
|
419
|
-
const { store,
|
|
332
|
+
it("keeps elapsed time across startup phases and leaves reconnection to the footer once cells are available", async () => {
|
|
333
|
+
const { store, conn, send } = await renderNotice();
|
|
420
334
|
send({
|
|
421
335
|
op: "startup-progress",
|
|
422
336
|
phase: "preparing-environment",
|
|
@@ -456,7 +370,7 @@ describe("connection notice", () => {
|
|
|
456
370
|
expect(
|
|
457
371
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
458
372
|
).not.toBeInTheDocument();
|
|
459
|
-
act(() =>
|
|
373
|
+
await act(async () => conn.drop());
|
|
460
374
|
act(() => vi.advanceTimersByTime(500));
|
|
461
375
|
expect(store.get(connectionAtom)).toEqual({
|
|
462
376
|
state: ConnectionState.CONNECTING,
|
|
@@ -467,8 +381,8 @@ describe("connection notice", () => {
|
|
|
467
381
|
).not.toBeInTheDocument();
|
|
468
382
|
});
|
|
469
383
|
|
|
470
|
-
it("restores startup output before appending live chunks and resets it for a new attempt", () => {
|
|
471
|
-
const { store, send } = renderNotice();
|
|
384
|
+
it("restores startup output before appending live chunks and resets it for a new attempt", async () => {
|
|
385
|
+
const { store, send } = await renderNotice();
|
|
472
386
|
const snapshot = {
|
|
473
387
|
op: "startup-progress",
|
|
474
388
|
phase: "starting-kernel",
|
|
@@ -494,7 +408,7 @@ describe("connection notice", () => {
|
|
|
494
408
|
});
|
|
495
409
|
|
|
496
410
|
it("restores completed startup output without putting a connected kernel back into startup", async () => {
|
|
497
|
-
const { store,
|
|
411
|
+
const { store, conn, send } = await renderNotice();
|
|
498
412
|
send({ op: "reconnected" });
|
|
499
413
|
send({
|
|
500
414
|
op: "startup-progress",
|
|
@@ -512,13 +426,15 @@ describe("connection notice", () => {
|
|
|
512
426
|
screen.queryByRole("region", { name: "Notebook startup" }),
|
|
513
427
|
).not.toBeInTheDocument();
|
|
514
428
|
|
|
515
|
-
// A new
|
|
516
|
-
await act(async () =>
|
|
429
|
+
// A new connection starts from the next session's authoritative snapshot.
|
|
430
|
+
await act(async () => conn.drop());
|
|
431
|
+
await act(async () => vi.advanceTimersByTime(2000));
|
|
432
|
+
await act(async () => conn.open());
|
|
517
433
|
expect(store.get(startupProgressAtom)).toBeNull();
|
|
518
434
|
});
|
|
519
435
|
|
|
520
|
-
it("does not flash a notice when startup finishes within the delay", () => {
|
|
521
|
-
const { send } = renderNotice();
|
|
436
|
+
it("does not flash a notice when startup finishes within the delay", async () => {
|
|
437
|
+
const { send } = await renderNotice();
|
|
522
438
|
send({
|
|
523
439
|
op: "startup-progress",
|
|
524
440
|
phase: "preparing-environment",
|
|
@@ -546,16 +462,11 @@ describe("connection notice", () => {
|
|
|
546
462
|
it.each(["preparing-environment", "starting-kernel"] as const)(
|
|
547
463
|
"keeps a %s failure available until the user retries",
|
|
548
464
|
async (phase) => {
|
|
549
|
-
const { store,
|
|
465
|
+
const { store, fake, conn, send } = await renderNotice();
|
|
550
466
|
send({ op: "startup-progress", phase, logs: "", log_mode: "replace" });
|
|
551
467
|
const error = "A full diagnostic\nwith <stderr> details";
|
|
552
468
|
send({ op: "kernel-startup-error", error });
|
|
553
|
-
|
|
554
|
-
act(() =>
|
|
555
|
-
options.onClose(
|
|
556
|
-
new CloseEvent("close", { reason: "MARIMO_KERNEL_STARTUP_ERROR" }),
|
|
557
|
-
),
|
|
558
|
-
);
|
|
469
|
+
await act(async () => conn.close("MARIMO_KERNEL_STARTUP_ERROR"));
|
|
559
470
|
expect(screen.getByRole("status")).toHaveTextContent(
|
|
560
471
|
"Kernel failed to start",
|
|
561
472
|
);
|
|
@@ -566,11 +477,11 @@ describe("connection notice", () => {
|
|
|
566
477
|
expect(screen.getByLabelText("Error details")).toHaveTextContent(error, {
|
|
567
478
|
normalizeWhitespace: false,
|
|
568
479
|
});
|
|
569
|
-
expect(
|
|
480
|
+
expect(fake.attempts).toHaveLength(1);
|
|
570
481
|
await act(async () =>
|
|
571
482
|
fireEvent.click(screen.getByRole("button", { name: "Try again" })),
|
|
572
483
|
);
|
|
573
|
-
expect(
|
|
484
|
+
expect(fake.attempts).toHaveLength(2);
|
|
574
485
|
expect(store.get(kernelStartupErrorAtom)).toBeNull();
|
|
575
486
|
expect(
|
|
576
487
|
screen.queryByRole("button", { name: "Try again" }),
|
|
@@ -590,23 +501,14 @@ describe("connection notice", () => {
|
|
|
590
501
|
);
|
|
591
502
|
});
|
|
592
503
|
|
|
593
|
-
it("replaces environment state from snapshots, then continues live progress", () => {
|
|
504
|
+
it("replaces environment state from snapshots, then continues live progress", async () => {
|
|
594
505
|
const store = createStore();
|
|
595
|
-
|
|
596
|
-
vi.mocked(createConnectionTransport).mockReturnValue(
|
|
597
|
-
makeTransport(WebSocket.OPEN),
|
|
598
|
-
);
|
|
506
|
+
const fake = withTransport();
|
|
599
507
|
vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
|
|
600
508
|
renderConnectionHook(store);
|
|
601
|
-
|
|
509
|
+
await started();
|
|
602
510
|
function receive(data: NotificationPayload["data"]) {
|
|
603
|
-
act(() =>
|
|
604
|
-
options?.onMessage(
|
|
605
|
-
new MessageEvent("message", {
|
|
606
|
-
data: JSON.stringify({ op: data.op, data }),
|
|
607
|
-
}),
|
|
608
|
-
),
|
|
609
|
-
);
|
|
511
|
+
act(() => connection(fake).send(data));
|
|
610
512
|
}
|
|
611
513
|
receive({
|
|
612
514
|
op: "environment-operation",
|