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
44 changes: 28 additions & 16 deletions src-rust/crates/core/src/coven_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ pub struct RegisterExternalSession {
pub harness: String,
pub title: String,
pub transcript_path: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1140,26 +1142,36 @@ mod tests {
harness: "coven-code".to_string(),
title: "My session".to_string(),
transcript_path: Some("/home/user/.coven-code/sessions/sess-1.jsonl".to_string()),
labels: vec!["source:psyche-build".to_string()],
};
let json = serde_json::to_string(&req).unwrap();
assert!(
json.contains("\"projectRoot\""),
"expected camelCase projectRoot, got: {json}"
);
assert!(
json.contains("\"transcriptPath\""),
"expected camelCase transcriptPath, got: {json}"
);
assert!(
!json.contains("\"project_root\""),
"snake_case leaked: {json}"
);
assert!(
!json.contains("\"transcript_path\""),
"snake_case leaked: {json}"
let json = serde_json::to_value(&req).unwrap();
assert_eq!(
json,
serde_json::json!({
"id": "sess-1",
"projectRoot": "/home/user/repo",
"harness": "coven-code",
"title": "My session",
"transcriptPath": "/home/user/.coven-code/sessions/sess-1.jsonl",
"labels": ["source:psyche-build"],
})
);
}

#[test]
fn register_external_session_omits_empty_labels() {
let req = RegisterExternalSession {
id: "sess-1".to_string(),
project_root: "/home/user/repo".to_string(),
harness: "coven-code".to_string(),
title: "My session".to_string(),
transcript_path: None,
labels: Vec::new(),
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json.get("labels"), None);
}

#[test]
fn complete_session_body_serializes_exit_code() {
// Verify the exitCode key (camelCase) is what the daemon expects.
Expand Down
37 changes: 37 additions & 0 deletions src-rust/crates/core/src/coven_ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
//! Every failure is swallowed to a debug log — a dead or absent daemon must
//! never affect the TUI.

const COVEN_SESSION_SOURCE: &str = "COVEN_SESSION_SOURCE";
const PSYCHE_SESSION_SOURCE: &str = "psyche-build";

fn registration_labels(source: Option<&str>) -> Vec<String> {
match source {
Some(PSYCHE_SESSION_SOURCE) => vec![format!("source:{PSYCHE_SESSION_SOURCE}")],
_ => Vec::new(),
}
}

#[cfg(unix)]
pub fn notify_session_start(id: &str, project_root: &std::path::Path, title: &str) {
let Some(client) = crate::coven_daemon::DaemonClient::new() else {
Expand All @@ -17,6 +27,7 @@ pub fn notify_session_start(id: &str, project_root: &std::path::Path, title: &st
harness: "coven-code".to_string(),
title: title.to_string(),
transcript_path,
labels: registration_labels(std::env::var(COVEN_SESSION_SOURCE).ok().as_deref()),
};
if let Err(e) = client.register_external_session(&req) {
tracing::debug!("coven ledger register failed (ignored): {e}");
Expand All @@ -38,3 +49,29 @@ pub fn notify_session_start(_id: &str, _project_root: &std::path::Path, _title:

#[cfg(not(unix))]
pub fn notify_session_complete(_id: &str, _exit_code: Option<i32>) {}

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

#[test]
fn maps_only_the_exact_psyche_source() {
assert_eq!(
registration_labels(Some(PSYCHE_SESSION_SOURCE)),
vec![format!("source:{PSYCHE_SESSION_SOURCE}")]
);

for source in [
None,
Some(""),
Some("Psyche-Build"),
Some("foreign"),
Some("psyche-build-extra"),
] {
assert!(
registration_labels(source).is_empty(),
"unexpected label for {source:?}"
);
}
}
}