use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Mutex, PoisonError};
use tokio::time::{Duration, Instant};
use super::Attribution;
use super::claude::{ClaudeSessionFile, WatcherSnapshotHandle, fork_parent};
use super::codex::request::{self as codex_request, CodexRequestIdentity};
use super::codex::select::{CodexHookEvidence, CodexSelectionEvidence};
use super::codex::{CodexSessionFile, CodexWatcherSnapshotHandle, select as codex_select};
use tapes_capture::envelope::{HARNESS_ID_CLAUDE, HARNESS_ID_CODEX_APP, TapesAttribution};
use tapes_capture::peer_pid;
pub const DEFAULT_CLAUDE_TIMEOUT: Duration = Duration::from_secs(2);
pub const DEFAULT_CLAUDE_POLL: Duration = Duration::from_millis(25);
pub const DEFAULT_CODEX_TIMEOUT: Duration = Duration::from_secs(2);
pub const DEFAULT_CODEX_POLL: Duration = Duration::from_millis(25);
pub const DEFAULT_CODEX_RECENT_WINDOW: time::Duration = time::Duration::minutes(10);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexProviderFilter {
base: String,
}
impl CodexProviderFilter {
#[must_use]
pub fn new(base: impl Into<String>) -> Self {
Self { base: base.into() }
}
#[must_use]
pub fn matches(&self, provider: Option<&str>) -> bool {
provider.is_some_and(|provider| {
provider == self.base || provider.starts_with(&format!("{}-", self.base))
})
}
#[must_use]
pub fn matches_session(&self, session: &CodexSessionFile) -> bool {
self.matches(session.model_provider.as_deref())
}
}
pub trait UserAgentHarness: std::fmt::Debug + Send + Sync {
fn harness_id(&self, user_agent: &str) -> Option<&'static str>;
}
#[derive(Debug, Clone)]
pub struct AttributionConfig {
pub claude_timeout: Duration,
pub claude_poll: Duration,
pub codex_timeout: Duration,
pub codex_poll: Duration,
pub codex_recent_window: time::Duration,
pub codex_provider: CodexProviderFilter,
pub user_agents: std::sync::Arc<dyn UserAgentHarness>,
}
impl AttributionConfig {
#[must_use]
pub fn new(
codex_provider: CodexProviderFilter,
user_agents: impl UserAgentHarness + 'static,
) -> Self {
Self {
claude_timeout: DEFAULT_CLAUDE_TIMEOUT,
claude_poll: DEFAULT_CLAUDE_POLL,
codex_timeout: DEFAULT_CODEX_TIMEOUT,
codex_poll: DEFAULT_CODEX_POLL,
codex_recent_window: DEFAULT_CODEX_RECENT_WINDOW,
codex_provider,
user_agents: std::sync::Arc::new(user_agents),
}
}
}
#[derive(Debug, Default)]
pub struct ForkParentCache {
entries: Mutex<HashMap<String, ForkParentEntry>>,
}
#[derive(Debug, Clone)]
enum ForkParentEntry {
Parent(String),
Negative { first: Instant, last_probe: Instant },
PermanentlyNone,
}
const NEGATIVE_RETRY_INTERVAL: Duration = Duration::from_secs(1);
const NEGATIVE_GIVE_UP: Duration = Duration::from_secs(30);
impl ForkParentCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn get(&self, sid: &str) -> Option<Option<String>> {
let entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
match entries.get(sid) {
None => None,
Some(ForkParentEntry::Parent(parent)) => Some(Some(parent.clone())),
Some(ForkParentEntry::PermanentlyNone) => Some(None),
Some(ForkParentEntry::Negative { last_probe, .. }) => {
if last_probe.elapsed() < NEGATIVE_RETRY_INTERVAL {
Some(None)
} else {
None }
}
}
}
fn insert(&self, sid: String, parent: Option<String>) {
let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
match parent {
Some(parent) => {
entries.insert(sid, ForkParentEntry::Parent(parent));
}
None => {
let first = match entries.get(&sid) {
Some(ForkParentEntry::Parent(_)) => return,
Some(ForkParentEntry::Negative { first, .. }) => *first,
_ => Instant::now(),
};
if first.elapsed() >= NEGATIVE_GIVE_UP {
entries.insert(sid, ForkParentEntry::PermanentlyNone);
return;
}
entries.insert(
sid,
ForkParentEntry::Negative {
first,
last_probe: Instant::now(),
},
);
}
}
}
}
#[derive(Debug, Clone)]
pub struct AttributionState {
pub claude_watcher: WatcherSnapshotHandle,
pub codex_watcher: CodexWatcherSnapshotHandle,
pub fork_parents: std::sync::Arc<ForkParentCache>,
}
impl AttributionState {
#[must_use]
pub fn new(
claude_watcher: WatcherSnapshotHandle,
codex_watcher: CodexWatcherSnapshotHandle,
) -> Self {
Self {
claude_watcher,
codex_watcher,
fork_parents: std::sync::Arc::new(ForkParentCache::new()),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RequestFacts<'a> {
pub peer: Option<SocketAddr>,
pub user_agent: Option<&'a str>,
pub codex_marker: Option<&'a str>,
pub codex_rollout_id: Option<&'a str>,
pub codex_route: bool,
pub codex_identity: Option<&'a CodexRequestIdentity>,
pub codex_hook_evidence: Option<&'a dyn CodexHookEvidence>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Attributed {
Claude {
session: ClaudeSessionFile,
parent_session_id: Option<String>,
},
Codex {
session: Option<CodexSessionFile>,
identity: Box<CodexRequestIdentity>,
codex_app: bool,
},
UnknownHarness,
Undecided,
}
impl Attributed {
#[must_use]
pub fn envelope(&self) -> Option<TapesAttribution> {
match self {
Self::Claude {
session,
parent_session_id,
} => Some(TapesAttribution::from_session(
session,
parent_session_id.as_deref(),
)),
Self::Codex {
session,
identity,
codex_app,
} => {
let mut envelope = match session {
Some(session) => codex_request::codex_envelope(session, identity),
None => codex_request::request_envelope(identity),
};
if *codex_app {
envelope.harness_id = HARNESS_ID_CODEX_APP.to_owned();
}
Some(envelope)
}
Self::UnknownHarness => Some(TapesAttribution::unknown()),
Self::Undecided => None,
}
}
pub fn stamp(
&self,
headers: &mut http::HeaderMap,
) -> Result<(), tapes_capture::envelope::HeaderError> {
match self {
Self::Undecided => Ok(()),
Self::UnknownHarness => tapes_capture::envelope::inject_unattributed_envelope(headers),
Self::Claude {
session,
parent_session_id,
} => tapes_capture::envelope::inject_session_envelope(
headers,
session,
parent_session_id.as_deref(),
),
Self::Codex { .. } => match self.envelope() {
Some(envelope) => {
tapes_capture::envelope::inject_tapes_attribution(headers, envelope)
}
None => Ok(()),
},
}
}
#[must_use]
pub fn attribution(&self) -> Attribution {
match self {
Self::Claude {
session,
parent_session_id,
} => Attribution {
session_id: Some(session.session_id.clone()),
parent_session_id: parent_session_id.clone(),
cwd: session.cwd.clone(),
auth_subject: None,
},
Self::Codex {
session, identity, ..
} => Attribution {
session_id: codex_request::envelope_session_id(identity, session.as_ref())
.map(str::to_owned),
parent_session_id: None,
cwd: session.as_ref().and_then(|session| session.cwd.clone()),
auth_subject: None,
},
Self::UnknownHarness | Self::Undecided => Attribution::default(),
}
}
#[must_use]
pub fn claude_session(&self) -> Option<&ClaudeSessionFile> {
match self {
Self::Claude { session, .. } => Some(session),
_ => None,
}
}
#[must_use]
pub fn codex_session(&self) -> Option<&CodexSessionFile> {
match self {
Self::Codex { session, .. } => session.as_ref(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AttributionOutcome {
pub attributed: Attributed,
pub codex_evidence: Option<CodexSelectionEvidence>,
}
pub async fn attribute(
state: &AttributionState,
config: &AttributionConfig,
facts: RequestFacts<'_>,
) -> Attributed {
attribute_with_evidence(state, config, facts)
.await
.attributed
}
pub async fn attribute_with_evidence(
state: &AttributionState,
config: &AttributionConfig,
facts: RequestFacts<'_>,
) -> AttributionOutcome {
if facts.codex_route || facts.codex_marker.is_some() {
let owned;
let identity = match facts.codex_identity {
Some(identity) => identity,
None => {
owned = CodexRequestIdentity::default();
&owned
}
};
let selected = codex_select::select(state, config, facts, identity).await;
let codex_app = codex_request::envelope_session_id(identity, selected.session.as_ref())
.is_some_and(|session_id| {
facts
.codex_hook_evidence
.is_some_and(|hooks| hooks.has_hook_session(session_id))
});
return AttributionOutcome {
attributed: Attributed::Codex {
session: selected.session,
identity: Box::new(identity.clone()),
codex_app,
},
codex_evidence: Some(selected.evidence),
};
}
let attributed = match attribute_claude(state, config, facts).await {
Some(session) => {
let parent_session_id = discover_parent_cached(state, &session).await;
Attributed::Claude {
session,
parent_session_id,
}
}
None => Attributed::UnknownHarness,
};
AttributionOutcome {
attributed,
codex_evidence: None,
}
}
async fn attribute_claude(
state: &AttributionState,
config: &AttributionConfig,
facts: RequestFacts<'_>,
) -> Option<ClaudeSessionFile> {
let harness = facts
.user_agent
.and_then(|ua| config.user_agents.harness_id(ua));
if harness != Some(HARNESS_ID_CLAUDE) {
return None;
}
let peer = facts.peer?;
let deadline = Instant::now() + config.claude_timeout;
loop {
if let Some(session) = attribute_claude_once(state, peer) {
return Some(session);
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return None;
}
tokio::time::sleep(config.claude_poll.min(remaining)).await;
}
}
fn attribute_claude_once(state: &AttributionState, peer: SocketAddr) -> Option<ClaudeSessionFile> {
let snapshot = state.claude_watcher.load_full();
if snapshot.candidate_pids.is_empty() {
return None;
}
let mut candidates: Vec<i32> = snapshot.candidate_pids.iter().copied().collect();
candidates.sort_unstable();
let pid = peer_pid::lookup(&candidates, peer).pid?;
snapshot.pid_metadata.get(&pid).cloned()
}
async fn discover_parent_cached(
state: &AttributionState,
session: &ClaudeSessionFile,
) -> Option<String> {
let sid = session.session_id.clone();
if let Some(cached) = state.fork_parents.get(&sid) {
return cached;
}
let Some(cwd) = session.cwd.as_deref() else {
state.fork_parents.insert(sid, None);
return None;
};
let parent = fork_parent::discover_parent(cwd, &sid).await;
state.fork_parents.insert(sid, parent.clone());
parent
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::attribution::codex::session as codex_session;
use crate::attribution::{CodexWatcherSnapshot, WatcherSnapshot};
use arc_swap::ArcSwap;
use std::net::{IpAddr, Ipv4Addr};
use std::path::PathBuf;
use std::sync::Arc;
use tapes_capture::envelope::{HARNESS_ID_CLAUDE, HARNESS_ID_CODEX, HARNESS_ID_UNKNOWN};
fn filter() -> CodexProviderFilter {
CodexProviderFilter::new("paper-openai")
}
fn config() -> AttributionConfig {
AttributionConfig::new(filter(), crate::harness::RegistryUserAgents)
}
fn state_with(claude: WatcherSnapshot, codex: CodexWatcherSnapshot) -> AttributionState {
AttributionState::new(
Arc::new(ArcSwap::from_pointee(claude)),
Arc::new(ArcSwap::from_pointee(codex)),
)
}
fn empty_state() -> AttributionState {
state_with(WatcherSnapshot::default(), CodexWatcherSnapshot::default())
}
fn peer() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 49152)
}
fn claude_session(sid: &str) -> ClaudeSessionFile {
let raw = format!(r#"{{"pid":4242,"sessionId":"{sid}","cwd":"/tmp"}}"#);
serde_json::from_str(&raw).unwrap()
}
fn codex_file(sid: &str, provider: &str, age: time::Duration) -> CodexSessionFile {
let now = time::OffsetDateTime::now_utc();
CodexSessionFile {
session_id: sid.to_owned(),
root_session_id: None,
parent_thread_id: None,
subagent_kind: None,
timestamp: now - age,
modified_at: Some(now - age),
cwd: Some("/tmp".to_owned()),
originator: Some("codex_cli_rs".to_owned()),
cli_version: Some("0.9.0".to_owned()),
source: None,
thread_source: None,
model_provider: Some(provider.to_owned()),
path: PathBuf::from(format!("/tmp/rollout-{sid}.jsonl")),
}
}
#[test]
fn the_lane_gate_matches_any_casing_but_only_as_a_prefix() {
let claims = |ua: &str| config().user_agents.harness_id(ua) == Some(HARNESS_ID_CLAUDE);
assert!(claims("claude-cli/2.1.145"));
assert!(claims("Claude-CLI/2.1.145"));
assert!(claims("CLAUDE/0.0"));
assert!(!claims("curl/8.0"));
assert!(!claims("OpenAI/python"));
assert!(!claims(""));
assert!(!claims("some-claude-like"));
}
#[derive(Debug)]
struct AlwaysCodex;
impl UserAgentHarness for AlwaysCodex {
fn harness_id(&self, _user_agent: &str) -> Option<&'static str> {
Some(HARNESS_ID_CODEX)
}
}
#[tokio::test(start_paused = true)]
async fn a_user_agent_naming_another_harness_does_not_take_the_claude_lane() {
let state = empty_state();
let config = AttributionConfig::new(filter(), AlwaysCodex);
let facts = RequestFacts {
peer: Some(peer()),
user_agent: Some("claude-cli/2.1.145"),
..RequestFacts::default()
};
let got = tokio::time::timeout(
std::time::Duration::from_millis(1),
attribute(&state, &config, facts),
)
.await
.expect("a foreign harness id must not open the claude lane");
assert_eq!(got, Attributed::UnknownHarness);
}
#[tokio::test(start_paused = true)]
async fn non_claude_callers_do_not_pay_the_bounded_wait() {
let state = empty_state();
let facts = RequestFacts {
peer: Some(peer()),
user_agent: Some("curl/8.0"),
..RequestFacts::default()
};
let got = tokio::time::timeout(
std::time::Duration::from_millis(1),
attribute(&state, &config(), facts),
)
.await
.expect("non-claude callers must not wait");
assert_eq!(got, Attributed::UnknownHarness);
}
fn live_peer() -> (SocketAddr, std::net::TcpStream, std::net::TcpStream) {
use std::net::{TcpListener, TcpStream};
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let server_addr = listener.local_addr().expect("addr");
let client = TcpStream::connect(server_addr).expect("connect");
let (server, _) = listener.accept().expect("accept");
let peer = client.local_addr().expect("peer addr");
(peer, client, server)
}
fn snapshot_with_session(pid: i32, session_id: &str) -> WatcherSnapshot {
let mut snapshot = WatcherSnapshot::default();
snapshot.candidate_pids.insert(pid);
let raw = format!(r#"{{"pid":{pid},"sessionId":"{session_id}","cwd":"/tmp"}}"#);
snapshot
.pid_metadata
.insert(pid, serde_json::from_str(&raw).expect("parse"));
snapshot
}
#[tokio::test(start_paused = true)]
async fn a_claude_caller_waits_out_the_whole_budget_before_giving_up() {
let state = empty_state();
let facts = RequestFacts {
peer: Some(peer()),
user_agent: Some("claude-cli/2.1.161"),
..RequestFacts::default()
};
let config = config();
let mut task = std::pin::pin!(attribute(&state, &config, facts));
assert!(
tokio::time::timeout(std::time::Duration::from_millis(1), task.as_mut())
.await
.is_err(),
"attribution must still be waiting before the budget expires",
);
tokio::time::advance(std::time::Duration::from_secs(2)).await;
assert_eq!(task.await, Attributed::UnknownHarness);
}
#[tokio::test(start_paused = true)]
async fn a_session_file_appearing_mid_wait_is_picked_up() {
let state = empty_state();
let (peer, _client, _server) = live_peer();
let pid = std::process::id() as i32;
let facts = RequestFacts {
peer: Some(peer),
user_agent: Some("claude-cli/2.1.161"),
..RequestFacts::default()
};
let config = config();
let mut task = std::pin::pin!(attribute(&state, &config, facts));
assert!(
tokio::time::timeout(std::time::Duration::from_millis(1), task.as_mut())
.await
.is_err(),
"attribution must wait while the session file is absent",
);
state
.claude_watcher
.store(std::sync::Arc::new(snapshot_with_session(
pid,
"mid-wait-session",
)));
let got = tokio::time::timeout(DEFAULT_CLAUDE_TIMEOUT, task.as_mut())
.await
.expect("the session must be found inside the budget");
assert_eq!(
got.attribution().session_id.as_deref(),
Some("mid-wait-session"),
);
}
#[test]
fn claude_lane_miss_emits_an_unknown_envelope() {
let envelope = Attributed::UnknownHarness
.envelope()
.expect("claude-lane miss must still emit an envelope");
assert_eq!(envelope.harness_id, HARNESS_ID_UNKNOWN);
assert_eq!(envelope.session_id, None);
}
#[test]
fn an_explicit_no_assertion_emits_no_envelope_at_all() {
assert!(Attributed::Undecided.envelope().is_none());
}
#[test]
fn a_codex_miss_still_carries_the_request_identity_it_had() {
let attributed = Attributed::Codex {
session: None,
identity: Box::new(
CodexRequestIdentity::default().with_correlation_id("correlation-1"),
),
codex_app: false,
};
let envelope = attributed.envelope().expect("a codex miss still stamps");
assert_eq!(envelope.harness_id, HARNESS_ID_CODEX);
assert_eq!(envelope.session_id, None);
assert_eq!(
envelope.metadata[crate::attribution::codex::request::REQUEST_CORRELATION_METADATA_KEY],
"correlation-1",
);
}
#[test]
fn hook_evidence_files_a_session_under_the_desktop_app_harness() {
let attributed = Attributed::Codex {
session: Some(codex_file(
"sid-app",
"paper-openai",
time::Duration::seconds(1),
)),
identity: Box::new(CodexRequestIdentity::default()),
codex_app: true,
};
let envelope = attributed.envelope().unwrap();
assert_eq!(
envelope.harness_id,
tapes_capture::envelope::HARNESS_ID_CODEX_APP
);
assert_eq!(envelope.session_id.as_deref(), Some("sid-app"));
}
#[test]
fn provider_filter_matches_exact_and_suffixed_ids_only() {
let f = filter();
assert!(f.matches(Some("paper-openai")));
assert!(f.matches(Some("paper-openai-transparent")));
assert!(!f.matches(Some("paper-openai2")));
assert!(!f.matches(Some("other-openai")));
assert!(!f.matches(Some("")));
assert!(!f.matches(None));
}
#[test]
fn provider_filter_is_per_consumer_not_paper_specific() {
let f = CodexProviderFilter::new("tapesctl-openai");
assert!(f.matches(Some("tapesctl-openai")));
assert!(f.matches(Some("tapesctl-openai-local")));
assert!(!f.matches(Some("paper-openai")));
}
#[tokio::test(start_paused = true)]
async fn codex_ignores_sessions_belonging_to_another_provider() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot.sessions.push(codex_file(
"other",
"some-other-provider",
time::Duration::seconds(1),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
..RequestFacts::default()
};
assert_eq!(
attribute(&state, &config(), facts).await.codex_session(),
None
);
}
#[tokio::test(start_paused = true)]
async fn unmarked_codex_falls_back_to_a_single_recent_session() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot.sessions.push(codex_file(
"sole",
"paper-openai",
time::Duration::seconds(30),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
..RequestFacts::default()
};
let got = attribute(&state, &config(), facts).await;
assert_eq!(
got.codex_session().map(|s| s.session_id.as_str()),
Some("sole"),
);
}
#[tokio::test(start_paused = true)]
async fn ambiguous_recent_sessions_are_refused_rather_than_guessed() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot
.sessions
.push(codex_file("a", "paper-openai", time::Duration::seconds(30)));
snapshot
.sessions
.push(codex_file("b", "paper-openai", time::Duration::seconds(10)));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
..RequestFacts::default()
};
assert_eq!(
attribute(&state, &config(), facts).await.codex_session(),
None
);
}
#[tokio::test(start_paused = true)]
async fn the_fallback_refuses_a_lone_session_that_is_not_the_named_rollout() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot.sessions.push(codex_file(
"parent",
"paper-openai",
time::Duration::seconds(30),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
codex_rollout_id: Some("child-not-yet-on-disk"),
..RequestFacts::default()
};
assert_eq!(
attribute(&state, &config(), facts).await.codex_session(),
None
);
}
#[tokio::test(start_paused = true)]
async fn the_fallback_selects_the_named_rollout_among_several() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot
.sessions
.push(codex_file("a", "paper-openai", time::Duration::seconds(30)));
snapshot
.sessions
.push(codex_file("b", "paper-openai", time::Duration::seconds(10)));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
codex_rollout_id: Some("a"),
..RequestFacts::default()
};
let got = attribute(&state, &config(), facts).await;
assert_eq!(
got.codex_session().map(|s| s.session_id.as_str()),
Some("a"),
);
}
#[tokio::test(start_paused = true)]
async fn stale_sessions_are_outside_the_recent_window() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot.sessions.push(codex_file(
"stale",
"paper-openai",
time::Duration::hours(3),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_route: true,
..RequestFacts::default()
};
assert_eq!(
attribute(&state, &config(), facts).await.codex_session(),
None
);
}
#[tokio::test(start_paused = true)]
async fn a_supplied_marker_never_falls_back_to_a_recent_session() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot.sessions.push(codex_file(
"sole",
"paper-openai",
time::Duration::seconds(30),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_marker: Some("paper-openai-somethingelse"),
codex_route: true,
..RequestFacts::default()
};
assert_eq!(
attribute(&state, &config(), facts).await.codex_session(),
None
);
}
#[tokio::test(start_paused = true)]
async fn a_marker_selects_its_own_session_among_several() {
let mut snapshot = CodexWatcherSnapshot::default();
snapshot
.sessions
.push(codex_file("a", "paper-openai", time::Duration::seconds(30)));
snapshot.sessions.push(codex_file(
"wanted",
"paper-openai-transparent",
time::Duration::seconds(30),
));
let state = state_with(WatcherSnapshot::default(), snapshot);
let facts = RequestFacts {
codex_marker: Some("paper-openai-transparent"),
codex_route: true,
..RequestFacts::default()
};
let got = attribute(&state, &config(), facts).await;
assert_eq!(
got.codex_session().map(|s| s.session_id.as_str()),
Some("wanted"),
);
}
#[test]
fn rollout_id_prefers_the_thread_over_the_root_session() {
let mut headers = http::HeaderMap::new();
headers.insert("session-id", http::HeaderValue::from_static("sid-parent"));
headers.insert("thread-id", http::HeaderValue::from_static("sid-child-a"));
assert_eq!(codex_session::rollout_id(&headers), Some("sid-child-a"));
}
#[test]
fn rollout_id_falls_back_to_the_session_when_no_thread_is_named() {
let mut headers = http::HeaderMap::new();
headers.insert("session-id", http::HeaderValue::from_static("sid-parent"));
assert_eq!(codex_session::rollout_id(&headers), Some("sid-parent"));
}
#[test]
fn rollout_id_treats_a_blank_header_as_absent() {
let mut headers = http::HeaderMap::new();
headers.insert("thread-id", http::HeaderValue::from_static(" "));
headers.insert("session-id", http::HeaderValue::from_static("sid-parent"));
assert_eq!(codex_session::rollout_id(&headers), Some("sid-parent"));
assert_eq!(codex_session::rollout_id(&http::HeaderMap::new()), None);
}
#[tokio::test(start_paused = true)]
async fn negative_fork_parent_cache_retries_then_hardens() {
let cache = ForkParentCache::new();
cache.insert("sid".into(), None);
assert_eq!(cache.get("sid"), Some(None));
tokio::time::advance(NEGATIVE_RETRY_INTERVAL + Duration::from_millis(10)).await;
assert_eq!(cache.get("sid"), None);
cache.insert("sid".into(), Some("parent".into()));
assert_eq!(cache.get("sid"), Some(Some("parent".into())));
}
#[tokio::test(start_paused = true)]
async fn negative_insert_never_erases_a_found_parent() {
let cache = ForkParentCache::new();
cache.insert("sid".into(), Some("parent".into()));
cache.insert("sid".into(), None);
assert_eq!(cache.get("sid"), Some(Some("parent".into())));
}
#[tokio::test(start_paused = true)]
async fn negative_fork_parent_cache_gives_up_only_after_a_final_probe() {
let cache = ForkParentCache::new();
cache.insert("sid".into(), None);
tokio::time::advance(NEGATIVE_GIVE_UP + Duration::from_secs(1)).await;
assert_eq!(cache.get("sid"), None);
cache.insert("sid".into(), Some("parent".into()));
assert_eq!(cache.get("sid"), Some(Some("parent".into())));
let done = ForkParentCache::new();
done.insert("done".into(), None);
tokio::time::advance(NEGATIVE_GIVE_UP + Duration::from_secs(1)).await;
assert_eq!(done.get("done"), None);
done.insert("done".into(), None);
assert_eq!(done.get("done"), Some(None));
tokio::time::advance(NEGATIVE_RETRY_INTERVAL * 5).await;
assert_eq!(
done.get("done"),
Some(None),
"hardened after the final probe missed"
);
}
#[test]
fn codex_envelope_carries_the_metadata_ingest_stores() {
let mut session = codex_file("sid-1", "paper-openai", time::Duration::seconds(5));
session.source = Some("cli".to_owned());
session.thread_source = Some("main".to_owned());
let envelope = crate::attribution::codex::request::codex_envelope(
&session,
&CodexRequestIdentity::default(),
);
assert_eq!(envelope.harness_id, HARNESS_ID_CODEX);
assert_eq!(envelope.session_id.as_deref(), Some("sid-1"));
assert_eq!(envelope.version.as_deref(), Some("0.9.0"));
assert_eq!(envelope.cwd.as_deref(), Some("/tmp"));
assert_eq!(envelope.metadata["originator"], "codex_cli_rs");
assert_eq!(envelope.metadata["source"], "cli");
assert_eq!(envelope.metadata["threadSource"], "main");
assert_eq!(envelope.metadata["modelProvider"], "paper-openai");
assert_eq!(
envelope.metadata["transcriptPath"],
"/tmp/rollout-sid-1.jsonl",
);
}
#[test]
fn absent_codex_metadata_fields_are_omitted_not_nulled() {
let session = codex_file("sid-2", "paper-openai", time::Duration::seconds(5));
let envelope = crate::attribution::codex::request::codex_envelope(
&session,
&CodexRequestIdentity::default(),
);
assert!(!envelope.metadata.contains_key("source"));
assert!(!envelope.metadata.contains_key("threadSource"));
}
#[test]
fn claude_envelope_carries_lineage_when_recovered() {
let attributed = Attributed::Claude {
session: claude_session("sid-claude"),
parent_session_id: Some("sid-parent".to_owned()),
};
let envelope = attributed.envelope().unwrap();
assert_eq!(envelope.harness_id, HARNESS_ID_CLAUDE);
assert_eq!(envelope.session_id.as_deref(), Some("sid-claude"));
assert_eq!(envelope.parent_sid.as_deref(), Some("sid-parent"));
}
fn headers_with(pairs: &[(&'static str, &'static str)]) -> http::HeaderMap {
let mut headers = http::HeaderMap::new();
for (name, value) in pairs {
headers.insert(
http::HeaderName::from_static(name),
http::HeaderValue::from_static(value),
);
}
headers
}
#[test]
fn an_unattributed_request_preserves_a_complete_inbound_envelope() {
let mut headers = headers_with(&[
("x-tapes-harness-id", "pi"),
("x-tapes-harness-session-id", "sid-from-harness"),
]);
Attributed::UnknownHarness.stamp(&mut headers).unwrap();
assert_eq!(headers["x-tapes-harness-id"], "pi");
assert_eq!(headers["x-tapes-harness-session-id"], "sid-from-harness");
}
#[test]
fn an_unattributed_request_without_an_inbound_envelope_is_marked_unknown() {
let mut headers = http::HeaderMap::new();
Attributed::UnknownHarness.stamp(&mut headers).unwrap();
assert_eq!(headers["x-tapes-harness-id"], HARNESS_ID_UNKNOWN);
}
#[test]
fn an_undecided_codex_request_is_left_entirely_unstamped() {
let mut headers = http::HeaderMap::new();
Attributed::Undecided.stamp(&mut headers).unwrap();
assert!(headers.is_empty(), "got: {headers:?}");
}
#[test]
fn an_attributed_claude_request_overrides_whatever_arrived() {
let mut headers = headers_with(&[("x-tapes-harness-id", "pi")]);
Attributed::Claude {
session: claude_session("sid-claude"),
parent_session_id: None,
}
.stamp(&mut headers)
.unwrap();
assert_eq!(headers["x-tapes-harness-id"], HARNESS_ID_CLAUDE);
assert_eq!(headers["x-tapes-harness-session-id"], "sid-claude");
}
#[test]
fn summary_leaves_auth_subject_to_the_consumer() {
let attributed = Attributed::Claude {
session: claude_session("sid-claude"),
parent_session_id: Some("sid-parent".to_owned()),
};
let summary = attributed.attribution();
assert_eq!(summary.session_id.as_deref(), Some("sid-claude"));
assert_eq!(summary.parent_session_id.as_deref(), Some("sid-parent"));
assert_eq!(summary.cwd.as_deref(), Some("/tmp"));
assert_eq!(summary.auth_subject, None);
}
#[test]
fn summary_of_a_miss_is_entirely_unknown() {
assert_eq!(
Attributed::UnknownHarness.attribution(),
Attribution::default()
);
assert_eq!(Attributed::Undecided.attribution(), Attribution::default());
}
#[tokio::test]
async fn a_session_without_a_cwd_caches_its_none() {
let state = empty_state();
let mut session = claude_session("sid-nocwd");
session.cwd = None;
assert_eq!(discover_parent_cached(&state, &session).await, None);
assert_eq!(state.fork_parents.get("sid-nocwd"), Some(None));
}
#[tokio::test]
async fn a_cached_parent_short_circuits_discovery() {
let state = empty_state();
state
.fork_parents
.insert("sid-x".to_owned(), Some("sid-parent".to_owned()));
let session = claude_session("sid-x");
assert_eq!(
discover_parent_cached(&state, &session).await,
Some("sid-parent".to_owned()),
);
}
}