/* Copyright 2026 Marimo. All rights reserved. */
// @vitest-environment jsdom
import { act, renderHook } from "@testing-library/react";
import { createStore, Provider } from "jotai";
import { expect, it } from "vitest";
import type { EnvironmentOperation } from "../environment";
import { alertAtom, getPackageAlert, useAlertActions } from "../state";
it.each(["install", "remove"] as const)(
"restores active %s progress without announcing old successes",
(action) => {
const store = createStore();
const { result } = renderHook(() => useAlertActions(), {
wrapper: ({ children }) => {children},
});
const operation: EnvironmentOperation = {
operation_id: "package",
action,
source: "kernel",
status: { kind: "succeeded" },
packages: { numpy: "succeeded" },
logs: { numpy: "Done\n" },
};
const snapshot = (value: EnvironmentOperation) =>
result.current.setEnvironment({
source: value.source,
state: { restart_required: false, operations: [value] },
});
act(() => snapshot(operation));
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
expect(store.get(alertAtom).environments.kernel.operations).toEqual([
operation,
]);
const active: EnvironmentOperation = {
...operation,
status: { kind: "running" },
packages: { numpy: "running" },
logs: { numpy: "Working\n" },
};
act(() => snapshot(active));
expect(getPackageAlert(store.get(alertAtom))).toEqual({
...active,
id: active.operation_id,
kind: "environment",
restartRequired: false,
});
// Reconnecting can complete work that was already visible on this page.
act(() => snapshot(operation));
expect(getPackageAlert(store.get(alertAtom))).toEqual({
...operation,
id: operation.operation_id,
kind: "environment",
restartRequired: false,
});
act(() => result.current.clearPackageAlert(operation.operation_id));
act(() => snapshot(operation));
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
// A live result still gives feedback for a new package action.
act(() =>
result.current.updateEnvironment({
...operation,
operation_id: "new",
log_mode: "replace",
}),
);
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("new");
// Earlier changes can still require a restart after a later action succeeds.
act(() => result.current.clearPackageAlert("new"));
act(() =>
result.current.setEnvironment({
source: operation.source,
state: { restart_required: true, operations: [operation] },
}),
);
expect(getPackageAlert(store.get(alertAtom))).toEqual({
...operation,
id: operation.operation_id,
kind: "environment",
restartRequired: true,
});
},
);
it.each(["prepare", "sync"] as const)(
"retains %s progress without turning it into a package alert",
(action) => {
const store = createStore();
const { result } = renderHook(() => useAlertActions(), {
wrapper: ({ children }) => {children},
});
const operation: EnvironmentOperation = {
operation_id: "sandbox",
action,
source: "kernel",
status: { kind: "succeeded" },
packages: {},
logs: { environment: "Environment ready\n" },
};
const environment = { restart_required: false, operations: [operation] };
// A refreshed browser restores the result without announcing it again.
act(() =>
result.current.setEnvironment({ source: "kernel", state: environment }),
);
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
expect(store.get(alertAtom).environments.kernel).toEqual(environment);
const install: EnvironmentOperation = {
...operation,
operation_id: "install",
action: "install",
status: { kind: "running" },
packages: { numpy: "running" },
logs: { numpy: "Installing\n" },
};
act(() =>
result.current.updateEnvironment({ ...install, log_mode: "replace" }),
);
const packageAlert = getPackageAlert(store.get(alertAtom));
for (const status of [
{ kind: "running" },
{ kind: "failed", error: "Sync failed" },
] as const) {
act(() =>
result.current.updateEnvironment({
...operation,
status,
log_mode: "replace",
}),
);
expect(getPackageAlert(store.get(alertAtom))).toEqual(packageAlert);
}
act(() => result.current.clearPackageAlert(install.operation_id));
act(() =>
result.current.updateEnvironment({ ...operation, log_mode: "replace" }),
);
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
},
);
it.each(["kernel", "server"] as const)(
"preserves the selected %s result across snapshots and follows new work",
(source) => {
const store = createStore();
const { result } = renderHook(() => useAlertActions(), {
wrapper: ({ children }) => {children},
});
const selected: EnvironmentOperation = {
operation_id: "selected",
action: "install",
source,
status: { kind: "failed", error: "Network unavailable" },
packages: { numpy: "failed" },
logs: { numpy: "Selected logs\n" },
};
const other: EnvironmentOperation = {
...selected,
operation_id: "other",
source: source === "kernel" ? "server" : "kernel",
status: { kind: "succeeded" },
packages: { numpy: "succeeded" },
logs: { numpy: "Other logs\n" },
};
const snapshot = (operation: EnvironmentOperation) =>
result.current.setEnvironment({
source: operation.source,
state: { restart_required: false, operations: [operation] },
});
act(() => snapshot(selected));
for (const operations of [
[selected, other],
[other, selected],
]) {
act(() => operations.forEach(snapshot));
expect(getPackageAlert(store.get(alertAtom))).toEqual({
...selected,
id: selected.operation_id,
kind: "environment",
restartRequired: false,
});
}
// Live work in the other environment should still take over the banner.
const active: EnvironmentOperation = {
...other,
operation_id: "new-work",
status: { kind: "running" },
packages: { numpy: "running" },
logs: { numpy: "Installing\n" },
};
act(() =>
result.current.updateEnvironment({ ...active, log_mode: "replace" }),
);
act(() => snapshot(selected));
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(active.operation_id);
act(() =>
result.current.updateEnvironment({
...active,
status: { kind: "succeeded" },
packages: { numpy: "succeeded" },
logs: {},
log_mode: "append",
}),
);
act(() => snapshot(selected));
expect(getPackageAlert(store.get(alertAtom))).toEqual({
...active,
id: active.operation_id,
kind: "environment",
restartRequired: false,
status: { kind: "succeeded" },
packages: { numpy: "succeeded" },
});
// Replacing that environment with an empty snapshot removes its selection.
act(() =>
result.current.setEnvironment({
source: active.source,
state: { restart_required: false, operations: [] },
}),
);
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(
selected.operation_id,
);
act(() =>
result.current.updateEnvironment({
...other,
operation_id: "latest-result",
log_mode: "replace",
}),
);
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("latest-result");
},
);