diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 9af0a33..4cd10c7 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -4014,6 +4014,42 @@ export function mergeHermesIoSafetyConfig(config = {}, form = {}) { return next } +export function buildHermesCheckpointsConfigValues(config = {}) { + const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} + const checkpoints = root.checkpoints && typeof root.checkpoints === 'object' && !Array.isArray(root.checkpoints) + ? root.checkpoints + : {} + return { + checkpointsEnabled: readHermesBool(checkpoints.enabled, false), + checkpointMaxSnapshots: parseHermesInteger(checkpoints.max_snapshots, 'checkpoints.max_snapshots', 20, 1, 10000, false), + checkpointMaxTotalSizeMb: parseHermesInteger(checkpoints.max_total_size_mb, 'checkpoints.max_total_size_mb', 500, 0, 10485760, false), + checkpointMaxFileSizeMb: parseHermesInteger(checkpoints.max_file_size_mb, 'checkpoints.max_file_size_mb', 10, 0, 1048576, false), + checkpointAutoPrune: readHermesBool(checkpoints.auto_prune, true), + checkpointRetentionDays: parseHermesInteger(checkpoints.retention_days, 'checkpoints.retention_days', 7, 1, 3650, false), + checkpointDeleteOrphans: readHermesBool(checkpoints.delete_orphans, true), + checkpointMinIntervalHours: parseHermesInteger(checkpoints.min_interval_hours, 'checkpoints.min_interval_hours', 24, 0, 8760, false), + } +} + +export function mergeHermesCheckpointsConfig(config = {}, form = {}) { + const next = mergeConfigsPreservingFields({}, config && typeof config === 'object' && !Array.isArray(config) ? config : {}) + const currentValues = buildHermesCheckpointsConfigValues(next) + const checkpoints = next.checkpoints && typeof next.checkpoints === 'object' && !Array.isArray(next.checkpoints) + ? mergeConfigsPreservingFields(next.checkpoints, {}) + : {} + + checkpoints.enabled = formHermesBool(form, 'checkpointsEnabled', currentValues.checkpointsEnabled) + checkpoints.max_snapshots = parseHermesInteger(Object.hasOwn(form, 'checkpointMaxSnapshots') ? form.checkpointMaxSnapshots : currentValues.checkpointMaxSnapshots, 'checkpoints.max_snapshots', 20, 1, 10000, true) + checkpoints.max_total_size_mb = parseHermesInteger(Object.hasOwn(form, 'checkpointMaxTotalSizeMb') ? form.checkpointMaxTotalSizeMb : currentValues.checkpointMaxTotalSizeMb, 'checkpoints.max_total_size_mb', 500, 0, 10485760, true) + checkpoints.max_file_size_mb = parseHermesInteger(Object.hasOwn(form, 'checkpointMaxFileSizeMb') ? form.checkpointMaxFileSizeMb : currentValues.checkpointMaxFileSizeMb, 'checkpoints.max_file_size_mb', 10, 0, 1048576, true) + checkpoints.auto_prune = formHermesBool(form, 'checkpointAutoPrune', currentValues.checkpointAutoPrune) + checkpoints.retention_days = parseHermesInteger(Object.hasOwn(form, 'checkpointRetentionDays') ? form.checkpointRetentionDays : currentValues.checkpointRetentionDays, 'checkpoints.retention_days', 7, 1, 3650, true) + checkpoints.delete_orphans = formHermesBool(form, 'checkpointDeleteOrphans', currentValues.checkpointDeleteOrphans) + checkpoints.min_interval_hours = parseHermesInteger(Object.hasOwn(form, 'checkpointMinIntervalHours') ? form.checkpointMinIntervalHours : currentValues.checkpointMinIntervalHours, 'checkpoints.min_interval_hours', 24, 0, 8760, true) + next.checkpoints = checkpoints + return next +} + export function buildHermesPrivacyConfigValues(config = {}) { const root = config && typeof config === 'object' && !Array.isArray(config) ? config : {} const privacy = root.privacy && typeof root.privacy === 'object' && !Array.isArray(root.privacy) @@ -10706,6 +10742,27 @@ const handlers = { } }, + hermes_checkpoints_config_read() { + const { configPath, exists, config } = readHermesConfigYamlObject() + return { + exists, + configPath, + values: buildHermesCheckpointsConfigValues(config), + } + }, + + hermes_checkpoints_config_save({ form } = {}) { + const { configPath, config } = readHermesConfigYamlObject() + const next = mergeHermesCheckpointsConfig(config, form || {}) + const backup = writeHermesConfigYamlObject(configPath, next) + return { + ok: true, + configPath, + backup, + values: buildHermesCheckpointsConfigValues(next), + } + }, + hermes_privacy_config_read() { const { configPath, exists, config } = readHermesConfigYamlObject() return { diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 37f1052..9474fcc 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -4907,6 +4907,150 @@ fn merge_hermes_io_safety_config( Ok(()) } +fn build_hermes_checkpoints_config_values(config: &serde_yaml::Value) -> Value { + let root = config.as_mapping(); + let checkpoints = root.and_then(|map| yaml_get_mapping(map, "checkpoints")); + let checkpoints_enabled = checkpoints + .and_then(|map| yaml_bool_field(map, "enabled")) + .unwrap_or(false); + let checkpoint_max_snapshots = checkpoints + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_snapshots"), 20, 1, 10000)) + .unwrap_or(20); + let checkpoint_max_total_size_mb = checkpoints + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_total_size_mb"), 500, 0, 10485760)) + .unwrap_or(500); + let checkpoint_max_file_size_mb = checkpoints + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "max_file_size_mb"), 10, 0, 1048576)) + .unwrap_or(10); + let checkpoint_auto_prune = checkpoints + .and_then(|map| yaml_bool_field(map, "auto_prune")) + .unwrap_or(true); + let checkpoint_retention_days = checkpoints + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "retention_days"), 7, 1, 3650)) + .unwrap_or(7); + let checkpoint_delete_orphans = checkpoints + .and_then(|map| yaml_bool_field(map, "delete_orphans")) + .unwrap_or(true); + let checkpoint_min_interval_hours = checkpoints + .map(|map| bounded_hermes_i64(yaml_i64_field(map, "min_interval_hours"), 24, 0, 8760)) + .unwrap_or(24); + + serde_json::json!({ + "checkpointsEnabled": checkpoints_enabled, + "checkpointMaxSnapshots": checkpoint_max_snapshots, + "checkpointMaxTotalSizeMb": checkpoint_max_total_size_mb, + "checkpointMaxFileSizeMb": checkpoint_max_file_size_mb, + "checkpointAutoPrune": checkpoint_auto_prune, + "checkpointRetentionDays": checkpoint_retention_days, + "checkpointDeleteOrphans": checkpoint_delete_orphans, + "checkpointMinIntervalHours": checkpoint_min_interval_hours, + }) +} + +fn merge_hermes_checkpoints_config( + config: &mut serde_yaml::Value, + form: &Value, +) -> Result<(), String> { + let current = build_hermes_checkpoints_config_values(config); + let checkpoints_enabled = form_bool(form, "checkpointsEnabled") + .unwrap_or_else(|| current["checkpointsEnabled"].as_bool().unwrap_or(false)); + let checkpoint_max_snapshots = validate_hermes_i64( + if form.get("checkpointMaxSnapshots").is_some() { + form_i64(form, "checkpointMaxSnapshots") + } else { + Some(current["checkpointMaxSnapshots"].as_i64().unwrap_or(20)) + }, + "checkpoints.max_snapshots", + 20, + 1, + 10000, + )?; + let checkpoint_max_total_size_mb = validate_hermes_i64( + if form.get("checkpointMaxTotalSizeMb").is_some() { + form_i64(form, "checkpointMaxTotalSizeMb") + } else { + Some(current["checkpointMaxTotalSizeMb"].as_i64().unwrap_or(500)) + }, + "checkpoints.max_total_size_mb", + 500, + 0, + 10485760, + )?; + let checkpoint_max_file_size_mb = validate_hermes_i64( + if form.get("checkpointMaxFileSizeMb").is_some() { + form_i64(form, "checkpointMaxFileSizeMb") + } else { + Some(current["checkpointMaxFileSizeMb"].as_i64().unwrap_or(10)) + }, + "checkpoints.max_file_size_mb", + 10, + 0, + 1048576, + )?; + let checkpoint_auto_prune = form_bool(form, "checkpointAutoPrune") + .unwrap_or_else(|| current["checkpointAutoPrune"].as_bool().unwrap_or(true)); + let checkpoint_retention_days = validate_hermes_i64( + if form.get("checkpointRetentionDays").is_some() { + form_i64(form, "checkpointRetentionDays") + } else { + Some(current["checkpointRetentionDays"].as_i64().unwrap_or(7)) + }, + "checkpoints.retention_days", + 7, + 1, + 3650, + )?; + let checkpoint_delete_orphans = form_bool(form, "checkpointDeleteOrphans") + .unwrap_or_else(|| current["checkpointDeleteOrphans"].as_bool().unwrap_or(true)); + let checkpoint_min_interval_hours = validate_hermes_i64( + if form.get("checkpointMinIntervalHours").is_some() { + form_i64(form, "checkpointMinIntervalHours") + } else { + Some(current["checkpointMinIntervalHours"].as_i64().unwrap_or(24)) + }, + "checkpoints.min_interval_hours", + 24, + 0, + 8760, + )?; + + let root = ensure_yaml_object(config)?; + let checkpoints = yaml_child_object(root, "checkpoints")?; + checkpoints.insert( + yaml_key("enabled"), + serde_yaml::Value::Bool(checkpoints_enabled), + ); + checkpoints.insert( + yaml_key("max_snapshots"), + serde_yaml::Value::Number(checkpoint_max_snapshots.into()), + ); + checkpoints.insert( + yaml_key("max_total_size_mb"), + serde_yaml::Value::Number(checkpoint_max_total_size_mb.into()), + ); + checkpoints.insert( + yaml_key("max_file_size_mb"), + serde_yaml::Value::Number(checkpoint_max_file_size_mb.into()), + ); + checkpoints.insert( + yaml_key("auto_prune"), + serde_yaml::Value::Bool(checkpoint_auto_prune), + ); + checkpoints.insert( + yaml_key("retention_days"), + serde_yaml::Value::Number(checkpoint_retention_days.into()), + ); + checkpoints.insert( + yaml_key("delete_orphans"), + serde_yaml::Value::Bool(checkpoint_delete_orphans), + ); + checkpoints.insert( + yaml_key("min_interval_hours"), + serde_yaml::Value::Number(checkpoint_min_interval_hours.into()), + ); + Ok(()) +} + fn build_hermes_privacy_config_values(config: &serde_yaml::Value) -> Value { let root = config.as_mapping(); let privacy = root.and_then(|map| yaml_get_mapping(map, "privacy")); @@ -6507,6 +6651,30 @@ pub fn hermes_io_safety_config_save(form: Value) -> Result { })) } +#[tauri::command] +pub fn hermes_checkpoints_config_read() -> Result { + let (config_path, exists, config) = read_hermes_channel_yaml_config()?; + ensure_yaml_object(&mut config.clone())?; + Ok(serde_json::json!({ + "exists": exists, + "configPath": config_path.to_string_lossy(), + "values": build_hermes_checkpoints_config_values(&config), + })) +} + +#[tauri::command] +pub fn hermes_checkpoints_config_save(form: Value) -> Result { + let (config_path, _exists, mut config) = read_hermes_channel_yaml_config()?; + merge_hermes_checkpoints_config(&mut config, &form)?; + let backup = write_hermes_yaml_config(&config_path, &config)?; + Ok(serde_json::json!({ + "ok": true, + "configPath": config_path.to_string_lossy(), + "backup": backup, + "values": build_hermes_checkpoints_config_values(&config), + })) +} + #[tauri::command] pub fn hermes_privacy_config_read() -> Result { let (config_path, exists, config) = read_hermes_channel_yaml_config()?; @@ -12326,6 +12494,134 @@ streaming: } } +#[cfg(test)] +mod hermes_checkpoints_config_tests { + use super::{build_hermes_checkpoints_config_values, merge_hermes_checkpoints_config}; + use serde_json::json; + + #[test] + fn checkpoints_values_have_upstream_defaults() { + let config: serde_yaml::Value = serde_yaml::from_str("{}").unwrap(); + let values = build_hermes_checkpoints_config_values(&config); + assert_eq!(values["checkpointsEnabled"], false); + assert_eq!(values["checkpointMaxSnapshots"], 20); + assert_eq!(values["checkpointMaxTotalSizeMb"], 500); + assert_eq!(values["checkpointMaxFileSizeMb"], 10); + assert_eq!(values["checkpointAutoPrune"], true); + assert_eq!(values["checkpointRetentionDays"], 7); + assert_eq!(values["checkpointDeleteOrphans"], true); + assert_eq!(values["checkpointMinIntervalHours"], 24); + } + + #[test] + fn checkpoints_values_read_yaml_fields() { + let config: serde_yaml::Value = serde_yaml::from_str( + r#" +checkpoints: + enabled: true + max_snapshots: 12 + max_total_size_mb: 900 + max_file_size_mb: 25 + auto_prune: false + retention_days: 14 + delete_orphans: false + min_interval_hours: 6 +"#, + ) + .unwrap(); + let values = build_hermes_checkpoints_config_values(&config); + assert_eq!(values["checkpointsEnabled"], true); + assert_eq!(values["checkpointMaxSnapshots"], 12); + assert_eq!(values["checkpointMaxTotalSizeMb"], 900); + assert_eq!(values["checkpointMaxFileSizeMb"], 25); + assert_eq!(values["checkpointAutoPrune"], false); + assert_eq!(values["checkpointRetentionDays"], 14); + assert_eq!(values["checkpointDeleteOrphans"], false); + assert_eq!(values["checkpointMinIntervalHours"], 6); + } + + #[test] + fn merge_checkpoints_config_preserves_unknown_fields() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +model: + provider: anthropic +checkpoints: + enabled: true + custom_flag: keep-checkpoints +streaming: + enabled: true +"#, + ) + .unwrap(); + + merge_hermes_checkpoints_config( + &mut config, + &json!({ + "checkpointsEnabled": false, + "checkpointMaxSnapshots": "30", + "checkpointMaxTotalSizeMb": "0", + "checkpointMaxFileSizeMb": "0", + "checkpointAutoPrune": true, + "checkpointRetentionDays": "21", + "checkpointDeleteOrphans": true, + "checkpointMinIntervalHours": "12", + }), + ) + .unwrap(); + + assert_eq!(config["model"]["provider"].as_str(), Some("anthropic")); + assert_eq!(config["streaming"]["enabled"].as_bool(), Some(true)); + assert_eq!(config["checkpoints"]["enabled"].as_bool(), Some(false)); + assert_eq!(config["checkpoints"]["max_snapshots"].as_i64(), Some(30)); + assert_eq!(config["checkpoints"]["max_total_size_mb"].as_i64(), Some(0)); + assert_eq!(config["checkpoints"]["max_file_size_mb"].as_i64(), Some(0)); + assert_eq!(config["checkpoints"]["auto_prune"].as_bool(), Some(true)); + assert_eq!(config["checkpoints"]["retention_days"].as_i64(), Some(21)); + assert_eq!( + config["checkpoints"]["delete_orphans"].as_bool(), + Some(true) + ); + assert_eq!( + config["checkpoints"]["min_interval_hours"].as_i64(), + Some(12) + ); + assert_eq!( + config["checkpoints"]["custom_flag"].as_str(), + Some("keep-checkpoints") + ); + } + + #[test] + fn merge_checkpoints_config_rejects_invalid_values() { + let mut config = serde_yaml::Value::Mapping(serde_yaml::Mapping::new()); + let err = + merge_hermes_checkpoints_config(&mut config, &json!({ "checkpointMaxSnapshots": 0 })) + .unwrap_err(); + assert!(err.contains("checkpoints.max_snapshots")); + let err = merge_hermes_checkpoints_config( + &mut config, + &json!({ "checkpointMaxTotalSizeMb": -1 }), + ) + .unwrap_err(); + assert!(err.contains("checkpoints.max_total_size_mb")); + let err = + merge_hermes_checkpoints_config(&mut config, &json!({ "checkpointMaxFileSizeMb": -1 })) + .unwrap_err(); + assert!(err.contains("checkpoints.max_file_size_mb")); + let err = + merge_hermes_checkpoints_config(&mut config, &json!({ "checkpointRetentionDays": 0 })) + .unwrap_err(); + assert!(err.contains("checkpoints.retention_days")); + let err = merge_hermes_checkpoints_config( + &mut config, + &json!({ "checkpointMinIntervalHours": -1 }), + ) + .unwrap_err(); + assert!(err.contains("checkpoints.min_interval_hours")); + } +} + #[cfg(test)] mod hermes_terminal_config_tests { use super::{build_hermes_terminal_config_values, merge_hermes_terminal_config}; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a05e221..5288faf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -287,6 +287,8 @@ pub fn run() { hermes::hermes_execution_limits_config_save, hermes::hermes_io_safety_config_read, hermes::hermes_io_safety_config_save, + hermes::hermes_checkpoints_config_read, + hermes::hermes_checkpoints_config_save, hermes::hermes_privacy_config_read, hermes::hermes_privacy_config_save, hermes::hermes_browser_config_read, diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js index bf01d0e..5a6f1f3 100644 --- a/src/engines/hermes/pages/config.js +++ b/src/engines/hermes/pages/config.js @@ -125,6 +125,17 @@ const IO_SAFETY_DEFAULTS = { toolOutputMaxLineLength: 2000, } +const CHECKPOINTS_DEFAULTS = { + checkpointsEnabled: false, + checkpointMaxSnapshots: 20, + checkpointMaxTotalSizeMb: 500, + checkpointMaxFileSizeMb: 10, + checkpointAutoPrune: true, + checkpointRetentionDays: 7, + checkpointDeleteOrphans: true, + checkpointMinIntervalHours: 24, +} + const PRIVACY_DEFAULTS = { redactPii: false, } @@ -181,6 +192,7 @@ export function render() { let streamingValues = { ...STREAMING_DEFAULTS } let executionLimitsValues = { ...EXECUTION_LIMITS_DEFAULTS } let ioSafetyValues = { ...IO_SAFETY_DEFAULTS } + let checkpointsValues = { ...CHECKPOINTS_DEFAULTS } let privacyValues = { ...PRIVACY_DEFAULTS } let browserValues = { ...BROWSER_DEFAULTS } let terminalValues = { ...TERMINAL_DEFAULTS } @@ -200,6 +212,7 @@ export function render() { let streamingLoading = true let executionLimitsLoading = true let ioSafetyLoading = true + let checkpointsLoading = true let privacyLoading = true let browserLoading = true let terminalLoading = true @@ -219,6 +232,7 @@ export function render() { let streamingSaving = false let executionLimitsSaving = false let ioSafetySaving = false + let checkpointsSaving = false let privacySaving = false let browserSaving = false let terminalSaving = false @@ -238,6 +252,7 @@ export function render() { let streamingError = null let executionLimitsError = null let ioSafetyError = null + let checkpointsError = null let privacyError = null let browserError = null let terminalError = null @@ -251,7 +266,7 @@ export function render() { } function isBusy() { - return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving + return loading || runtimeLoading || compressionLoading || toolGuardrailsLoading || memoryLoading || skillsLoading || quickCommandsLoading || agentToolsetsLoading || agentRuntimeLoading || unauthorizedDmLoading || securityLoading || displayLoading || humanDelayLoading || streamingLoading || executionLimitsLoading || ioSafetyLoading || checkpointsLoading || privacyLoading || browserLoading || terminalLoading || saving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving } function option(labelKey, value, selected) { @@ -268,7 +283,7 @@ export function render() { } function renderRuntimePanel() { - const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || runtimeLoading || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -316,7 +331,7 @@ export function render() { } function renderCompressionPanel() { - const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || compressionLoading || compressionSaving || runtimeSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -366,7 +381,7 @@ export function render() { } function renderToolGuardrailsPanel() { - const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || toolGuardrailsLoading || toolGuardrailsSaving || runtimeSaving || compressionSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -428,7 +443,7 @@ export function render() { } function renderMemoryPanel() { - const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || memoryLoading || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -478,7 +493,7 @@ export function render() { } function renderSkillsConfigPanel() { - const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || skillsLoading || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -510,7 +525,7 @@ export function render() { } function renderQuickCommandsConfigPanel() { - const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || quickCommandsLoading || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -536,7 +551,7 @@ export function render() { } function renderAgentToolsetsConfigPanel() { - const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || agentToolsetsLoading || agentToolsetsSaving || agentRuntimeSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -562,7 +577,7 @@ export function render() { } function renderAgentRuntimeConfigPanel() { - const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving + const disabled = loading || saving || agentRuntimeLoading || agentRuntimeSaving || agentToolsetsSaving || unauthorizedDmSaving || securitySaving || displaySaving || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving || privacySaving || browserSaving || terminalSaving return `
@@ -624,7 +639,7 @@ export function render() { } function renderUnauthorizedDmConfigPanel() { - const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || unauthorizedDmLoading || unauthorizedDmSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -654,7 +669,7 @@ export function render() { } function renderSecurityConfigPanel() { - const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || securityLoading || securitySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -696,7 +711,7 @@ export function render() { } function renderDisplayConfigPanel() { - const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || displayLoading || displaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || humanDelaySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -760,7 +775,7 @@ export function render() { } function renderHumanDelayConfigPanel() { - const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || humanDelayLoading || humanDelaySaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || streamingSaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -798,7 +813,7 @@ export function render() { } function renderStreamingPanel() { - const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || terminalSaving + const disabled = loading || saving || streamingLoading || streamingSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || securitySaving || executionLimitsSaving || checkpointsSaving || terminalSaving return `
@@ -850,7 +865,7 @@ export function render() { } function renderExecutionLimitsPanel() { - const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving + const disabled = loading || saving || executionLimitsLoading || executionLimitsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || checkpointsSaving return `
@@ -922,7 +937,7 @@ export function render() { } function renderIoSafetyPanel() { - const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || ioSafetyLoading || ioSafetySaving || checkpointsSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving return `
@@ -961,8 +976,66 @@ export function render() { ` } + function renderCheckpointsPanel() { + const disabled = loading || saving || checkpointsLoading || checkpointsSaving || ioSafetySaving || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + return ` +
+
+
+
${t('engine.hermesCheckpointsConfigTitle')}
+
${t('engine.hermesCheckpointsConfigDesc')}
+
+
+ ${checkpointsSaving ? t('engine.hermesConfigStatusSaving') : checkpointsLoading ? t('engine.hermesConfigStatusLoading') : t('engine.hermesCheckpointsConfigStatusReady')} + +
+
+
+ ${renderError(checkpointsError)} +
+ + + +
+
+ + + + + +
+
${t('engine.hermesCheckpointsConfigFootnote')}
+
+
+ ` + } + function renderPrivacyPanel() { - const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || privacyLoading || privacySaving || browserSaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -990,7 +1063,7 @@ export function render() { } function renderBrowserPanel() { - const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving + const disabled = loading || saving || browserLoading || browserSaving || privacySaving || terminalSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || ioSafetySaving || checkpointsSaving return `
@@ -1034,7 +1107,7 @@ export function render() { } function renderTerminalPanel() { - const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving + const disabled = loading || saving || terminalLoading || terminalSaving || browserSaving || runtimeSaving || compressionSaving || toolGuardrailsSaving || memorySaving || skillsSaving || quickCommandsSaving || agentToolsetsSaving || agentRuntimeSaving || unauthorizedDmSaving || streamingSaving || executionLimitsSaving || checkpointsSaving return `
@@ -1123,6 +1196,7 @@ export function render() { ${renderStreamingPanel()} ${renderExecutionLimitsPanel()} ${renderIoSafetyPanel()} + ${renderCheckpointsPanel()} ${renderPrivacyPanel()} ${renderBrowserPanel()} ${renderCompressionPanel()} @@ -1170,6 +1244,7 @@ export function render() { el.querySelector('#hm-streaming-save')?.addEventListener('click', saveStreaming) el.querySelector('#hm-execution-limits-save')?.addEventListener('click', saveExecutionLimits) el.querySelector('#hm-io-safety-save')?.addEventListener('click', saveIoSafety) + el.querySelector('#hm-checkpoints-save')?.addEventListener('click', saveCheckpoints) el.querySelector('#hm-privacy-save')?.addEventListener('click', savePrivacyConfig) el.querySelector('#hm-browser-save')?.addEventListener('click', saveBrowserConfig) el.querySelector('#hm-terminal-save')?.addEventListener('click', saveTerminal) @@ -1255,6 +1330,11 @@ export function render() { ioSafetyValues = { ...IO_SAFETY_DEFAULTS, ...(data?.values || {}) } } + async function loadCheckpoints() { + const data = await api.hermesCheckpointsConfigRead() + checkpointsValues = { ...CHECKPOINTS_DEFAULTS, ...(data?.values || {}) } + } + async function loadPrivacyConfig() { const data = await api.hermesPrivacyConfigRead() privacyValues = { ...PRIVACY_DEFAULTS, ...(data?.values || {}) } @@ -1287,6 +1367,7 @@ export function render() { streamingLoading = true executionLimitsLoading = true ioSafetyLoading = true + checkpointsLoading = true privacyLoading = true browserLoading = true terminalLoading = true @@ -1306,6 +1387,7 @@ export function render() { streamingError = null executionLimitsError = null ioSafetyError = null + checkpointsError = null privacyError = null browserError = null terminalError = null @@ -1365,6 +1447,14 @@ export function render() { ioSafetyLoading = false draw() } + try { + await loadCheckpoints() + } catch (err) { + checkpointsError = humanizeError(err, t('engine.hermesCheckpointsConfigLoadFailed') || 'Load checkpoints config failed') + } finally { + checkpointsLoading = false + draw() + } try { await loadPrivacyConfig() } catch (err) { @@ -1527,6 +1617,9 @@ export function render() { try { await loadIoSafety() } catch {} + try { + await loadCheckpoints() + } catch {} try { await loadPrivacyConfig() } catch {} @@ -1979,6 +2072,38 @@ export function render() { } } + async function saveCheckpoints() { + const form = { + checkpointsEnabled: !!el.querySelector('#hm-checkpoints-enabled')?.checked, + checkpointMaxSnapshots: el.querySelector('#hm-checkpoints-max-snapshots')?.value || '20', + checkpointMaxTotalSizeMb: el.querySelector('#hm-checkpoints-max-total-size-mb')?.value || '500', + checkpointMaxFileSizeMb: el.querySelector('#hm-checkpoints-max-file-size-mb')?.value || '10', + checkpointAutoPrune: !!el.querySelector('#hm-checkpoints-auto-prune')?.checked, + checkpointRetentionDays: el.querySelector('#hm-checkpoints-retention-days')?.value || '7', + checkpointDeleteOrphans: !!el.querySelector('#hm-checkpoints-delete-orphans')?.checked, + checkpointMinIntervalHours: el.querySelector('#hm-checkpoints-min-interval-hours')?.value || '24', + } + checkpointsSaving = true + checkpointsError = null + draw() + try { + const result = await api.hermesCheckpointsConfigSave(form) + checkpointsValues = { ...CHECKPOINTS_DEFAULTS, ...(result?.values || form) } + await refreshRawAfterStructuredSave() + const backup = result?.backup || '' + toast({ + message: t('engine.hermesCheckpointsConfigSaveSuccess'), + hint: backup ? t('engine.hermesConfigBackupHint', { path: backup }) : '', + }, 'success') + } catch (err) { + checkpointsError = humanizeError(err, t('engine.hermesCheckpointsConfigSaveFailed') || 'Save checkpoints config failed') + toast(checkpointsError, 'error') + } finally { + checkpointsSaving = false + draw() + } + } + async function savePrivacyConfig() { const form = { redactPii: !!el.querySelector('#hm-privacy-redact-pii')?.checked, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index 5624442..ae063ad 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -539,6 +539,8 @@ export const api = { hermesExecutionLimitsConfigSave: (form) => invoke('hermes_execution_limits_config_save', { form }), hermesIoSafetyConfigRead: () => invoke('hermes_io_safety_config_read'), hermesIoSafetyConfigSave: (form) => invoke('hermes_io_safety_config_save', { form }), + hermesCheckpointsConfigRead: () => invoke('hermes_checkpoints_config_read'), + hermesCheckpointsConfigSave: (form) => invoke('hermes_checkpoints_config_save', { form }), hermesPrivacyConfigRead: () => invoke('hermes_privacy_config_read'), hermesPrivacyConfigSave: (form) => invoke('hermes_privacy_config_save', { form }), hermesBrowserConfigRead: () => invoke('hermes_browser_config_read'), diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 8946db4..57b1fe4 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -576,6 +576,22 @@ export default { hermesIoSafetyToolOutputMaxLines: _('文件分页最大行数', 'File page line cap', '檔案分頁最大行數'), hermesIoSafetyToolOutputMaxLineLength: _('单行显示字符上限', 'Per-line character cap', '單行顯示字元上限'), hermesIoSafetyFootnote: _('默认值适合大多数模型;小上下文模型可降低这些上限。其他 tool_output 高级字段会保留在 raw YAML 中。', 'Defaults fit most models. Lower these limits for small-context models. Other advanced tool_output fields are preserved in raw YAML.', '預設值適合多數模型;小上下文模型可降低這些上限。其他 tool_output 進階欄位會保留在 raw YAML 中。'), + hermesCheckpointsConfigTitle: _('文件快照回滚', 'File checkpoint rollback', '檔案快照回滾'), + hermesCheckpointsConfigDesc: _('在首次写文件或补丁前为工作目录创建快照,便于长跑任务误改后通过 /rollback 恢复。', 'Create a working-directory snapshot before the first file write or patch so long runs can recover with /rollback after accidental edits.', '在首次寫檔或套用補丁前為工作目錄建立快照,便於長跑任務誤改後透過 /rollback 復原。'), + hermesCheckpointsConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), + hermesCheckpointsConfigSave: _('保存回滚保护', 'Save rollback protection', '儲存回滾保護'), + hermesCheckpointsConfigSaveSuccess: _('文件快照回滚配置已保存,建议重启 Hermes Gateway 生效', 'File checkpoint rollback settings saved. Restart Hermes Gateway to take effect.', '檔案快照回滾設定已儲存,建議重啟 Hermes Gateway 生效'), + hermesCheckpointsConfigLoadFailed: _('加载文件快照回滚配置失败', 'Load file checkpoint rollback settings failed', '載入檔案快照回滾設定失敗'), + hermesCheckpointsConfigSaveFailed: _('保存文件快照回滚配置失败', 'Save file checkpoint rollback settings failed', '儲存檔案快照回滾設定失敗'), + hermesCheckpointsConfigEnabled: _('启用写入前快照', 'Enable snapshots before writes', '啟用寫入前快照'), + hermesCheckpointsConfigAutoPrune: _('自动清理旧快照', 'Automatically prune old snapshots', '自動清理舊快照'), + hermesCheckpointsConfigDeleteOrphans: _('删除孤儿项目快照', 'Delete orphaned project snapshots', '刪除孤兒專案快照'), + hermesCheckpointsConfigMaxSnapshots: _('每个目录最多快照数', 'Max snapshots per directory', '每個目錄最多快照數'), + hermesCheckpointsConfigMaxTotalSizeMb: _('总容量上限 MB', 'Total size cap MB', '總容量上限 MB'), + hermesCheckpointsConfigMaxFileSizeMb: _('单文件纳入上限 MB', 'Single-file inclusion cap MB', '單檔納入上限 MB'), + hermesCheckpointsConfigRetentionDays: _('保留天数', 'Retention days', '保留天數'), + hermesCheckpointsConfigMinIntervalHours: _('自动清理最小间隔小时', 'Minimum auto-prune interval hours', '自動清理最小間隔小時'), + hermesCheckpointsConfigFootnote: _('这里写入 checkpoints.*。容量上限或单文件上限设为 0 表示关闭对应限制;其他 checkpoints 高级字段会保留在 raw YAML 中。', 'This writes checkpoints.*. Set total size cap or single-file cap to 0 to disable that limit. Other advanced checkpoint fields stay in raw YAML.', '這裡寫入 checkpoints.*。容量上限或單檔上限設為 0 表示關閉對應限制;其他 checkpoints 進階欄位會保留在 raw YAML 中。'), hermesPrivacyConfigTitle: _('隐私脱敏', 'Privacy redaction', '隱私脫敏'), hermesPrivacyConfigDesc: _('对支持的 Gateway 渠道在送入模型前脱敏用户 ID、手机号和会话标识,降低公网渠道泄露风险。', 'Redact user IDs, phone numbers, and chat identifiers before supported Gateway channels send context to the model, reducing public-channel exposure risk.', '對支援的 Gateway 渠道在送入模型前脫敏使用者 ID、電話號碼和會話識別,降低公開渠道外洩風險。'), hermesPrivacyConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'), diff --git a/tests/hermes-checkpoints-config.test.js b/tests/hermes-checkpoints-config.test.js new file mode 100644 index 0000000..ea61e8c --- /dev/null +++ b/tests/hermes-checkpoints-config.test.js @@ -0,0 +1,101 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildHermesCheckpointsConfigValues, + mergeHermesCheckpointsConfig, +} from '../scripts/dev-api.js' + +test('Hermes 文件快照回滚配置读取会提供上游默认值', () => { + const values = buildHermesCheckpointsConfigValues({}) + + assert.deepEqual(values, { + checkpointsEnabled: false, + checkpointMaxSnapshots: 20, + checkpointMaxTotalSizeMb: 500, + checkpointMaxFileSizeMb: 10, + checkpointAutoPrune: true, + checkpointRetentionDays: 7, + checkpointDeleteOrphans: true, + checkpointMinIntervalHours: 24, + }) +}) + +test('Hermes 文件快照回滚配置读取会回显 YAML 字段', () => { + const values = buildHermesCheckpointsConfigValues({ + checkpoints: { + enabled: true, + max_snapshots: 12, + max_total_size_mb: 900, + max_file_size_mb: 25, + auto_prune: false, + retention_days: 14, + delete_orphans: false, + min_interval_hours: 6, + }, + }) + + assert.equal(values.checkpointsEnabled, true) + assert.equal(values.checkpointMaxSnapshots, 12) + assert.equal(values.checkpointMaxTotalSizeMb, 900) + assert.equal(values.checkpointMaxFileSizeMb, 25) + assert.equal(values.checkpointAutoPrune, false) + assert.equal(values.checkpointRetentionDays, 14) + assert.equal(values.checkpointDeleteOrphans, false) + assert.equal(values.checkpointMinIntervalHours, 6) +}) + +test('Hermes 文件快照回滚配置保存会保留未知字段并写入 checkpoints', () => { + const next = mergeHermesCheckpointsConfig({ + model: { provider: 'anthropic' }, + checkpoints: { + enabled: true, + custom_flag: 'keep-checkpoints', + }, + streaming: { enabled: true }, + }, { + checkpointsEnabled: false, + checkpointMaxSnapshots: '30', + checkpointMaxTotalSizeMb: '0', + checkpointMaxFileSizeMb: '0', + checkpointAutoPrune: true, + checkpointRetentionDays: '21', + checkpointDeleteOrphans: true, + checkpointMinIntervalHours: '12', + }) + + assert.deepEqual(next.model, { provider: 'anthropic' }) + assert.deepEqual(next.streaming, { enabled: true }) + assert.equal(next.checkpoints.enabled, false) + assert.equal(next.checkpoints.max_snapshots, 30) + assert.equal(next.checkpoints.max_total_size_mb, 0) + assert.equal(next.checkpoints.max_file_size_mb, 0) + assert.equal(next.checkpoints.auto_prune, true) + assert.equal(next.checkpoints.retention_days, 21) + assert.equal(next.checkpoints.delete_orphans, true) + assert.equal(next.checkpoints.min_interval_hours, 12) + assert.equal(next.checkpoints.custom_flag, 'keep-checkpoints') +}) + +test('Hermes 文件快照回滚配置保存会拒绝越界值', () => { + assert.throws( + () => mergeHermesCheckpointsConfig({}, { checkpointMaxSnapshots: '0' }), + /checkpoints\.max_snapshots/, + ) + assert.throws( + () => mergeHermesCheckpointsConfig({}, { checkpointMaxTotalSizeMb: '-1' }), + /checkpoints\.max_total_size_mb/, + ) + assert.throws( + () => mergeHermesCheckpointsConfig({}, { checkpointMaxFileSizeMb: '-1' }), + /checkpoints\.max_file_size_mb/, + ) + assert.throws( + () => mergeHermesCheckpointsConfig({}, { checkpointRetentionDays: '0' }), + /checkpoints\.retention_days/, + ) + assert.throws( + () => mergeHermesCheckpointsConfig({}, { checkpointMinIntervalHours: '-1' }), + /checkpoints\.min_interval_hours/, + ) +}) diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js index 7313751..1d7c29f 100644 --- a/tests/hermes-config-page-ui.test.js +++ b/tests/hermes-config-page-ui.test.js @@ -176,6 +176,22 @@ test('Hermes 配置页会暴露输入输出保护结构化配置字段', () => { } }) +test('Hermes 配置页会暴露文件快照回滚结构化配置字段', () => { + for (const id of [ + 'hm-checkpoints-save', + 'hm-checkpoints-enabled', + 'hm-checkpoints-max-snapshots', + 'hm-checkpoints-max-total-size-mb', + 'hm-checkpoints-max-file-size-mb', + 'hm-checkpoints-auto-prune', + 'hm-checkpoints-retention-days', + 'hm-checkpoints-delete-orphans', + 'hm-checkpoints-min-interval-hours', + ]) { + assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`) + } +}) + test('Hermes 配置页会暴露隐私脱敏结构化配置字段', () => { for (const id of [ 'hm-privacy-save', @@ -235,7 +251,8 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => { key.includes('ExecutionLimits') || key.includes('PrivacyConfig') || key.includes('BrowserConfig') || - key.includes('TerminalConfig') + key.includes('TerminalConfig') || + key.includes('CheckpointsConfig') ))) assert.ok(keys.size > 0, '应能提取新增结构化配置用到的 engine 翻译 key')