feat(tools): set up proper CI (#2)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 57s
CI / Lint & Test (push) Successful in 14m1s
CI / Build Linux (push) Successful in 16m8s
CI / Build Windows (cross-compile) (push) Successful in 26m18s

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #2
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #2.
This commit is contained in:
2026-01-15 20:06:47 -08:00
committed by Naomi Carrigan
parent bd04328e40
commit c241544743
80 changed files with 2689 additions and 266 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+132 -14
View File
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum CharacterState {
#[default]
Idle,
Thinking,
Typing,
@@ -14,27 +15,17 @@ pub enum CharacterState {
Error,
}
impl Default for CharacterState {
fn default() -> Self {
CharacterState::Idle
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ConnectionStatus {
#[default]
Disconnected,
Connecting,
Connected,
Error,
}
impl Default for ConnectionStatus {
fn default() -> Self {
ConnectionStatus::Disconnected
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalLine {
pub id: String,
@@ -46,6 +37,7 @@ pub struct TerminalLine {
pub tool_name: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequest {
pub id: String,
@@ -186,3 +178,129 @@ pub struct PermissionPromptEvent {
pub tool_input: serde_json::Value,
pub description: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_character_state_default() {
let state = CharacterState::default();
assert_eq!(state, CharacterState::Idle);
}
#[test]
fn test_connection_status_default() {
let status = ConnectionStatus::default();
matches!(status, ConnectionStatus::Disconnected);
}
#[test]
fn test_character_state_serialization() {
let state = CharacterState::Thinking;
let serialized = serde_json::to_string(&state).unwrap();
assert_eq!(serialized, "\"thinking\"");
let deserialized: CharacterState = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, CharacterState::Thinking);
}
#[test]
fn test_all_character_states_serialize() {
let states = vec![
(CharacterState::Idle, "\"idle\""),
(CharacterState::Thinking, "\"thinking\""),
(CharacterState::Typing, "\"typing\""),
(CharacterState::Searching, "\"searching\""),
(CharacterState::Coding, "\"coding\""),
(CharacterState::Mcp, "\"mcp\""),
(CharacterState::Permission, "\"permission\""),
(CharacterState::Success, "\"success\""),
(CharacterState::Error, "\"error\""),
];
for (state, expected) in states {
let serialized = serde_json::to_string(&state).unwrap();
assert_eq!(serialized, expected, "Failed for state: {:?}", state);
}
}
#[test]
fn test_terminal_line_serialization() {
let line = TerminalLine {
id: "test-123".to_string(),
line_type: "assistant".to_string(),
content: "Hello, world!".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
tool_name: None,
};
let serialized = serde_json::to_string(&line).unwrap();
assert!(serialized.contains("\"type\":\"assistant\""));
assert!(serialized.contains("\"content\":\"Hello, world!\""));
assert!(!serialized.contains("tool_name"));
}
#[test]
fn test_terminal_line_with_tool_name() {
let line = TerminalLine {
id: "test-456".to_string(),
line_type: "tool".to_string(),
content: "Reading file...".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
tool_name: Some("Read".to_string()),
};
let serialized = serde_json::to_string(&line).unwrap();
assert!(serialized.contains("\"tool_name\":\"Read\""));
}
#[test]
fn test_content_block_text() {
let block = ContentBlock::Text {
text: "Hello!".to_string(),
};
let serialized = serde_json::to_string(&block).unwrap();
assert!(serialized.contains("\"type\":\"text\""));
assert!(serialized.contains("\"text\":\"Hello!\""));
}
#[test]
fn test_content_block_tool_use() {
let block = ContentBlock::ToolUse {
id: "tool-123".to_string(),
name: "Read".to_string(),
input: serde_json::json!({"file_path": "/test.txt"}),
};
let serialized = serde_json::to_string(&block).unwrap();
assert!(serialized.contains("\"type\":\"tool_use\""));
assert!(serialized.contains("\"name\":\"Read\""));
}
#[test]
fn test_state_change_event() {
let event = StateChangeEvent {
state: CharacterState::Coding,
tool_name: Some("Edit".to_string()),
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"state\":\"coding\""));
assert!(serialized.contains("\"tool_name\":\"Edit\""));
}
#[test]
fn test_output_event() {
let event = OutputEvent {
line_type: "assistant".to_string(),
content: "Test output".to_string(),
tool_name: None,
};
let serialized = serde_json::to_string(&event).unwrap();
assert!(serialized.contains("\"line_type\":\"assistant\""));
assert!(serialized.contains("\"content\":\"Test output\""));
}
}
+124
View File
@@ -472,3 +472,127 @@ pub type SharedBridge = Arc<Mutex<WslBridge>>;
pub fn create_shared_bridge() -> SharedBridge {
Arc::new(Mutex::new(WslBridge::new()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_tool_state_search_tools() {
assert!(matches!(get_tool_state("Read"), CharacterState::Searching));
assert!(matches!(get_tool_state("Glob"), CharacterState::Searching));
assert!(matches!(get_tool_state("Grep"), CharacterState::Searching));
assert!(matches!(get_tool_state("WebSearch"), CharacterState::Searching));
assert!(matches!(get_tool_state("WebFetch"), CharacterState::Searching));
}
#[test]
fn test_get_tool_state_coding_tools() {
assert!(matches!(get_tool_state("Edit"), CharacterState::Coding));
assert!(matches!(get_tool_state("Write"), CharacterState::Coding));
assert!(matches!(get_tool_state("NotebookEdit"), CharacterState::Coding));
}
#[test]
fn test_get_tool_state_mcp_tools() {
assert!(matches!(get_tool_state("mcp__github__create_issue"), CharacterState::Mcp));
assert!(matches!(get_tool_state("mcp__notion__search"), CharacterState::Mcp));
}
#[test]
fn test_get_tool_state_task() {
assert!(matches!(get_tool_state("Task"), CharacterState::Thinking));
}
#[test]
fn test_get_tool_state_unknown() {
assert!(matches!(get_tool_state("SomeUnknownTool"), CharacterState::Typing));
assert!(matches!(get_tool_state("Bash"), CharacterState::Typing));
}
#[test]
fn test_format_tool_description_read() {
let input = serde_json::json!({"file_path": "/home/test/file.txt"});
let desc = format_tool_description("Read", &input);
assert_eq!(desc, "Reading file: /home/test/file.txt");
}
#[test]
fn test_format_tool_description_read_no_path() {
let input = serde_json::json!({});
let desc = format_tool_description("Read", &input);
assert_eq!(desc, "Reading file...");
}
#[test]
fn test_format_tool_description_glob() {
let input = serde_json::json!({"pattern": "**/*.rs"});
let desc = format_tool_description("Glob", &input);
assert_eq!(desc, "Searching for files: **/*.rs");
}
#[test]
fn test_format_tool_description_grep() {
let input = serde_json::json!({"pattern": "TODO"});
let desc = format_tool_description("Grep", &input);
assert_eq!(desc, "Searching for: TODO");
}
#[test]
fn test_format_tool_description_edit() {
let input = serde_json::json!({"file_path": "/home/test/main.rs"});
let desc = format_tool_description("Edit", &input);
assert_eq!(desc, "Editing: /home/test/main.rs");
}
#[test]
fn test_format_tool_description_write() {
let input = serde_json::json!({"file_path": "/home/test/new.txt"});
let desc = format_tool_description("Write", &input);
assert_eq!(desc, "Editing: /home/test/new.txt");
}
#[test]
fn test_format_tool_description_bash_short() {
let input = serde_json::json!({"command": "ls -la"});
let desc = format_tool_description("Bash", &input);
assert_eq!(desc, "Running: ls -la");
}
#[test]
fn test_format_tool_description_bash_long() {
let long_cmd = "a".repeat(100);
let input = serde_json::json!({"command": long_cmd});
let desc = format_tool_description("Bash", &input);
assert!(desc.starts_with("Running: "));
assert!(desc.ends_with("..."));
assert!(desc.len() < 70);
}
#[test]
fn test_format_tool_description_unknown() {
let input = serde_json::json!({"some": "data"});
let desc = format_tool_description("CustomTool", &input);
assert_eq!(desc, "Using tool: CustomTool");
}
#[test]
fn test_wsl_bridge_new() {
let bridge = WslBridge::new();
assert!(!bridge.is_running());
assert_eq!(bridge.get_working_directory(), "");
}
#[test]
fn test_wsl_bridge_default() {
let bridge = WslBridge::default();
assert!(!bridge.is_running());
}
#[test]
fn test_create_shared_bridge() {
let shared = create_shared_bridge();
let bridge = shared.lock();
assert!(!bridge.is_running());
}
}