fix: guard note suggestions during reload
This commit is contained in:
@@ -191,6 +191,30 @@ fn map_claude_event(event: crate::claude_cli::ClaudeStreamEvent) -> Option<AiAge
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn request_with_permission(
|
||||
permission_mode: Option<AiAgentPermissionMode>,
|
||||
) -> AiAgentStreamRequest {
|
||||
AiAgentStreamRequest {
|
||||
agent: AiAgentId::Codex,
|
||||
message: "Summarize this vault".into(),
|
||||
system_prompt: None,
|
||||
vault_path: "/tmp/vault".into(),
|
||||
permission_mode,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stream_request_uses_default_or_explicit_permission_mode() {
|
||||
assert_eq!(
|
||||
request_with_permission(None).permission_mode(),
|
||||
AiAgentPermissionMode::Safe
|
||||
);
|
||||
assert_eq!(
|
||||
request_with_permission(Some(AiAgentPermissionMode::PowerUser)).permission_mode(),
|
||||
AiAgentPermissionMode::PowerUser
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_status_contains_all_agents() {
|
||||
let status = get_ai_agents_status();
|
||||
@@ -214,6 +238,66 @@ mod tests {
|
||||
assert!(matches!(mapped, Some(AiAgentStreamEvent::Done)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_claude_text_events_preserve_stream_data() {
|
||||
assert!(matches!(
|
||||
map_claude_event(crate::claude_cli::ClaudeStreamEvent::Init {
|
||||
session_id: "session-1".into(),
|
||||
}),
|
||||
Some(AiAgentStreamEvent::Init { session_id }) if session_id == "session-1"
|
||||
));
|
||||
assert!(matches!(
|
||||
map_claude_event(crate::claude_cli::ClaudeStreamEvent::TextDelta {
|
||||
text: "visible output".into(),
|
||||
}),
|
||||
Some(AiAgentStreamEvent::TextDelta { text }) if text == "visible output"
|
||||
));
|
||||
assert!(matches!(
|
||||
map_claude_event(crate::claude_cli::ClaudeStreamEvent::ThinkingDelta {
|
||||
text: "thinking".into(),
|
||||
}),
|
||||
Some(AiAgentStreamEvent::ThinkingDelta { text }) if text == "thinking"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_claude_tool_events_preserve_stream_data() {
|
||||
let started = map_claude_event(crate::claude_cli::ClaudeStreamEvent::ToolStart {
|
||||
tool_name: "Read".into(),
|
||||
tool_id: "tool-1".into(),
|
||||
input: Some("{\"file\":\"note.md\"}".into()),
|
||||
});
|
||||
let finished = map_claude_event(crate::claude_cli::ClaudeStreamEvent::ToolDone {
|
||||
tool_id: "tool-1".into(),
|
||||
output: Some("done".into()),
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
started,
|
||||
Some(AiAgentStreamEvent::ToolStart { tool_name, tool_id, input })
|
||||
if tool_name == "Read"
|
||||
&& tool_id == "tool-1"
|
||||
&& input.as_deref() == Some("{\"file\":\"note.md\"}")
|
||||
));
|
||||
assert!(matches!(
|
||||
finished,
|
||||
Some(AiAgentStreamEvent::ToolDone { tool_id, output })
|
||||
if tool_id == "tool-1" && output.as_deref() == Some("done")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_claude_error_event_preserves_message() {
|
||||
let mapped = map_claude_event(crate::claude_cli::ClaudeStreamEvent::Error {
|
||||
message: "missing auth".into(),
|
||||
});
|
||||
|
||||
assert!(matches!(
|
||||
mapped,
|
||||
Some(AiAgentStreamEvent::Error { message }) if message == "missing auth"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_claude_result_event_preserves_final_text() {
|
||||
let mapped = map_claude_event(crate::claude_cli::ClaudeStreamEvent::Result {
|
||||
@@ -226,4 +310,14 @@ mod tests {
|
||||
Some(AiAgentStreamEvent::TextDelta { text }) if text == "Final answer from Claude"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_claude_empty_result_event_is_ignored() {
|
||||
let mapped = map_claude_event(crate::claude_cli::ClaudeStreamEvent::Result {
|
||||
text: String::new(),
|
||||
session_id: "session-1".into(),
|
||||
});
|
||||
|
||||
assert!(mapped.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ pub async fn download_and_install_app_update<R: Runtime>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ReleaseChannel;
|
||||
use super::{AppUpdateDownloadEvent, AppUpdateMetadata, ReleaseChannel};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn release_channel_defaults_to_stable() {
|
||||
@@ -176,4 +177,54 @@ mod tests {
|
||||
"https://refactoringhq.github.io/tolaria/stable/latest.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_metadata_serializes_for_frontend_consumers() {
|
||||
let metadata = AppUpdateMetadata {
|
||||
current_version: "2026.4.1".into(),
|
||||
version: "2026.4.2".into(),
|
||||
date: Some("2026-04-30T12:00:00Z".into()),
|
||||
body: Some("Bug fixes".into()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(metadata).unwrap(),
|
||||
json!({
|
||||
"currentVersion": "2026.4.1",
|
||||
"version": "2026.4.2",
|
||||
"date": "2026-04-30T12:00:00Z",
|
||||
"body": "Bug fixes"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_events_serialize_as_tagged_frontend_events() {
|
||||
let events = [
|
||||
(
|
||||
AppUpdateDownloadEvent::Started {
|
||||
content_length: Some(4096),
|
||||
},
|
||||
json!({
|
||||
"event": "Started",
|
||||
"data": { "contentLength": 4096 }
|
||||
}),
|
||||
),
|
||||
(
|
||||
AppUpdateDownloadEvent::Progress { chunk_length: 512 },
|
||||
json!({
|
||||
"event": "Progress",
|
||||
"data": { "chunkLength": 512 }
|
||||
}),
|
||||
),
|
||||
(
|
||||
AppUpdateDownloadEvent::Finished,
|
||||
json!({ "event": "Finished" }),
|
||||
),
|
||||
];
|
||||
|
||||
for (event, expected) in events {
|
||||
assert_eq!(serde_json::to_value(event).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,3 +90,67 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
|
||||
git::ensure_gitignore(&vault_path)?;
|
||||
Ok("Vault repaired".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn empty_vault_target_validation_allows_missing_or_empty_directories() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let missing = dir.path().join("new-vault");
|
||||
let empty = dir.path().join("empty-vault");
|
||||
fs::create_dir(&empty).unwrap();
|
||||
|
||||
assert_eq!(ensure_directory_is_missing_or_empty(&missing), Ok(()));
|
||||
assert_eq!(ensure_directory_is_missing_or_empty(&empty), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_vault_target_validation_rejects_files_and_nonempty_directories() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let file = dir.path().join("vault.md");
|
||||
let nonempty = dir.path().join("vault");
|
||||
fs::write(&file, "# Not a folder").unwrap();
|
||||
fs::create_dir(&nonempty).unwrap();
|
||||
fs::write(nonempty.join("note.md"), "# Existing note").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ensure_directory_is_missing_or_empty(&file),
|
||||
Err("Choose a folder path for the new vault".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
ensure_directory_is_missing_or_empty(&nonempty),
|
||||
Err("Choose an empty folder to create a new vault".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_vault_path_uses_existing_canonical_path_or_original_path() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let existing = dir.path().join("existing");
|
||||
let missing = dir.path().join("missing");
|
||||
fs::create_dir(&existing).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
canonical_vault_path_string(&existing),
|
||||
existing.canonicalize().unwrap().to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_vault_path_string(&missing),
|
||||
missing.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getting_started_target_uses_explicit_path_when_provided() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let explicit = dir.path().join("starter");
|
||||
|
||||
assert_eq!(
|
||||
resolve_getting_started_target(explicit.to_str()),
|
||||
Ok(explicit.to_string_lossy().to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,3 +35,50 @@ pub fn delete_view_cmd(vault_path: String, filename: String) -> Result<(), Strin
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::vault::{FilterCondition, FilterGroup, FilterNode, FilterOp};
|
||||
|
||||
fn definition(name: &str) -> ViewDefinition {
|
||||
ViewDefinition {
|
||||
name: name.to_string(),
|
||||
icon: Some("star".to_string()),
|
||||
color: None,
|
||||
order: None,
|
||||
sort: Some("modified:desc".to_string()),
|
||||
list_properties_display: vec!["Priority".to_string()],
|
||||
filters: FilterGroup::All(vec![FilterNode::Condition(FilterCondition {
|
||||
field: "type".to_string(),
|
||||
op: FilterOp::Equals,
|
||||
value: Some(serde_yaml::Value::String("Project".to_string())),
|
||||
regex: false,
|
||||
})]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_commands_roundtrip_through_validated_vault_paths() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let vault_path = dir.path().to_string_lossy().to_string();
|
||||
|
||||
assert!(list_views(vault_path.clone()).unwrap().is_empty());
|
||||
|
||||
save_view_cmd(
|
||||
vault_path.clone(),
|
||||
"active-projects.yml".to_string(),
|
||||
definition("Active Projects"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let views = list_views(vault_path.clone()).unwrap();
|
||||
assert_eq!(views.len(), 1);
|
||||
assert_eq!(views[0].filename, "active-projects.yml");
|
||||
assert_eq!(views[0].definition.name, "Active Projects");
|
||||
|
||||
delete_view_cmd(vault_path.clone(), "active-projects.yml".to_string()).unwrap();
|
||||
|
||||
assert!(list_views(vault_path).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{
|
||||
App, AppHandle, LogicalPosition, LogicalSize, Manager, Position, RunEvent, Size, WebviewWindow,
|
||||
@@ -263,7 +263,11 @@ fn window_state_path() -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
fn read_main_window_frame(scale_factor: f64) -> Option<WindowFrame> {
|
||||
let content = fs::read_to_string(window_state_path().ok()?).ok()?;
|
||||
read_main_window_frame_at(&window_state_path().ok()?, scale_factor)
|
||||
}
|
||||
|
||||
fn read_main_window_frame_at(path: &Path, scale_factor: f64) -> Option<WindowFrame> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let persisted: PersistedWindowState = serde_json::from_str(&content).ok()?;
|
||||
persisted
|
||||
.main
|
||||
@@ -276,7 +280,10 @@ fn read_main_window_frame(scale_factor: f64) -> Option<WindowFrame> {
|
||||
}
|
||||
|
||||
fn write_main_window_frame(frame: WindowFrame) -> Result<(), String> {
|
||||
let path = window_state_path()?;
|
||||
write_main_window_frame_at(&window_state_path()?, frame)
|
||||
}
|
||||
|
||||
fn write_main_window_frame_at(path: &Path, frame: WindowFrame) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create window state directory: {e}"))?;
|
||||
@@ -511,4 +518,50 @@ mod tests {
|
||||
&RunEvent::Resumed
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persists_and_reads_logical_frame_from_disk() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("nested/window-state.json");
|
||||
let saved = frame(80, 120, 1100, 700);
|
||||
|
||||
write_main_window_frame_at(&path, saved).unwrap();
|
||||
|
||||
let json = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(json.contains("\"coordinate_space\": \"logical\""));
|
||||
assert_eq!(read_main_window_frame_at(&path, 2.0), Some(saved));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_legacy_physical_frame_from_disk_as_logical_points() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("window-state.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
"main": { "x": 160, "y": 240, "width": 2200, "height": 1400 },
|
||||
"coordinate_space": "physical"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
read_main_window_frame_at(&path, 2.0),
|
||||
Some(frame(80, 120, 1100, 700))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_missing_corrupt_or_tiny_window_state_files() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("window-state.json");
|
||||
|
||||
assert_eq!(read_main_window_frame_at(&path, 1.0), None);
|
||||
|
||||
std::fs::write(&path, "not json").unwrap();
|
||||
assert_eq!(read_main_window_frame_at(&path, 1.0), None);
|
||||
|
||||
std::fs::write(&path, r#"{"main":{"x":0,"y":0,"width":100,"height":100}}"#).unwrap();
|
||||
assert_eq!(read_main_window_frame_at(&path, 1.0), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user