@marimo-team/frontend 0.24.1-dev57 → 0.24.1-dev59
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/add-cell-with-ai-DVGZNNUv.js +76 -0
- package/dist/assets/{agent-panel-D7mo8YLQ.js → agent-panel-BTY3oeHD.js} +3 -3
- package/dist/assets/cell-editor-D3YsOG7a.js +21 -0
- package/dist/assets/{chat-display-Cgg0xLir.js → chat-display-NTNQQyoE.js} +1 -1
- package/dist/assets/chat-panel-Bow0QKrA.js +4 -0
- package/dist/assets/{chat-ui-DYLpA8l4.js → chat-ui-Cg69zAqt.js} +4 -4
- package/dist/assets/{command-palette-DiT0E_K7.js → command-palette-QFmIvDcT.js} +1 -1
- package/dist/assets/{edit-page-DAC4cNKe.js → edit-page-ds4edlkF.js} +6 -6
- package/dist/assets/{index-DTQ4RS9q.js → index-B36dhEV6.js} +3 -3
- package/dist/assets/index-ecw9ngZu.css +2 -0
- package/dist/assets/{layout-D3zwsTvR.js → layout-iNJ-1JI2.js} +2 -2
- package/dist/assets/{panels-ChFSBlsU.js → panels-BbkuRtEi.js} +1 -1
- package/dist/assets/{reveal-component-BFRmOcQr.js → reveal-component-C4pF9OE7.js} +1 -1
- package/dist/assets/{run-page-MZCCEXct.js → run-page-Bo57bjbp.js} +1 -1
- package/dist/assets/{scratchpad-panel-BqYT2vj3.js → scratchpad-panel-vBIq9BfA.js} +1 -1
- package/dist/assets/{skeleton-gmxAkHfa.js → skeleton-B5iAa5n_.js} +1 -1
- package/dist/assets/{useNotebookActions-BoFJmccb.js → useNotebookActions-DGuDG0Px.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +0 -178
- package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +106 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +83 -62
- package/src/components/editor/ai/ai-completion-editor.tsx +71 -37
- package/src/components/editor/ai/completion-handlers.tsx +4 -0
- package/src/components/editor/ai/completion-utils.ts +0 -92
- package/src/components/editor/ai/staged-cell-submission.ts +46 -0
- package/src/components/editor/cell/StagedAICell.tsx +4 -2
- package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +64 -0
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +77 -0
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +8 -2
- package/src/core/ai/__tests__/staged-cells.test.ts +442 -125
- package/src/core/ai/__tests__/stream-completion-text.test.ts +74 -0
- package/src/core/ai/completion-output.ts +56 -0
- package/src/core/ai/staged-cells.ts +245 -164
- package/src/core/ai/stream-completion-text.ts +23 -6
- package/src/css/globals.css +1 -0
- package/dist/assets/add-cell-with-ai-0dqk0f7O.js +0 -77
- package/dist/assets/cell-editor-e7_o4mEh.js +0 -24
- package/dist/assets/chat-panel-BaG-5LiN.js +0 -4
- package/dist/assets/index-BrmH4-pI.css +0 -2
- package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +0 -133
- package/src/core/ai/strip-wrapping-backticks.ts +0 -88
|
@@ -6,20 +6,23 @@ import { createElement, type ReactNode } from "react";
|
|
|
6
6
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { cellId } from "@/__tests__/branded";
|
|
8
8
|
import { CellId } from "@/core/cells/ids";
|
|
9
|
+
import { getCellEditorView } from "../../cells/cells";
|
|
9
10
|
import { updateEditorCodeFromPython } from "../../codemirror/language/utils";
|
|
10
11
|
import {
|
|
11
|
-
type StagedAICells,
|
|
12
12
|
stagedAICellsAtom,
|
|
13
|
+
stagedGenerationInProgressAtom,
|
|
13
14
|
useStagedAICell,
|
|
15
|
+
useStagedCellGeneration,
|
|
14
16
|
useStagedCells,
|
|
15
17
|
visibleForTesting,
|
|
16
18
|
} from "../staged-cells";
|
|
17
19
|
|
|
18
|
-
const { createActions, reducer, initialState } =
|
|
20
|
+
const { stagedGenerationAtom, createActions, reducer, initialState } =
|
|
21
|
+
visibleForTesting;
|
|
19
22
|
|
|
20
23
|
// Mock the dependencies
|
|
21
24
|
const mockCreateNewCell = vi.fn();
|
|
22
|
-
const
|
|
25
|
+
const mockUpdateCellCode = vi.fn();
|
|
23
26
|
const mockDeleteCellCallback = vi.fn();
|
|
24
27
|
|
|
25
28
|
// Mock cell handle with editor view
|
|
@@ -31,21 +34,29 @@ const mockCellHandle = {
|
|
|
31
34
|
},
|
|
32
35
|
};
|
|
33
36
|
|
|
34
|
-
vi.mock("../../cells/cells", () =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
vi.mock("../../cells/cells", async () => {
|
|
38
|
+
const { atom } = await import("jotai");
|
|
39
|
+
return {
|
|
40
|
+
notebookAtom: atom({ cellData: {}, cellIds: { inOrderIds: [] } }),
|
|
41
|
+
useCellActions: () => ({
|
|
42
|
+
createNewCell: mockCreateNewCell,
|
|
43
|
+
updateCellCode: mockUpdateCellCode,
|
|
44
|
+
}),
|
|
45
|
+
cellHandleAtom: vi.fn(() => ({
|
|
46
|
+
read: vi.fn(() => mockCellHandle),
|
|
47
|
+
})),
|
|
48
|
+
getCellEditorView: vi.fn(() => mockCellHandle.current.editorViewOrNull),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
44
51
|
|
|
45
52
|
vi.mock("@/components/editor/cell/useDeleteCell", () => ({
|
|
46
53
|
useDeleteCellCallback: () => mockDeleteCellCallback,
|
|
47
54
|
}));
|
|
48
55
|
|
|
56
|
+
vi.mock("@/core/network/requests", () => ({
|
|
57
|
+
getRequestClient: () => ({ sendRun: vi.fn() }),
|
|
58
|
+
}));
|
|
59
|
+
|
|
49
60
|
vi.mock("../../codemirror/language/utils", () => ({
|
|
50
61
|
updateEditorCodeFromPython: vi.fn(),
|
|
51
62
|
}));
|
|
@@ -69,9 +80,11 @@ describe("staged-cells", () => {
|
|
|
69
80
|
|
|
70
81
|
// Reset mocks
|
|
71
82
|
vi.clearAllMocks();
|
|
83
|
+
vi.mocked(CellId.create).mockReset();
|
|
72
84
|
|
|
73
85
|
// Reset the atom state
|
|
74
86
|
store.set(stagedAICellsAtom, new Map());
|
|
87
|
+
store.set(stagedGenerationAtom, null);
|
|
75
88
|
});
|
|
76
89
|
|
|
77
90
|
describe("reducer and actions", () => {
|
|
@@ -160,20 +173,6 @@ describe("staged-cells", () => {
|
|
|
160
173
|
|
|
161
174
|
expect(newState).toBe(state);
|
|
162
175
|
});
|
|
163
|
-
|
|
164
|
-
it("should clear all cells", () => {
|
|
165
|
-
const state = new Map([
|
|
166
|
-
[cellId1, { type: "add_cell" as const }],
|
|
167
|
-
[cellId2, { type: "add_cell" as const }],
|
|
168
|
-
]);
|
|
169
|
-
const newState = reducer(state, {
|
|
170
|
-
type: "clearStagedCells",
|
|
171
|
-
payload: undefined,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
expect(newState).toEqual(new Map());
|
|
175
|
-
});
|
|
176
|
-
|
|
177
176
|
it("should not mutate original state when adding", () => {
|
|
178
177
|
const state = new Map([[cellId1, { type: "add_cell" as const }]]);
|
|
179
178
|
const originalSize = state.size;
|
|
@@ -211,7 +210,6 @@ describe("staged-cells", () => {
|
|
|
211
210
|
|
|
212
211
|
expect(typeof actions.addStagedCell).toBe("function");
|
|
213
212
|
expect(typeof actions.removeStagedCell).toBe("function");
|
|
214
|
-
expect(typeof actions.clearStagedCells).toBe("function");
|
|
215
213
|
});
|
|
216
214
|
|
|
217
215
|
it("should initialize atom with empty map", () => {
|
|
@@ -282,34 +280,6 @@ describe("staged-cells", () => {
|
|
|
282
280
|
});
|
|
283
281
|
});
|
|
284
282
|
|
|
285
|
-
it("should delete all staged cells when none exist", () => {
|
|
286
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
287
|
-
|
|
288
|
-
// Should not throw when no cells exist
|
|
289
|
-
expect(() => result.current.deleteAllStagedCells()).not.toThrow();
|
|
290
|
-
expect(mockDeleteCellCallback).not.toHaveBeenCalled();
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("should delete all staged cells when cells exist", () => {
|
|
294
|
-
// First set the atom state before rendering the hook
|
|
295
|
-
const initialState: StagedAICells = new Map([
|
|
296
|
-
[cellId1, { type: "add_cell" }],
|
|
297
|
-
[cellId2, { type: "add_cell" }],
|
|
298
|
-
]);
|
|
299
|
-
store.set(stagedAICellsAtom, initialState);
|
|
300
|
-
|
|
301
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
302
|
-
result.current.deleteAllStagedCells();
|
|
303
|
-
|
|
304
|
-
expect(mockDeleteCellCallback).toHaveBeenCalledTimes(2);
|
|
305
|
-
expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
|
|
306
|
-
expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId2 });
|
|
307
|
-
|
|
308
|
-
// Verify cells were cleared from the atom
|
|
309
|
-
const state = store.get(stagedAICellsAtom);
|
|
310
|
-
expect(state).toEqual(new Map());
|
|
311
|
-
});
|
|
312
|
-
|
|
313
283
|
it("should add staged cell with edit info", () => {
|
|
314
284
|
const { result } = renderHook(() => useStagedCells(store));
|
|
315
285
|
|
|
@@ -349,27 +319,6 @@ describe("staged-cells", () => {
|
|
|
349
319
|
expect(state.has(cellId2)).toBe(true);
|
|
350
320
|
});
|
|
351
321
|
|
|
352
|
-
it("should clear all staged cells", () => {
|
|
353
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
354
|
-
|
|
355
|
-
// First add some cells
|
|
356
|
-
result.current.addStagedCell({
|
|
357
|
-
cellId: cellId1,
|
|
358
|
-
edit: { type: "add_cell" },
|
|
359
|
-
});
|
|
360
|
-
result.current.addStagedCell({
|
|
361
|
-
cellId: cellId2,
|
|
362
|
-
edit: { type: "add_cell" },
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// Then clear all
|
|
366
|
-
result.current.clearStagedCells();
|
|
367
|
-
|
|
368
|
-
// Check that no cells remain
|
|
369
|
-
const state = store.get(stagedAICellsAtom);
|
|
370
|
-
expect(state).toEqual(new Map());
|
|
371
|
-
});
|
|
372
|
-
|
|
373
322
|
it("should handle multiple operations correctly", () => {
|
|
374
323
|
const { result } = renderHook(() => useStagedCells(store));
|
|
375
324
|
|
|
@@ -434,92 +383,460 @@ describe("staged-cells", () => {
|
|
|
434
383
|
});
|
|
435
384
|
});
|
|
436
385
|
|
|
437
|
-
describe("
|
|
386
|
+
describe("staged cell generation", () => {
|
|
438
387
|
let store: ReturnType<typeof getDefaultStore>;
|
|
388
|
+
const renderGenerationHook = () =>
|
|
389
|
+
renderHook(() => useStagedCellGeneration(store));
|
|
390
|
+
|
|
439
391
|
beforeEach(() => {
|
|
440
392
|
store = getDefaultStore();
|
|
393
|
+
vi.clearAllMocks();
|
|
394
|
+
vi.mocked(CellId.create).mockReset();
|
|
395
|
+
store.set(stagedAICellsAtom, new Map());
|
|
396
|
+
store.set(stagedGenerationAtom, null);
|
|
441
397
|
});
|
|
442
398
|
|
|
443
|
-
it("should
|
|
444
|
-
const { result } =
|
|
445
|
-
result.current.
|
|
399
|
+
it("should begin generation", () => {
|
|
400
|
+
const { result } = renderGenerationHook();
|
|
401
|
+
result.current.beginStagedCellGeneration();
|
|
446
402
|
|
|
447
403
|
// No cell or cell update should have been called
|
|
448
404
|
expect(mockCreateNewCell).not.toHaveBeenCalled();
|
|
449
|
-
expect(
|
|
405
|
+
expect(updateEditorCodeFromPython).not.toHaveBeenCalled();
|
|
406
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should mark generation complete after a successful finish", () => {
|
|
410
|
+
const { result } = renderGenerationHook();
|
|
411
|
+
result.current.beginStagedCellGeneration();
|
|
412
|
+
result.current.finishStagedCellGeneration(true);
|
|
413
|
+
|
|
414
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(false);
|
|
450
415
|
});
|
|
451
416
|
|
|
452
|
-
it("should not
|
|
453
|
-
const { result } = renderHook(() => useStagedCells(store));
|
|
454
|
-
result
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
417
|
+
it("should not couple unrelated staged-cell removal to generation state", () => {
|
|
418
|
+
const { result: stagedCells } = renderHook(() => useStagedCells(store));
|
|
419
|
+
const { result } = renderGenerationHook();
|
|
420
|
+
result.current.beginStagedCellGeneration();
|
|
421
|
+
|
|
422
|
+
const chatCellId = cellId("chat-edited-cell");
|
|
423
|
+
stagedCells.current.addStagedCell({
|
|
424
|
+
cellId: chatCellId,
|
|
425
|
+
edit: { type: "update_cell", previousCode: "before" },
|
|
458
426
|
});
|
|
427
|
+
stagedCells.current.removeStagedCell(chatCellId);
|
|
459
428
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
429
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should only accept cells owned by the completed generation", () => {
|
|
433
|
+
const firstGeneration = renderGenerationHook();
|
|
434
|
+
firstGeneration.result.current.beginStagedCellGeneration();
|
|
435
|
+
|
|
436
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("superseded-cell"));
|
|
437
|
+
firstGeneration.result.current.onData({
|
|
438
|
+
type: "data-notebook-cells-completion",
|
|
439
|
+
data: {
|
|
440
|
+
cells: [{ language: "python", code: "old" }],
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
firstGeneration.result.current.finishStagedCellGeneration(true);
|
|
444
|
+
|
|
445
|
+
const activeGeneration = renderGenerationHook();
|
|
446
|
+
activeGeneration.result.current.beginStagedCellGeneration();
|
|
447
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("active-cell"));
|
|
448
|
+
activeGeneration.result.current.onData({
|
|
449
|
+
type: "data-notebook-cells-completion",
|
|
450
|
+
data: {
|
|
451
|
+
cells: [{ language: "python", code: "active" }],
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
activeGeneration.result.current.finishStagedCellGeneration(true);
|
|
455
|
+
|
|
456
|
+
expect(firstGeneration.result.current.hasOwnedStagedCells()).toBe(false);
|
|
457
|
+
expect(firstGeneration.result.current.acceptOwnedStagedCells()).toBe(false);
|
|
458
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
459
|
+
new Map([
|
|
460
|
+
[cellId("superseded-cell"), { type: "add_cell" }],
|
|
461
|
+
[cellId("active-cell"), { type: "add_cell" }],
|
|
462
|
+
]),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(activeGeneration.result.current.hasOwnedStagedCells()).toBe(true);
|
|
466
|
+
expect(activeGeneration.result.current.acceptOwnedStagedCells()).toBe(true);
|
|
467
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
468
|
+
new Map([[cellId("superseded-cell"), { type: "add_cell" }]]),
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should replace only the previous generation owned by this hook", () => {
|
|
473
|
+
const { result: stagedCells } = renderHook(() => useStagedCells(store));
|
|
474
|
+
const { result } = renderGenerationHook();
|
|
475
|
+
stagedCells.current.addStagedCell({
|
|
476
|
+
cellId: cellId("chat-edited-cell"),
|
|
477
|
+
edit: { type: "update_cell", previousCode: "before" },
|
|
478
|
+
});
|
|
479
|
+
result.current.beginStagedCellGeneration();
|
|
480
|
+
|
|
481
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("generated-cell"));
|
|
482
|
+
result.current.onData({
|
|
483
|
+
type: "data-notebook-cells-completion",
|
|
484
|
+
data: {
|
|
485
|
+
cells: [{ language: "python", code: "generated" }],
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
result.current.finishStagedCellGeneration(true);
|
|
489
|
+
mockDeleteCellCallback.mockClear();
|
|
490
|
+
|
|
491
|
+
result.current.beginStagedCellGeneration();
|
|
492
|
+
|
|
493
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledExactlyOnceWith({
|
|
494
|
+
cellId: cellId("generated-cell"),
|
|
495
|
+
});
|
|
496
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
497
|
+
new Map([
|
|
498
|
+
[
|
|
499
|
+
cellId("chat-edited-cell"),
|
|
500
|
+
{ type: "update_cell", previousCode: "before" },
|
|
501
|
+
],
|
|
502
|
+
]),
|
|
503
|
+
);
|
|
504
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should leave staged cells from another workflow untouched when accepting", () => {
|
|
508
|
+
const { result: stagedCells } = renderHook(() => useStagedCells(store));
|
|
509
|
+
const { result } = renderGenerationHook();
|
|
510
|
+
result.current.beginStagedCellGeneration();
|
|
511
|
+
|
|
512
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("generated-cell"));
|
|
513
|
+
result.current.onData({
|
|
514
|
+
type: "data-notebook-cells-completion",
|
|
515
|
+
data: {
|
|
516
|
+
cells: [{ language: "python", code: "generated" }],
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
result.current.finishStagedCellGeneration(true);
|
|
520
|
+
stagedCells.current.addStagedCell({
|
|
521
|
+
cellId: cellId("chat-edited-cell"),
|
|
522
|
+
edit: { type: "update_cell", previousCode: "before" },
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(result.current.acceptOwnedStagedCells()).toBe(true);
|
|
526
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
527
|
+
new Map([
|
|
528
|
+
[
|
|
529
|
+
cellId("chat-edited-cell"),
|
|
530
|
+
{ type: "update_cell", previousCode: "before" },
|
|
531
|
+
],
|
|
532
|
+
]),
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("should only discard the generation's remaining staged cells", () => {
|
|
537
|
+
const { result: stagedCells } = renderHook(() => useStagedCells(store));
|
|
538
|
+
const { result } = renderGenerationHook();
|
|
539
|
+
result.current.beginStagedCellGeneration();
|
|
540
|
+
|
|
541
|
+
vi.mocked(CellId.create)
|
|
542
|
+
.mockReturnValueOnce(cellId("accepted-cell"))
|
|
543
|
+
.mockReturnValueOnce(cellId("pending-cell"));
|
|
544
|
+
result.current.onData({
|
|
545
|
+
type: "data-notebook-cells-completion",
|
|
546
|
+
data: {
|
|
547
|
+
cells: [
|
|
548
|
+
{ language: "python", code: "accepted" },
|
|
549
|
+
{ language: "python", code: "pending" },
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
result.current.finishStagedCellGeneration(true);
|
|
554
|
+
stagedCells.current.removeStagedCell(cellId("accepted-cell"));
|
|
555
|
+
stagedCells.current.addStagedCell({
|
|
556
|
+
cellId: cellId("chat-edited-cell"),
|
|
557
|
+
edit: { type: "update_cell", previousCode: "before" },
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(result.current.discardOwnedStagedCells()).toBe(true);
|
|
561
|
+
expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
|
|
562
|
+
cellId: cellId("accepted-cell"),
|
|
563
|
+
});
|
|
564
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
565
|
+
cellId: cellId("pending-cell"),
|
|
566
|
+
});
|
|
567
|
+
expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
|
|
568
|
+
cellId: cellId("chat-edited-cell"),
|
|
569
|
+
});
|
|
570
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
571
|
+
new Map([
|
|
572
|
+
[
|
|
573
|
+
cellId("chat-edited-cell"),
|
|
574
|
+
{ type: "update_cell", previousCode: "before" },
|
|
575
|
+
],
|
|
576
|
+
]),
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it.each([
|
|
581
|
+
{ name: "no cells", cells: [] },
|
|
582
|
+
{
|
|
583
|
+
name: "an empty cell",
|
|
584
|
+
cells: [{ language: "python" as const, code: "" }],
|
|
585
|
+
},
|
|
586
|
+
])("should reject $name", ({ cells }) => {
|
|
587
|
+
const { result } = renderGenerationHook();
|
|
588
|
+
result.current.beginStagedCellGeneration();
|
|
589
|
+
|
|
590
|
+
expect(() =>
|
|
591
|
+
result.current.onData({
|
|
592
|
+
type: "data-notebook-cells-completion",
|
|
593
|
+
data: { cells },
|
|
594
|
+
}),
|
|
595
|
+
).toThrow();
|
|
463
596
|
});
|
|
464
597
|
|
|
465
|
-
it("should create cells
|
|
466
|
-
const { result } =
|
|
467
|
-
result.current.
|
|
598
|
+
it("should create cells from a validated completion", () => {
|
|
599
|
+
const { result } = renderGenerationHook();
|
|
600
|
+
result.current.beginStagedCellGeneration();
|
|
468
601
|
|
|
469
602
|
// Mock CellId.create to return a predictable ID
|
|
470
603
|
const mockCellId = cellId("mock-cell-id");
|
|
471
604
|
vi.mocked(CellId.create).mockReturnValue(mockCellId);
|
|
472
605
|
|
|
473
|
-
result.current.
|
|
474
|
-
type: "
|
|
475
|
-
|
|
476
|
-
|
|
606
|
+
result.current.onData({
|
|
607
|
+
type: "data-notebook-cells-completion",
|
|
608
|
+
data: {
|
|
609
|
+
cells: [{ language: "python", code: "print('```')" }],
|
|
610
|
+
},
|
|
477
611
|
});
|
|
478
612
|
|
|
479
613
|
expect(mockCreateNewCell).toHaveBeenCalledWith({
|
|
480
614
|
cellId: "__end__",
|
|
481
|
-
code: "
|
|
615
|
+
code: "print('```')",
|
|
482
616
|
before: false,
|
|
483
617
|
newCellId: "mock-cell-id",
|
|
484
618
|
});
|
|
485
619
|
});
|
|
486
620
|
|
|
487
|
-
it("should
|
|
488
|
-
const { result } =
|
|
489
|
-
result.current.
|
|
621
|
+
it("should create multiple cells", () => {
|
|
622
|
+
const { result } = renderGenerationHook();
|
|
623
|
+
result.current.beginStagedCellGeneration();
|
|
624
|
+
|
|
625
|
+
vi.mocked(CellId.create)
|
|
626
|
+
.mockReturnValueOnce(cellId("first-cell"))
|
|
627
|
+
.mockReturnValueOnce(cellId("second-cell"));
|
|
628
|
+
|
|
629
|
+
result.current.onData({
|
|
630
|
+
type: "data-notebook-cells-completion",
|
|
631
|
+
data: {
|
|
632
|
+
cells: [
|
|
633
|
+
{ language: "python", code: "value = 1" },
|
|
634
|
+
{ language: "python", code: "value" },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
});
|
|
490
638
|
|
|
491
|
-
|
|
492
|
-
|
|
639
|
+
expect(mockCreateNewCell).toHaveBeenCalledWith(
|
|
640
|
+
expect.objectContaining({
|
|
641
|
+
code: "value = 1",
|
|
642
|
+
newCellId: "first-cell",
|
|
643
|
+
}),
|
|
644
|
+
);
|
|
645
|
+
expect(mockCreateNewCell).toHaveBeenCalledWith(
|
|
646
|
+
expect.objectContaining({
|
|
647
|
+
code: "value",
|
|
648
|
+
newCellId: "second-cell",
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("should apply cumulative snapshots without recreating cells", () => {
|
|
654
|
+
const { result } = renderGenerationHook();
|
|
655
|
+
result.current.beginStagedCellGeneration();
|
|
493
656
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
657
|
+
vi.mocked(CellId.create)
|
|
658
|
+
.mockReturnValueOnce(cellId("first-cell"))
|
|
659
|
+
.mockReturnValueOnce(cellId("second-cell"));
|
|
660
|
+
|
|
661
|
+
result.current.onData({
|
|
662
|
+
type: "data-notebook-cells-completion",
|
|
663
|
+
data: {
|
|
664
|
+
cells: [{ language: "python", code: "value" }],
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
result.current.onData({
|
|
668
|
+
type: "data-notebook-cells-completion",
|
|
669
|
+
data: {
|
|
670
|
+
cells: [
|
|
671
|
+
{ language: "python", code: "value = 1" },
|
|
672
|
+
{ language: "python", code: "value" },
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
result.current.onData({
|
|
677
|
+
type: "data-notebook-cells-completion",
|
|
678
|
+
data: {
|
|
679
|
+
cells: [
|
|
680
|
+
{ language: "python", code: "value = 1" },
|
|
681
|
+
{ language: "python", code: "value + 1" },
|
|
682
|
+
],
|
|
683
|
+
},
|
|
498
684
|
});
|
|
499
685
|
|
|
500
|
-
expect(mockCreateNewCell).
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
686
|
+
expect(mockCreateNewCell).toHaveBeenCalledTimes(2);
|
|
687
|
+
expect(store.get(stagedAICellsAtom).has(cellId("first-cell"))).toBe(true);
|
|
688
|
+
expect(store.get(stagedAICellsAtom).has(cellId("second-cell"))).toBe(true);
|
|
689
|
+
expect(updateEditorCodeFromPython).toHaveBeenCalledTimes(2);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("should update notebook state when the cell editor is not mounted", () => {
|
|
693
|
+
const { result } = renderGenerationHook();
|
|
694
|
+
result.current.beginStagedCellGeneration();
|
|
695
|
+
|
|
696
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("unmounted-cell"));
|
|
697
|
+
result.current.onData({
|
|
698
|
+
type: "data-notebook-cells-completion",
|
|
699
|
+
data: {
|
|
700
|
+
cells: [{ language: "python", code: "value" }],
|
|
701
|
+
},
|
|
505
702
|
});
|
|
506
703
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
704
|
+
vi.mocked(getCellEditorView).mockReturnValueOnce(null);
|
|
705
|
+
result.current.onData({
|
|
706
|
+
type: "data-notebook-cells-completion",
|
|
707
|
+
data: {
|
|
708
|
+
cells: [{ language: "python", code: "value = 1" }],
|
|
709
|
+
},
|
|
511
710
|
});
|
|
512
711
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
712
|
+
expect(mockUpdateCellCode).toHaveBeenCalledWith({
|
|
713
|
+
cellId: cellId("unmounted-cell"),
|
|
714
|
+
code: "value = 1",
|
|
715
|
+
formattingChange: false,
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("should remove trailing cells when a retry produces a shorter snapshot", () => {
|
|
720
|
+
const { result } = renderGenerationHook();
|
|
721
|
+
result.current.beginStagedCellGeneration();
|
|
722
|
+
|
|
723
|
+
vi.mocked(CellId.create)
|
|
724
|
+
.mockReturnValueOnce(cellId("first-cell"))
|
|
725
|
+
.mockReturnValueOnce(cellId("second-cell"));
|
|
726
|
+
|
|
727
|
+
result.current.onData({
|
|
728
|
+
type: "data-notebook-cells-completion",
|
|
729
|
+
data: {
|
|
730
|
+
cells: [
|
|
731
|
+
{ language: "python", code: "value = 1" },
|
|
732
|
+
{ language: "python", code: "value" },
|
|
733
|
+
],
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
result.current.onData({
|
|
737
|
+
type: "data-notebook-cells-completion",
|
|
738
|
+
data: {
|
|
739
|
+
cells: [{ language: "python", code: "replacement = 2" }],
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
744
|
+
cellId: cellId("second-cell"),
|
|
745
|
+
});
|
|
746
|
+
expect(store.get(stagedAICellsAtom).has(cellId("first-cell"))).toBe(true);
|
|
747
|
+
expect(store.get(stagedAICellsAtom).has(cellId("second-cell"))).toBe(false);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("should remove a provisional marimo import when a retry no longer needs it", () => {
|
|
751
|
+
const { result } = renderGenerationHook();
|
|
752
|
+
result.current.beginStagedCellGeneration();
|
|
753
|
+
|
|
754
|
+
vi.mocked(CellId.create)
|
|
755
|
+
.mockReturnValueOnce(cellId("generated-cell"))
|
|
756
|
+
.mockReturnValueOnce(cellId("marimo-import"));
|
|
757
|
+
|
|
758
|
+
result.current.onData({
|
|
759
|
+
type: "data-notebook-cells-completion",
|
|
760
|
+
data: {
|
|
761
|
+
cells: [{ language: "sql", code: "result = mo.sql('SELECT 1')" }],
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
result.current.onData({
|
|
765
|
+
type: "data-notebook-cells-completion",
|
|
766
|
+
data: {
|
|
767
|
+
cells: [{ language: "python", code: "result = 1" }],
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
772
|
+
cellId: cellId("marimo-import"),
|
|
773
|
+
});
|
|
774
|
+
expect(store.get(stagedAICellsAtom).has(cellId("marimo-import"))).toBe(
|
|
775
|
+
false,
|
|
517
776
|
);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("should discard provisional cells when generation fails", () => {
|
|
780
|
+
const { result } = renderGenerationHook();
|
|
781
|
+
result.current.beginStagedCellGeneration();
|
|
782
|
+
|
|
783
|
+
vi.mocked(CellId.create).mockReturnValue(cellId("partial-cell"));
|
|
784
|
+
result.current.onData({
|
|
785
|
+
type: "data-notebook-cells-completion",
|
|
786
|
+
data: {
|
|
787
|
+
cells: [{ language: "python", code: "partial" }],
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
result.current.finishStagedCellGeneration(false);
|
|
791
|
+
|
|
792
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
793
|
+
cellId: cellId("partial-cell"),
|
|
794
|
+
});
|
|
795
|
+
expect(store.get(stagedAICellsAtom)).toEqual(new Map());
|
|
796
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(false);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it("should not let a superseded generation mutate active cells", () => {
|
|
800
|
+
const firstGeneration = renderGenerationHook();
|
|
801
|
+
firstGeneration.result.current.beginStagedCellGeneration();
|
|
518
802
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
803
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("superseded-cell"));
|
|
804
|
+
firstGeneration.result.current.onData({
|
|
805
|
+
type: "data-notebook-cells-completion",
|
|
806
|
+
data: {
|
|
807
|
+
cells: [{ language: "python", code: "old" }],
|
|
808
|
+
},
|
|
523
809
|
});
|
|
810
|
+
|
|
811
|
+
const activeGeneration = renderGenerationHook();
|
|
812
|
+
activeGeneration.result.current.beginStagedCellGeneration();
|
|
813
|
+
expect(mockDeleteCellCallback).toHaveBeenCalledWith({
|
|
814
|
+
cellId: cellId("superseded-cell"),
|
|
815
|
+
});
|
|
816
|
+
mockDeleteCellCallback.mockClear();
|
|
817
|
+
|
|
818
|
+
vi.mocked(CellId.create).mockReturnValueOnce(cellId("active-cell"));
|
|
819
|
+
activeGeneration.result.current.onData({
|
|
820
|
+
type: "data-notebook-cells-completion",
|
|
821
|
+
data: {
|
|
822
|
+
cells: [{ language: "python", code: "active" }],
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
firstGeneration.result.current.finishStagedCellGeneration(true);
|
|
827
|
+
firstGeneration.result.current.onData({
|
|
828
|
+
type: "data-notebook-cells-completion",
|
|
829
|
+
data: {
|
|
830
|
+
cells: [{ language: "python", code: "stale" }],
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
|
|
835
|
+
cellId: cellId("active-cell"),
|
|
836
|
+
});
|
|
837
|
+
expect(store.get(stagedAICellsAtom)).toEqual(
|
|
838
|
+
new Map([[cellId("active-cell"), { type: "add_cell" }]]),
|
|
839
|
+
);
|
|
840
|
+
expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
|
|
524
841
|
});
|
|
525
842
|
});
|