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: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Fuzzy-matching for command palette — character-level gap-penalty scoring replaces substring filter; `daemon_command_registry()` merged into `filter_commands`
- `TuiCommand::ToggleTheme` variant in command palette (placeholder — theme switching not yet implemented)
- `--init` wizard daemon step — prompts for A2A server host, port, and auth token; writes `config.a2a.*`
- Snapshot tests for `Config::default()` TOML serialization (zeph-core), git filter diff/status output, cargo-build filter success/error output, and clippy grouped warnings output — using insta for regression detection
- Tests for `handle_tool_result` covering blocked, cancelled, sandbox violation, empty output, exit-code failure, and success paths (zeph-core agent/tool_execution.rs)
- Tests for `maybe_redact` (redaction enabled/disabled) and `last_user_query` helper in agent/tool_execution.rs
- Tests for `handle_skill_command` dispatch covering unknown subcommand, missing arguments, and no-memory early-exit paths for stats, versions, activate, approve, and reset subcommands (zeph-core agent/learning.rs)
- Tests for `record_skill_outcomes` noop path when no active skills are present
- `insta` added to workspace dev-dependencies and to zeph-core and zeph-tools crate dev-deps
- `Embeddable` trait and `EmbeddingRegistry<T>` in zeph-memory — generic Qdrant sync/search extracted from duplicated code in QdrantSkillMatcher and McpToolRegistry (~350 lines removed)
- MCP server command allowlist validation — only permitted commands (npx, uvx, node, python3, python, docker, deno, bun) can spawn child processes; configurable via `mcp.allowed_commands`
- MCP env var blocklist — blocks 21 dangerous variables (LD_PRELOAD, DYLD_*, NODE_OPTIONS, PYTHONPATH, JAVA_TOOL_OPTIONS, etc.) and BASH_FUNC_* prefix from MCP server processes
Expand Down
44 changes: 35 additions & 9 deletions Cargo.lock

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

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ glob = "0.3.3"
hf-hub = { version = "0.4", default-features = false, features = ["tokio", "rustls-tls", "ureq"] }
http-body-util = "0.1"
ignore = "0.4"
insta = { version = "1.46.3", features = ["toml", "filters"] }
notify = "8"
notify-debouncer-mini = "0.7"
nucleo-matcher = "0.3.1"
Expand All @@ -40,7 +41,7 @@ opentelemetry = "0.31"
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic"] }
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio"] }
pdf-extract = "0.7"
proptest = "1.6"
proptest = "1.10"
pulldown-cmark = "0.13"
qdrant-client = { version = "1.16", default-features = false }
ratatui = "0.30"
Expand Down Expand Up @@ -139,7 +140,6 @@ dialoguer.workspace = true
futures.workspace = true
toml.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
tokio-util.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
opentelemetry = { workspace = true, optional = true }
Expand All @@ -160,7 +160,6 @@ zeph-scheduler = { workspace = true, optional = true }
zeph-tui = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true, features = ["rustls"] }
serde_json = { workspace = true, optional = true }
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] }

[dev-dependencies]
serial_test.workspace = true
Expand Down
1 change: 0 additions & 1 deletion crates/zeph-a2a/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ subtle = { workspace = true, optional = true }
serde_json.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["net", "sync"] }
tokio-util.workspace = true
url.workspace = true
tokio-stream.workspace = true
tower = { workspace = true, optional = true }
Expand Down
60 changes: 60 additions & 0 deletions crates/zeph-channels/src/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,63 @@ impl Channel for AnyChannel {
)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::cli::CliChannel;
use zeph_core::channel::Channel;

#[tokio::test]
async fn any_channel_cli_send_returns_ok() {
let mut ch = AnyChannel::Cli(CliChannel::new());
assert!(ch.send("hello").await.is_ok());
}

#[tokio::test]
async fn any_channel_cli_send_chunk_returns_ok() {
let mut ch = AnyChannel::Cli(CliChannel::new());
assert!(ch.send_chunk("chunk").await.is_ok());
}

#[tokio::test]
async fn any_channel_cli_flush_chunks_returns_ok() {
let mut ch = AnyChannel::Cli(CliChannel::new());
ch.send_chunk("data").await.unwrap();
assert!(ch.flush_chunks().await.is_ok());
}

#[tokio::test]
async fn any_channel_cli_send_typing_returns_ok() {
let mut ch = AnyChannel::Cli(CliChannel::new());
assert!(ch.send_typing().await.is_ok());
}

#[tokio::test]
async fn any_channel_cli_send_status_returns_ok() {
let mut ch = AnyChannel::Cli(CliChannel::new());
assert!(ch.send_status("thinking...").await.is_ok());
}

// crossterm on Windows uses ReadConsoleInputW which blocks indefinitely
// without a real console handle (headless CI), while Unix poll() gets EOF
#[cfg(not(target_os = "windows"))]
#[tokio::test]
async fn any_channel_cli_confirm_returns_bool() {
let mut ch = AnyChannel::Cli(CliChannel::new());
let _ = ch.confirm("confirm?").await;
}

#[test]
fn any_channel_cli_try_recv_returns_none() {
let mut ch = AnyChannel::Cli(CliChannel::new());
assert!(ch.try_recv().is_none());
}

#[test]
fn any_channel_debug() {
let ch = AnyChannel::Cli(CliChannel::new());
let debug = format!("{ch:?}");
assert!(debug.contains("Cli"));
}
}
121 changes: 121 additions & 0 deletions crates/zeph-channels/src/line_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,124 @@ fn unicode_display_width(s: &str) -> usize {
use unicode_width::UnicodeWidthStr;
UnicodeWidthStr::width(s)
}

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

#[test]
fn char_count_ascii() {
assert_eq!(char_count("hello"), 5);
assert_eq!(char_count(""), 0);
}

#[test]
fn char_count_unicode() {
assert_eq!(char_count("héllo"), 5);
assert_eq!(char_count("日本語"), 3);
}

#[test]
fn byte_offset_start() {
assert_eq!(byte_offset("hello", 0), 0);
}

#[test]
fn byte_offset_end() {
assert_eq!(byte_offset("hello", 5), 5);
}

#[test]
fn byte_offset_beyond() {
assert_eq!(byte_offset("hello", 100), 5);
}

#[test]
fn byte_offset_unicode() {
// "é" is 2 bytes, so char index 1 = byte offset 2
let s = "éllo";
assert_eq!(byte_offset(s, 1), 2);
}

#[test]
fn prev_word_boundary_from_end() {
// "hello world" cursor at 11 (end), boundary should be at start of "world"=6
assert_eq!(prev_word_boundary("hello world", 11), 6);
}

#[test]
fn prev_word_boundary_at_start() {
assert_eq!(prev_word_boundary("hello", 0), 0);
}

#[test]
fn prev_word_boundary_skips_spaces() {
// "hello world" cursor after spaces at 8, boundary = after "hello" at 5? no, past spaces
// spaces are non-alphanumeric, then alphanumeric of "hello"
assert_eq!(prev_word_boundary("hello world", 8), 0);
}

#[test]
fn navigate_history_up_empty_history_no_op() {
let history: Vec<String> = vec![];
let mut input = String::from("test");
let mut cursor = 4;
let mut idx = None;
let mut draft = String::new();
navigate_history_up(&history, &mut input, &mut cursor, &mut idx, &mut draft);
assert_eq!(input, "test");
assert!(idx.is_none());
}

#[test]
fn navigate_history_up_selects_last_entry() {
let history = vec!["cmd1".to_string(), "cmd2".to_string()];
let mut input = String::new();
let mut cursor = 0;
let mut idx = None;
let mut draft = String::new();
navigate_history_up(&history, &mut input, &mut cursor, &mut idx, &mut draft);
assert_eq!(input, "cmd2");
assert_eq!(idx, Some(1));
assert_eq!(cursor, 4);
}

#[test]
fn navigate_history_up_twice_goes_further_back() {
let history = vec!["cmd1".to_string(), "cmd2".to_string()];
let mut input = String::new();
let mut cursor = 0;
let mut idx = None;
let mut draft = String::new();
navigate_history_up(&history, &mut input, &mut cursor, &mut idx, &mut draft);
navigate_history_up(&history, &mut input, &mut cursor, &mut idx, &mut draft);
assert_eq!(input, "cmd1");
assert_eq!(idx, Some(0));
}

#[test]
fn navigate_history_down_restores_draft() {
let history = vec!["cmd1".to_string()];
// Simulate having gone up: idx is Some(0), input is the history entry
let mut input = String::from("cmd1");
let mut cursor = 4;
let mut idx = Some(0);
// Draft preserves what the user typed before navigating up
let mut draft = String::from("draft");
// Now go back down — should restore draft
navigate_history_down(&history, &mut input, &mut cursor, &mut idx, &mut draft);
assert_eq!(input, "draft");
assert!(idx.is_none());
}

#[test]
fn navigate_history_down_no_op_when_no_index() {
let history = vec!["cmd1".to_string()];
let mut input = String::from("unchanged");
let mut cursor = 9;
let mut idx = None;
let mut draft = String::new();
navigate_history_down(&history, &mut input, &mut cursor, &mut idx, &mut draft);
assert_eq!(input, "unchanged");
}
}
1 change: 1 addition & 0 deletions crates/zeph-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ harness = false

[dev-dependencies]
criterion.workspace = true
insta.workspace = true
proptest.workspace = true
schemars.workspace = true
serial_test.workspace = true
Expand Down
Loading
Loading