1use axum::{
4 extract::{Query, State},
5 Json,
6};
7use serde::Deserialize;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::warn;
11
12use crate::error::{Result, WebError};
13use crate::models::{
14 AdaptiveDreamState, CognitionOverviewResponse, DashboardResponse, DigestEntry,
15 DigestFreshnessState, DigestListResponse, DreamState, JobEntry, JobListResponse,
16 JobSummaryResponse, QueryIntrospectionResponse, RecallComposition, ReflectionSampleEntry,
17 ReflectionStateResponse, RuntimeResponse,
18};
19use crate::state::AppState;
20use nexus_core::CognitiveLevel;
21
22#[derive(Debug, Deserialize, Default)]
25pub struct JobQueryParams {
26 pub namespace: String,
27 pub status: Option<String>,
28 pub job_type: Option<String>,
29 #[serde(default = "default_limit")]
30 pub limit: i64,
31 #[serde(default)]
32 pub offset: i64,
33}
34
35#[derive(Debug, Deserialize, Default)]
36pub struct JobSummaryQueryParams {
37 pub namespace: String,
38 pub job_type: Option<String>,
39}
40
41#[derive(Debug, Deserialize, Default)]
42pub struct DigestQueryParams {
43 pub namespace: String,
44 pub session_key: Option<String>,
45 #[serde(default = "default_limit")]
46 pub limit: i64,
47 #[serde(default)]
48 pub offset: i64,
49}
50
51fn default_limit() -> i64 {
52 50
53}
54
55pub async fn list_jobs(
59 State(state): State<Arc<RwLock<AppState>>>,
60 Query(params): Query<JobQueryParams>,
61) -> Result<Json<JobListResponse>> {
62 if params.namespace.trim().is_empty() {
63 return Err(WebError::InvalidRequest(
64 "namespace query parameter is required".to_string(),
65 ));
66 }
67
68 let state = state.read().await;
69
70 let namespace = state
71 .namespace_repo
72 .get_by_name(¶ms.namespace)
73 .await?
74 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
75
76 let limit = params.limit.clamp(1, 200);
77 let offset = params.offset.max(0);
78
79 let rows = state
80 .memory_repo
81 .list_jobs(
82 namespace.id,
83 params.job_type.as_deref(),
84 params.status.as_deref(),
85 limit,
86 offset,
87 )
88 .await?;
89
90 let total = state
91 .memory_repo
92 .count_jobs(
93 namespace.id,
94 params.job_type.as_deref(),
95 params.status.as_deref(),
96 )
97 .await?;
98
99 let jobs: Vec<JobEntry> = rows.into_iter().map(JobEntry::from).collect();
100
101 Ok(Json(JobListResponse {
102 success: true,
103 namespace: params.namespace,
104 jobs,
105 total,
106 }))
107}
108
109pub async fn job_summary(
111 State(state): State<Arc<RwLock<AppState>>>,
112 Query(params): Query<JobSummaryQueryParams>,
113) -> Result<Json<JobSummaryResponse>> {
114 if params.namespace.trim().is_empty() {
115 return Err(WebError::InvalidRequest(
116 "namespace query parameter is required".to_string(),
117 ));
118 }
119
120 let state = state.read().await;
121
122 let namespace = state
123 .namespace_repo
124 .get_by_name(¶ms.namespace)
125 .await?
126 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
127
128 let rows = state
129 .memory_repo
130 .count_jobs_by_status(namespace.id, params.job_type.as_deref())
131 .await?;
132
133 let counts = rows.into_iter().collect();
134
135 Ok(Json(JobSummaryResponse {
136 success: true,
137 namespace: params.namespace,
138 counts,
139 }))
140}
141
142pub async fn list_digests(
144 State(state): State<Arc<RwLock<AppState>>>,
145 Query(params): Query<DigestQueryParams>,
146) -> Result<Json<DigestListResponse>> {
147 if params.namespace.trim().is_empty() {
148 return Err(WebError::InvalidRequest(
149 "namespace query parameter is required".to_string(),
150 ));
151 }
152
153 let state = state.read().await;
154
155 let namespace = state
156 .namespace_repo
157 .get_by_name(¶ms.namespace)
158 .await?
159 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
160
161 let limit = params.limit.clamp(1, 200);
162 let offset = params.offset.max(0);
163
164 let rows = state
165 .memory_repo
166 .list_digests(namespace.id, params.session_key.as_deref(), limit, offset)
167 .await?;
168
169 let total = state
170 .memory_repo
171 .count_digests(namespace.id, params.session_key.as_deref())
172 .await?;
173
174 let digests: Vec<DigestEntry> = rows.into_iter().map(DigestEntry::from).collect();
175
176 Ok(Json(DigestListResponse {
177 success: true,
178 namespace: params.namespace,
179 digests,
180 total,
181 }))
182}
183
184pub async fn runtime_health(
186 State(state): State<Arc<RwLock<AppState>>>,
187) -> Result<Json<RuntimeResponse>> {
188 let state = state.read().await;
189
190 let db_connected = sqlx::query_scalar::<_, i64>("SELECT 1")
192 .fetch_one(state.pool())
193 .await
194 .is_ok();
195
196 let agent_enabled = state.agent_supervisor.is_some();
197 let active_sessions = state.orchestrator.active_session_count().await;
198
199 Ok(Json(RuntimeResponse {
200 success: true,
201 version: env!("CARGO_PKG_VERSION").to_string(),
202 uptime_seconds: state.uptime_seconds(),
203 db_connected,
204 agent_enabled,
205 active_sessions,
206 }))
207}
208
209#[derive(Debug, Deserialize, Default)]
210pub struct OverviewQueryParams {
211 pub namespace: String,
212}
213
214#[derive(Debug, Deserialize, Default)]
215pub struct ReflectionQueryParams {
216 pub namespace: String,
217 #[serde(default = "default_limit")]
218 pub limit: i64,
219}
220
221pub async fn cognition_overview(
223 State(state): State<Arc<RwLock<AppState>>>,
224 Query(params): Query<OverviewQueryParams>,
225) -> Result<Json<CognitionOverviewResponse>> {
226 if params.namespace.trim().is_empty() {
227 return Err(WebError::InvalidRequest(
228 "namespace query parameter is required".to_string(),
229 ));
230 }
231
232 let state = state.read().await;
233
234 let namespace = state
235 .namespace_repo
236 .get_by_name(¶ms.namespace)
237 .await?
238 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
239
240 let status_rows = state
241 .memory_repo
242 .count_jobs_by_status(namespace.id, None)
243 .await?;
244 let jobs_by_status: std::collections::HashMap<String, i64> = status_rows.into_iter().collect();
245
246 let digest_count = state.memory_repo.count_digests(namespace.id, None).await?;
247
248 let evidence_count = state.memory_repo.count_evidence(namespace.id).await?;
249 let stage_metrics = state
250 .memory_repo
251 .latest_metrics_for_namespace(namespace.id, Some("cognition."), 64)
252 .await?
253 .into_iter()
254 .fold(
255 std::collections::HashMap::new(),
256 |mut acc: std::collections::HashMap<String, f64>, metric| {
257 acc.entry(metric.metric_name).or_insert(metric.metric_value);
258 acc
259 },
260 );
261
262 Ok(Json(CognitionOverviewResponse {
263 success: true,
264 namespace: params.namespace,
265 jobs_by_status,
266 digest_count,
267 evidence_count,
268 stage_metrics,
269 }))
270}
271
272pub async fn reflection_state(
274 State(state): State<Arc<RwLock<AppState>>>,
275 Query(params): Query<ReflectionQueryParams>,
276) -> Result<Json<ReflectionStateResponse>> {
277 if params.namespace.trim().is_empty() {
278 return Err(WebError::InvalidRequest(
279 "namespace query parameter is required".to_string(),
280 ));
281 }
282
283 let state = state.read().await;
284
285 let namespace = state
286 .namespace_repo
287 .get_by_name(¶ms.namespace)
288 .await?
289 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
290
291 let limit = params.limit.clamp(1, 50);
292 let contradiction_count = state
293 .memory_repo
294 .count_by_cognitive_level(namespace.id, CognitiveLevel::Contradiction)
295 .await?;
296 let derived_count = state
297 .memory_repo
298 .count_by_cognitive_level(namespace.id, CognitiveLevel::Derived)
299 .await?;
300 let recent_contradictions = state
301 .memory_repo
302 .get_by_cognitive_level(namespace.id, CognitiveLevel::Contradiction, limit)
303 .await?
304 .into_iter()
305 .map(ReflectionSampleEntry::from)
306 .collect();
307 let recent_derived = state
308 .memory_repo
309 .get_by_cognitive_level(namespace.id, CognitiveLevel::Derived, limit)
310 .await?
311 .into_iter()
312 .map(ReflectionSampleEntry::from)
313 .collect();
314
315 Ok(Json(ReflectionStateResponse {
316 success: true,
317 namespace: params.namespace,
318 contradiction_count,
319 derived_count,
320 recent_contradictions,
321 recent_derived,
322 }))
323}
324
325#[derive(Debug, Deserialize, Default)]
328pub struct QueryIntrospectionQueryParams {
329 pub namespace: String,
330 pub question: String,
331}
332
333pub async fn query_introspection(
339 State(state): State<Arc<RwLock<AppState>>>,
340 Query(params): Query<QueryIntrospectionQueryParams>,
341) -> Result<Json<QueryIntrospectionResponse>> {
342 if params.namespace.trim().is_empty() {
343 return Err(WebError::InvalidRequest(
344 "namespace query parameter is required".to_string(),
345 ));
346 }
347 if params.question.trim().is_empty() {
348 return Err(WebError::InvalidRequest(
349 "question query parameter is required".to_string(),
350 ));
351 }
352
353 let state = state.read().await;
354
355 let namespace = state
356 .namespace_repo
357 .get_by_name(¶ms.namespace)
358 .await?
359 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
360
361 let query_context_limit = nexus_core::Config::from_env()
362 .map(|config| config.agent.query_context_limit)
363 .unwrap_or_else(|_| nexus_core::config::AgentConfig::default().query_context_limit);
364
365 let request = nexus_core::WorkingRepresentationRequest {
366 namespace_id: namespace.id,
367 perspective: None,
368 query: Some(params.question.clone()),
369 max_items: query_context_limit,
370 include_raw: false,
371 ..nexus_core::WorkingRepresentationRequest::default()
372 };
373
374 let introspection =
375 nexus_agent::introspect_query(&request, ¶ms.question, &state.memory_repo)
376 .await
377 .map_err(|e| WebError::Storage(format!("Introspection failed: {}", e)))?;
378
379 Ok(Json(QueryIntrospectionResponse {
380 success: true,
381 namespace: params.namespace,
382 question: params.question,
383 introspection,
384 }))
385}
386
387#[derive(Debug, Deserialize, Default)]
390pub struct DashboardQueryParams {
391 pub namespace: String,
392}
393
394pub async fn dashboard(
396 State(state): State<Arc<RwLock<AppState>>>,
397 Query(params): Query<DashboardQueryParams>,
398) -> Result<Json<DashboardResponse>> {
399 if params.namespace.trim().is_empty() {
400 return Err(WebError::InvalidRequest(
401 "namespace query parameter is required".to_string(),
402 ));
403 }
404
405 let state = state.read().await;
406
407 let namespace = state
408 .namespace_repo
409 .get_by_name(¶ms.namespace)
410 .await?
411 .ok_or_else(|| WebError::NotFound(format!("Namespace '{}' not found", params.namespace)))?;
412
413 let completed_reflections = state
415 .memory_repo
416 .count_jobs(namespace.id, Some("reflect_namespace"), Some("completed"))
417 .await?
418 + state
419 .memory_repo
420 .count_jobs(namespace.id, Some("reflect_perspective"), Some("completed"))
421 .await?;
422 let completed_digests = state
423 .memory_repo
424 .count_jobs(namespace.id, Some("digest_session"), Some("completed"))
425 .await?;
426 let failed_jobs = state
427 .memory_repo
428 .count_jobs(namespace.id, None, Some("failed"))
429 .await?;
430 let pending_jobs = state
431 .memory_repo
432 .count_jobs(namespace.id, None, Some("enqueued"))
433 .await?;
434
435 let last_dream_at = {
437 let reflect_jobs = state
438 .memory_repo
439 .list_jobs(
440 namespace.id,
441 Some("reflect_namespace"),
442 Some("completed"),
443 1,
444 0,
445 )
446 .await
447 .unwrap_or_else(|e| {
448 warn!(error = %e, "Failed to list reflection jobs for dashboard");
449 Vec::new()
450 });
451 let digest_jobs = state
452 .memory_repo
453 .list_jobs(
454 namespace.id,
455 Some("digest_session"),
456 Some("completed"),
457 1,
458 0,
459 )
460 .await
461 .unwrap_or_else(|e| {
462 warn!(error = %e, "Failed to list digest jobs for dashboard");
463 Vec::new()
464 });
465 let most_recent = reflect_jobs
466 .iter()
467 .chain(digest_jobs.iter())
468 .max_by_key(|j| j.updated_at.as_str());
469 most_recent.map(|j| j.updated_at.clone())
470 };
471
472 let total_digests = state.memory_repo.count_digests(namespace.id, None).await?;
474 let sessions_with_cognition = state
475 .memory_repo
476 .count_distinct_session_keys_with_cognition(namespace.id)
477 .await?;
478
479 let (latest_digest_at, latest_digest_age_seconds) = {
480 let recent = state
481 .memory_repo
482 .list_digests(namespace.id, None, 1, 0)
483 .await
484 .unwrap_or_else(|e| {
485 warn!(error = %e, "Failed to list digests for dashboard");
486 Vec::new()
487 });
488 match recent.into_iter().next() {
489 Some(d) => match parse_timestamp(&d.created_at) {
490 Some(dt) => {
491 let age = chrono::Utc::now()
492 .signed_duration_since(dt)
493 .num_seconds()
494 .max(0);
495 (Some(d.created_at), Some(age))
496 }
497 None => {
498 warn!(
499 created_at = %d.created_at,
500 "Malformed digest timestamp; returning None for age"
501 );
502 (None, None)
503 }
504 },
505 None => (None, None),
506 }
507 };
508
509 let raw = state
511 .memory_repo
512 .count_by_cognitive_level(namespace.id, CognitiveLevel::Raw)
513 .await?;
514 let explicit = state
515 .memory_repo
516 .count_by_cognitive_level(namespace.id, CognitiveLevel::Explicit)
517 .await?;
518 let derived = state
519 .memory_repo
520 .count_by_cognitive_level(namespace.id, CognitiveLevel::Derived)
521 .await?;
522 let summary_short = state
523 .memory_repo
524 .count_by_cognitive_level(namespace.id, CognitiveLevel::SummaryShort)
525 .await?;
526 let summary_long = state
527 .memory_repo
528 .count_by_cognitive_level(namespace.id, CognitiveLevel::SummaryLong)
529 .await?;
530 let contradiction = state
531 .memory_repo
532 .count_by_cognitive_level(namespace.id, CognitiveLevel::Contradiction)
533 .await?;
534 let total = raw + explicit + derived + summary_short + summary_long + contradiction;
535
536 let cognition_config = match nexus_core::Config::from_env() {
538 Ok(c) => c.cognition,
539 Err(e) => {
540 warn!(error = %e, "Failed to load cognition config for dashboard; using defaults");
541 nexus_core::config::CognitionConfig::default()
542 }
543 };
544 let contradiction_density = if total > 0 {
545 contradiction as f64 / total as f64
546 } else {
547 0.0
548 };
549
550 let base_interval = cognition_config.adaptive_dream_min_interval_secs;
551 let factor = 1.0 - ((contradiction as f32 * 0.10).min(0.9));
552 let adapted = (base_interval as f32 * factor) as u64;
553 let current_interval_secs = adapted.clamp(
554 cognition_config.adaptive_dream_min_interval_secs,
555 cognition_config.adaptive_dream_max_interval_secs,
556 );
557
558 Ok(Json(DashboardResponse {
559 success: true,
560 namespace: params.namespace,
561 dream: DreamState {
562 completed_reflections,
563 completed_digests,
564 failed_jobs,
565 pending_jobs,
566 last_dream_at,
567 },
568 digest: DigestFreshnessState {
569 total_digests,
570 sessions_with_cognition,
571 latest_digest_age_seconds,
572 latest_digest_at,
573 },
574 recall: RecallComposition {
575 raw,
576 explicit,
577 derived,
578 summary_short,
579 summary_long,
580 contradiction,
581 total,
582 },
583 adaptive: AdaptiveDreamState {
584 enabled: cognition_config.adaptive_dream_enabled,
585 current_interval_secs,
586 min_interval_secs: cognition_config.adaptive_dream_min_interval_secs,
587 max_interval_secs: cognition_config.adaptive_dream_max_interval_secs,
588 contradiction_count: contradiction,
589 contradiction_density,
590 },
591 }))
592}
593
594fn parse_timestamp(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
599 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
601 return Some(dt.with_timezone(&chrono::Utc));
602 }
603 if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
605 return Some(naive.and_utc());
606 }
607 if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
609 return Some(naive.and_utc());
610 }
611 None
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use axum::body::Body;
618 use axum::http::{Request, StatusCode};
619 use axum::routing::get;
620 use axum::Router;
621 use nexus_orchestrator::Orchestrator;
622 use serde_json::Value;
623 use std::sync::Arc;
624 use tower::ServiceExt;
625
626 struct TestApp {
627 app: Router,
628 state: Arc<RwLock<crate::state::AppState>>,
629 }
630
631 async fn test_app() -> TestApp {
632 let pool = sqlx::SqlitePool::connect("sqlite::memory:")
633 .await
634 .expect("connect to in-memory db");
635 nexus_storage::migrations::run_migrations(&pool)
636 .await
637 .expect("run migrations");
638
639 let mut storage = nexus_storage::StorageManager::new(pool.clone());
640 storage.initialize().await.expect("initialize storage");
641
642 let orchestrator = Orchestrator::default();
643 let state = Arc::new(RwLock::new(
644 crate::state::AppState::new(storage, orchestrator)
645 .await
646 .expect("create app state"),
647 ));
648
649 let app = Router::new()
650 .route("/api/cognition/jobs", get(list_jobs))
651 .route("/api/cognition/jobs/summary", get(job_summary))
652 .route("/api/cognition/digests", get(list_digests))
653 .route("/api/cognition/overview", get(cognition_overview))
654 .route("/api/cognition/reflection", get(reflection_state))
655 .route("/api/cognition/runtime", get(runtime_health))
656 .route(
657 "/api/cognition/query-introspection",
658 get(query_introspection),
659 )
660 .route("/api/cognition/dashboard", get(dashboard))
661 .with_state(state.clone());
662
663 TestApp { app, state }
664 }
665
666 async fn create_namespace_in_test(state: &Arc<RwLock<crate::state::AppState>>, name: &str) {
668 let s = state.read().await;
669 s.namespace_repo
670 .get_or_create(name, "test-agent")
671 .await
672 .expect("create namespace");
673 }
674
675 fn body_to_json(body: axum::body::Bytes) -> Value {
677 serde_json::from_slice(&body).expect("valid JSON")
678 }
679
680 #[tokio::test]
681 async fn test_runtime_returns_honest_fields() {
682 let test = test_app().await;
683 let resp = test
684 .app
685 .oneshot(
686 Request::builder()
687 .uri("/api/cognition/runtime")
688 .body(Body::empty())
689 .unwrap(),
690 )
691 .await
692 .unwrap();
693
694 assert_eq!(resp.status(), StatusCode::OK);
695 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
696 .await
697 .unwrap();
698 let json = body_to_json(body);
699
700 assert_eq!(json["success"], true);
701 assert!(!json["version"].as_str().unwrap().is_empty());
702 assert!(json["uptime_seconds"].as_u64().is_some());
703 assert!(json["db_connected"].is_boolean());
704 assert!(json["agent_enabled"].is_boolean());
705 }
706
707 #[tokio::test]
708 async fn test_query_introspection_missing_question_returns_400() {
709 let test = test_app().await;
710 create_namespace_in_test(&test.state, "intro-missing").await;
711
712 let resp = test
713 .app
714 .oneshot(
715 Request::builder()
716 .uri("/api/cognition/query-introspection?namespace=intro-missing")
717 .body(Body::empty())
718 .unwrap(),
719 )
720 .await
721 .unwrap();
722
723 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
724 }
725
726 #[tokio::test]
727 async fn test_query_introspection_returns_structured_payload() {
728 let test = test_app().await;
729 create_namespace_in_test(&test.state, "intro-ns").await;
730
731 {
732 let state = test.state.read().await;
733 let namespace = state
734 .namespace_repo
735 .get_by_name("intro-ns")
736 .await
737 .unwrap()
738 .unwrap();
739
740 state
741 .memory_repo
742 .store(nexus_storage::StoreMemoryParams {
743 namespace_id: namespace.id,
744 content: "Authentication now uses session cookies with http-only flags.",
745 category: &nexus_core::MemoryCategory::Facts,
746 memory_lane_type: None,
747 labels: &[],
748 metadata: &serde_json::json!({
749 "cognitive": {
750 "level": "explicit",
751 "observer": "claude-code",
752 "subject": "claude-code",
753 "generated_by": "test_fixture",
754 "confidence": 0.92
755 }
756 }),
757 embedding: None,
758 embedding_model: None,
759 })
760 .await
761 .unwrap();
762 }
763
764 let resp = test
765 .app
766 .oneshot(
767 Request::builder()
768 .uri("/api/cognition/query-introspection?namespace=intro-ns&question=session%20cookies")
769 .body(Body::empty())
770 .unwrap(),
771 )
772 .await
773 .unwrap();
774
775 assert_eq!(resp.status(), StatusCode::OK);
776 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
777 .await
778 .unwrap();
779 let json = body_to_json(body);
780
781 assert_eq!(json["success"], true);
782 assert_eq!(json["namespace"], "intro-ns");
783 assert_eq!(json["question"], "session cookies");
784 assert!(json["introspection"]["included"].is_array());
785 assert!(!json["introspection"]["included"]
786 .as_array()
787 .unwrap()
788 .is_empty());
789 assert!(json["introspection"]["bucket_stats"].is_array());
790 }
791
792 #[tokio::test]
793 async fn test_jobs_missing_namespace_returns_400() {
794 let test = test_app().await;
795 let resp = test
796 .app
797 .oneshot(
798 Request::builder()
799 .uri("/api/cognition/jobs")
800 .body(Body::empty())
801 .unwrap(),
802 )
803 .await
804 .unwrap();
805 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
806 }
807
808 #[tokio::test]
809 async fn test_jobs_unknown_namespace_returns_404() {
810 let test = test_app().await;
811 let resp = test
812 .app
813 .oneshot(
814 Request::builder()
815 .uri("/api/cognition/jobs?namespace=nonexistent")
816 .body(Body::empty())
817 .unwrap(),
818 )
819 .await
820 .unwrap();
821 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
822 }
823
824 #[tokio::test]
825 async fn test_reflection_missing_namespace_returns_400() {
826 let test = test_app().await;
827 let resp = test
828 .app
829 .oneshot(
830 Request::builder()
831 .uri("/api/cognition/reflection")
832 .body(Body::empty())
833 .unwrap(),
834 )
835 .await
836 .unwrap();
837 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
838 }
839
840 #[tokio::test]
841 async fn test_reflection_returns_counts_and_samples() {
842 let test = test_app().await;
843 create_namespace_in_test(&test.state, "reflect-ns").await;
844
845 {
846 let state = test.state.read().await;
847 let namespace = state
848 .namespace_repo
849 .get_by_name("reflect-ns")
850 .await
851 .unwrap()
852 .unwrap();
853
854 for (content, level) in [
855 ("derived insight", CognitiveLevel::Derived),
856 ("contradiction note", CognitiveLevel::Contradiction),
857 ] {
858 state
859 .memory_repo
860 .store(nexus_storage::repository::StoreMemoryParams {
861 namespace_id: namespace.id,
862 content,
863 category: &nexus_core::MemoryCategory::Facts,
864 memory_lane_type: None,
865 labels: &[],
866 metadata: &serde_json::json!({
867 "cognitive": {
868 "level": level.as_str(),
869 "observer": "claude-code",
870 "subject": "claude-code",
871 "confidence": 0.9,
872 "generated_by": "test"
873 }
874 }),
875 embedding: None,
876 embedding_model: None,
877 })
878 .await
879 .unwrap();
880 }
881 }
882
883 let resp = test
884 .app
885 .oneshot(
886 Request::builder()
887 .uri("/api/cognition/reflection?namespace=reflect-ns")
888 .body(Body::empty())
889 .unwrap(),
890 )
891 .await
892 .unwrap();
893
894 assert_eq!(resp.status(), StatusCode::OK);
895 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
896 .await
897 .unwrap();
898 let json = body_to_json(body);
899
900 assert_eq!(json["success"], true);
901 assert_eq!(json["derived_count"], 1);
902 assert_eq!(json["contradiction_count"], 1);
903 assert_eq!(json["recent_derived"][0]["content"], "derived insight");
904 assert_eq!(
905 json["recent_contradictions"][0]["content"],
906 "contradiction note"
907 );
908 }
909
910 #[tokio::test]
911 async fn test_overview_returns_latest_stage_metrics() {
912 let test = test_app().await;
913 create_namespace_in_test(&test.state, "overview-ns").await;
914
915 {
916 let state = test.state.read().await;
917 let namespace = state
918 .namespace_repo
919 .get_by_name("overview-ns")
920 .await
921 .unwrap()
922 .unwrap();
923
924 state
925 .memory_repo
926 .record_metric(
927 "cognition.query.total_ms",
928 11.0,
929 &serde_json::json!({"namespace_id": namespace.id, "stage": "total", "unit": "ms"}),
930 )
931 .await
932 .unwrap();
933 state
934 .memory_repo
935 .record_metric(
936 "cognition.query.total_ms",
937 15.5,
938 &serde_json::json!({"namespace_id": namespace.id, "stage": "total", "unit": "ms"}),
939 )
940 .await
941 .unwrap();
942 state
943 .memory_repo
944 .record_metric(
945 "cognition.dream.total_ms",
946 44.0,
947 &serde_json::json!({"namespace_id": namespace.id, "stage": "total", "unit": "ms"}),
948 )
949 .await
950 .unwrap();
951 }
952
953 let resp = test
954 .app
955 .oneshot(
956 Request::builder()
957 .uri("/api/cognition/overview?namespace=overview-ns")
958 .body(Body::empty())
959 .unwrap(),
960 )
961 .await
962 .unwrap();
963
964 assert_eq!(resp.status(), StatusCode::OK);
965 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
966 .await
967 .unwrap();
968 let json = body_to_json(body);
969
970 assert_eq!(json["success"], true);
971 assert_eq!(json["stage_metrics"]["cognition.query.total_ms"], 15.5);
972 assert_eq!(json["stage_metrics"]["cognition.dream.total_ms"], 44.0);
973 }
974
975 #[tokio::test]
976 async fn test_digests_missing_namespace_returns_400() {
977 let test = test_app().await;
978 let resp = test
979 .app
980 .oneshot(
981 Request::builder()
982 .uri("/api/cognition/digests")
983 .body(Body::empty())
984 .unwrap(),
985 )
986 .await
987 .unwrap();
988 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
989 }
990
991 #[tokio::test]
992 async fn test_overview_unknown_namespace_returns_404() {
993 let test = test_app().await;
994 let resp = test
995 .app
996 .oneshot(
997 Request::builder()
998 .uri("/api/cognition/overview?namespace=nope")
999 .body(Body::empty())
1000 .unwrap(),
1001 )
1002 .await
1003 .unwrap();
1004 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1005 }
1006
1007 #[tokio::test]
1008 async fn test_jobs_with_existing_namespace_returns_empty_list() {
1009 let test = test_app().await;
1010 create_namespace_in_test(&test.state, "jobs-test-ns").await;
1011
1012 let resp = test
1013 .app
1014 .oneshot(
1015 Request::builder()
1016 .uri("/api/cognition/jobs?namespace=jobs-test-ns")
1017 .body(Body::empty())
1018 .unwrap(),
1019 )
1020 .await
1021 .unwrap();
1022
1023 assert_eq!(resp.status(), StatusCode::OK);
1024 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1025 .await
1026 .unwrap();
1027 let json = body_to_json(body);
1028
1029 assert_eq!(json["success"], true);
1030 assert_eq!(json["namespace"], "jobs-test-ns");
1031 assert_eq!(json["jobs"], Value::Array(vec![]));
1032 assert_eq!(json["total"], 0);
1033 }
1034
1035 #[tokio::test]
1036 async fn test_jobs_reports_total_matching_rows_not_page_len() {
1037 let test = test_app().await;
1038 create_namespace_in_test(&test.state, "jobs-page-ns").await;
1039
1040 {
1041 let state = test.state.read().await;
1042 let namespace = state
1043 .namespace_repo
1044 .get_by_name("jobs-page-ns")
1045 .await
1046 .unwrap()
1047 .unwrap();
1048
1049 for idx in 0..3 {
1050 state
1051 .memory_repo
1052 .enqueue_job(nexus_storage::EnqueueJobParams {
1053 namespace_id: namespace.id,
1054 job_type: "derive",
1055 priority: 10 - idx,
1056 perspective: None,
1057 payload: &serde_json::json!({ "idx": idx }),
1058 })
1059 .await
1060 .unwrap();
1061 }
1062 }
1063
1064 let resp = test
1065 .app
1066 .oneshot(
1067 Request::builder()
1068 .uri("/api/cognition/jobs?namespace=jobs-page-ns&limit=2")
1069 .body(Body::empty())
1070 .unwrap(),
1071 )
1072 .await
1073 .unwrap();
1074
1075 assert_eq!(resp.status(), StatusCode::OK);
1076 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1077 .await
1078 .unwrap();
1079 let json = body_to_json(body);
1080
1081 assert_eq!(json["success"], true);
1082 assert_eq!(json["jobs"].as_array().unwrap().len(), 2);
1083 assert_eq!(json["total"], 3);
1084 }
1085
1086 #[tokio::test]
1087 async fn test_overview_with_existing_namespace() {
1088 let test = test_app().await;
1089 create_namespace_in_test(&test.state, "overview-test-ns").await;
1090
1091 let resp = test
1092 .app
1093 .oneshot(
1094 Request::builder()
1095 .uri("/api/cognition/overview?namespace=overview-test-ns")
1096 .body(Body::empty())
1097 .unwrap(),
1098 )
1099 .await
1100 .unwrap();
1101
1102 assert_eq!(resp.status(), StatusCode::OK);
1103 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1104 .await
1105 .unwrap();
1106 let json = body_to_json(body);
1107
1108 assert_eq!(json["success"], true);
1109 assert_eq!(json["namespace"], "overview-test-ns");
1110 assert_eq!(json["digest_count"], 0);
1111 assert_eq!(json["evidence_count"], 0);
1112 }
1113
1114 #[tokio::test]
1115 async fn test_job_summary_with_existing_namespace() {
1116 let test = test_app().await;
1117 create_namespace_in_test(&test.state, "summary-test-ns").await;
1118
1119 let resp = test
1120 .app
1121 .oneshot(
1122 Request::builder()
1123 .uri("/api/cognition/jobs/summary?namespace=summary-test-ns")
1124 .body(Body::empty())
1125 .unwrap(),
1126 )
1127 .await
1128 .unwrap();
1129
1130 assert_eq!(resp.status(), StatusCode::OK);
1131 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1132 .await
1133 .unwrap();
1134 let json = body_to_json(body);
1135
1136 assert_eq!(json["success"], true);
1137 assert_eq!(json["namespace"], "summary-test-ns");
1138 assert!(json["counts"].is_object());
1139 }
1140
1141 #[tokio::test]
1142 async fn test_job_summary_returns_real_counts() {
1143 let test = test_app().await;
1144 create_namespace_in_test(&test.state, "summary-data-ns").await;
1145
1146 {
1147 let state = test.state.read().await;
1148 let namespace = state
1149 .namespace_repo
1150 .get_by_name("summary-data-ns")
1151 .await
1152 .unwrap()
1153 .unwrap();
1154
1155 state
1156 .memory_repo
1157 .enqueue_job(nexus_storage::EnqueueJobParams {
1158 namespace_id: namespace.id,
1159 job_type: "derive",
1160 priority: 10,
1161 perspective: None,
1162 payload: &serde_json::json!({ "idx": 1 }),
1163 })
1164 .await
1165 .unwrap();
1166 state
1167 .memory_repo
1168 .enqueue_job(nexus_storage::EnqueueJobParams {
1169 namespace_id: namespace.id,
1170 job_type: "digest",
1171 priority: 5,
1172 perspective: None,
1173 payload: &serde_json::json!({ "idx": 2 }),
1174 })
1175 .await
1176 .unwrap();
1177 }
1178
1179 let resp = test
1180 .app
1181 .oneshot(
1182 Request::builder()
1183 .uri("/api/cognition/jobs/summary?namespace=summary-data-ns")
1184 .body(Body::empty())
1185 .unwrap(),
1186 )
1187 .await
1188 .unwrap();
1189
1190 assert_eq!(resp.status(), StatusCode::OK);
1191 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1192 .await
1193 .unwrap();
1194 let json = body_to_json(body);
1195
1196 assert_eq!(json["success"], true);
1197 assert_eq!(json["counts"]["pending"], 2);
1198 }
1199
1200 #[tokio::test]
1201 async fn test_digests_with_existing_namespace() {
1202 let test = test_app().await;
1203 create_namespace_in_test(&test.state, "digest-test-ns").await;
1204
1205 let resp = test
1206 .app
1207 .oneshot(
1208 Request::builder()
1209 .uri("/api/cognition/digests?namespace=digest-test-ns")
1210 .body(Body::empty())
1211 .unwrap(),
1212 )
1213 .await
1214 .unwrap();
1215
1216 assert_eq!(resp.status(), StatusCode::OK);
1217 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1218 .await
1219 .unwrap();
1220 let json = body_to_json(body);
1221
1222 assert_eq!(json["success"], true);
1223 assert_eq!(json["namespace"], "digest-test-ns");
1224 assert_eq!(json["total"], 0);
1225 }
1226
1227 #[tokio::test]
1228 async fn test_digests_support_pagination_and_total() {
1229 let test = test_app().await;
1230 create_namespace_in_test(&test.state, "digest-page-ns").await;
1231
1232 {
1233 let state = test.state.read().await;
1234 let namespace = state
1235 .namespace_repo
1236 .get_by_name("digest-page-ns")
1237 .await
1238 .unwrap()
1239 .unwrap();
1240
1241 for idx in 0..3 {
1242 let content = format!("digest memory {idx}");
1243 let memory = state
1244 .memory_repo
1245 .store(nexus_storage::StoreMemoryParams {
1246 namespace_id: namespace.id,
1247 content: &content,
1248 category: &nexus_core::MemoryCategory::Session,
1249 memory_lane_type: None,
1250 labels: &[],
1251 metadata: &serde_json::json!({}),
1252 embedding: None,
1253 embedding_model: None,
1254 })
1255 .await
1256 .unwrap();
1257
1258 state
1259 .memory_repo
1260 .store_digest(nexus_storage::StoreDigestParams {
1261 namespace_id: namespace.id,
1262 session_key: "digest-session",
1263 digest_kind: if idx % 2 == 0 {
1264 "summary_short"
1265 } else {
1266 "summary_long"
1267 },
1268 memory_id: memory.id,
1269 start_memory_id: Some(memory.id),
1270 end_memory_id: Some(memory.id),
1271 token_count: 100 + idx,
1272 })
1273 .await
1274 .unwrap();
1275 }
1276 }
1277
1278 let resp = test
1279 .app
1280 .oneshot(
1281 Request::builder()
1282 .uri("/api/cognition/digests?namespace=digest-page-ns&limit=2")
1283 .body(Body::empty())
1284 .unwrap(),
1285 )
1286 .await
1287 .unwrap();
1288
1289 assert_eq!(resp.status(), StatusCode::OK);
1290 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1291 .await
1292 .unwrap();
1293 let json = body_to_json(body);
1294
1295 assert_eq!(json["success"], true);
1296 assert_eq!(json["digests"].as_array().unwrap().len(), 2);
1297 assert_eq!(json["total"], 3);
1298 }
1299
1300 #[test]
1301 fn test_job_entry_serialization() {
1302 let job = JobEntry {
1303 id: 1,
1304 job_type: "derive".to_string(),
1305 status: "pending".to_string(),
1306 priority: 10,
1307 attempts: 0,
1308 last_error: None,
1309 lease_owner: Some("worker-1".to_string()),
1310 lease_expires_at: None,
1311 created_at: "2026-01-01T00:00:00Z".to_string(),
1312 updated_at: "2026-01-01T00:00:00Z".to_string(),
1313 };
1314 let json = serde_json::to_value(&job).unwrap();
1315 assert_eq!(json["id"], 1);
1316 assert_eq!(json["job_type"], "derive");
1317 assert_eq!(json["lease_owner"], "worker-1");
1318 assert!(json["last_error"].is_null());
1319 }
1320
1321 #[test]
1322 fn test_digest_entry_serialization() {
1323 let digest = DigestEntry {
1324 id: 1,
1325 session_key: "sess-1".to_string(),
1326 digest_kind: "summary_short".to_string(),
1327 memory_id: 42,
1328 start_memory_id: Some(1),
1329 end_memory_id: Some(10),
1330 token_count: 200,
1331 created_at: "2026-01-01T00:00:00Z".to_string(),
1332 };
1333 let json = serde_json::to_value(&digest).unwrap();
1334 assert_eq!(json["session_key"], "sess-1");
1335 assert_eq!(json["memory_id"], 42);
1336 assert_eq!(json["token_count"], 200);
1337 }
1338
1339 #[tokio::test]
1340 async fn test_overview_with_enqueued_job_and_evidence() {
1341 let test = test_app().await;
1342 create_namespace_in_test(&test.state, "data-test-ns").await;
1343
1344 {
1345 let s = test.state.read().await;
1346 let ns = s
1347 .namespace_repo
1348 .get_by_name("data-test-ns")
1349 .await
1350 .unwrap()
1351 .expect("namespace exists");
1352
1353 let mem_id = s
1354 .memory_repo
1355 .store(nexus_storage::StoreMemoryParams {
1356 namespace_id: ns.id,
1357 content: "test memory for evidence",
1358 category: &nexus_core::MemoryCategory::Session,
1359 memory_lane_type: None,
1360 labels: &[],
1361 metadata: &serde_json::json!({}),
1362 embedding: None,
1363 embedding_model: None,
1364 })
1365 .await
1366 .unwrap();
1367
1368 s.memory_repo
1369 .enqueue_job(nexus_storage::EnqueueJobParams {
1370 namespace_id: ns.id,
1371 job_type: "derive",
1372 priority: 5,
1373 perspective: None,
1374 payload: &serde_json::json!({"test": true}),
1375 })
1376 .await
1377 .unwrap();
1378
1379 s.memory_repo
1380 .store_with_lineage(nexus_storage::StoreMemoryWithLineageParams {
1381 store: nexus_storage::StoreMemoryParams {
1382 namespace_id: ns.id,
1383 content: "derived from evidence",
1384 category: &nexus_core::MemoryCategory::Facts,
1385 memory_lane_type: None,
1386 labels: &[],
1387 metadata: &serde_json::json!({"cognitive": {"level": "derived"}}),
1388 embedding: None,
1389 embedding_model: None,
1390 },
1391 source_memory_ids: &[mem_id.id],
1392 evidence_role: "source",
1393 })
1394 .await
1395 .unwrap();
1396
1397 let _ = mem_id;
1398 }
1399
1400 let resp = test
1401 .app
1402 .oneshot(
1403 Request::builder()
1404 .uri("/api/cognition/overview?namespace=data-test-ns")
1405 .body(Body::empty())
1406 .unwrap(),
1407 )
1408 .await
1409 .unwrap();
1410
1411 assert_eq!(resp.status(), StatusCode::OK);
1412 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1413 .await
1414 .unwrap();
1415 let json = body_to_json(body);
1416
1417 assert_eq!(json["success"], true);
1418 assert_eq!(json["jobs_by_status"]["pending"], 1);
1419 assert!(json["evidence_count"].as_i64().unwrap() >= 1);
1420 }
1421
1422 #[tokio::test]
1425 async fn test_dashboard_missing_namespace_returns_400() {
1426 let test = test_app().await;
1427 let resp = test
1428 .app
1429 .oneshot(
1430 Request::builder()
1431 .uri("/api/cognition/dashboard")
1432 .body(Body::empty())
1433 .unwrap(),
1434 )
1435 .await
1436 .unwrap();
1437 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1438 }
1439
1440 #[tokio::test]
1441 async fn test_dashboard_unknown_namespace_returns_404() {
1442 let test = test_app().await;
1443 let resp = test
1444 .app
1445 .oneshot(
1446 Request::builder()
1447 .uri("/api/cognition/dashboard?namespace=nonexistent")
1448 .body(Body::empty())
1449 .unwrap(),
1450 )
1451 .await
1452 .unwrap();
1453 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1454 }
1455
1456 #[tokio::test]
1457 async fn test_dashboard_returns_all_sections() {
1458 let test = test_app().await;
1459 create_namespace_in_test(&test.state, "dash-empty-ns").await;
1460
1461 let resp = test
1462 .app
1463 .oneshot(
1464 Request::builder()
1465 .uri("/api/cognition/dashboard?namespace=dash-empty-ns")
1466 .body(Body::empty())
1467 .unwrap(),
1468 )
1469 .await
1470 .unwrap();
1471
1472 assert_eq!(resp.status(), StatusCode::OK);
1473 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1474 .await
1475 .unwrap();
1476 let json = body_to_json(body);
1477
1478 assert_eq!(json["success"], true);
1479 assert_eq!(json["namespace"], "dash-empty-ns");
1480
1481 assert_eq!(json["dream"]["completed_reflections"], 0);
1483 assert_eq!(json["dream"]["completed_digests"], 0);
1484 assert_eq!(json["dream"]["failed_jobs"], 0);
1485 assert_eq!(json["dream"]["pending_jobs"], 0);
1486 assert!(json["dream"]["last_dream_at"].is_null());
1487
1488 assert_eq!(json["digest"]["total_digests"], 0);
1490 assert_eq!(json["digest"]["sessions_with_cognition"], 0);
1491 assert!(json["digest"]["latest_digest_at"].is_null());
1492 assert!(json["digest"]["latest_digest_age_seconds"].is_null());
1493
1494 assert_eq!(json["recall"]["raw"], 0);
1496 assert_eq!(json["recall"]["explicit"], 0);
1497 assert_eq!(json["recall"]["contradiction"], 0);
1498 assert_eq!(json["recall"]["total"], 0);
1499
1500 assert!(json["adaptive"]["enabled"].is_boolean());
1502 assert!(json["adaptive"]["current_interval_secs"].as_u64().is_some());
1503 assert!(json["adaptive"]["contradiction_density"].is_number());
1504 }
1505
1506 #[tokio::test]
1507 async fn test_dashboard_populates_recall_and_dream_from_data() {
1508 let test = test_app().await;
1509 create_namespace_in_test(&test.state, "dash-data-ns").await;
1510
1511 {
1512 let state = test.state.read().await;
1513 let namespace = state
1514 .namespace_repo
1515 .get_by_name("dash-data-ns")
1516 .await
1517 .unwrap()
1518 .unwrap();
1519
1520 for (content, level) in [
1522 ("raw event", CognitiveLevel::Raw),
1523 ("explicit fact", CognitiveLevel::Explicit),
1524 ("derived insight", CognitiveLevel::Derived),
1525 ("contradiction note", CognitiveLevel::Contradiction),
1526 ] {
1527 state
1528 .memory_repo
1529 .store(nexus_storage::repository::StoreMemoryParams {
1530 namespace_id: namespace.id,
1531 content,
1532 category: &nexus_core::MemoryCategory::Facts,
1533 memory_lane_type: None,
1534 labels: &[],
1535 metadata: &serde_json::json!({
1536 "cognitive": {
1537 "level": level.as_str(),
1538 "observer": "claude-code",
1539 "subject": "claude-code",
1540 "confidence": 0.9,
1541 "generated_by": "test"
1542 }
1543 }),
1544 embedding: None,
1545 embedding_model: None,
1546 })
1547 .await
1548 .unwrap();
1549 }
1550 }
1551
1552 let resp = test
1553 .app
1554 .oneshot(
1555 Request::builder()
1556 .uri("/api/cognition/dashboard?namespace=dash-data-ns")
1557 .body(Body::empty())
1558 .unwrap(),
1559 )
1560 .await
1561 .unwrap();
1562
1563 assert_eq!(resp.status(), StatusCode::OK);
1564 let body = axum::body::to_bytes(resp.into_body(), 1_000_000)
1565 .await
1566 .unwrap();
1567 let json = body_to_json(body);
1568
1569 assert_eq!(json["recall"]["raw"], 1);
1570 assert_eq!(json["recall"]["explicit"], 1);
1571 assert_eq!(json["recall"]["derived"], 1);
1572 assert_eq!(json["recall"]["contradiction"], 1);
1573 assert_eq!(json["recall"]["total"], 4);
1574 assert!((json["adaptive"]["contradiction_density"].as_f64().unwrap() - 0.25).abs() < 0.01);
1576 }
1577
1578 #[test]
1579 fn test_dashboard_response_serialization_roundtrip() {
1580 let dash = DashboardResponse {
1581 success: true,
1582 namespace: "test".to_string(),
1583 dream: DreamState {
1584 completed_reflections: 5,
1585 completed_digests: 3,
1586 failed_jobs: 1,
1587 pending_jobs: 2,
1588 last_dream_at: Some("2026-03-27T12:00:00Z".to_string()),
1589 },
1590 digest: DigestFreshnessState {
1591 total_digests: 10,
1592 sessions_with_cognition: 4,
1593 latest_digest_age_seconds: Some(3600),
1594 latest_digest_at: Some("2026-03-27T11:00:00Z".to_string()),
1595 },
1596 recall: RecallComposition {
1597 raw: 50,
1598 explicit: 30,
1599 derived: 10,
1600 summary_short: 5,
1601 summary_long: 3,
1602 contradiction: 2,
1603 total: 100,
1604 },
1605 adaptive: AdaptiveDreamState {
1606 enabled: true,
1607 current_interval_secs: 120,
1608 min_interval_secs: 60,
1609 max_interval_secs: 600,
1610 contradiction_count: 2,
1611 contradiction_density: 0.02,
1612 },
1613 };
1614 let json = serde_json::to_value(&dash).unwrap();
1615 assert_eq!(json["success"], true);
1616 assert_eq!(json["dream"]["completed_reflections"], 5);
1617 assert_eq!(json["recall"]["total"], 100);
1618 assert_eq!(json["adaptive"]["enabled"], true);
1619 assert_eq!(json["adaptive"]["current_interval_secs"], 120);
1620
1621 let deserialized: DashboardResponse = serde_json::from_value(json).unwrap();
1623 assert_eq!(deserialized.dream.completed_reflections, 5);
1624 assert_eq!(deserialized.recall.total, 100);
1625 }
1626}