feat(FN-3891): document memory backup settings in settings reference
Documents the memory backup feature in the settings reference and adds a changeset to prepare for publishing. Fusion-Task-Id: FN-3891
This commit is contained in:
@@ -4498,6 +4498,82 @@ export function SettingsModal({
|
||||
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h4 className="settings-section-heading">Memory Backups</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupEnabled" className="checkbox-label">
|
||||
<input
|
||||
id="memoryBackupEnabled"
|
||||
type="checkbox"
|
||||
checked={form.memoryBackupEnabled || false}
|
||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupEnabled: e.target.checked }))}
|
||||
/>
|
||||
Enable automatic memory backups
|
||||
</label>
|
||||
<small>When enabled, project and agent memory files are backed up automatically on a schedule.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupSchedule">Memory Backup Schedule (Cron)</label>
|
||||
<input
|
||||
id="memoryBackupSchedule"
|
||||
type="text"
|
||||
placeholder="0 3 * * *"
|
||||
value={form.memoryBackupSchedule || "0 3 * * *"}
|
||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupSchedule: e.target.value }))}
|
||||
disabled={!form.memoryBackupEnabled}
|
||||
/>
|
||||
<small>Cron expression for memory backup timing. Default: 0 3 * * * (daily at 3 AM).</small>
|
||||
{form.memoryBackupSchedule && !/^[\s\d*,/-]+$/.test(form.memoryBackupSchedule) && (
|
||||
<small className="field-error">Invalid cron expression format</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupRetention">Memory Retention Count</label>
|
||||
<input
|
||||
id="memoryBackupRetention"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={form.memoryBackupRetention ?? ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm((f) => ({ ...f, memoryBackupRetention: val === "" ? undefined : Number(val) }));
|
||||
}}
|
||||
disabled={!form.memoryBackupEnabled}
|
||||
/>
|
||||
<small>Number of memory backups to keep (oldest are deleted first). Range: 1-100.</small>
|
||||
{form.memoryBackupRetention !== undefined && (form.memoryBackupRetention < 1 || form.memoryBackupRetention > 100) && (
|
||||
<small className="field-error">Must be between 1 and 100</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupDir">Memory Backup Directory</label>
|
||||
<input
|
||||
id="memoryBackupDir"
|
||||
type="text"
|
||||
placeholder=".fusion/backups/memory"
|
||||
value={form.memoryBackupDir || ".fusion/backups/memory"}
|
||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupDir: e.target.value }))}
|
||||
disabled={!form.memoryBackupEnabled}
|
||||
/>
|
||||
<small>Directory for memory backups, relative to project root.</small>
|
||||
{form.memoryBackupDir && form.memoryBackupDir.includes("..") && (
|
||||
<small className="field-error">Path cannot contain parent directory traversal (..)</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryBackupScope">Memory Backup Scope</label>
|
||||
<select
|
||||
id="memoryBackupScope"
|
||||
value={form.memoryBackupScope || "all"}
|
||||
onChange={(e) => setForm((f) => ({ ...f, memoryBackupScope: e.target.value as "project" | "agents" | "all" }))}
|
||||
disabled={!form.memoryBackupEnabled}
|
||||
>
|
||||
<option value="all">All (project + agents)</option>
|
||||
<option value="project">Project only (.fusion/memory)</option>
|
||||
<option value="agents">Agents only (.fusion/agent-memory)</option>
|
||||
</select>
|
||||
</div>
|
||||
{backupLoading ? (
|
||||
<div className="settings-empty-state">Loading backup info…</div>
|
||||
) : backupInfo ? (
|
||||
|
||||
@@ -2988,6 +2988,39 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory backups settings", () => {
|
||||
it("renders memory backup fields with defaults and saves changes", async () => {
|
||||
renderModal({ initialSection: "backups" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Memory Backups" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Memory Backup Schedule (Cron)")).toHaveValue("0 3 * * *");
|
||||
expect(screen.getByLabelText("Memory Retention Count")).toHaveValue(null);
|
||||
expect(screen.getByLabelText("Memory Backup Directory")).toHaveValue(".fusion/backups/memory");
|
||||
expect(screen.getByLabelText("Memory Backup Scope")).toHaveValue("all");
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Enable automatic memory backups"));
|
||||
fireEvent.change(screen.getByLabelText("Memory Backup Schedule (Cron)"), { target: { value: "0 5 * * *" } });
|
||||
fireEvent.change(screen.getByLabelText("Memory Retention Count"), { target: { value: "21" } });
|
||||
fireEvent.change(screen.getByLabelText("Memory Backup Directory"), { target: { value: ".fusion/backups/custom-memory" } });
|
||||
await userEvent.selectOptions(screen.getByLabelText("Memory Backup Scope"), "agents");
|
||||
await userEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
memoryBackupEnabled: true,
|
||||
memoryBackupSchedule: "0 5 * * *",
|
||||
memoryBackupRetention: 21,
|
||||
memoryBackupDir: ".fusion/backups/custom-memory",
|
||||
memoryBackupScope: "agents",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("research settings sections", () => {
|
||||
beforeEach(() => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user