diff --git a/src/channels/web/handlers/routines.rs b/src/channels/web/handlers/routines.rs index d7c4f764..8fbcc97b 100644 --- a/src/channels/web/handlers/routines.rs +++ b/src/channels/web/handlers/routines.rs @@ -108,6 +108,7 @@ pub async fn routines_detail_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); @@ -252,6 +253,7 @@ pub async fn routines_runs_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index d6605eee..f454363d 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -2017,6 +2017,7 @@ async fn routines_detail_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); @@ -2169,6 +2170,7 @@ async fn routines_runs_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index 4d85c671..b6d0d05a 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -776,6 +776,7 @@ pub struct RoutineRunInfo { pub status: String, pub result_summary: Option, pub tokens_used: Option, + pub job_id: Option, } // --- Settings --- diff --git a/src/db/libsql/jobs.rs b/src/db/libsql/jobs.rs index d5172360..0750873d 100644 --- a/src/db/libsql/jobs.rs +++ b/src/db/libsql/jobs.rs @@ -28,14 +28,16 @@ impl JobStore for LibSqlBackend { r#" INSERT INTO agent_jobs ( id, conversation_id, title, description, category, status, source, + user_id, budget_amount, budget_token, bid_amount, estimated_cost, estimated_time_secs, actual_cost, repair_attempts, created_at, started_at, completed_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17) + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18) ON CONFLICT (id) DO UPDATE SET title = excluded.title, description = excluded.description, category = excluded.category, status = excluded.status, + user_id = excluded.user_id, estimated_cost = excluded.estimated_cost, estimated_time_secs = excluded.estimated_time_secs, actual_cost = excluded.actual_cost, @@ -51,6 +53,7 @@ impl JobStore for LibSqlBackend { opt_text(ctx.category.as_deref()), status, "direct", + ctx.user_id.as_str(), opt_text_owned(ctx.budget.map(|d| d.to_string())), opt_text(ctx.budget_token.as_deref()), opt_text_owned(ctx.bid_amount.map(|d| d.to_string())), diff --git a/src/db/libsql/mod.rs b/src/db/libsql/mod.rs index 2845c757..404441e6 100644 --- a/src/db/libsql/mod.rs +++ b/src/db/libsql/mod.rs @@ -482,6 +482,24 @@ mod tests { assert_eq!(timeout, 5000); } + /// Regression test: save_job must persist user_id and get_job must return it. + #[tokio::test] + async fn test_save_job_persists_user_id() { + use crate::context::JobContext; + use crate::db::JobStore; + + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test_user_id.db"); + let backend = LibSqlBackend::new_local(&db_path).await.unwrap(); + backend.run_migrations().await.unwrap(); + + let ctx = JobContext::with_user("test-user-42", "Test Job", "A test job"); + backend.save_job(&ctx).await.unwrap(); + + let loaded = backend.get_job(ctx.job_id).await.unwrap().unwrap(); + assert_eq!(loaded.user_id, "test-user-42"); + } + #[tokio::test] async fn test_concurrent_writes_succeed() { // Use a temp file so connections share state (in-memory DBs are connection-local) diff --git a/src/history/store.rs b/src/history/store.rs index f0b0b144..1153f3e4 100644 --- a/src/history/store.rs +++ b/src/history/store.rs @@ -149,14 +149,16 @@ impl Store { r#" INSERT INTO agent_jobs ( id, conversation_id, title, description, category, status, source, + user_id, budget_amount, budget_token, bid_amount, estimated_cost, estimated_time_secs, actual_cost, repair_attempts, created_at, started_at, completed_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description, category = EXCLUDED.category, status = EXCLUDED.status, + user_id = EXCLUDED.user_id, estimated_cost = EXCLUDED.estimated_cost, estimated_time_secs = EXCLUDED.estimated_time_secs, actual_cost = EXCLUDED.actual_cost, @@ -172,6 +174,7 @@ impl Store { &ctx.category, &status, &"direct", // source + &ctx.user_id, &ctx.budget, &ctx.budget_token, &ctx.bid_amount, @@ -2133,4 +2136,36 @@ mod tests { assert_eq!(summary.channel, ch); } } + + /// Regression test: save_job must persist user_id and get_job must return it. + /// Requires a running PostgreSQL instance (integration tier). + #[cfg(feature = "postgres")] + #[tokio::test] + #[ignore] + async fn test_save_job_persists_user_id() { + use crate::config::Config; + use crate::context::JobContext; + + let _ = dotenvy::dotenv(); + let config = Config::from_env().await.expect("Failed to load config"); + let store = Store::new(&config.database) + .await + .expect("Failed to connect to database"); + store + .run_migrations() + .await + .expect("Failed to run migrations"); + + let ctx = JobContext::with_user("test-user-42", "PG user_id test", "regression test"); + store.save_job(&ctx).await.unwrap(); + + let loaded = store.get_job(ctx.job_id).await.unwrap().unwrap(); + assert_eq!(loaded.user_id, "test-user-42"); + + // Clean up + let conn = store.conn().await.unwrap(); + conn.execute("DELETE FROM agent_jobs WHERE id = $1", &[&ctx.job_id]) + .await + .unwrap(); + } }