@marimo-team/islands 0.25.3-dev308 → 0.25.3-dev310
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/{ConnectedDataExplorerComponent-DJnSxHJB.js → ConnectedDataExplorerComponent-DieU6L4x.js} +1 -1
- package/dist/{ErrorBoundary-flfhLFje.js → ErrorBoundary-DjoeT4kv.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-CR0U5hup.js → abnfDiagram-VCTEODGH-LOVrjbdp.js} +2 -2
- package/dist/{any-language-editor-C5dsTpeB.js → any-language-editor-DMC2JE9_.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-Bc_fbI1Q.js → architectureDiagram-5GKGNRK7-RZsm312h.js} +1 -1
- package/dist/assets/__vite-browser-external-BSp9tJ_k.js +1 -0
- package/dist/assets/worker-BdfPA0g5.js +171 -0
- package/dist/{chat-ui-C4iLRzY0.js → chat-ui-DkuxGEDp.js} +418 -418
- package/dist/{chunk-IMKFNOWR-y9z5oeAY.js → chunk-IMKFNOWR-B6agbLjo.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Tu-V-e_v.js → chunk-SHT3W25Y-CfULafYG.js} +1 -1
- package/dist/{chunk-SVP7TREG-CJFxs5R_.js → chunk-SVP7TREG-DEOpPMJp.js} +1 -1
- package/dist/{chunk-TICWLB2K-zkTP7ya7.js → chunk-TICWLB2K-DaH-gkX5.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-DxJAZI0N.js → classDiagram-ZZMXUADV-C0CyiWQ7.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-TmHo2BpS.js → classDiagram-v2-VYDZK3BY-xfIc9e2Y.js} +2 -2
- package/dist/{code-visibility-BDs_r7jH.js → code-visibility-CduMVVFS.js} +32 -32
- package/dist/{common-CMTsL35V.js → common-CMcEcucH.js} +919 -919
- package/dist/{config-BG2qKzDg.js → config-BVYA1P0C.js} +103 -76
- package/dist/{cynefinDiagram-5FMLGOSQ-Eb40hmhD.js → cynefinDiagram-5FMLGOSQ-DwGYhKu0.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-_paaVwit.js → dagre-GXQ25YYZ-DfMaWJOV.js} +1 -1
- package/dist/{data-grid-overlay-editor-es6V6qf-.js → data-grid-overlay-editor-BYrIVut3.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BtKJm1_4.js → diagram-S7CK7UJ4-Bw_L87GZ.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DQbxhEMd.js → diagram-UQ7AKVKN-nsrs3Fbz.js} +1 -1
- package/dist/{diagram-VX7I27RA-DITDIfGC.js → diagram-VX7I27RA-7ST9lEqP.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Ct9_zo6F.js → diagram-Z3DM3KII-BnGte-HX.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-6cOntwfv.js → ebnfDiagram-PWID7BFC-zaKyJ1YL.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CJiOER41.js → erDiagram-RLTQ6QDP-Cx7g95FZ.js} +1 -1
- package/dist/{error-banner-DGOrmBk5.js → error-banner-DkQNU4bn.js} +128 -128
- package/dist/{error-entries-BWmleiL0.js → error-entries-istW7Xbs.js} +589 -544
- package/dist/{feedback-button-D4eVjeNs.js → feedback-button-Zvm26aOQ.js} +107 -106
- package/dist/{flowDiagram-HODETNUW-1lPBEUuW.js → flowDiagram-HODETNUW-C7nIqaHP.js} +2 -2
- package/dist/{formats-qhBmYb0l.js → formats-CMa7L3E8.js} +1 -1
- package/dist/{glide-data-editor-ahfaPdFW.js → glide-data-editor-BE-GgVp4.js} +3 -3
- package/dist/{globals-m8oswz--.js → globals-Dbru-WLP.js} +1 -1
- package/dist/{html-to-image-B8hE6HIH.js → html-to-image-pPbR2cDx.js} +4512 -4464
- package/dist/{infoDiagram-27XIBGKW-C6K4tri5.js → infoDiagram-27XIBGKW-BTM_NXsn.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DwcwgRNH.js → ishikawaDiagram-5VMMS53U-K0FZrlvv.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-E489OMdc.js → kanban-definition-UXKFOSKX-ftznOyrz.js} +1 -1
- package/dist/main.js +6595 -6186
- package/dist/{mermaid-DMPMkVqY.js → mermaid-CNgH43ED.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-DWHjasOp.js → mindmap-definition-YA3MSWOX-CmzbxKfM.js} +1 -1
- package/dist/{number-overlay-editor-C6klKzEr.js → number-overlay-editor-qvlPZ-l-.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-COjcPOZT.js → pegDiagram-XKGWAZYB-DfTAWfGE.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BHAVZ38h.js → pieDiagram-E7YTZNPT-_kGQstZj.js} +1 -1
- package/dist/{process-output-DZDL_j1a.js → process-output-BySO1nIs.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-94ThFyKX.js → railroadDiagram-O6MQD6OU-D41WmIff.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CpF-Fxbb.js → requirementDiagram-BXWQKSXE-Bh53ZI3I.js} +1 -1
- package/dist/{reveal-component-DwnxPyVz.js → reveal-component-DQaegdcW.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CvANK2x7.js → sequenceDiagram-WJ2MYXX4-DBX27dNm.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-WHuJ4mJL.js → stateDiagram-D77RDMKH-CSYz2IZi.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DbO7lwYq.js → stateDiagram-v2-MP3YSRHH-DVLvtOqn.js} +2 -2
- package/dist/{swimlanes-42K2YHIH-CS2s29Vo.js → swimlanes-42K2YHIH-D59N0ktA.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CXaA1vQg.js → swimlanesDiagram-VR7AAH4N-CRgpmVSW.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B-SQMg5h.js → timeline-definition-24CTP7MA-C_xIIZDM.js} +1 -1
- package/dist/toDate-5qPtZ6HL.js +94 -0
- package/dist/{vega-component-DwqGfN3_.js → vega-component-Bly2lDhz.js} +10 -10
- package/dist/{vennDiagram-4TSXK5OY-etEFgvaG.js → vennDiagram-4TSXK5OY-DlOf0Mjs.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-BEQN7g62.js → wardleyDiagram-VM6X3IG4-zD_2guVe.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-D8-e5Z0R.js → xychartDiagram-S5SC5T6Z-CA0Dy1ZM.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/executions.ts +35 -13
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +86 -101
- package/src/components/editor/cell/useRunCells.ts +38 -65
- package/src/components/editor/notebook-cell.tsx +32 -20
- package/src/components/editor/output/ansi-reduce.ts +7 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +2 -0
- package/src/core/cells/__tests__/cells.test.ts +0 -14
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +15 -0
- package/src/core/cells/__tests__/utils.test.ts +1 -1
- package/src/core/cells/add-missing-import.ts +2 -5
- package/src/core/cells/cell.ts +1 -0
- package/src/core/cells/cells.ts +1 -2
- package/src/core/executions/__tests__/atoms.test.ts +227 -43
- package/src/core/executions/__tests__/hooks.test.tsx +1 -2
- package/src/core/executions/__tests__/input.test.ts +86 -0
- package/src/core/executions/__tests__/run.test.ts +390 -0
- package/src/core/executions/__tests__/select.test.ts +105 -30
- package/src/core/executions/atoms.ts +186 -71
- package/src/core/executions/index.ts +7 -6
- package/src/core/executions/input.ts +55 -0
- package/src/core/executions/run.ts +120 -0
- package/src/core/executions/select.ts +59 -24
- package/src/core/islands/__tests__/bridge.test.ts +35 -54
- package/src/core/islands/bootstrap.ts +5 -1
- package/src/core/islands/bridge.ts +16 -28
- package/src/core/islands/components/IslandControls.tsx +15 -7
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +36 -53
- package/src/core/islands/worker/worker.tsx +45 -19
- package/src/core/network/__tests__/requests-lazy.test.ts +14 -24
- package/src/core/network/__tests__/requests-toasting.test.tsx +7 -7
- package/src/core/network/requests-lazy.ts +2 -3
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -2
- package/src/core/notebook/replica/__tests__/fixtures.ts +17 -1
- package/src/core/notebook/replica/backend.ts +3 -1
- package/src/core/runtime/__tests__/runtime.test.ts +36 -0
- package/src/core/runtime/config.ts +36 -1
- package/src/core/runtime/runtime.ts +21 -1
- package/src/core/transports/static.ts +4 -3
- package/src/core/wasm/bridge.ts +0 -17
- package/dist/assets/__vite-browser-external-DzTYIHH_.js +0 -1
- package/dist/assets/worker-BH08I2Il.js +0 -171
- package/dist/static-state-Dtocd9UO.js +0 -68
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
|
+
import { atom } from "jotai";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { MockExecutions } from "@/__mocks__/executions";
|
|
7
|
+
import { cellId } from "@/__tests__/branded";
|
|
8
|
+
import type { DocumentModel, EditableDocument } from "@/core/notebook/document";
|
|
9
|
+
import {
|
|
10
|
+
cell,
|
|
11
|
+
CONFIG,
|
|
12
|
+
document as documentOf,
|
|
13
|
+
withCells,
|
|
14
|
+
} from "@/core/notebook/document/__tests__/fixtures";
|
|
15
|
+
import {
|
|
16
|
+
ConnectionClosedReason,
|
|
17
|
+
ConnectionState,
|
|
18
|
+
} from "@/core/notebook/replica";
|
|
19
|
+
import {
|
|
20
|
+
type FakeRun,
|
|
21
|
+
fakeBackend,
|
|
22
|
+
} from "@/core/notebook/replica/__tests__/fixtures";
|
|
23
|
+
import { activeDocumentModelAtom } from "@/core/session/active-notebook";
|
|
24
|
+
import { store } from "@/core/state/jotai";
|
|
25
|
+
import { cellExecutionInputsAtom } from "../atoms";
|
|
26
|
+
import { runCells } from "../run";
|
|
27
|
+
import { cellExecutionView } from "../select";
|
|
28
|
+
|
|
29
|
+
const toast = vi.fn();
|
|
30
|
+
vi.mock("@/components/ui/use-toast", () => ({
|
|
31
|
+
toast: (...args: unknown[]) => toast(...args),
|
|
32
|
+
}));
|
|
33
|
+
vi.mock("@/core/runtime/config", () => ({
|
|
34
|
+
startRuntimeIfLazy: async () => undefined,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const C1 = cellId("c1");
|
|
38
|
+
const C2 = cellId("c2");
|
|
39
|
+
|
|
40
|
+
function accepted(cells: Protocol.CellId[]): Protocol.Run {
|
|
41
|
+
return {
|
|
42
|
+
id: "run-1",
|
|
43
|
+
notebook_id: "n1",
|
|
44
|
+
runtime_id: "r1",
|
|
45
|
+
cells,
|
|
46
|
+
status: "queued",
|
|
47
|
+
outcome: null,
|
|
48
|
+
started_at: null,
|
|
49
|
+
completed_at: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function editable(disabled: Protocol.CellId[] = []): EditableDocument {
|
|
54
|
+
const confirmed = documentOf();
|
|
55
|
+
return {
|
|
56
|
+
order: confirmed.revision.order,
|
|
57
|
+
cells: new Map(
|
|
58
|
+
[...confirmed.cells.values()].map((cell) => [
|
|
59
|
+
cell.id,
|
|
60
|
+
{
|
|
61
|
+
...cell,
|
|
62
|
+
config: { ...cell.config, disabled: disabled.includes(cell.id) },
|
|
63
|
+
},
|
|
64
|
+
]),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A model whose edits sync with `sync`, confirming the fixture document by default. */
|
|
70
|
+
function model(
|
|
71
|
+
sync: () => Promise<Protocol.Document> = async () => documentOf().revision,
|
|
72
|
+
document: EditableDocument | null = editable(),
|
|
73
|
+
): DocumentModel {
|
|
74
|
+
const model = {
|
|
75
|
+
retry: vi.fn(sync),
|
|
76
|
+
documentAtom: atom(document),
|
|
77
|
+
} as unknown as DocumentModel;
|
|
78
|
+
store.set(activeDocumentModelAtom, model);
|
|
79
|
+
return model;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function status(id: Protocol.CellId) {
|
|
83
|
+
return cellExecutionView(store.get(cellExecutionInputsAtom), id).status;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("runCells", () => {
|
|
87
|
+
let run: ReturnType<typeof vi.fn<FakeRun>>;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
toast.mockClear();
|
|
91
|
+
run = vi.fn<FakeRun>(async (_notebookId, request) => ({
|
|
92
|
+
ok: true,
|
|
93
|
+
status: 202,
|
|
94
|
+
data: accepted(request.cells?.map((cell) => cell.id) ?? []),
|
|
95
|
+
}));
|
|
96
|
+
const replica = MockExecutions.seed(
|
|
97
|
+
store,
|
|
98
|
+
[],
|
|
99
|
+
MockExecutions.runtime("running"),
|
|
100
|
+
documentOf(),
|
|
101
|
+
);
|
|
102
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
store.set(activeDocumentModelAtom, null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("queues the cells at once, then runs them by id after the edits sync", async () => {
|
|
110
|
+
const { retry } = model();
|
|
111
|
+
|
|
112
|
+
const pending = runCells(store, [C1, C2]);
|
|
113
|
+
expect(status(C1)).toBe("queued");
|
|
114
|
+
expect(status(C2)).toBe("queued");
|
|
115
|
+
await pending;
|
|
116
|
+
|
|
117
|
+
// Retrying covers a synchronization that stalled before the run.
|
|
118
|
+
expect(retry).toHaveBeenCalledOnce();
|
|
119
|
+
expect(run).toHaveBeenCalledWith("n1", {
|
|
120
|
+
cells: [{ id: C1 }, { id: C2 }],
|
|
121
|
+
});
|
|
122
|
+
expect(vi.mocked(retry).mock.invocationCallOrder[0]).toBeLessThan(
|
|
123
|
+
run.mock.invocationCallOrder[0],
|
|
124
|
+
);
|
|
125
|
+
// Queued until the host plans the run or ends it.
|
|
126
|
+
expect(status(C1)).toBe("queued");
|
|
127
|
+
expect(toast).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("sends a disabled cell without reading it as queued", async () => {
|
|
131
|
+
const replica = MockExecutions.seed(
|
|
132
|
+
store,
|
|
133
|
+
[],
|
|
134
|
+
MockExecutions.runtime("running"),
|
|
135
|
+
withCells(documentOf(), {
|
|
136
|
+
c2: cell("c2", "y = 2", { config: { ...CONFIG, disabled: true } }),
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
140
|
+
model(undefined, editable([C2]));
|
|
141
|
+
|
|
142
|
+
await runCells(store, [C1, C2]);
|
|
143
|
+
|
|
144
|
+
expect(run).toHaveBeenCalledWith("n1", {
|
|
145
|
+
cells: [{ id: C1 }, { id: C2 }],
|
|
146
|
+
});
|
|
147
|
+
expect(status(C1)).toBe("queued");
|
|
148
|
+
expect(status(C2)).toBe("idle");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("waits for the runtime to run before submitting", async () => {
|
|
152
|
+
const replica = MockExecutions.seed(
|
|
153
|
+
store,
|
|
154
|
+
[],
|
|
155
|
+
MockExecutions.runtime("starting"),
|
|
156
|
+
documentOf(),
|
|
157
|
+
);
|
|
158
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
159
|
+
model();
|
|
160
|
+
|
|
161
|
+
const pending = runCells(store, [C1]);
|
|
162
|
+
await Promise.resolve();
|
|
163
|
+
expect(status(C1)).toBe("queued");
|
|
164
|
+
expect(run).not.toHaveBeenCalled();
|
|
165
|
+
|
|
166
|
+
store.set(replica.runtimeAtom, MockExecutions.runtime("running"));
|
|
167
|
+
await pending;
|
|
168
|
+
expect(run).toHaveBeenCalledOnce();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("records the request once the stream has a notebook, as after a reconnect", async () => {
|
|
172
|
+
const replica = MockExecutions.seed(store, [], null, documentOf());
|
|
173
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
174
|
+
store.set(replica.notebookAtom, null);
|
|
175
|
+
model();
|
|
176
|
+
|
|
177
|
+
const pending = runCells(store, [C1]);
|
|
178
|
+
await Promise.resolve();
|
|
179
|
+
expect(run).not.toHaveBeenCalled();
|
|
180
|
+
|
|
181
|
+
// The real replica reports both at once, from one snapshot.
|
|
182
|
+
store.set(replica.runtimeAtom, MockExecutions.runtime("running"));
|
|
183
|
+
store.set(replica.notebookAtom, MockExecutions.notebook());
|
|
184
|
+
await pending;
|
|
185
|
+
|
|
186
|
+
expect(run).toHaveBeenCalledWith("n1", { cells: [{ id: C1 }] });
|
|
187
|
+
expect(status(C1)).toBe("queued");
|
|
188
|
+
expect(toast).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("withdraws the submission and reports when the edits cannot sync", async () => {
|
|
192
|
+
model(async () => {
|
|
193
|
+
throw new Error("offline");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await runCells(store, [C1]);
|
|
197
|
+
|
|
198
|
+
expect(run).not.toHaveBeenCalled();
|
|
199
|
+
expect(status(C1)).toBe("idle");
|
|
200
|
+
expect(toast).toHaveBeenCalledWith(
|
|
201
|
+
expect.objectContaining({ title: "Failed to run", variant: "danger" }),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("withdraws the submission and reports when the runtime has failed", async () => {
|
|
206
|
+
const replica = MockExecutions.seed(
|
|
207
|
+
store,
|
|
208
|
+
[],
|
|
209
|
+
{ ...MockExecutions.runtime("failed"), message: "The kernel exited" },
|
|
210
|
+
documentOf(),
|
|
211
|
+
);
|
|
212
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
213
|
+
model();
|
|
214
|
+
|
|
215
|
+
await runCells(store, [C1]);
|
|
216
|
+
|
|
217
|
+
expect(run).not.toHaveBeenCalled();
|
|
218
|
+
expect(status(C1)).toBe("idle");
|
|
219
|
+
expect(toast).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({ description: "The kernel exited" }),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("withdraws the submission and reports when the notebook has no runtime", async () => {
|
|
225
|
+
const replica = MockExecutions.seed(store, [], null, documentOf());
|
|
226
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
227
|
+
model();
|
|
228
|
+
|
|
229
|
+
await runCells(store, [C1]);
|
|
230
|
+
|
|
231
|
+
expect(run).not.toHaveBeenCalled();
|
|
232
|
+
expect(status(C1)).toBe("idle");
|
|
233
|
+
expect(toast).toHaveBeenCalledWith(
|
|
234
|
+
expect.objectContaining({ description: "The notebook has no runtime" }),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("stops waiting for the runtime once the connection closes", async () => {
|
|
239
|
+
const replica = MockExecutions.seed(
|
|
240
|
+
store,
|
|
241
|
+
[],
|
|
242
|
+
MockExecutions.runtime("starting"),
|
|
243
|
+
documentOf(),
|
|
244
|
+
);
|
|
245
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
246
|
+
model();
|
|
247
|
+
|
|
248
|
+
const pending = runCells(store, [C1]);
|
|
249
|
+
await Promise.resolve();
|
|
250
|
+
expect(status(C1)).toBe("queued");
|
|
251
|
+
|
|
252
|
+
store.set(replica.connectionAtom, {
|
|
253
|
+
state: ConnectionState.CLOSED,
|
|
254
|
+
code: ConnectionClosedReason.KERNEL_DISCONNECTED,
|
|
255
|
+
reason: "kernel not found",
|
|
256
|
+
});
|
|
257
|
+
await pending;
|
|
258
|
+
|
|
259
|
+
expect(run).not.toHaveBeenCalled();
|
|
260
|
+
expect(status(C1)).toBe("idle");
|
|
261
|
+
expect(toast).toHaveBeenCalledWith(
|
|
262
|
+
expect.objectContaining({ description: "The notebook is disconnected" }),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("withdraws the submission and reports when the host refuses the run", async () => {
|
|
267
|
+
run.mockResolvedValue({
|
|
268
|
+
ok: false,
|
|
269
|
+
status: 409,
|
|
270
|
+
problem: {
|
|
271
|
+
type: "tag:marimo.io,2026:host/conflict",
|
|
272
|
+
title: "Conflict",
|
|
273
|
+
status: 409,
|
|
274
|
+
detail: "the runtime is starting.",
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
model();
|
|
278
|
+
|
|
279
|
+
await runCells(store, [C1]);
|
|
280
|
+
|
|
281
|
+
expect(status(C1)).toBe("idle");
|
|
282
|
+
expect(toast).toHaveBeenCalledWith(
|
|
283
|
+
expect.objectContaining({ description: "the runtime is starting." }),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("withdraws quietly when the connection is read-only", async () => {
|
|
288
|
+
run.mockResolvedValue({
|
|
289
|
+
ok: false,
|
|
290
|
+
status: 403,
|
|
291
|
+
problem: {
|
|
292
|
+
type: "tag:marimo.io,2026:host/forbidden",
|
|
293
|
+
title: "Forbidden",
|
|
294
|
+
status: 403,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
model();
|
|
298
|
+
|
|
299
|
+
await runCells(store, [C1]);
|
|
300
|
+
|
|
301
|
+
expect(status(C1)).toBe("idle");
|
|
302
|
+
expect(toast).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("does not run when the notebook closed while the edits synced", async () => {
|
|
306
|
+
model(async () => {
|
|
307
|
+
store.set(activeDocumentModelAtom, null);
|
|
308
|
+
return documentOf().revision;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await runCells(store, [C1]);
|
|
312
|
+
|
|
313
|
+
expect(run).not.toHaveBeenCalled();
|
|
314
|
+
expect(toast).not.toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("does not run in a notebook that replaced the one the request was made in", async () => {
|
|
318
|
+
// Islands keep the replica and model while the worker's session changes.
|
|
319
|
+
const replica = MockExecutions.seed(
|
|
320
|
+
store,
|
|
321
|
+
[],
|
|
322
|
+
MockExecutions.runtime("running"),
|
|
323
|
+
documentOf(),
|
|
324
|
+
);
|
|
325
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
326
|
+
model(async () => {
|
|
327
|
+
store.set(replica.notebookAtom, MockExecutions.notebook("n2"));
|
|
328
|
+
return documentOf("n2").revision;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const pending = runCells(store, [C1]);
|
|
332
|
+
expect(status(C1)).toBe("queued");
|
|
333
|
+
await pending;
|
|
334
|
+
|
|
335
|
+
expect(run).not.toHaveBeenCalled();
|
|
336
|
+
expect(status(C1)).toBe("idle");
|
|
337
|
+
expect(toast).not.toHaveBeenCalled();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("leaves the replacing notebook's request when a late refusal lands", async () => {
|
|
341
|
+
const replica = MockExecutions.seed(
|
|
342
|
+
store,
|
|
343
|
+
[],
|
|
344
|
+
MockExecutions.runtime("running"),
|
|
345
|
+
documentOf(),
|
|
346
|
+
);
|
|
347
|
+
store.set(replica.backendAtom, fakeBackend({ run }));
|
|
348
|
+
model(
|
|
349
|
+
async () =>
|
|
350
|
+
documentOf(store.get(replica.notebookAtom)?.id ?? "n1").revision,
|
|
351
|
+
);
|
|
352
|
+
let refuse = (): void => undefined;
|
|
353
|
+
run.mockImplementationOnce(
|
|
354
|
+
() =>
|
|
355
|
+
new Promise((resolve) => {
|
|
356
|
+
refuse = () =>
|
|
357
|
+
resolve({
|
|
358
|
+
ok: false,
|
|
359
|
+
status: 409,
|
|
360
|
+
problem: {
|
|
361
|
+
type: "tag:marimo.io,2026:host/conflict",
|
|
362
|
+
title: "Conflict",
|
|
363
|
+
status: 409,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const first = runCells(store, [C1]);
|
|
370
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledOnce());
|
|
371
|
+
store.set(replica.notebookAtom, MockExecutions.notebook("n2"));
|
|
372
|
+
const second = runCells(store, [C1]);
|
|
373
|
+
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2));
|
|
374
|
+
expect(status(C1)).toBe("queued");
|
|
375
|
+
|
|
376
|
+
refuse();
|
|
377
|
+
await first;
|
|
378
|
+
await second;
|
|
379
|
+
|
|
380
|
+
expect(run).toHaveBeenLastCalledWith("n2", { cells: [{ id: C1 }] });
|
|
381
|
+
expect(status(C1)).toBe("queued");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("does nothing without an open notebook", async () => {
|
|
385
|
+
await runCells(store, [C1]);
|
|
386
|
+
|
|
387
|
+
expect(run).not.toHaveBeenCalled();
|
|
388
|
+
expect(status(C1)).toBe("idle");
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type * as Protocol from "@marimo-team/client/protocol";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { CONFIG } from "@/core/notebook/document/__tests__/fixtures";
|
|
5
6
|
import type { Execution, Executions, Results } from "@/core/notebook/replica";
|
|
6
7
|
import {
|
|
7
8
|
anyRunning,
|
|
@@ -39,19 +40,43 @@ function execution(overrides: Partial<Protocol.Execution> & { id: string }) {
|
|
|
39
40
|
return { record: record(overrides), items: new Map(), variables: new Map() };
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function run(
|
|
44
|
+
cells: string[],
|
|
45
|
+
status: Protocol.Run["status"],
|
|
46
|
+
id = "run-1",
|
|
47
|
+
): Protocol.Run {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
notebook_id: "n1",
|
|
51
|
+
runtime_id: "r1",
|
|
52
|
+
cells,
|
|
53
|
+
status,
|
|
54
|
+
outcome: status === "completed" ? "succeeded" : null,
|
|
55
|
+
started_at: null,
|
|
56
|
+
completed_at: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CELLS = ["c1", "c2", "c3", "c4", "c5"];
|
|
61
|
+
|
|
62
|
+
/** Inputs whose document has `CELLS`, with `disabled` kept from running. */
|
|
42
63
|
function inputs({
|
|
43
64
|
executions = [],
|
|
65
|
+
runs = [],
|
|
44
66
|
results = new Map(),
|
|
45
|
-
|
|
67
|
+
requested = new Set(),
|
|
68
|
+
disabled = [],
|
|
46
69
|
blocked = new Set(),
|
|
47
70
|
}: {
|
|
48
71
|
executions?: Execution[];
|
|
72
|
+
runs?: Protocol.Run[];
|
|
49
73
|
results?: Results;
|
|
50
|
-
|
|
74
|
+
requested?: ReadonlySet<string>;
|
|
75
|
+
disabled?: readonly string[];
|
|
51
76
|
blocked?: ReadonlySet<string>;
|
|
52
77
|
}): CellExecutionInputs {
|
|
53
78
|
const stream: Executions = {
|
|
54
|
-
runs: new Map(),
|
|
79
|
+
runs: new Map(runs.map((r) => [r.id, r])),
|
|
55
80
|
executions: new Map(executions.map((e) => [e.record.id, e])),
|
|
56
81
|
latest: new Map(
|
|
57
82
|
executions.flatMap((e) =>
|
|
@@ -59,7 +84,13 @@ function inputs({
|
|
|
59
84
|
),
|
|
60
85
|
),
|
|
61
86
|
};
|
|
62
|
-
|
|
87
|
+
const cells = new Map(
|
|
88
|
+
CELLS.map((id) => [
|
|
89
|
+
id,
|
|
90
|
+
{ config: { ...CONFIG, disabled: disabled.includes(id) } },
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
return { executions: stream, results, cells, requested, blocked };
|
|
63
94
|
}
|
|
64
95
|
|
|
65
96
|
describe("cellExecutionView", () => {
|
|
@@ -165,40 +196,87 @@ describe("cellExecutionView", () => {
|
|
|
165
196
|
expect(ended({ kind: "interrupted" }).outcome).toBe("interrupted");
|
|
166
197
|
});
|
|
167
198
|
|
|
168
|
-
it("reads as queued from a
|
|
169
|
-
const
|
|
199
|
+
it("reads as queued from a request the stream has not shown a run for", () => {
|
|
200
|
+
const requested = inputs({
|
|
170
201
|
executions: [execution({ id: "e1" })],
|
|
171
202
|
results: new Map([["c1", record({ id: "e1" })]]),
|
|
172
|
-
|
|
203
|
+
requested: new Set(["c1"]),
|
|
173
204
|
});
|
|
174
|
-
expect(cellExecutionView(
|
|
175
|
-
expect(cellExecutionView(
|
|
205
|
+
expect(cellExecutionView(requested, "c1").status).toBe("queued");
|
|
206
|
+
expect(cellExecutionView(requested, "c1").outcome).toBeNull();
|
|
176
207
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
const fresh = inputs({ requested: new Set(["c1"]) });
|
|
209
|
+
expect(cellExecutionView(fresh, "c1").status).toBe("queued");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("reads as queued from an unfinished run that names the cell until it is planned", () => {
|
|
213
|
+
const result = new Map([["c1", record({ id: "e1" })]]);
|
|
214
|
+
const accepted = inputs({
|
|
215
|
+
runs: [run(["c1"], "queued", "run-2")],
|
|
216
|
+
executions: [execution({ id: "e1" })],
|
|
217
|
+
results: result,
|
|
181
218
|
});
|
|
182
|
-
expect(cellExecutionView(
|
|
219
|
+
expect(cellExecutionView(accepted, "c1").status).toBe("queued");
|
|
220
|
+
expect(cellExecutionView(accepted, "c1").outcome).toBeNull();
|
|
183
221
|
|
|
184
|
-
|
|
222
|
+
// The cell's execution in the run takes over once the kernel plans it.
|
|
223
|
+
const planned = inputs({
|
|
224
|
+
runs: [run(["c1"], "running", "run-2")],
|
|
185
225
|
executions: [
|
|
186
|
-
execution({
|
|
226
|
+
execution({
|
|
227
|
+
id: "e2",
|
|
228
|
+
run_id: "run-2",
|
|
229
|
+
status: "running",
|
|
230
|
+
completed_at: null,
|
|
231
|
+
}),
|
|
187
232
|
],
|
|
188
|
-
results:
|
|
189
|
-
submissions: new Map([["c1", "e1"]]),
|
|
233
|
+
results: result,
|
|
190
234
|
});
|
|
191
|
-
expect(cellExecutionView(
|
|
235
|
+
expect(cellExecutionView(planned, "c1").status).toBe("running");
|
|
192
236
|
|
|
193
237
|
const done = inputs({
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
238
|
+
runs: [run(["c1", "c2"], "running", "run-2")],
|
|
239
|
+
executions: [execution({ id: "e2", run_id: "run-2" })],
|
|
240
|
+
results: new Map([["c1", record({ id: "e2", run_id: "run-2" })]]),
|
|
197
241
|
});
|
|
198
242
|
expect(cellExecutionView(done, "c1").status).toBe("idle");
|
|
243
|
+
expect(cellExecutionView(done, "c1").outcome).toBe("value");
|
|
244
|
+
expect(cellExecutionView(done, "c2").status).toBe("queued");
|
|
199
245
|
|
|
200
|
-
|
|
201
|
-
|
|
246
|
+
// The stream let the execution go while its run goes on with other cells.
|
|
247
|
+
const retained = inputs({
|
|
248
|
+
runs: [run(["c1", "c2"], "running", "run-2")],
|
|
249
|
+
results: new Map([["c1", record({ id: "e2", run_id: "run-2" })]]),
|
|
250
|
+
});
|
|
251
|
+
expect(cellExecutionView(retained, "c1").status).toBe("idle");
|
|
252
|
+
expect(cellExecutionView(retained, "c2").status).toBe("queued");
|
|
253
|
+
|
|
254
|
+
// A later run names the cell again while the earlier one's result stands.
|
|
255
|
+
const again = inputs({
|
|
256
|
+
runs: [run(["c1"], "running", "run-2"), run(["c1"], "queued", "run-3")],
|
|
257
|
+
executions: [execution({ id: "e2", run_id: "run-2" })],
|
|
258
|
+
results: new Map([["c1", record({ id: "e2", run_id: "run-2" })]]),
|
|
259
|
+
});
|
|
260
|
+
expect(cellExecutionView(again, "c1").status).toBe("queued");
|
|
261
|
+
|
|
262
|
+
const over = inputs({ runs: [run(["c1"], "completed")] });
|
|
263
|
+
expect(cellExecutionView(over, "c1").status).toBe("idle");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("does not read as queued a named cell a run cannot reach", () => {
|
|
267
|
+
const runs = [run(["c1", "c9"], "queued")];
|
|
268
|
+
expect(
|
|
269
|
+
cellExecutionView(inputs({ runs, disabled: ["c1"] }), "c1").status,
|
|
270
|
+
).toBe("idle");
|
|
271
|
+
expect(
|
|
272
|
+
cellExecutionView(inputs({ runs, blocked: new Set(["c1"]) }), "c1")
|
|
273
|
+
.status,
|
|
274
|
+
).toBe("idle");
|
|
275
|
+
// The document no longer has c9.
|
|
276
|
+
expect(cellExecutionView(inputs({ runs }), "c9").status).toBe("idle");
|
|
277
|
+
expect(
|
|
278
|
+
cellExecutionView(inputs({ requested: new Set(["c9"]) }), "c9").status,
|
|
279
|
+
).toBe("idle");
|
|
202
280
|
});
|
|
203
281
|
|
|
204
282
|
it("shows a blocked cell as disabled transitively, with its last outcome", () => {
|
|
@@ -234,15 +312,12 @@ describe("notebook-level views", () => {
|
|
|
234
312
|
completed_at: null,
|
|
235
313
|
}),
|
|
236
314
|
],
|
|
315
|
+
runs: [run(["c1", "c2", "c5"], "running"), run(["c5"], "queued", "run-2")],
|
|
237
316
|
results: new Map([["c5", record({ id: "e5", cell_id: "c5" })]]),
|
|
238
|
-
|
|
239
|
-
["c2", "e2"],
|
|
240
|
-
["c4", null],
|
|
241
|
-
["c5", "e5"],
|
|
242
|
-
]),
|
|
317
|
+
requested: new Set(["c2", "c4"]),
|
|
243
318
|
});
|
|
244
319
|
|
|
245
|
-
it("counts unfinished executions and
|
|
320
|
+
it("counts unfinished executions and cells awaiting a run the kernel has not planned", () => {
|
|
246
321
|
expect(anyRunning(stream.executions)).toBe(true);
|
|
247
322
|
expect(queuedOrRunningCount(stream)).toBe(5);
|
|
248
323
|
expect(runningCellIds(stream.executions)).toEqual(["c1"]);
|