Fixed some bot tests.

This commit is contained in:
Dave
2026-03-18 16:37:23 +00:00
parent d080e8b12d
commit 52ec989c3a
3 changed files with 106 additions and 87 deletions

View File

@@ -1,21 +0,0 @@
---
name: "Show server logs in web UI"
---
# Story 292: Show server logs in web UI
## User Story
As a project owner using the web UI, I want to see live server logs in the interface, so that I can debug agent behavior and pipeline issues without needing terminal access.
## Acceptance Criteria
- [ ] Web UI has a server logs panel accessible from the main interface
- [ ] Logs stream in real-time via WebSocket or SSE
- [ ] Logs can be filtered by keyword (same as get_server_logs MCP tool's filter param)
- [ ] Log entries show timestamp and severity level
- [ ] Panel doesn't interfere with the existing pipeline board and work item views
## Out of Scope
- TBD

34
Cargo.lock generated
View File

@@ -4026,7 +4026,7 @@ dependencies = [
"tempfile",
"tokio",
"tokio-tungstenite 0.29.0",
"toml 1.0.6+spec-1.1.0",
"toml 1.0.7+spec-1.1.0",
"uuid",
"wait-timeout",
"walkdir",
@@ -4367,22 +4367,22 @@ dependencies = [
"serde_spanned",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.14",
]
[[package]]
name = "toml"
version = "1.0.6+spec-1.1.0"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime 1.0.0+spec-1.1.0",
"toml_datetime 1.0.1+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow",
"winnow 1.0.0",
]
[[package]]
@@ -4396,9 +4396,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
version = "1.0.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
dependencies = [
"serde_core",
]
@@ -4412,23 +4412,23 @@ dependencies = [
"indexmap",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"winnow",
"winnow 0.7.14",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.0.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [
"winnow",
"winnow 1.0.0",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
version = "1.0.7+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
[[package]]
name = "tower"
@@ -5444,6 +5444,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
[[package]]
name = "winreg"
version = "0.10.1"

View File

@@ -103,11 +103,7 @@ pub fn load_history(project_root: &std::path::Path) -> HashMap<OwnedRoomId, Room
persisted
.rooms
.into_iter()
.filter_map(|(k, v)| {
k.parse::<OwnedRoomId>()
.ok()
.map(|room_id| (room_id, v))
})
.filter_map(|(k, v)| k.parse::<OwnedRoomId>().ok().map(|room_id| (room_id, v)))
.collect()
}
@@ -237,10 +233,7 @@ fn read_stage_items(
project_root: &std::path::Path,
stage_dir: &str,
) -> Vec<(String, Option<String>)> {
let dir = project_root
.join(".story_kit")
.join("work")
.join(stage_dir);
let dir = project_root.join(".story_kit").join("work").join(stage_dir);
if !dir.exists() {
return Vec::new();
}
@@ -252,9 +245,7 @@ fn read_stage_items(
continue;
}
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let name = std::fs::read_to_string(&path)
.ok()
.and_then(|contents| {
let name = std::fs::read_to_string(&path).ok().and_then(|contents| {
crate::io::story_metadata::parse_front_matter(&contents)
.ok()
.and_then(|m| m.name)
@@ -408,7 +399,10 @@ pub async fn run_bot(
.ok_or_else(|| "No user ID after login".to_string())?
.to_owned();
slog!("[matrix-bot] Logged in as {bot_user_id} (device: {})", login_response.device_id);
slog!(
"[matrix-bot] Logged in as {bot_user_id} (device: {})",
login_response.device_id
);
// Bootstrap cross-signing keys for E2EE verification support.
// Pass the bot's password for UIA (User-Interactive Authentication) —
@@ -540,7 +534,9 @@ pub async fn run_bot(
agents,
};
slog!("[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected");
slog!(
"[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected"
);
// Register event handlers and inject shared context.
client.add_event_handler_context(ctx);
@@ -679,9 +675,7 @@ pub fn parse_ambient_command(
// Strip a leading @mention (handles "@localpart" and "@localpart:server").
let rest = if let Some(after_at) = lower.strip_prefix('@') {
// Skip everything up to the first whitespace (the full mention token).
let word_end = after_at
.find(char::is_whitespace)
.unwrap_or(after_at.len());
let word_end = after_at.find(char::is_whitespace).unwrap_or(after_at.len());
after_at[word_end..].trim()
} else if let Some(after) = lower.strip_prefix(display_lower.as_str()) {
after.trim()
@@ -734,10 +728,7 @@ async fn is_reply_to_bot(
/// is the correct trust model: a user is accepted when they have cross-signing
/// configured, regardless of whether the bot has run an explicit verification
/// ceremony with a specific device.
async fn check_sender_verified(
client: &Client,
sender: &OwnedUserId,
) -> Result<bool, String> {
async fn check_sender_verified(client: &Client, sender: &OwnedUserId) -> Result<bool, String> {
let identity = client
.encryption()
.get_user_identity(sender)
@@ -803,8 +794,9 @@ async fn on_to_device_verification_request(
}
break;
}
VerificationRequestState::Done
| VerificationRequestState::Cancelled(_) => break,
VerificationRequestState::Done | VerificationRequestState::Cancelled(_) => {
break;
}
_ => {}
}
}
@@ -1022,11 +1014,9 @@ async fn on_room_message(
slog!("[matrix-bot] Message from {sender}: {user_message}");
// Check for bot-level commands (e.g. "help") before invoking the LLM.
if let Some(response) = super::commands::try_handle_command(
&ctx.bot_name,
ctx.bot_user_id.as_str(),
&user_message,
) {
if let Some(response) =
super::commands::try_handle_command(&ctx.bot_name, ctx.bot_user_id.as_str(), &user_message)
{
slog!("[matrix-bot] Handled bot command from {sender}");
let html = markdown_to_html(&response);
if let Ok(resp) = room
@@ -1083,9 +1073,7 @@ async fn handle_message(
// flattening history into a text prefix.
let resume_session_id: Option<String> = {
let guard = ctx.history.lock().await;
guard
.get(&room_id)
.and_then(|conv| conv.session_id.clone())
guard.get(&room_id).and_then(|conv| conv.session_id.clone())
};
// The prompt is just the current message with sender attribution.
@@ -1260,7 +1248,11 @@ async fn handle_message(
let conv = guard.entry(room_id).or_default();
// Store the session ID so the next turn uses --resume.
slog!("[matrix-bot] storing session_id: {:?} (was: {:?})", new_session_id, conv.session_id);
slog!(
"[matrix-bot] storing session_id: {:?} (was: {:?})",
new_session_id,
conv.session_id
);
if new_session_id.is_some() {
conv.session_id = new_session_id;
}
@@ -2017,7 +2009,10 @@ mod tests {
#[test]
fn startup_announcement_uses_configured_display_name_not_hardcoded() {
assert_eq!(format_startup_announcement("HAL"), "HAL is online.");
assert_eq!(format_startup_announcement("Assistant"), "Assistant is online.");
assert_eq!(
format_startup_announcement("Assistant"),
"Assistant is online."
);
}
// -- extract_command (status trigger) ------------------------------------
@@ -2072,7 +2067,7 @@ mod tests {
let pool = AgentPool::new_test(3001);
let out = build_pipeline_status(dir.path(), &pool);
assert!(out.contains("Upcoming"), "missing Upcoming: {out}");
assert!(out.contains("Backlog"), "missing Backlog: {out}");
assert!(out.contains("In Progress"), "missing In Progress: {out}");
assert!(out.contains("QA"), "missing QA: {out}");
assert!(out.contains("Merge"), "missing Merge: {out}");
@@ -2104,7 +2099,10 @@ mod tests {
let pool = AgentPool::new_test(3001);
let out = build_pipeline_status(dir.path(), &pool);
assert!(out.contains("Free Agents"), "missing Free Agents section: {out}");
assert!(
out.contains("Free Agents"),
"missing Free Agents section: {out}"
);
}
#[test]
@@ -2114,8 +2112,11 @@ mod tests {
let out = build_pipeline_status(dir.path(), &pool);
// Stages and headers should use markdown bold (**text**).
assert!(out.contains("**Pipeline Status**"), "missing bold title: {out}");
assert!(out.contains("**Upcoming**"), "stage should use bold: {out}");
assert!(
out.contains("**Pipeline Status**"),
"missing bold title: {out}"
);
assert!(out.contains("**Backlog**"), "stage should use bold: {out}");
}
#[test]
@@ -2157,13 +2158,19 @@ mod tests {
#[test]
fn ambient_command_on_with_at_mention() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("@timmy ambient on", &uid, "Timmy"), Some(true));
assert_eq!(
parse_ambient_command("@timmy ambient on", &uid, "Timmy"),
Some(true)
);
}
#[test]
fn ambient_command_off_with_at_mention() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("@timmy ambient off", &uid, "Timmy"), Some(false));
assert_eq!(
parse_ambient_command("@timmy ambient off", &uid, "Timmy"),
Some(false)
);
}
#[test]
@@ -2178,39 +2185,60 @@ mod tests {
#[test]
fn ambient_command_on_with_display_name() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("timmy ambient on", &uid, "Timmy"), Some(true));
assert_eq!(
parse_ambient_command("timmy ambient on", &uid, "Timmy"),
Some(true)
);
}
#[test]
fn ambient_command_off_with_display_name() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("timmy ambient off", &uid, "Timmy"), Some(false));
assert_eq!(
parse_ambient_command("timmy ambient off", &uid, "Timmy"),
Some(false)
);
}
#[test]
fn ambient_command_on_bare() {
// "ambient on" without any bot mention is also recognised.
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("ambient on", &uid, "Timmy"), Some(true));
assert_eq!(
parse_ambient_command("ambient on", &uid, "Timmy"),
Some(true)
);
}
#[test]
fn ambient_command_off_bare() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("ambient off", &uid, "Timmy"), Some(false));
assert_eq!(
parse_ambient_command("ambient off", &uid, "Timmy"),
Some(false)
);
}
#[test]
fn ambient_command_case_insensitive() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("@Timmy AMBIENT ON", &uid, "Timmy"), Some(true));
assert_eq!(parse_ambient_command("TIMMY AMBIENT OFF", &uid, "Timmy"), Some(false));
assert_eq!(
parse_ambient_command("@Timmy AMBIENT ON", &uid, "Timmy"),
Some(true)
);
assert_eq!(
parse_ambient_command("TIMMY AMBIENT OFF", &uid, "Timmy"),
Some(false)
);
}
#[test]
fn ambient_command_unrelated_message_returns_none() {
let uid = make_user_id("@timmy:homeserver.local");
assert_eq!(parse_ambient_command("@timmy what is the status?", &uid, "Timmy"), None);
assert_eq!(
parse_ambient_command("@timmy what is the status?", &uid, "Timmy"),
None
);
assert_eq!(parse_ambient_command("hello there", &uid, "Timmy"), None);
assert_eq!(parse_ambient_command("ambient", &uid, "Timmy"), None);
}
@@ -2237,11 +2265,17 @@ mod tests {
let guard = ambient_rooms.lock().await;
assert!(guard.contains(&room_a), "room_a should be in ambient mode");
assert!(!guard.contains(&room_b), "room_b should NOT be in ambient mode");
assert!(
!guard.contains(&room_b),
"room_b should NOT be in ambient mode"
);
drop(guard);
// Disable ambient mode for room_a.
ambient_rooms.lock().await.remove(&room_a);
assert!(!ambient_rooms.lock().await.contains(&room_a), "room_a ambient mode should be off");
assert!(
!ambient_rooms.lock().await.contains(&room_a),
"room_a ambient mode should be off"
);
}
}