Fixed some bot tests.
This commit is contained in:
@@ -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
34
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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,13 +245,11 @@ 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| {
|
||||
crate::io::story_metadata::parse_front_matter(&contents)
|
||||
.ok()
|
||||
.and_then(|m| m.name)
|
||||
});
|
||||
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)
|
||||
});
|
||||
items.push((stem.to_string(), 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user