@@ -368,3 +368,260 @@ async fn test_logogen_export_invalid_base64() {
368368 . unwrap( )
369369 . contains( "Icon export failed" ) ) ;
370370}
371+
372+ // ── Metrics endpoints ────────────────────────────────────────────────
373+
374+ #[ tokio:: test]
375+ async fn test_metrics_summary_returns_empty_when_no_tasks ( ) {
376+ let ( url, _handle) = start_test_server ( ) . await ;
377+ let client = Client :: new ( ) ;
378+ let resp = client
379+ . get ( format ! ( "{url}/api/metrics" ) )
380+ . send ( )
381+ . await
382+ . unwrap ( ) ;
383+ assert_eq ! ( resp. status( ) , 200 ) ;
384+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
385+ assert_eq ! ( body[ "total_cost_usd" ] , 0.0 ) ;
386+ assert_eq ! ( body[ "total_tokens" ] , 0 ) ;
387+ assert_eq ! ( body[ "total_tasks" ] , 0 ) ;
388+ assert_eq ! ( body[ "total_llm_calls" ] , 0 ) ;
389+ assert ! ( body[ "by_agent" ] . as_array( ) . unwrap( ) . is_empty( ) ) ;
390+ assert ! ( body[ "by_model" ] . as_array( ) . unwrap( ) . is_empty( ) ) ;
391+ }
392+
393+ #[ tokio:: test]
394+ async fn test_metrics_task_not_found ( ) {
395+ let ( url, _handle) = start_test_server ( ) . await ;
396+ let client = Client :: new ( ) ;
397+ let resp = client
398+ . get ( format ! ( "{url}/api/metrics/99999" ) )
399+ . send ( )
400+ . await
401+ . unwrap ( ) ;
402+ assert_eq ! ( resp. status( ) , 404 ) ;
403+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
404+ assert ! ( body[ "error" ]
405+ . as_str( )
406+ . unwrap( )
407+ . contains( "No metrics found for task 99999" ) ) ;
408+ }
409+
410+ // ── Sync endpoints (503 when cloud_client: None) ─────────────────────
411+
412+ #[ tokio:: test]
413+ async fn test_sync_push_no_cloud_returns_503 ( ) {
414+ let ( url, _handle) = start_test_server ( ) . await ;
415+ let client = Client :: new ( ) ;
416+ let resp = client
417+ . post ( format ! ( "{url}/api/sync/push" ) )
418+ . json ( & json ! ( { } ) )
419+ . send ( )
420+ . await
421+ . unwrap ( ) ;
422+ assert_eq ! ( resp. status( ) , 503 ) ;
423+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
424+ assert ! ( body[ "error" ] . as_str( ) . unwrap( ) . contains( "not available" ) ) ;
425+ }
426+
427+ #[ tokio:: test]
428+ async fn test_sync_pull_no_cloud_returns_503 ( ) {
429+ let ( url, _handle) = start_test_server ( ) . await ;
430+ let client = Client :: new ( ) ;
431+ let resp = client
432+ . post ( format ! ( "{url}/api/sync/pull" ) )
433+ . json ( & json ! ( { } ) )
434+ . send ( )
435+ . await
436+ . unwrap ( ) ;
437+ assert_eq ! ( resp. status( ) , 503 ) ;
438+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
439+ assert ! ( body[ "error" ] . as_str( ) . unwrap( ) . contains( "not available" ) ) ;
440+ }
441+
442+ #[ tokio:: test]
443+ async fn test_sync_now_no_cloud_returns_503 ( ) {
444+ let ( url, _handle) = start_test_server ( ) . await ;
445+ let client = Client :: new ( ) ;
446+ let resp = client
447+ . post ( format ! ( "{url}/api/sync/now" ) )
448+ . send ( )
449+ . await
450+ . unwrap ( ) ;
451+ assert_eq ! ( resp. status( ) , 503 ) ;
452+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
453+ assert ! ( body[ "error" ] . as_str( ) . unwrap( ) . contains( "not available" ) ) ;
454+ }
455+
456+ // ── Templates endpoint ───────────────────────────────────────────────
457+
458+ #[ tokio:: test]
459+ async fn test_templates_no_cloud_returns_503 ( ) {
460+ let ( url, _handle) = start_test_server ( ) . await ;
461+ let client = Client :: new ( ) ;
462+ let resp = client
463+ . get ( format ! ( "{url}/api/templates" ) )
464+ . send ( )
465+ . await
466+ . unwrap ( ) ;
467+ assert_eq ! ( resp. status( ) , 503 ) ;
468+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
469+ assert ! ( body[ "error" ] . as_str( ) . unwrap( ) . contains( "not available" ) ) ;
470+ }
471+
472+ // ── Auth endpoints ───────────────────────────────────────────────────
473+
474+ #[ tokio:: test]
475+ async fn test_auth_login_no_cloud_returns_503 ( ) {
476+ let ( url, _handle) = start_test_server ( ) . await ;
477+ let client = Client :: new ( ) ;
478+ let resp = client
479+ . post ( format ! ( "{url}/api/auth/login" ) )
480+ . send ( )
481+ . await
482+ . unwrap ( ) ;
483+ assert_eq ! ( resp. status( ) , 503 ) ;
484+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
485+ assert ! ( body[ "error" ] . as_str( ) . unwrap( ) . contains( "not available" ) ) ;
486+ }
487+
488+ #[ tokio:: test]
489+ async fn test_auth_profile_unauthenticated_returns_401 ( ) {
490+ let ( url, _handle) = start_test_server ( ) . await ;
491+ let client = Client :: new ( ) ;
492+ let resp = client
493+ . get ( format ! ( "{url}/api/auth/profile" ) )
494+ . send ( )
495+ . await
496+ . unwrap ( ) ;
497+ assert_eq ! ( resp. status( ) , 401 ) ;
498+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
499+ assert ! ( body[ "error" ]
500+ . as_str( )
501+ . unwrap( )
502+ . contains( "Not authenticated" ) ) ;
503+ }
504+
505+ #[ tokio:: test]
506+ async fn test_auth_logout_returns_success ( ) {
507+ let ( url, _handle) = start_test_server ( ) . await ;
508+ let client = Client :: new ( ) ;
509+ let resp = client
510+ . post ( format ! ( "{url}/api/auth/logout" ) )
511+ . send ( )
512+ . await
513+ . unwrap ( ) ;
514+ assert_eq ! ( resp. status( ) , 200 ) ;
515+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
516+ assert_eq ! ( body[ "success" ] , true ) ;
517+ }
518+
519+ // ── Task lifecycle (multi-step) ──────────────────────────────────────
520+
521+ #[ tokio:: test]
522+ async fn test_task_full_lifecycle ( ) {
523+ let ( url, _handle) = start_test_server ( ) . await ;
524+ let client = Client :: new ( ) ;
525+
526+ // Step 1: Create a task
527+ let resp = client
528+ . post ( format ! ( "{url}/api/tasks" ) )
529+ . json ( & json ! ( {
530+ "title" : "Lifecycle test" ,
531+ "agent_id" : "claude-code"
532+ } ) )
533+ . send ( )
534+ . await
535+ . unwrap ( ) ;
536+ assert_eq ! ( resp. status( ) , 201 ) ;
537+ let task: Value = resp. json ( ) . await . unwrap ( ) ;
538+ let id = task[ "id" ] . as_i64 ( ) . unwrap ( ) ;
539+ assert_eq ! ( task[ "title" ] , "Lifecycle test" ) ;
540+ assert_eq ! ( task[ "status" ] , "queued" ) ;
541+
542+ // Step 2: List tasks — should have exactly 1
543+ let tasks: Vec < Value > = client
544+ . get ( format ! ( "{url}/api/tasks" ) )
545+ . send ( )
546+ . await
547+ . unwrap ( )
548+ . json ( )
549+ . await
550+ . unwrap ( ) ;
551+ assert_eq ! ( tasks. len( ) , 1 ) ;
552+ assert_eq ! ( tasks[ 0 ] [ "id" ] , id) ;
553+
554+ // Step 3: Approve the task
555+ let resp = client
556+ . post ( format ! ( "{}/api/tasks/{}/approve" , url, id) )
557+ . send ( )
558+ . await
559+ . unwrap ( ) ;
560+ assert_eq ! ( resp. status( ) , 200 ) ;
561+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
562+ assert_eq ! ( body[ "id" ] , id) ;
563+ assert_eq ! ( body[ "status" ] , "running" ) ;
564+
565+ // Step 4: Delete the task
566+ let resp = client
567+ . delete ( format ! ( "{}/api/tasks/{}" , url, id) )
568+ . send ( )
569+ . await
570+ . unwrap ( ) ;
571+ assert_eq ! ( resp. status( ) , 200 ) ;
572+ let body: Value = resp. json ( ) . await . unwrap ( ) ;
573+ assert_eq ! ( body[ "deleted" ] , id) ;
574+
575+ // Step 5: List tasks — should be empty
576+ let tasks: Vec < Value > = client
577+ . get ( format ! ( "{url}/api/tasks" ) )
578+ . send ( )
579+ . await
580+ . unwrap ( )
581+ . json ( )
582+ . await
583+ . unwrap ( ) ;
584+ assert ! ( tasks. is_empty( ) ) ;
585+ }
586+
587+ // ── iTerm2 session endpoints ─────────────────────────────────────────
588+
589+ #[ tokio:: test]
590+ async fn test_iterm2_sessions_task_not_found ( ) {
591+ let ( url, _handle) = start_test_server ( ) . await ;
592+ let client = Client :: new ( ) ;
593+ let resp = client
594+ . get ( format ! ( "{url}/api/sessions/99999/claude-sessions" ) )
595+ . send ( )
596+ . await
597+ . unwrap ( ) ;
598+ assert_eq ! ( resp. status( ) , 404 ) ;
599+ }
600+
601+ #[ tokio:: test]
602+ async fn test_iterm2_resume_task_not_found ( ) {
603+ let ( url, _handle) = start_test_server ( ) . await ;
604+ let client = Client :: new ( ) ;
605+ let resp = client
606+ . post ( format ! ( "{url}/api/sessions/99999/resume" ) )
607+ . json ( & json ! ( { } ) )
608+ . send ( )
609+ . await
610+ . unwrap ( ) ;
611+ assert_eq ! ( resp. status( ) , 404 ) ;
612+ }
613+
614+ // ── WebSocket connectivity ───────────────────────────────────────────
615+
616+ #[ tokio:: test]
617+ async fn test_websocket_upgrade_succeeds ( ) {
618+ let ( url, _handle) = start_test_server ( ) . await ;
619+ let client = Client :: new ( ) ;
620+ // A plain GET to /ws without the upgrade headers should not return 404.
621+ // The WebSocket handler requires an upgrade, so it should return 400 or similar.
622+ let resp = client. get ( format ! ( "{url}/ws" ) ) . send ( ) . await . unwrap ( ) ;
623+ // The endpoint exists — it should NOT be 404 or 405.
624+ // Without proper WebSocket upgrade headers, Axum returns 400.
625+ assert_ne ! ( resp. status( ) , 404 ) ;
626+ assert_ne ! ( resp. status( ) , 405 ) ;
627+ }
0 commit comments