Files
kandev/apps/web/components/settings/system/retention-settings-card.test.tsx
T

278 lines
10 KiB
TypeScript

import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SettingsSaveContributor } from "@/components/settings/settings-save-provider";
import type { RetentionSettings, RetentionStatus } from "@/lib/types/system";
import { StateProvider } from "@/components/state-provider";
const fetchRetentionStatusMock = vi.fn();
const saveRetentionSettingsMock = vi.fn();
let saveContributor: SettingsSaveContributor | null = null;
let currentRole: "admin" | "member" | undefined = "admin";
const ENABLED_TOGGLE_TEST_ID = "retention-enabled";
const BATCH_LIMIT_TEST_ID = "retention-batch-limit";
const EXPECTED_SAVE_CONTRIBUTOR_ERROR = "expected save contributor";
vi.mock("@/lib/api/domains/system-api", () => ({
fetchRetentionStatus: (...args: unknown[]) => fetchRetentionStatusMock(...args),
saveRetentionSettings: (...args: unknown[]) => saveRetentionSettingsMock(...args),
}));
vi.mock("@/components/settings/settings-save-provider", () => ({
useSettingsSaveContributor: (contributor: SettingsSaveContributor) => {
saveContributor = contributor;
},
}));
import { RetentionSettingsCard } from "./retention-settings-card";
function defaultSettings(overrides: Partial<RetentionSettings> = {}): RetentionSettings {
return {
enabled: true,
sweep_interval_hours: 6,
batch_limit: 5000,
routine_runs: { window_days: 30, floor_per_owner: 50, warn_rows: 25000 },
runs: { window_days: 30, floor_per_owner: 50, warn_rows: 25000 },
run_events: { warn_rows: 250000 },
...overrides,
};
}
function statusOf(overrides: Partial<RetentionStatus> = {}): RetentionStatus {
return {
settings: defaultSettings(),
last_sweep: null,
skip_count: 0,
retained_counts: {
office_routine_runs: { state: "not_computed", retained_count: 0, as_of: "" },
runs: { state: "not_computed", retained_count: 0, as_of: "" },
run_events: { state: "not_computed", retained_count: 0, as_of: "" },
},
...overrides,
};
}
function renderCard() {
return render(
<StateProvider
initialState={{
auth: {
mode: "enabled",
authenticated: true,
user: currentRole
? {
id: "user-1",
email: "user@example.com",
display_name: "Test User",
role: currentRole,
status: "active",
}
: null,
ssoProviders: [],
},
}}
>
<RetentionSettingsCard />
</StateProvider>,
);
}
async function correctInvalidBatchLimit() {
const batchLimit = screen.getByTestId(BATCH_LIMIT_TEST_ID);
fireEvent.change(batchLimit, { target: { value: "0" } });
await waitFor(() => {
expect(screen.getByTestId("retention-advanced-settings").getAttribute("open")).toBe("");
expect(batchLimit).toHaveProperty("disabled", false);
expect(batchLimit.getAttribute("aria-invalid")).toBe("true");
expect(screen.getByTestId(`${BATCH_LIMIT_TEST_ID}-error`)).toBeTruthy();
expect(saveContributor?.canSave).toBe(false);
});
if (!saveContributor) throw new Error(EXPECTED_SAVE_CONTRIBUTOR_ERROR);
await act(async () => {
await expect(saveContributor?.save(saveContributor.revision)).rejects.toThrow(
"Fix the highlighted retention fields before saving.",
);
});
expect(saveRetentionSettingsMock).not.toHaveBeenCalled();
fireEvent.change(batchLimit, { target: { value: "5000" } });
await waitFor(() => {
expect(batchLimit).toHaveProperty("disabled", false);
expect(batchLimit).toHaveProperty("value", "5000");
expect(batchLimit.getAttribute("aria-invalid")).toBeNull();
expect(saveContributor?.canSave).toBe(true);
});
}
beforeEach(() => {
fetchRetentionStatusMock.mockReset();
saveRetentionSettingsMock.mockReset();
fetchRetentionStatusMock.mockResolvedValue(statusOf());
currentRole = "admin";
saveContributor = null;
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe("RetentionSettingsCard", () => {
it("loads settings and renders the swept-table fields from the fetched status", async () => {
renderCard();
const windowDays = await screen.findByTestId("retention-routine-runs-window-days");
expect(windowDays).toHaveProperty("value", "30");
expect(screen.getByTestId("retention-runs-warn-rows")).toHaveProperty("value", "25000");
expect(screen.getByTestId("retention-run-events-warn-rows")).toHaveProperty("value", "250000");
expect(screen.getByTestId("retention-never-swept")).toBeTruthy();
});
it("keeps members read-only while preserving the loaded values", async () => {
currentRole = "member";
renderCard();
const windowDays = await screen.findByTestId("retention-routine-runs-window-days");
expect(windowDays).toHaveProperty("disabled", true);
expect(screen.getByTestId(ENABLED_TOGGLE_TEST_ID)).toHaveProperty("disabled", true);
expect(screen.getByText("Only an admin can change retention settings.")).toBeTruthy();
expect(saveContributor?.isDirty).toBe(false);
});
it("reports a failed save without clearing the dirty draft", async () => {
renderCard();
await screen.findByTestId(ENABLED_TOGGLE_TEST_ID);
fireEvent.click(screen.getByTestId(ENABLED_TOGGLE_TEST_ID));
if (!saveContributor) throw new Error(EXPECTED_SAVE_CONTRIBUTOR_ERROR);
saveRetentionSettingsMock.mockRejectedValueOnce(new Error("offline"));
await act(async () => {
await expect(saveContributor?.save(saveContributor.revision)).rejects.toThrow("offline");
});
expect(saveContributor?.isDirty).toBe(true);
await screen.findByTestId("retention-save-error");
});
it("opens Advanced settings and annotates an invalid field before saving", async () => {
renderCard();
await screen.findByTestId(ENABLED_TOGGLE_TEST_ID);
await correctInvalidBatchLimit();
});
it("renders the last sweep outcome, backlog flag, and retained counts", async () => {
fetchRetentionStatusMock.mockResolvedValue(
statusOf({
last_sweep: {
started_at: "2026-09-01T00:00:00Z",
finished_at: "2026-09-01T00:00:05Z",
office_routine_runs: {
deleted: 12,
backlog: true,
error: "",
previewed: false,
would_delete: 0,
},
runs: { deleted: 3, backlog: false, error: "", previewed: true, would_delete: 40 },
run_events: { deleted: 100, backlog: false, error: "" },
route_attempts: { deleted: 0, backlog: false, error: "" },
run_skills: { deleted: 0, backlog: false, error: "" },
},
skip_count: 2,
last_skip_at: "2026-09-01T00:10:00Z",
retained_counts: {
office_routine_runs: {
state: "fresh",
retained_count: 1200,
as_of: "2026-09-01T00:00:00Z",
top_routine_id: "routine-1",
top_routine_share: 0.42,
},
runs: { state: "stale", retained_count: 800, as_of: "2026-08-31T00:00:00Z" },
run_events: { state: "not_computed", retained_count: 0, as_of: "" },
},
}),
);
renderCard();
await screen.findByTestId("retention-last-sweep");
expect(screen.getByTestId("retention-backlog-office_routine_runs")).toBeTruthy();
expect(screen.getByTestId("retention-retained-office_routine_runs").textContent).toContain(
"1200",
);
expect(screen.getByTestId("retention-retained-office_routine_runs").textContent).toContain(
"42%",
);
expect(screen.getByText(/Stale: last measurement failed/)).toBeTruthy();
expect(screen.getByTestId("retention-skip-count").textContent).toContain("2");
});
});
describe("RetentionSettingsCard save/reload consistency", () => {
it("stages an admin edit until the shared save contributor runs, then reloads", async () => {
renderCard();
await screen.findByTestId(ENABLED_TOGGLE_TEST_ID);
const toggle = screen.getByTestId(ENABLED_TOGGLE_TEST_ID);
fireEvent.click(toggle);
expect(saveRetentionSettingsMock).not.toHaveBeenCalled();
expect(saveContributor?.isDirty).toBe(true);
if (!saveContributor) throw new Error(EXPECTED_SAVE_CONTRIBUTOR_ERROR);
saveRetentionSettingsMock.mockResolvedValueOnce(defaultSettings({ enabled: false }));
fetchRetentionStatusMock.mockResolvedValueOnce(
statusOf({ settings: defaultSettings({ enabled: false }) }),
);
await act(async () => saveContributor?.save(saveContributor.revision));
expect(saveRetentionSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({ enabled: false }),
);
await waitFor(() => expect(saveContributor?.isDirty).toBe(false));
});
it("clears the dirty draft from the save response even when the post-save reload fails", async () => {
renderCard();
await screen.findByTestId(ENABLED_TOGGLE_TEST_ID);
fireEvent.click(screen.getByTestId(ENABLED_TOGGLE_TEST_ID));
if (!saveContributor) throw new Error(EXPECTED_SAVE_CONTRIBUTOR_ERROR);
saveRetentionSettingsMock.mockResolvedValueOnce(defaultSettings({ enabled: false }));
fetchRetentionStatusMock.mockRejectedValueOnce(new Error("offline"));
await act(async () => saveContributor?.save(saveContributor.revision));
expect(saveRetentionSettingsMock).toHaveBeenCalledWith(
expect.objectContaining({ enabled: false }),
);
await waitFor(() => expect(saveContributor?.isDirty).toBe(false));
});
});
describe("RetentionSettingsCard unknown-status reporting", () => {
it("renders each unrecognized status with its row count", async () => {
fetchRetentionStatusMock.mockResolvedValue(
statusOf({
retained_counts: {
office_routine_runs: {
state: "fresh",
retained_count: 5,
as_of: "2026-09-01T00:00:00Z",
unknown_statuses: [{ status: "quarantined", count: 2 }],
},
runs: { state: "not_computed", retained_count: 0, as_of: "" },
run_events: { state: "not_computed", retained_count: 0, as_of: "" },
},
}),
);
renderCard();
const row = await screen.findByTestId("retention-retained-office_routine_runs");
expect(row.textContent).toContain("quarantined (2)");
});
});