use super::*;
#[tokio::test]
async fn session_activity_routes_project_list_detail_and_report_stats() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("api-session-activity");
let conn = db::open_db()?;
conn.execute(
"INSERT INTO raw_messages
(id, session_id, project, role, content, content_hash, source, cwd,
created_at_epoch, source_root, event_time_source)
VALUES
(901, 'activity-session', 'activity/project', 'user',
'Add an evidence-first session view', 'activity-user', 'transcript',
'/activity', 100, 'local', 'transcript_event'),
(902, 'activity-session', 'activity/project', 'assistant',
'The session activity view is implemented and verified.',
'activity-assistant', 'transcript', '/activity', 120, 'local',
'transcript_event')",
[],
)?;
drop(conn);
crate::api::ensure_api_token()?;
let token = crate::api::load_api_token()?;
let app = crate::api::build_router(0).with_state(DbState);
let unauthorized = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/session-stats")
.body(Body::empty())?,
)
.await?;
assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED);
let projected = app
.clone()
.oneshot(authorized_json_request(
Method::POST,
"/api/v1/session-activity/project",
&token,
r#"{"source_root":"local","project":"activity/project","session_id":"activity-session"}"#,
))
.await?;
assert_eq!(projected.status(), StatusCode::OK);
let projected: Value =
serde_json::from_slice(&to_bytes(projected.into_body(), usize::MAX).await?)?;
assert_eq!(projected["data"]["changed"], true);
assert_eq!(projected["data"]["turn_count"], 1);
let sessions = app
.clone()
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-activity/sessions?project=activity%2Fproject",
&token,
Body::empty(),
))
.await?;
assert_eq!(sessions.status(), StatusCode::OK);
let sessions: Value =
serde_json::from_slice(&to_bytes(sessions.into_body(), usize::MAX).await?)?;
assert_eq!(sessions["data"][0]["projected_turn_count"], 1);
assert!(sessions["data"][0]["session_row_id"].is_null());
assert_eq!(sessions["data"][0]["override_available"], false);
assert_eq!(sessions["data"][0]["mmdd"], "0101");
assert!(sessions["data"][0]["display_label"].is_null());
let turns = app
.clone()
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-activity?session_id=activity-session",
&token,
Body::empty(),
))
.await?;
assert_eq!(turns.status(), StatusCode::OK);
let turns: Value = serde_json::from_slice(&to_bytes(turns.into_body(), usize::MAX).await?)?;
assert_eq!(
turns["data"][0]["user_said"],
"Add an evidence-first session view"
);
assert_eq!(turns["data"][0]["capture_health"], "unavailable");
let id = turns["data"][0]["id"].as_i64().expect("turn id");
let detail = app
.clone()
.oneshot(authorized_request(
Method::GET,
&format!("/api/v1/session-activity/{id}"),
&token,
Body::empty(),
))
.await?;
assert_eq!(detail.status(), StatusCode::OK);
let stats = app
.clone()
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-stats?project=activity%2Fproject&since_epoch=90&until_epoch=200",
&token,
Body::empty(),
))
.await?;
assert_eq!(stats.status(), StatusCode::OK);
let stats: Value = serde_json::from_slice(&to_bytes(stats.into_body(), usize::MAX).await?)?;
assert_eq!(stats["data"]["sessions"], 1);
assert_eq!(stats["data"]["turns"], 1);
assert_eq!(stats["data"]["actions"], 0);
let conn = db::open_db()?;
conn.execute("INSERT INTO memory_suppressions(target_kind,target_value,reason,actor,status,created_at_epoch,updated_at_epoch) VALUES ('pattern','activity/project','test','test','active',1,1)", [])?;
drop(conn);
let hidden = app
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-activity/sessions?session_intent=abstain",
&token,
Body::empty(),
))
.await?;
assert_eq!(hidden.status(), StatusCode::OK);
let hidden: Value = serde_json::from_slice(&to_bytes(hidden.into_body(), usize::MAX).await?)?;
assert_eq!(hidden["data"], serde_json::json!([]));
Ok(())
}
#[tokio::test]
async fn session_activity_rejects_invalid_windows_and_ids() -> anyhow::Result<()> {
let _test_dir = ScopedTestDataDir::new("api-session-activity-invalid");
db::open_db()?;
crate::api::ensure_api_token()?;
let token = crate::api::load_api_token()?;
let app = crate::api::build_router(0).with_state(DbState);
for query in [
"session_intent=bugfix",
"since_epoch=20&until_epoch=10",
"since_epoch=20&until_epoch=20",
] {
let response = app
.clone()
.oneshot(authorized_request(
Method::GET,
&format!("/api/v1/session-activity/sessions?{query}"),
&token,
Body::empty(),
))
.await?;
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
let window = app
.clone()
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-stats?since_epoch=20&until_epoch=10",
&token,
Body::empty(),
))
.await?;
assert_eq!(window.status(), StatusCode::BAD_REQUEST);
let id = app
.clone()
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-activity/sessions?cursor=not-a-cursor",
&token,
Body::empty(),
))
.await?;
assert_eq!(id.status(), StatusCode::BAD_REQUEST);
let id = app
.oneshot(authorized_request(
Method::GET,
"/api/v1/session-activity/not-an-id",
&token,
Body::empty(),
))
.await?;
assert_eq!(id.status(), StatusCode::BAD_REQUEST);
Ok(())
}