Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
"@milkdown/plugin-cursor": "^7.21.2",
"@milkdown/plugin-history": "^7.21.2",
"@milkdown/plugin-listener": "^7.21.2",
"@milkdown/plugin-math": "^7.21.2",
"@milkdown/plugin-table": "^7.21.2",
"@milkdown/plugin-math": "^7.5.9",
"@milkdown/plugin-table": "^5.3.1",
"@milkdown/plugin-tooltip": "^7.21.2",
"@milkdown/preset-commonmark": "^7.21.2",
"@milkdown/preset-gfm": "^7.21.2",
"@milkdown/prose": "^7.21.2",
"@milkdown/utils": "^7.21.2",
"@tauri-apps/api": "^2.11.4",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/src/commands/voice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ pub fn voice_is_recording() -> Result<bool, String> {
}

/// Stop the active recording, write the temporary WAV file, transcribe it
/// with whisper, and return the recognised text.
/// with whisper, and return the recognized text.
#[tauri::command]
pub fn voice_stop_recording() -> Result<VoiceRecognitionResult, String> {
let active = RECORDING
Expand Down
13 changes: 8 additions & 5 deletions src-tauri/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl GitEngine {
))?;

for entry in statuses.iter() {
if let Some(path) = entry.path() {
if let Ok(path) = entry.path() {
let _ = index.add_path(Path::new(path));
}
}
Expand Down Expand Up @@ -198,7 +198,7 @@ impl GitEngine {

let mut unstaged = Vec::new();
for entry in statuses.iter() {
if let Some(path) = entry.path() {
if let Ok(path) = entry.path() {
unstaged.push(path.to_string());
}
}
Expand All @@ -207,15 +207,18 @@ impl GitEngine {
let behind = 0;

if let Ok(head) = repo.head() {
let head_oid = head.peel_to_commit().map(|c| c.id()).unwrap_or(Oid::zero());
if let Some(branch_name) = head.shorthand() {
let head_oid = head
.peel_to_commit()
.map(|c| c.id())
.unwrap_or(Oid::ZERO_SHA1);
if let Ok(branch_name) = head.shorthand() {
if let Ok(branch) = repo.find_branch(branch_name, git2::BranchType::Local) {
if let Ok(upstream) = branch.upstream() {
let upstream_oid = upstream
.get()
.peel_to_commit()
.map(|c| c.id())
.unwrap_or(Oid::zero());
.unwrap_or(Oid::ZERO_SHA1);

let mut revwalk = repo.revwalk()?;
revwalk.push(upstream_oid)?;
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl MCPClient {
.collect()
})
.unwrap_or_default();
Box::new(transport::StdioTransport::new(&command, args))
Box::new(transport::StdioTransport::new(&command, args)?)
}
TransportType::Http => {
let url = config["url"]
Expand Down
40 changes: 28 additions & 12 deletions src-tauri/src/mcp/transport.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
Expand Down Expand Up @@ -31,30 +31,26 @@ pub struct StdioTransport {
}

impl StdioTransport {
pub fn new(command: &str, args: Vec<String>) -> Self {
pub fn new(command: &str, args: Vec<String>) -> Result<Self> {
let mut child = Command::new(command)
.args(&args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()
.unwrap_or_else(|e| {
// Log the error rather than panicking — callers handle connection failure
tracing::error!("Failed to spawn MCP subprocess '{}': {}", command, e);
panic!("MCP subprocess spawn failed: {}", e)
});

let stdin = child.stdin.take().expect("Failed to capture stdin");
let stdout = child.stdout.take().expect("Failed to capture stdout");
.with_context(|| format!("Failed to spawn MCP subprocess '{}'", command))?;

let stdin = child.stdin.take().context("Failed to capture stdin")?;
let stdout = child.stdout.take().context("Failed to capture stdout")?;
let reader = BufReader::new(stdout).lines();

Self {
Ok(Self {
process: Mutex::new(child),
stdin: Mutex::new(stdin),
stdout: Mutex::new(reader),
request_id: Mutex::new(0),
}
})
}

async fn next_id(&self) -> u64 {
Expand Down Expand Up @@ -265,3 +261,23 @@ impl McpTransport for HttpTransport {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::StdioTransport;

#[test]
fn stdio_transport_spawn_failure_returns_error() {
let command = format!(
"zarishnote-definitely-missing-mcp-server-{}",
std::process::id()
);

let result = StdioTransport::new(&command, Vec::new());

assert!(
result.is_err(),
"invalid MCP command should return an error"
);
}
}
4 changes: 2 additions & 2 deletions src-tauri/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ RUST_LOG=debug cargo test --test integration
| `test_git_engine` | Commit‑message generation (`generate_commit_message`), change‑type detection |
| `test_mcp_protocol` | JSON‑RPC parsing (request, response, error, notification), helper builders, `McpToolRouter` routing and confirmation logic |
| `test_sandbox_executor` | `CapabilityChecker` (permissions, network, path), `glob_match`, `SandboxNetworkProxy` allow/block logic |
| `test_vector_store` | `VectorStore` index/query/delete/rebuild, chunking, deduplication, empty‑store behaviour |
| `test_vector_store` | `VectorStore` index/query/delete/rebuild, chunking, deduplication, empty‑store behavior |

## Design Principles

Expand All @@ -55,6 +55,6 @@ RUST_LOG=debug cargo test --test integration
2. **No hard‑coded paths** — all filesystem operations use `tempfile::TempDir`.
3. **Public API only** — integration tests exercise the public interface of
`zs_note_lib`; private implementation details are left to unit tests.
4. **Focused** — each test covers exactly one behaviour.
4. **Focused** — each test covers exactly one behavior.
5. **Deterministic** — tests are self‑contained and do not rely on global
state or environment variables.
2 changes: 1 addition & 1 deletion src-tauri/tests/integration/test_config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Integration tests for configuration management.
//!
//! Covers default construction, YAML round‑trip serialisation and
//! Covers default construction, YAML round‑trip serialization and
//! the validation logic on [`Config`](zs_note_lib::config::Config).

use std::path::Path;
Expand Down
20 changes: 13 additions & 7 deletions src-tauri/tests/integration/test_vector_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Tests the public API of [`zs_note_lib::vector`]:
//! indexing, querying, deletion, rebuild, deduplication and
//! empty‑store behaviour.
//! empty‑store behavior.

use std::fs;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -61,17 +61,23 @@ fn test_vector_store_index_and_query() {
fn test_vector_store_query_multiple_docs() {
let (_dir, store) = setup_temp_store();

let alpha = PathBuf::from("alpha.md");
let beta = PathBuf::from("beta.md");

store.index_document(&alpha, "The quick brown fox").unwrap();
store
.index_document(&PathBuf::from("alpha.md"), "The quick brown fox")
.unwrap();
store
.index_document(&PathBuf::from("beta.md"), "jumps over the lazy dog")
.index_document(&beta, "jumps over the lazy dog")
.unwrap();

// Both documents share no common words; "fox" only appears in alpha
// "fox" only appears in alpha, so the query should return alpha exactly.
let results = store.query("fox", "default", 10);
assert_eq!(results.len(), 1, "only alpha should match 'fox'");
assert!(results[0].path.to_string_lossy().contains("alpha"));
assert_eq!(results[0].path, alpha);

// "dog" only appears in beta, so the query should return beta exactly.
let results = store.query("dog", "default", 10);
assert_eq!(results.len(), 1, "only beta should match 'dog'");
assert_eq!(results[0].path, beta);
}

// ---------------------------------------------------------------------------
Expand Down