1use std::sync::Arc;
4
5use sqlx::SqlitePool;
6
7#[cfg(any(test, feature = "test-utils"))]
8use super::connection::open_in_memory_pool;
9use super::{
10 ActivityRepository, OperationRepository, OrchestrationRepository, ProjectRepository,
11 ReviewRepository, SessionRepository, SettingRepository, SqliteActivityRepository,
12 SqliteOperationRepository, SqliteOrchestrationRepository, SqliteProjectRepository,
13 SqliteReviewRepository, SqliteSessionRepository, SqliteSettingRepository,
14 SqliteUsageRepository, UsageRepository,
15};
16use crate::timestamp::TimestampSource;
17#[cfg(any(test, feature = "test-utils"))]
18use crate::timestamp::system_timestamp_source;
19
20#[derive(Clone)]
22pub struct AppRepositories {
23 activity: Arc<dyn ActivityRepository>,
24 operation: Arc<dyn OperationRepository>,
25 orchestration: Arc<dyn OrchestrationRepository>,
26 project: Arc<dyn ProjectRepository>,
27 review: Arc<dyn ReviewRepository>,
28 session: Arc<dyn SessionRepository>,
29 setting: Arc<dyn SettingRepository>,
30 usage: Arc<dyn UsageRepository>,
31}
32
33impl AppRepositories {
34 #[cfg(any(test, feature = "test-utils"))]
36 pub(crate) fn from_pool(pool: SqlitePool) -> Self {
37 Self::from_pool_and_timestamp_source(pool, system_timestamp_source())
38 }
39
40 pub(crate) fn from_pool_and_timestamp_source(
42 pool: SqlitePool,
43 timestamp_source: Arc<dyn TimestampSource>,
44 ) -> Self {
45 Self::from_parts(AppRepositoryParts {
46 activity: Arc::new(SqliteActivityRepository::new(pool.clone())),
47 operation: Arc::new(SqliteOperationRepository::new(
48 pool.clone(),
49 Arc::clone(×tamp_source),
50 )),
51 orchestration: Arc::new(SqliteOrchestrationRepository::new(
52 pool.clone(),
53 Arc::clone(×tamp_source),
54 )),
55 project: Arc::new(SqliteProjectRepository::new(
56 pool.clone(),
57 Arc::clone(×tamp_source),
58 )),
59 review: Arc::new(SqliteReviewRepository::new(pool.clone())),
60 session: Arc::new(SqliteSessionRepository::new(
61 pool.clone(),
62 Arc::clone(×tamp_source),
63 )),
64 setting: Arc::new(SqliteSettingRepository::new(pool.clone())),
65 usage: Arc::new(SqliteUsageRepository::new(pool, timestamp_source)),
66 })
67 }
68
69 pub(crate) fn from_parts(parts: AppRepositoryParts) -> Self {
71 let AppRepositoryParts {
72 activity,
73 operation,
74 orchestration,
75 project,
76 review,
77 session,
78 setting,
79 usage,
80 } = parts;
81
82 Self {
83 activity,
84 operation,
85 orchestration,
86 project,
87 review,
88 session,
89 setting,
90 usage,
91 }
92 }
93
94 #[cfg(any(test, feature = "test-utils"))]
99 #[doc(hidden)]
100 pub async fn in_memory() -> Result<Self, crate::DbError> {
101 let (repositories, _pool) = Self::in_memory_with_pool().await?;
102
103 Ok(repositories)
104 }
105
106 #[cfg(any(test, feature = "test-utils"))]
112 #[doc(hidden)]
113 pub async fn in_memory_with_pool() -> Result<(Self, SqlitePool), crate::DbError> {
114 Self::from_new_in_memory_pool().await
115 }
116
117 #[cfg(any(test, feature = "test-utils"))]
120 async fn from_new_in_memory_pool() -> Result<(Self, SqlitePool), crate::DbError> {
121 let pool = open_in_memory_pool(1).await?;
122
123 let repositories = Self::from_pool(pool.clone());
124
125 Ok((repositories, pool))
126 }
127
128 pub fn activity(&self) -> &dyn ActivityRepository {
130 self.activity.as_ref()
131 }
132
133 pub fn operations(&self) -> &dyn OperationRepository {
135 self.operation.as_ref()
136 }
137
138 pub fn orchestrations(&self) -> &dyn OrchestrationRepository {
140 self.orchestration.as_ref()
141 }
142
143 pub fn orchestration_repository(&self) -> Arc<dyn OrchestrationRepository> {
146 Arc::clone(&self.orchestration)
147 }
148
149 pub fn projects(&self) -> &dyn ProjectRepository {
151 self.project.as_ref()
152 }
153
154 pub fn reviews(&self) -> &dyn ReviewRepository {
156 self.review.as_ref()
157 }
158
159 pub fn sessions(&self) -> &dyn SessionRepository {
161 self.session.as_ref()
162 }
163
164 pub fn settings(&self) -> &dyn SettingRepository {
166 self.setting.as_ref()
167 }
168
169 pub fn usage(&self) -> &dyn UsageRepository {
171 self.usage.as_ref()
172 }
173}
174
175pub(crate) struct AppRepositoryParts {
180 pub(crate) activity: Arc<dyn ActivityRepository>,
181 pub(crate) operation: Arc<dyn OperationRepository>,
182 pub(crate) orchestration: Arc<dyn OrchestrationRepository>,
183 pub(crate) project: Arc<dyn ProjectRepository>,
184 pub(crate) review: Arc<dyn ReviewRepository>,
185 pub(crate) session: Arc<dyn SessionRepository>,
186 pub(crate) setting: Arc<dyn SettingRepository>,
187 pub(crate) usage: Arc<dyn UsageRepository>,
188}
189
190#[cfg(test)]
191mod tests {
192 use std::sync::atomic::{AtomicI64, Ordering};
193
194 use ag_agent::{SessionDiffState, SessionStats};
195 use ag_session::SessionMessageKind;
196
197 use super::super::operation::MockOperationRepository;
198 use super::*;
199
200 struct AdvancingTimestampSource {
203 next_timestamp: AtomicI64,
204 }
205
206 impl TimestampSource for AdvancingTimestampSource {
207 fn now_timestamp_seconds(&self) -> i64 {
208 self.next_timestamp.fetch_add(1, Ordering::Relaxed)
209 }
210 }
211
212 struct FixedTimestampSource;
214
215 impl TimestampSource for FixedTimestampSource {
216 fn now_timestamp_seconds(&self) -> i64 {
217 456
218 }
219 }
220
221 #[tokio::test]
222 async fn injected_clock_drives_all_repository_timestamps() {
223 let pool = open_in_memory_pool(1)
225 .await
226 .expect("failed to open in-memory db");
227 let timestamp_source = Arc::new(AdvancingTimestampSource {
228 next_timestamp: AtomicI64::new(120),
229 });
230 let repositories =
231 AppRepositories::from_pool_and_timestamp_source(pool.clone(), timestamp_source);
232
233 let project_id = repositories
235 .projects()
236 .upsert_project("/tmp/injected-clock", Some("main".to_string()))
237 .await
238 .expect("failed to insert project");
239 repositories
240 .sessions()
241 .insert_session("session-a", "gpt-5.6-sol", "main", "Review", project_id)
242 .await
243 .expect("failed to insert session");
244 repositories
245 .sessions()
246 .append_session_message("session-a", SessionMessageKind::UserPrompt, "Persist this")
247 .await
248 .expect("failed to append message");
249 repositories
250 .operations()
251 .insert_session_operation("operation-a", "session-a", "reply")
252 .await
253 .expect("failed to insert operation");
254 let orchestration_id = repositories
255 .orchestrations()
256 .insert_orchestration("session-a", "Running", 2)
257 .await
258 .expect("failed to insert orchestration");
259 repositories
260 .usage()
261 .upsert_session_usage(
262 "session-a",
263 "gpt-5.6-sol",
264 &SessionStats {
265 added_lines: 0,
266 deleted_lines: 0,
267 diff_state: SessionDiffState::Unknown,
268 input_tokens: 1,
269 output_tokens: 2,
270 },
271 )
272 .await
273 .expect("failed to insert usage");
274 let timestamps = sqlx::query_as::<_, (i64, i64, i64, i64, i64, i64, i64, i64)>(
275 r"
276SELECT project.created_at,
277 project.updated_at,
278 session.created_at,
279 session.updated_at,
280 session_message.created_at,
281 session_operation.queued_at,
282 session_orchestration.created_at,
283 session_usage.created_at
284FROM project
285INNER JOIN session ON session.project_id = project.id
286INNER JOIN session_message ON session_message.session_id = session.id
287INNER JOIN session_operation ON session_operation.session_id = session.id
288INNER JOIN session_orchestration
289ON session_orchestration.controller_session_id = session.id
290INNER JOIN session_usage ON session_usage.session_id = session.id
291WHERE session_orchestration.id = ?
292",
293 )
294 .bind(orchestration_id)
295 .fetch_one(&pool)
296 .await
297 .expect("failed to load timestamps");
298
299 assert_eq!(timestamps, (120, 120, 121, 122, 122, 123, 124, 125));
301 }
302
303 #[tokio::test]
304 async fn fixed_clock_drives_session_metadata_status_and_usage_writes() {
305 let pool = open_in_memory_pool(1)
307 .await
308 .expect("failed to open in-memory db");
309 let timestamp_source = Arc::new(FixedTimestampSource);
310 let repositories =
311 AppRepositories::from_pool_and_timestamp_source(pool.clone(), timestamp_source);
312 let project_id = repositories
313 .projects()
314 .upsert_project("/tmp/fixed-clock", Some("main".to_string()))
315 .await
316 .expect("failed to insert project");
317 repositories
318 .sessions()
319 .insert_session("session-a", "gpt-5.6-sol", "main", "Review", project_id)
320 .await
321 .expect("failed to insert session");
322 let turn_metadata = super::super::SessionTurnMetadata {
323 applied_personality_id: None,
324 applied_personality_prompt_hash: None,
325 instruction_conversation_id: None,
326 model: "gpt-5.6-sol".to_string(),
327 provider_conversation_id: Some("conversation-a".to_string()),
328 questions_json: "[]".to_string(),
329 summary: "Persisted summary".to_string(),
330 token_usage_delta: SessionStats {
331 added_lines: 0,
332 deleted_lines: 0,
333 diff_state: SessionDiffState::Unknown,
334 input_tokens: 3,
335 output_tokens: 5,
336 },
337 };
338
339 repositories
341 .sessions()
342 .persist_session_turn_metadata("session-a", &turn_metadata)
343 .await
344 .expect("failed to persist turn metadata");
345 repositories
346 .sessions()
347 .update_session_status_with_timing_at("session-a", "InProgress", 789)
348 .await
349 .expect("failed to update status");
350 repositories
351 .usage()
352 .upsert_session_usage(
353 "session-a",
354 "review-model",
355 &SessionStats {
356 added_lines: 0,
357 deleted_lines: 0,
358 diff_state: SessionDiffState::Unknown,
359 input_tokens: 7,
360 output_tokens: 11,
361 },
362 )
363 .await
364 .expect("failed to persist usage");
365 let session_timestamps = sqlx::query_as::<_, (i64, i64)>(
366 "SELECT updated_at, in_progress_started_at FROM session WHERE id = ?",
367 )
368 .bind("session-a")
369 .fetch_one(&pool)
370 .await
371 .expect("failed to load session timestamps");
372 let usage_timestamps = sqlx::query_scalar::<_, i64>(
373 "SELECT created_at FROM session_usage WHERE session_id = ? ORDER BY model",
374 )
375 .bind("session-a")
376 .fetch_all(&pool)
377 .await
378 .expect("failed to load usage timestamps");
379
380 assert_eq!(session_timestamps, (456, 789));
382 assert_eq!(usage_timestamps, vec![456, 456]);
383 }
384
385 #[tokio::test]
386 async fn repository_parts_support_focused_adapter_injection() {
387 let pool = open_in_memory_pool(1)
389 .await
390 .expect("failed to open in-memory db");
391 let baseline = AppRepositories::from_pool(pool);
392 let mut operation = MockOperationRepository::new();
393 operation
394 .expect_is_cancel_requested_for_operation()
395 .withf(|operation_id| operation_id == "operation-a")
396 .times(1)
397 .returning(|_| Ok(true));
398 let repositories = AppRepositories::from_parts(AppRepositoryParts {
399 activity: Arc::clone(&baseline.activity),
400 operation: Arc::new(operation),
401 orchestration: Arc::clone(&baseline.orchestration),
402 project: Arc::clone(&baseline.project),
403 review: Arc::clone(&baseline.review),
404 session: Arc::clone(&baseline.session),
405 setting: Arc::clone(&baseline.setting),
406 usage: Arc::clone(&baseline.usage),
407 });
408
409 let is_cancel_requested = repositories
411 .operations()
412 .is_cancel_requested_for_operation("operation-a")
413 .await
414 .expect("mock operation query should succeed");
415
416 assert!(is_cancel_requested);
418 }
419}