1mod client;
25pub mod error;
26pub mod events;
27pub mod models;
28pub mod runtime;
29pub mod sub;
30pub mod suspend;
31pub mod task_end;
32pub mod token_safety;
33
34pub use client::{AkribesClient, AkribesClientBuilder};
36pub use error::{AkribesError, InputValidationEntry, Result, parse_input_validation_errors};
37pub use events::{EnvelopeDecodeError, EventCategory, WorkflowEvent};
38pub use models::*;
39pub use runtime::{
40 RuntimeEndPayload, RuntimeErrorKind, RuntimeErrorPayload, RuntimeEvent, RuntimeStartPayload,
41 RuntimeStderrPayload, RuntimeStdoutPayload,
42};
43pub use suspend::{SuspendTrigger, UnableRecord, ValidationErrorWire};
44pub use task_end::TaskEndVariant;
45
46pub use sub::bench::{BenchClient, BenchRunsClient};
48pub use sub::channels::ChannelsClient;
49pub use sub::clients::RegisteredClientsClient;
50pub use sub::convert::ConvertClient;
51pub use sub::documents::DocumentsClient;
52pub use sub::drafts::DraftsClient;
53pub use sub::events::EventsClient;
54pub use sub::executions::ExecutionsClient;
55pub use sub::projects::ProjectsClient;
56pub use sub::run_stream::{
57 EngineErrorPayload, RunStream, RunSummary, RunSummaryCost, RunSummaryDuration, RunSummaryTasks,
58 SuspendPayload, TaskEndPayload,
59};
60pub use sub::tokens::TokensClient;
61
62#[doc(hidden)]
66pub mod _test {
67 use tokio::sync::mpsc;
68 use tokio::task::JoinHandle;
69
70 pub fn make_run_stream(
74 execution_id: String,
75 rx: mpsc::UnboundedReceiver<crate::error::Result<crate::events::WorkflowEvent>>,
76 handle: JoinHandle<()>,
77 ) -> crate::RunStream {
78 let sub = crate::sub::events::EventSubscription::from_handle(handle);
79 crate::sub::run_stream::RunStream::new(execution_id, rx, sub)
80 }
81}
82pub use sub::scripts::ScriptsClient;
83pub use sub::versions::{PublishBuilder, VersionsClient};
84
85#[allow(dead_code)]
87const _: () = {
88 fn assert_send_sync_clone<T: Send + Sync + Clone>() {}
89 fn checks() {
90 assert_send_sync_clone::<AkribesClient>();
91 }
92};
93
94#[cfg(test)]
97mod tests {
98 use super::*;
99 use mockito::Server;
100
101 #[test]
102 fn client_is_send_sync() {
103 fn assert_send_sync<T: Send + Sync>() {}
104 assert_send_sync::<AkribesClient>();
105 assert_send_sync::<crate::sub::executions::ExecutionsClient>();
106 assert_send_sync::<crate::sub::scripts::ScriptsClient>();
107 assert_send_sync::<crate::sub::tokens::TokensClient>();
108 }
109
110 fn make_client(server: &Server) -> AkribesClient {
111 AkribesClient::builder(server.url())
112 .project_id(1)
113 .name("test-app")
114 .id("test-id")
115 .build()
116 }
117
118 fn make_authed_client(server: &Server) -> AkribesClient {
119 AkribesClient::builder(server.url())
120 .project_id(1)
121 .name("test-app")
122 .id("test-id")
123 .token("test-token-123")
124 .build()
125 }
126
127 fn project_json() -> &'static str {
130 r#"{"id":10,"name":"My Project","created_at":"2024-01-01T00:00:00Z"}"#
131 }
132
133 fn script_json() -> &'static str {
134 r#"{"id":5,"project_id":1,"name":"my_script","created_at":"2024-01-01T00:00:00Z"}"#
135 }
136
137 fn version_json() -> &'static str {
138 r#"{"id":3,"script_id":5,"source":"workflow main {}","label":null,"published_by":null,"created_at":"2024-01-01T00:00:00Z"}"#
139 }
140
141 fn channel_json() -> &'static str {
142 r#"{"id":1,"script_id":5,"name":"production","version_id":3,"updated_at":"2024-01-01T00:00:00Z"}"#
143 }
144
145 #[tokio::test]
148 async fn test_auth_header_sent() {
149 let mut server = Server::new_async().await;
150 let _m = server
151 .mock("GET", "/projects")
152 .match_header("authorization", "Bearer test-token-123")
153 .with_status(200)
154 .with_header("content-type", "application/json")
155 .with_body("[]")
156 .create_async()
157 .await;
158
159 let client = make_authed_client(&server);
160 client.projects().list().await.unwrap();
161 }
162
163 #[tokio::test]
164 async fn test_no_auth_header_without_token() {
165 let mut server = Server::new_async().await;
166 let _m = server
167 .mock("GET", "/projects")
168 .with_status(200)
169 .with_header("content-type", "application/json")
170 .with_body("[]")
171 .create_async()
172 .await;
173
174 let client = make_client(&server);
175 client.projects().list().await.unwrap();
176 }
177
178 #[tokio::test]
179 async fn test_set_token_updates_header() {
180 let mut server = Server::new_async().await;
181 let _m = server
182 .mock("GET", "/projects")
183 .match_header("authorization", "Bearer new-token")
184 .with_status(200)
185 .with_header("content-type", "application/json")
186 .with_body("[]")
187 .create_async()
188 .await;
189
190 let client = make_client(&server);
191 client.set_token(Some("new-token".to_string())).await;
192 client.projects().list().await.unwrap();
193 }
194
195 #[tokio::test]
196 async fn test_on_behalf_of_header_via_builder() {
197 let mut server = Server::new_async().await;
201 let _m = server
202 .mock("GET", "/projects")
203 .match_header("x-akribes-user", "alice@acme.com")
204 .with_status(200)
205 .with_header("content-type", "application/json")
206 .with_body("[]")
207 .create_async()
208 .await;
209
210 let client = AkribesClient::builder(server.url())
211 .project_id(1)
212 .name("test-app")
213 .id("test-id")
214 .on_behalf_of("alice@acme.com")
215 .build();
216 client.projects().list().await.unwrap();
217 }
218
219 #[tokio::test]
220 async fn test_set_on_behalf_of_updates_header_at_runtime() {
221 let mut server = Server::new_async().await;
222 let _m = server
223 .mock("GET", "/projects")
224 .match_header("x-akribes-user", "bob@acme.com")
225 .with_status(200)
226 .with_header("content-type", "application/json")
227 .with_body("[]")
228 .create_async()
229 .await;
230
231 let client = make_client(&server);
232 client
233 .set_on_behalf_of(Some("bob@acme.com".to_string()))
234 .await;
235 client.projects().list().await.unwrap();
236 }
237
238 #[tokio::test]
239 async fn test_set_on_behalf_of_none_clears_header() {
240 let mut server = Server::new_async().await;
245 let _m = server
246 .mock("GET", "/projects")
247 .match_header("x-akribes-user", mockito::Matcher::Missing)
248 .with_status(200)
249 .with_header("content-type", "application/json")
250 .with_body("[]")
251 .create_async()
252 .await;
253
254 let client = AkribesClient::builder(server.url())
255 .project_id(1)
256 .on_behalf_of("alice@acme.com")
257 .build();
258 client.set_on_behalf_of(None).await;
259 client.projects().list().await.unwrap();
260 }
261
262 #[test]
265 fn test_builder_defaults() {
266 let client = AkribesClient::builder("http://localhost:3001/")
267 .project_id(42)
268 .build();
269 assert_eq!(client.inner.base_url, "http://localhost:3001");
270 assert_eq!(client.project_id(), Some(42));
271 assert_eq!(client.inner.name, "rust-sdk");
272 assert!(!client.inner.id.is_empty()); }
274
275 #[test]
276 fn test_builder_custom() {
277 let client = AkribesClient::builder("http://localhost:3001")
278 .project_id(1)
279 .name("my-svc")
280 .id("custom-id")
281 .token("tok")
282 .build();
283 assert_eq!(client.inner.name, "my-svc");
284 assert_eq!(client.inner.id, "custom-id");
285 }
286
287 #[test]
288 fn test_builder_no_project_id() {
289 let client = AkribesClient::builder("http://localhost:3001").build();
290 assert_eq!(client.project_id(), None);
291 }
292
293 #[test]
294 fn test_client_is_clone() {
295 let client = AkribesClient::builder("http://localhost:3001")
296 .project_id(1)
297 .name("test")
298 .id("test")
299 .build();
300 let clone = client.clone();
301 assert_eq!(clone.project_id(), Some(1));
302 }
303
304 #[tokio::test]
307 async fn test_init() {
308 let mut server = Server::new_async().await;
309 let _m = server
310 .mock("POST", "/projects/1/clients")
311 .with_status(200)
312 .with_body(r#"{"status":"ok"}"#)
313 .create_async()
314 .await;
315
316 let client = make_client(&server);
317 assert!(
318 client
319 .project(1)
320 .registered_clients()
321 .init(vec![])
322 .await
323 .is_ok()
324 );
325 client.project(1).registered_clients().destroy();
326 }
327
328 #[tokio::test]
329 async fn test_init_registers_heartbeat_and_destroy_cancels() {
330 let mut server = Server::new_async().await;
331 let _m = server
332 .mock("POST", "/projects/1/clients")
333 .with_status(200)
334 .with_body("{}")
335 .create_async()
336 .await;
337
338 let client = make_client(&server);
339 client
340 .project(1)
341 .registered_clients()
342 .init(vec![])
343 .await
344 .unwrap();
345 assert!(client.inner.heartbeat_handle.lock().unwrap().is_some());
346 client.project(1).registered_clients().destroy();
347 assert!(client.inner.heartbeat_handle.lock().unwrap().is_none());
348 }
349
350 #[tokio::test]
353 async fn test_list_projects() {
354 let mut server = Server::new_async().await;
355 let body = format!("[{}]", project_json());
356 let _m = server
357 .mock("GET", "/projects")
358 .with_status(200)
359 .with_header("content-type", "application/json")
360 .with_body(&body)
361 .create_async()
362 .await;
363
364 let client = make_client(&server);
365 let projects = client.projects().list().await.unwrap();
366 assert_eq!(projects.len(), 1);
367 assert_eq!(projects[0].id, 10);
368 assert_eq!(projects[0].name, "My Project");
369 }
370
371 #[tokio::test]
372 async fn test_get_project() {
373 let mut server = Server::new_async().await;
374 let _m = server
375 .mock("GET", "/projects/10")
376 .with_status(200)
377 .with_header("content-type", "application/json")
378 .with_body(project_json())
379 .create_async()
380 .await;
381
382 let client = make_client(&server);
383 let proj = client.projects().get(10).await.unwrap().unwrap();
384 assert_eq!(proj.id, 10);
385 }
386
387 #[tokio::test]
388 async fn test_get_project_not_found() {
389 let mut server = Server::new_async().await;
390 let _m = server
391 .mock("GET", "/projects/999")
392 .with_status(404)
393 .create_async()
394 .await;
395
396 let client = make_client(&server);
397 assert!(client.projects().get(999).await.unwrap().is_none());
398 }
399
400 #[tokio::test]
401 async fn test_create_project() {
402 let mut server = Server::new_async().await;
403 let _m = server
404 .mock("POST", "/projects")
405 .with_status(201)
406 .with_header("content-type", "application/json")
407 .with_body(project_json())
408 .create_async()
409 .await;
410
411 let client = make_client(&server);
412 let proj = client.projects().create("My Project").await.unwrap();
413 assert_eq!(proj.id, 10);
414 }
415
416 #[tokio::test]
417 async fn test_update_project() {
418 let mut server = Server::new_async().await;
419 let _m = server
420 .mock("PATCH", "/projects/10")
421 .with_status(200)
422 .with_header("content-type", "application/json")
423 .with_body(project_json())
424 .create_async()
425 .await;
426
427 let client = make_client(&server);
428 let proj = client.projects().update(10, "My Project").await.unwrap();
429 assert_eq!(proj.id, 10);
430 }
431
432 #[tokio::test]
433 async fn test_delete_project() {
434 let mut server = Server::new_async().await;
435 let _m = server
436 .mock("DELETE", "/projects/10")
437 .with_status(204)
438 .create_async()
439 .await;
440
441 let client = make_client(&server);
442 assert!(client.projects().delete(10).await.is_ok());
443 }
444
445 #[tokio::test]
446 async fn test_duplicate_project() {
447 let mut server = Server::new_async().await;
448 let _m = server
449 .mock("POST", "/projects/10/duplicate")
450 .with_status(200)
451 .with_header("content-type", "application/json")
452 .with_body(project_json())
453 .create_async()
454 .await;
455
456 let client = make_client(&server);
457 let proj = client.projects().duplicate(10).await.unwrap();
458 assert_eq!(proj.id, 10);
459 }
460
461 #[tokio::test]
462 async fn test_reorder_projects() {
463 let mut server = Server::new_async().await;
464 let _m = server
465 .mock("PUT", "/projects/reorder")
466 .match_body(r#"{"order":[3,1,2]}"#)
467 .with_status(204)
468 .create_async()
469 .await;
470
471 let client = make_client(&server);
472 client.projects().reorder(vec![3, 1, 2]).await.unwrap();
473 }
474
475 #[tokio::test]
478 async fn test_list_scripts() {
479 let mut server = Server::new_async().await;
480 let body = format!("[{}]", script_json());
481 let _m = server
482 .mock("GET", "/projects/1/scripts")
483 .with_status(200)
484 .with_header("content-type", "application/json")
485 .with_body(&body)
486 .create_async()
487 .await;
488
489 let client = make_client(&server);
490 let scripts = client.project(1).scripts().list().await.unwrap();
491 assert_eq!(scripts.len(), 1);
492 assert_eq!(scripts[0].name, "my_script");
493 }
494
495 #[tokio::test]
496 async fn test_create_script() {
497 let mut server = Server::new_async().await;
498 let _m = server
499 .mock("POST", "/projects/1/scripts?name=my_script")
500 .with_status(201)
501 .with_header("content-type", "application/json")
502 .with_body(script_json())
503 .create_async()
504 .await;
505
506 let client = make_client(&server);
507 let s = client
508 .project(1)
509 .scripts()
510 .create("my_script", "workflow main {}")
511 .await
512 .unwrap();
513 assert_eq!(s.name, "my_script");
514 }
515
516 #[tokio::test]
517 async fn test_get_script() {
518 let mut server = Server::new_async().await;
519 let _m = server
520 .mock("GET", "/projects/1/scripts/my_script")
521 .with_status(200)
522 .with_header("content-type", "application/json")
523 .with_body(script_json())
524 .create_async()
525 .await;
526
527 let client = make_client(&server);
528 let s = client.project(1).scripts().get("my_script").await.unwrap();
529 assert!(s.is_some());
530 assert_eq!(s.unwrap().id, 5);
531 }
532
533 #[tokio::test]
534 async fn test_get_script_not_found() {
535 let mut server = Server::new_async().await;
536 let _m = server
537 .mock("GET", "/projects/1/scripts/missing")
538 .with_status(404)
539 .create_async()
540 .await;
541
542 let client = make_client(&server);
543 let s = client.project(1).scripts().get("missing").await.unwrap();
544 assert!(s.is_none());
545 }
546
547 #[tokio::test]
548 async fn test_rename_script() {
549 let mut server = Server::new_async().await;
550 let _m = server
551 .mock("PATCH", "/projects/1/scripts/old")
552 .with_status(204)
553 .create_async()
554 .await;
555
556 let client = make_client(&server);
557 assert!(
558 client
559 .project(1)
560 .scripts()
561 .rename("old", "new")
562 .await
563 .is_ok()
564 );
565 }
566
567 #[tokio::test]
568 async fn test_delete_script() {
569 let mut server = Server::new_async().await;
570 let _m = server
571 .mock("DELETE", "/projects/1/scripts/my_script")
572 .with_status(204)
573 .create_async()
574 .await;
575
576 let client = make_client(&server);
577 assert!(
578 client
579 .project(1)
580 .scripts()
581 .delete("my_script")
582 .await
583 .is_ok()
584 );
585 }
586
587 #[tokio::test]
588 async fn test_duplicate_script() {
589 let mut server = Server::new_async().await;
590 let _m = server
591 .mock("POST", "/projects/1/scripts/foo/duplicate")
592 .with_status(200)
593 .with_header("content-type", "application/json")
594 .with_body(script_json())
595 .create_async()
596 .await;
597
598 let client = make_client(&server);
599 let s = client.project(1).scripts().duplicate("foo").await.unwrap();
600 assert_eq!(s.name, "my_script");
601 }
602
603 #[tokio::test]
604 async fn test_move_script() {
605 let mut server = Server::new_async().await;
606 let _m = server
607 .mock("POST", "/projects/1/scripts/foo/move")
608 .match_body(r#"{"target_project_id":9}"#)
609 .with_status(200)
610 .with_header("content-type", "application/json")
611 .with_body(script_json())
612 .create_async()
613 .await;
614
615 let client = make_client(&server);
616 let s = client.project(1).scripts().move_to("foo", 9).await.unwrap();
617 assert_eq!(s.id, 5);
618 }
619
620 #[tokio::test]
621 async fn test_reorder_scripts() {
622 let mut server = Server::new_async().await;
623 let _m = server
624 .mock("PUT", "/projects/1/scripts/reorder")
625 .match_body(r#"{"order":[5,4,3]}"#)
626 .with_status(204)
627 .create_async()
628 .await;
629
630 let client = make_client(&server);
631 client
632 .project(1)
633 .scripts()
634 .reorder(vec![5, 4, 3])
635 .await
636 .unwrap();
637 }
638
639 #[tokio::test]
642 async fn test_get_draft_legacy_tuple_form() {
643 let mut server = Server::new_async().await;
646 let _m = server
647 .mock("GET", "/projects/1/scripts/my_script/draft")
648 .with_status(200)
649 .with_header("content-type", "application/json")
650 .with_body(r#"{"source":"workflow main {}","inputs":[["doc","string"]]}"#)
651 .create_async()
652 .await;
653
654 let client = make_client(&server);
655 let draft = client
656 .project(1)
657 .drafts()
658 .get("my_script")
659 .await
660 .unwrap()
661 .unwrap();
662 assert_eq!(draft.source, "workflow main {}");
663 assert_eq!(draft.inputs.len(), 1);
664 assert_eq!(draft.inputs[0], ("doc".to_string(), "string".to_string()));
665 }
666
667 #[tokio::test]
668 async fn test_get_draft_server_object_form() {
669 let body = r#"{
675 "source": "input doc: string",
676 "inputs": [
677 {
678 "name": "doc",
679 "ty": {"name": "string", "inner": null, "choices": null, "variants": null},
680 "docs": null
681 },
682 {
683 "name": "items",
684 "ty": {
685 "name": "list",
686 "inner": {"name": "string", "inner": null, "choices": null, "variants": null},
687 "choices": null,
688 "variants": null
689 },
690 "docs": "a list of strings"
691 }
692 ],
693 "type_defs": {}
694 }"#;
695 let mut server = Server::new_async().await;
696 let _m = server
697 .mock("GET", "/projects/1/scripts/my_script/draft")
698 .with_status(200)
699 .with_header("content-type", "application/json")
700 .with_body(body)
701 .create_async()
702 .await;
703
704 let client = make_client(&server);
705 let draft = client
706 .project(1)
707 .drafts()
708 .get("my_script")
709 .await
710 .unwrap()
711 .unwrap();
712 assert_eq!(draft.inputs.len(), 2);
713 assert_eq!(draft.inputs[0], ("doc".to_string(), "string".to_string()));
714 assert_eq!(
715 draft.inputs[1],
716 ("items".to_string(), "list[string]".to_string())
717 );
718 }
719
720 #[tokio::test]
721 async fn test_get_draft_not_found() {
722 let mut server = Server::new_async().await;
723 let _m = server
724 .mock("GET", "/projects/1/scripts/my_script/draft")
725 .with_status(404)
726 .create_async()
727 .await;
728
729 let client = make_client(&server);
730 assert!(
731 client
732 .project(1)
733 .drafts()
734 .get("my_script")
735 .await
736 .unwrap()
737 .is_none()
738 );
739 }
740
741 #[tokio::test]
742 async fn test_save_draft() {
743 let mut server = Server::new_async().await;
744 let _m = server
745 .mock("PUT", "/projects/1/scripts/my_script/draft")
746 .with_status(200)
747 .with_header("content-type", "application/json")
748 .with_body(r#"{"schema_warnings":[]}"#)
749 .create_async()
750 .await;
751
752 let client = make_client(&server);
753 let resp = client
754 .project(1)
755 .drafts()
756 .save("my_script", "workflow main {}")
757 .await
758 .unwrap();
759 assert!(resp.schema_warnings.is_empty());
760 }
761
762 #[tokio::test]
765 async fn test_list_versions() {
766 let mut server = Server::new_async().await;
767 let body = format!("[{}]", version_json());
768 let _m = server
769 .mock("GET", "/projects/1/scripts/my_script/versions")
770 .with_status(200)
771 .with_header("content-type", "application/json")
772 .with_body(&body)
773 .create_async()
774 .await;
775
776 let client = make_client(&server);
777 let versions = client
778 .project(1)
779 .versions()
780 .list("my_script")
781 .await
782 .unwrap();
783 assert_eq!(versions.len(), 1);
784 assert_eq!(versions[0].id, 3);
785 }
786
787 #[tokio::test]
788 async fn test_get_version() {
789 let mut server = Server::new_async().await;
790 let _m = server
791 .mock("GET", "/projects/1/scripts/my_script/versions/3")
792 .with_status(200)
793 .with_header("content-type", "application/json")
794 .with_body(version_json())
795 .create_async()
796 .await;
797
798 let client = make_client(&server);
799 let v = client
800 .project(1)
801 .versions()
802 .get("my_script", 3)
803 .await
804 .unwrap()
805 .unwrap();
806 assert_eq!(v.id, 3);
807 }
808
809 #[tokio::test]
810 async fn test_get_version_not_found() {
811 let mut server = Server::new_async().await;
812 let _m = server
813 .mock("GET", "/projects/1/scripts/my_script/versions/99")
814 .with_status(404)
815 .create_async()
816 .await;
817
818 let client = make_client(&server);
819 assert!(
820 client
821 .project(1)
822 .versions()
823 .get("my_script", 99)
824 .await
825 .unwrap()
826 .is_none()
827 );
828 }
829
830 #[tokio::test]
831 async fn test_get_latest_version() {
832 let mut server = Server::new_async().await;
833 let _m = server
834 .mock("GET", "/projects/1/scripts/my_script/latest")
835 .with_status(200)
836 .with_header("content-type", "application/json")
837 .with_body(r#"{"id":3,"script_id":5,"source":"workflow main {}","label":null,"published_by":null,"created_at":"2024-01-01T00:00:00Z","inputs":[]}"#)
838 .create_async()
839 .await;
840
841 let client = make_client(&server);
842 let v = client
843 .project(1)
844 .versions()
845 .get_latest("my_script")
846 .await
847 .unwrap();
848 assert!(v.is_some());
849 }
850
851 #[tokio::test]
852 async fn test_publish_version() {
853 let mut server = Server::new_async().await;
854 let _m = server
855 .mock("POST", "/projects/1/scripts/my_script/publish")
856 .with_status(201)
857 .with_header("content-type", "application/json")
858 .with_body(format!(r#"{{"version":{}}}"#, version_json()))
859 .create_async()
860 .await;
861
862 let client = make_client(&server);
863 let v = client
864 .project(1)
865 .versions()
866 .publish_version("my_script", vec!["production".into()], Some("v1"), None)
867 .await
868 .unwrap();
869 assert_eq!(v.id, 3);
870 }
871
872 #[tokio::test]
875 async fn test_list_channels() {
876 let mut server = Server::new_async().await;
877 let body = format!("[{}]", channel_json());
878 let _m = server
879 .mock("GET", "/projects/1/scripts/my_script/channels")
880 .with_status(200)
881 .with_header("content-type", "application/json")
882 .with_body(&body)
883 .create_async()
884 .await;
885
886 let client = make_client(&server);
887 let channels = client
888 .project(1)
889 .channels()
890 .list("my_script")
891 .await
892 .unwrap();
893 assert_eq!(channels.len(), 1);
894 assert_eq!(channels[0].name, "production");
895 }
896
897 #[tokio::test]
898 async fn test_create_channel() {
899 let mut server = Server::new_async().await;
900 let _m = server
901 .mock("POST", "/projects/1/scripts/my_script/channels")
902 .with_status(201)
903 .with_header("content-type", "application/json")
904 .with_body(channel_json())
905 .create_async()
906 .await;
907
908 let client = make_client(&server);
909 let ch = client
910 .project(1)
911 .channels()
912 .create("my_script", "production")
913 .await
914 .unwrap();
915 assert_eq!(ch.name, "production");
916 }
917
918 #[tokio::test]
919 async fn test_delete_channel() {
920 let mut server = Server::new_async().await;
921 let _m = server
922 .mock("DELETE", "/projects/1/scripts/my_script/channels/beta")
923 .with_status(204)
924 .create_async()
925 .await;
926
927 let client = make_client(&server);
928 assert!(
929 client
930 .project(1)
931 .channels()
932 .delete("my_script", "beta")
933 .await
934 .is_ok()
935 );
936 }
937
938 #[tokio::test]
939 async fn test_move_channel() {
940 let mut server = Server::new_async().await;
941 let _m = server
942 .mock("PATCH", "/projects/1/scripts/my_script/channels/staging")
943 .with_status(204)
944 .create_async()
945 .await;
946
947 let client = make_client(&server);
948 assert!(
949 client
950 .project(1)
951 .channels()
952 .move_to("my_script", "staging", 3, false)
953 .await
954 .is_ok()
955 );
956 }
957
958 #[tokio::test]
961 async fn test_run_builder() {
962 let mut server = Server::new_async().await;
963 let _m = server
964 .mock(
965 "POST",
966 "/projects/1/scripts/my_script/run?channel=production",
967 )
968 .with_status(200)
969 .with_header("content-type", "application/json")
970 .with_body(r#"{"execution_id":"abc-123"}"#)
971 .create_async()
972 .await;
973
974 let client = make_client(&server);
975 let result = client
976 .project(1)
977 .executions()
978 .run("my_script")
979 .execute()
980 .await
981 .unwrap();
982 assert_eq!(result.execution_id, "abc-123");
983 }
984
985 #[tokio::test]
986 async fn test_run_404_surfaces_as_http_status() {
987 let mut server = Server::new_async().await;
993 let _m = server
994 .mock(
995 "POST",
996 "/projects/1/scripts/my_script/run?channel=nonexistent",
997 )
998 .with_status(404)
999 .with_header("content-type", "application/json")
1000 .with_body(r#"{"error":"channel 'nonexistent' not found"}"#)
1001 .create_async()
1002 .await;
1003
1004 let client = make_client(&server);
1005 let err = client
1006 .project(1)
1007 .executions()
1008 .run("my_script")
1009 .channel("nonexistent")
1010 .execute()
1011 .await
1012 .unwrap_err();
1013
1014 match err {
1015 AkribesError::HttpStatus { status, message } => {
1016 assert_eq!(status, 404);
1017 assert!(
1018 message.contains("channel 'nonexistent' not found"),
1019 "expected server body in message, got: {message}"
1020 );
1021 }
1022 other => panic!("expected HttpStatus, got {other:?}"),
1023 }
1024 }
1025
1026 #[tokio::test]
1027 async fn test_decode_error_includes_body_snippet() {
1028 let mut server = Server::new_async().await;
1032 let _m = server
1033 .mock(
1034 "POST",
1035 "/projects/1/scripts/my_script/run?channel=production",
1036 )
1037 .with_status(200)
1038 .with_header("content-type", "application/json")
1039 .with_body(r#"{"surprise":"not what you wanted"}"#)
1041 .create_async()
1042 .await;
1043
1044 let client = make_client(&server);
1045 let err = client
1046 .project(1)
1047 .executions()
1048 .run("my_script")
1049 .execute()
1050 .await
1051 .unwrap_err();
1052
1053 let msg = err.to_string();
1054 assert!(
1055 msg.contains("failed to decode")
1056 && msg.contains("RunResult")
1057 && msg.contains("surprise"),
1058 "decode error should name the target type and include body; got: {msg}"
1059 );
1060 }
1061
1062 #[tokio::test]
1063 async fn test_run_builder_typed_inputs_body() {
1064 let mut server = Server::new_async().await;
1065 let _m = server
1069 .mock(
1070 "POST",
1071 "/projects/1/scripts/my_script/run?channel=production",
1072 )
1073 .match_body(mockito::Matcher::PartialJson(serde_json::json!({
1074 "inputs": {
1075 "age": 25,
1076 "name": "alice",
1077 "resume": "doc_00000000-0000-0000-0000-000000000001",
1078 "attachments": [
1079 "doc_00000000-0000-0000-0000-000000000002",
1080 "doc_00000000-0000-0000-0000-000000000003",
1081 ],
1082 }
1083 })))
1084 .with_status(200)
1085 .with_header("content-type", "application/json")
1086 .with_body(r#"{"execution_id":"abc-777"}"#)
1087 .create_async()
1088 .await;
1089
1090 let client = make_client(&server);
1091 let result = client
1092 .project(1)
1093 .executions()
1094 .run("my_script")
1095 .input("age", 25)
1096 .input("name", "alice")
1097 .document("resume", "doc_00000000-0000-0000-0000-000000000001")
1098 .documents(
1099 "attachments",
1100 [
1101 "doc_00000000-0000-0000-0000-000000000002",
1102 "doc_00000000-0000-0000-0000-000000000003",
1103 ],
1104 )
1105 .execute()
1106 .await
1107 .unwrap();
1108 assert_eq!(result.execution_id, "abc-777");
1109 }
1110
1111 #[tokio::test]
1112 async fn test_run_builder_custom_channel() {
1113 let mut server = Server::new_async().await;
1114 let _m = server
1115 .mock("POST", "/projects/1/scripts/my_script/run?channel=staging")
1116 .with_status(200)
1117 .with_header("content-type", "application/json")
1118 .with_body(r#"{"execution_id":"abc-456"}"#)
1119 .create_async()
1120 .await;
1121
1122 let client = make_client(&server);
1123 let result = client
1124 .project(1)
1125 .executions()
1126 .run("my_script")
1127 .channel("staging")
1128 .execute()
1129 .await
1130 .unwrap();
1131 assert_eq!(result.execution_id, "abc-456");
1132 }
1133
1134 #[tokio::test]
1135 async fn test_run_and_await_builder() {
1136 let mut server = Server::new_async().await;
1137 let _run = server
1138 .mock(
1139 "POST",
1140 "/projects/1/scripts/my_script/run?channel=production",
1141 )
1142 .with_status(200)
1143 .with_header("content-type", "application/json")
1144 .with_body(r#"{"execution_id":"eid-1"}"#)
1145 .create_async()
1146 .await;
1147 let _out = server
1148 .mock("GET", "/executions/eid-1/output")
1149 .with_status(200)
1150 .with_header("content-type", "application/json")
1151 .with_body(r#"{"status":"completed","error":null,"error_kind":null,"result":null}"#)
1152 .create_async()
1153 .await;
1154
1155 let client = make_client(&server);
1156 let (eid, out) = client
1157 .project(1)
1158 .executions()
1159 .run("my_script")
1160 .execute_and_await(None)
1161 .await
1162 .unwrap();
1163 assert_eq!(eid, "eid-1");
1164 assert_eq!(out.status, "completed");
1165 }
1166
1167 #[tokio::test]
1170 async fn test_cancel_run() {
1171 let mut server = Server::new_async().await;
1172 let _m = server
1173 .mock("DELETE", "/projects/1/scripts/my_script/run")
1174 .with_status(200)
1175 .create_async()
1176 .await;
1177
1178 let client = make_client(&server);
1179 assert!(
1180 client
1181 .project(1)
1182 .executions()
1183 .cancel_run("my_script")
1184 .await
1185 .unwrap()
1186 );
1187 }
1188
1189 #[tokio::test]
1190 async fn test_cancel_run_not_found() {
1191 let mut server = Server::new_async().await;
1192 let _m = server
1193 .mock("DELETE", "/projects/1/scripts/my_script/run")
1194 .with_status(404)
1195 .create_async()
1196 .await;
1197
1198 let client = make_client(&server);
1199 assert!(
1200 !client
1201 .project(1)
1202 .executions()
1203 .cancel_run("my_script")
1204 .await
1205 .unwrap()
1206 );
1207 }
1208
1209 #[tokio::test]
1210 async fn test_cancel_execution() {
1211 let mut server = Server::new_async().await;
1212 let _m = server
1213 .mock("DELETE", "/executions/abc-123")
1214 .with_status(200)
1215 .create_async()
1216 .await;
1217
1218 let client = make_client(&server);
1219 assert!(client.executions().cancel("abc-123").await.unwrap());
1220 }
1221
1222 #[tokio::test]
1223 async fn test_get_execution() {
1224 let mut server = Server::new_async().await;
1225 let body = r#"{"id":"abc-123","project_id":1,"script_name":"my_script","status":"completed","started_at":null,"finished_at":null,"version_id":null,"channel":null,"error":null,"error_kind":null,"result":null,"documents":null,"triggered_by":null}"#;
1226 let _m = server
1227 .mock("GET", "/executions/abc-123")
1228 .with_status(200)
1229 .with_header("content-type", "application/json")
1230 .with_body(body)
1231 .create_async()
1232 .await;
1233
1234 let client = make_client(&server);
1235 let status = client.executions().get("abc-123").await.unwrap().unwrap();
1236 assert_eq!(status.id, "abc-123");
1237 assert_eq!(status.status, "completed");
1238 }
1239
1240 #[tokio::test]
1241 async fn test_get_execution_not_found() {
1242 let mut server = Server::new_async().await;
1243 let _m = server
1244 .mock("GET", "/executions/missing")
1245 .with_status(404)
1246 .create_async()
1247 .await;
1248
1249 let client = make_client(&server);
1250 assert!(client.executions().get("missing").await.unwrap().is_none());
1251 }
1252
1253 #[tokio::test]
1254 async fn test_get_execution_output() {
1255 let mut server = Server::new_async().await;
1256 let body =
1257 r#"{"status":"completed","error":null,"error_kind":null,"result":{"answer":42}}"#;
1258 let _m = server
1259 .mock("GET", "/executions/abc-123/output")
1260 .with_status(200)
1261 .with_header("content-type", "application/json")
1262 .with_body(body)
1263 .create_async()
1264 .await;
1265
1266 let client = make_client(&server);
1267 let out = client
1268 .executions()
1269 .get_output("abc-123")
1270 .await
1271 .unwrap()
1272 .unwrap();
1273 assert_eq!(out.status, "completed");
1274 assert_eq!(out.result.unwrap()["answer"], 42);
1275 }
1276
1277 #[tokio::test]
1278 async fn test_await_execution_immediate_completion() {
1279 let mut server = Server::new_async().await;
1280 let body = r#"{"status":"completed","error":null,"error_kind":null,"result":{"ok":true}}"#;
1281 let _m = server
1282 .mock("GET", "/executions/abc-123/output")
1283 .with_status(200)
1284 .with_header("content-type", "application/json")
1285 .with_body(body)
1286 .create_async()
1287 .await;
1288
1289 let client = make_client(&server);
1290 let out = client
1291 .executions()
1292 .await_execution("abc-123", None, Some(0))
1293 .await
1294 .unwrap();
1295 assert_eq!(out.status, "completed");
1296 }
1297
1298 #[tokio::test]
1299 async fn test_await_execution_script_error() {
1300 let mut server = Server::new_async().await;
1301 let body = r#"{"status":"failed","error":"boom","error_kind":"ScriptError","result":null}"#;
1302 let _m = server
1303 .mock("GET", "/executions/abc-123/output")
1304 .with_status(200)
1305 .with_header("content-type", "application/json")
1306 .with_body(body)
1307 .create_async()
1308 .await;
1309
1310 let client = make_client(&server);
1311 let err = client
1312 .executions()
1313 .await_execution("abc-123", None, Some(0))
1314 .await
1315 .unwrap_err();
1316 assert!(matches!(err, AkribesError::Script { .. }));
1317 }
1318
1319 #[tokio::test]
1320 async fn test_await_execution_fatal_error() {
1321 let mut server = Server::new_async().await;
1322 let body =
1323 r#"{"status":"failed","error":"unauthorized","error_kind":"AuthError","result":null}"#;
1324 let _m = server
1325 .mock("GET", "/executions/abc-123/output")
1326 .with_status(200)
1327 .with_header("content-type", "application/json")
1328 .with_body(body)
1329 .create_async()
1330 .await;
1331
1332 let client = make_client(&server);
1333 let err = client
1334 .executions()
1335 .await_execution("abc-123", None, Some(0))
1336 .await
1337 .unwrap_err();
1338 assert!(matches!(err, AkribesError::Fatal { .. }));
1339 }
1340
1341 #[tokio::test]
1342 async fn test_await_execution_transient_error() {
1343 let mut server = Server::new_async().await;
1344 let body =
1345 r#"{"status":"failed","error":"rate limited","error_kind":"RateLimit","result":null}"#;
1346 let _m = server
1347 .mock("GET", "/executions/abc-123/output")
1348 .with_status(200)
1349 .with_header("content-type", "application/json")
1350 .with_body(body)
1351 .create_async()
1352 .await;
1353
1354 let client = make_client(&server);
1355 let err = client
1356 .executions()
1357 .await_execution("abc-123", None, Some(0))
1358 .await
1359 .unwrap_err();
1360 assert!(matches!(err, AkribesError::Transient { .. }));
1361 }
1362
1363 #[tokio::test]
1364 async fn test_await_execution_timeout() {
1365 let mut server = Server::new_async().await;
1366 let body = r#"{"status":"running","error":null,"error_kind":null,"result":null}"#;
1367 let _m = server
1368 .mock("GET", "/executions/timeout-id/output")
1369 .with_status(200)
1370 .with_header("content-type", "application/json")
1371 .with_body(body)
1372 .expect_at_least(1)
1373 .create_async()
1374 .await;
1375
1376 let client = make_client(&server);
1377 let err = client
1378 .executions()
1379 .await_execution("timeout-id", Some(50), Some(10))
1380 .await
1381 .unwrap_err();
1382 assert!(matches!(err, AkribesError::Timeout { .. }));
1383 }
1384
1385 fn exec_status_json() -> &'static str {
1388 r#"{"id":"exec-1","project_id":1,"script_name":"my_script","status":"completed","started_at":"2026-04-02","finished_at":"2026-04-02","version_id":5,"channel":"production","error":null,"error_kind":null,"result":null,"documents":{"doc":"content"},"triggered_by":"studio"}"#
1389 }
1390
1391 #[tokio::test]
1392 async fn test_list_executions() {
1393 let mut server = Server::new_async().await;
1394 let body = format!("[{}]", exec_status_json());
1395 let _m = server
1396 .mock("GET", "/projects/1/scripts/my_script/executions")
1397 .with_status(200)
1398 .with_header("content-type", "application/json")
1399 .with_body(&body)
1400 .create_async()
1401 .await;
1402
1403 let client = make_client(&server);
1404 let execs = client
1405 .project(1)
1406 .executions()
1407 .list("my_script")
1408 .fetch()
1409 .await
1410 .unwrap();
1411 assert_eq!(execs.len(), 1);
1412 assert_eq!(execs[0].id, "exec-1");
1413 assert_eq!(execs[0].triggered_by.as_deref(), Some("studio"));
1414 assert!(execs[0].documents.is_some());
1415 }
1416
1417 #[tokio::test]
1418 async fn test_list_executions_with_filters() {
1419 let mut server = Server::new_async().await;
1420 let _m = server
1421 .mock(
1422 "GET",
1423 "/projects/1/scripts/my_script/executions?status=failed&channel=draft&limit=10&offset=5",
1424 )
1425 .with_status(200)
1426 .with_header("content-type", "application/json")
1427 .with_body("[]")
1428 .create_async()
1429 .await;
1430
1431 let client = make_client(&server);
1432 let execs = client
1433 .project(1)
1434 .executions()
1435 .list("my_script")
1436 .status("failed")
1437 .channel("draft")
1438 .limit(10)
1439 .offset(5)
1440 .fetch()
1441 .await
1442 .unwrap();
1443 assert!(execs.is_empty());
1444 }
1445
1446 fn mock_events_json() -> serde_json::Value {
1449 serde_json::json!([
1450 {"type": "WorkflowStart", "payload": 2},
1451 {"type": "TaskStart", "payload": ["summarise", null]},
1452 {"type": "AgentOutput", "payload": {
1453 "task_name": "summarise",
1454 "agent_name": null,
1455 "task_id": "t1",
1456 "schema_type": null,
1457 "chunk": "hello "
1458 }},
1459 {"type": "AgentOutput", "payload": {
1460 "task_name": "summarise",
1461 "agent_name": null,
1462 "task_id": "t1",
1463 "schema_type": null,
1464 "chunk": "world"
1465 }},
1466 {"type": "WorkflowEnd", "payload": "done"},
1471 ])
1472 }
1473
1474 #[tokio::test]
1475 async fn test_get_execution_events_completed() {
1476 let mut server = Server::new_async().await;
1477 let body = serde_json::json!({
1478 "execution_id": "exec-1",
1479 "status": "completed",
1480 "complete": true,
1481 "events": mock_events_json(),
1482 });
1483 let _m = server
1484 .mock("GET", "/executions/exec-1/events")
1485 .with_status(200)
1486 .with_header("content-type", "application/json")
1487 .with_body(body.to_string())
1488 .create_async()
1489 .await;
1490
1491 let client = make_client(&server);
1492 let result = client
1493 .executions()
1494 .get_events("exec-1", None, None, None)
1495 .await
1496 .unwrap()
1497 .unwrap();
1498 assert_eq!(result.execution_id, "exec-1");
1499 assert_eq!(result.status, "completed");
1500 assert!(result.complete);
1501 assert_eq!(result.events.len(), 5);
1502 assert!(matches!(
1503 result.events[0],
1504 crate::models::EngineEvent::WorkflowStart(2)
1505 ));
1506 match &result.events[2] {
1507 crate::models::EngineEvent::AgentOutput { chunk, .. } => {
1508 assert_eq!(chunk, "hello ");
1509 }
1510 other => panic!("expected AgentOutput, got {other:?}"),
1511 }
1512 }
1513
1514 #[tokio::test]
1515 async fn test_get_execution_events_running_partial() {
1516 let mut server = Server::new_async().await;
1517 let body = serde_json::json!({
1518 "execution_id": "exec-2",
1519 "status": "running",
1520 "complete": false,
1521 "events": [
1522 {"type": "WorkflowStart", "payload": 2},
1523 {"type": "TaskStart", "payload": ["summarise", null]},
1524 ],
1525 });
1526 let _m = server
1527 .mock("GET", "/executions/exec-2/events")
1528 .with_status(200)
1529 .with_header("content-type", "application/json")
1530 .with_body(body.to_string())
1531 .create_async()
1532 .await;
1533
1534 let client = make_client(&server);
1535 let result = client
1536 .executions()
1537 .get_events("exec-2", None, None, None)
1538 .await
1539 .unwrap()
1540 .unwrap();
1541 assert!(!result.complete);
1542 assert_eq!(result.status, "running");
1543 assert_eq!(result.events.len(), 2);
1544 }
1545
1546 #[tokio::test]
1547 async fn test_get_execution_events_empty() {
1548 let mut server = Server::new_async().await;
1549 let body = serde_json::json!({
1550 "execution_id": "exec-3",
1551 "status": "running",
1552 "complete": false,
1553 "events": [],
1554 });
1555 let _m = server
1556 .mock("GET", "/executions/exec-3/events")
1557 .with_status(200)
1558 .with_header("content-type", "application/json")
1559 .with_body(body.to_string())
1560 .create_async()
1561 .await;
1562
1563 let client = make_client(&server);
1564 let result = client
1565 .executions()
1566 .get_events("exec-3", None, None, None)
1567 .await
1568 .unwrap()
1569 .unwrap();
1570 assert!(result.events.is_empty());
1571 }
1572
1573 #[tokio::test]
1574 async fn test_get_execution_events_not_found() {
1575 let mut server = Server::new_async().await;
1576 let _m = server
1577 .mock("GET", "/executions/missing/events")
1578 .with_status(404)
1579 .create_async()
1580 .await;
1581
1582 let client = make_client(&server);
1583 assert!(
1584 client
1585 .executions()
1586 .get_events("missing", None, None, None)
1587 .await
1588 .unwrap()
1589 .is_none()
1590 );
1591 }
1592
1593 #[tokio::test]
1596 async fn test_list_clients() {
1597 let mut server = Server::new_async().await;
1598 let _m = server
1599 .mock("GET", "/projects/1/clients")
1600 .with_status(200)
1601 .with_header("content-type", "application/json")
1602 .with_body(r#"[{"id":"c1","name":"sdk","last_seen":"2024-01-01T00:00:00Z","scripts":["my-script"]}]"#)
1603 .create_async()
1604 .await;
1605
1606 let client = make_client(&server);
1607 let clients = client.project(1).registered_clients().list().await.unwrap();
1608 assert_eq!(clients.len(), 1);
1609 }
1610
1611 #[tokio::test]
1612 async fn test_delete_client() {
1613 let mut server = Server::new_async().await;
1614 let _m = server
1615 .mock("DELETE", "/clients/c1")
1616 .with_status(204)
1617 .create_async()
1618 .await;
1619
1620 let client = make_client(&server);
1621 assert!(
1622 client
1623 .project(1)
1624 .registered_clients()
1625 .delete("c1")
1626 .await
1627 .is_ok()
1628 );
1629 }
1630
1631 #[tokio::test]
1632 async fn test_get_state() {
1633 let mut server = Server::new_async().await;
1634 let _m = server
1635 .mock("GET", "/state")
1636 .with_status(200)
1637 .with_header("content-type", "application/json")
1638 .with_body(r#"{"env":{}}"#)
1639 .create_async()
1640 .await;
1641
1642 let client = make_client(&server);
1643 let state = client.get_state().await.unwrap();
1644 assert!(state.get("env").is_some());
1645 }
1646
1647 #[tokio::test]
1650 async fn test_list_tokens() {
1651 let mut server = Server::new_async().await;
1652 let _m = server
1653 .mock("GET", "/tokens")
1654 .with_status(200)
1655 .with_header("content-type", "application/json")
1656 .with_body(r#"[{"id":"tok_1","label":"test","user_email":null,"scopes":{"projects":"*","role":"admin"},"minted_by":"studio","expires_at":"2026-02-01T00:00:00Z","revoked":false,"created_at":"2026-01-01T00:00:00Z","last_used_at":null}]"#)
1657 .create_async()
1658 .await;
1659
1660 let client = make_client(&server);
1661 let tokens = client.tokens().list().await.unwrap();
1662 assert_eq!(tokens.len(), 1);
1663 assert_eq!(tokens[0].label, "test");
1664 }
1665
1666 #[tokio::test]
1667 async fn test_mint_token() {
1668 let mut server = Server::new_async().await;
1669 let _m = server
1670 .mock("POST", "/tokens")
1671 .with_status(201)
1672 .with_header("content-type", "application/json")
1673 .with_body(r#"{"token":"aura_tk_abc123","token_id":"tok_1","expires_at":"2026-02-01T00:00:00Z"}"#)
1674 .create_async()
1675 .await;
1676
1677 let client = make_client(&server);
1678 let req = MintTokenRequest {
1679 user_email: None,
1680 scopes: TokenScopes {
1681 projects: ProjectScope::Wildcard(WildcardMarker),
1682 role: TokenRole::Admin,
1683 scripts: None,
1684 executions: None,
1685 can_mint: false,
1686 features: vec![],
1687 org_id: None,
1688 },
1689 expires_in: 3600,
1690 label: "test".to_string(),
1691 };
1692 let res = client.tokens().mint(&req).await.unwrap();
1693 assert_eq!(res.token, "aura_tk_abc123");
1694 assert_eq!(res.token_id, "tok_1");
1695 }
1696
1697 #[tokio::test]
1698 async fn test_revoke_token() {
1699 let mut server = Server::new_async().await;
1700 let _m = server
1701 .mock("DELETE", "/tokens/tok_1")
1702 .with_status(204)
1703 .create_async()
1704 .await;
1705
1706 let client = make_client(&server);
1707 assert!(client.tokens().revoke("tok_1").await.is_ok());
1708 }
1709
1710 #[tokio::test]
1713 async fn test_get_sandbox_project_id() {
1714 let mut server = Server::new_async().await;
1715 let _m = server
1716 .mock("GET", "/me/sandbox")
1717 .with_status(200)
1718 .with_header("content-type", "application/json")
1719 .with_body(r#"{"project_id":42}"#)
1720 .create_async()
1721 .await;
1722
1723 let client = make_client(&server);
1724 let pid = client.get_sandbox_project_id().await.unwrap();
1725 assert_eq!(pid, 42);
1726 }
1727
1728 #[tokio::test]
1729 async fn test_run_adhoc() {
1730 let mut server = Server::new_async().await;
1731 let _m = server
1732 .mock("POST", "/execute")
1733 .with_status(200)
1734 .with_header("content-type", "application/json")
1735 .match_body(r#"{"source":"workflow main {}"}"#)
1736 .with_body(r#"{"execution_id":"exec-1","project_id":42}"#)
1737 .create_async()
1738 .await;
1739
1740 let client = make_client(&server);
1741 let res = client
1742 .run_adhoc("workflow main {}", None, None)
1743 .await
1744 .unwrap();
1745 assert_eq!(res.execution_id, "exec-1");
1746 assert_eq!(res.project_id, 42);
1747 }
1748
1749 #[tokio::test]
1750 async fn test_run_adhoc_with_inputs() {
1751 let mut server = Server::new_async().await;
1752 let _m = server
1753 .mock("POST", "/execute")
1754 .with_status(200)
1755 .with_header("content-type", "application/json")
1756 .match_body(r#"{"source":"workflow main {}","inputs":{"doc":"hello"}}"#)
1757 .with_body(r#"{"execution_id":"exec-2","project_id":42}"#)
1758 .create_async()
1759 .await;
1760
1761 let client = make_client(&server);
1762 let mut inputs = std::collections::HashMap::new();
1763 inputs.insert(
1764 "doc".to_string(),
1765 serde_json::Value::String("hello".to_string()),
1766 );
1767 let res = client
1768 .run_adhoc("workflow main {}", Some(inputs), None)
1769 .await
1770 .unwrap();
1771 assert_eq!(res.execution_id, "exec-2");
1772 }
1773
1774 #[tokio::test]
1777 async fn test_fatal_error_on_401() {
1778 let mut server = Server::new_async().await;
1779 let _m = server
1780 .mock("GET", "/projects")
1781 .with_status(401)
1782 .with_body("Unauthorized")
1783 .create_async()
1784 .await;
1785
1786 let client = make_client(&server);
1787 let err = client.projects().list().await.unwrap_err();
1788 assert!(matches!(err, AkribesError::Fatal { .. }));
1789 }
1790
1791 #[tokio::test]
1792 async fn test_fatal_error_on_403() {
1793 let mut server = Server::new_async().await;
1794 let _m = server
1795 .mock("GET", "/projects")
1796 .with_status(403)
1797 .with_body("Forbidden")
1798 .create_async()
1799 .await;
1800
1801 let client = make_client(&server);
1802 let err = client.projects().list().await.unwrap_err();
1803 assert!(matches!(err, AkribesError::Fatal { .. }));
1804 }
1805
1806 #[tokio::test]
1807 async fn test_transient_error_on_429() {
1808 let mut server = Server::new_async().await;
1809 let _m = server
1810 .mock("GET", "/projects")
1811 .with_status(429)
1812 .with_body("Too Many Requests")
1813 .create_async()
1814 .await;
1815
1816 let client = make_client(&server);
1817 let err = client.projects().list().await.unwrap_err();
1818 assert!(matches!(err, AkribesError::Transient { .. }));
1819 }
1820
1821 #[tokio::test]
1822 async fn test_transient_error_on_503() {
1823 let mut server = Server::new_async().await;
1824 let _m = server
1825 .mock("GET", "/projects")
1826 .with_status(503)
1827 .with_body("Service Unavailable")
1828 .create_async()
1829 .await;
1830
1831 let client = make_client(&server);
1832 let err = client.projects().list().await.unwrap_err();
1833 assert!(matches!(err, AkribesError::Transient { .. }));
1834 }
1835
1836 #[tokio::test]
1838 async fn test_retry_after_populated_on_429() {
1839 let mut server = Server::new_async().await;
1840 let _m = server
1841 .mock("GET", "/projects")
1842 .with_status(429)
1843 .with_header("Retry-After", "7")
1844 .with_body("Too Many Requests")
1845 .create_async()
1846 .await;
1847
1848 let client = make_client(&server);
1849 let err = client.projects().list().await.unwrap_err();
1850 match err {
1851 AkribesError::Transient {
1852 retry_after: Some(d),
1853 ..
1854 } => {
1855 assert_eq!(d.as_secs(), 7);
1856 }
1857 other => panic!("expected Transient with retry_after=Some(7s), got {other:?}"),
1858 }
1859 }
1860
1861 #[tokio::test]
1863 async fn test_retry_after_none_when_header_absent() {
1864 let mut server = Server::new_async().await;
1865 let _m = server
1866 .mock("GET", "/projects")
1867 .with_status(503)
1868 .with_body("down")
1869 .create_async()
1870 .await;
1871
1872 let client = make_client(&server);
1873 let err = client.projects().list().await.unwrap_err();
1874 match err {
1875 AkribesError::Transient {
1876 retry_after: None, ..
1877 } => {}
1878 other => panic!("expected Transient with retry_after=None, got {other:?}"),
1879 }
1880 }
1881
1882 #[tokio::test]
1884 async fn test_retry_after_ignored_on_http_date() {
1885 let mut server = Server::new_async().await;
1886 let _m = server
1887 .mock("GET", "/projects")
1888 .with_status(429)
1889 .with_header("Retry-After", "Wed, 21 Oct 2026 07:28:00 GMT")
1890 .with_body("rate-limited")
1891 .create_async()
1892 .await;
1893
1894 let client = make_client(&server);
1895 let err = client.projects().list().await.unwrap_err();
1896 match err {
1897 AkribesError::Transient {
1898 retry_after: None, ..
1899 } => {}
1900 other => {
1901 panic!("expected Transient with retry_after=None for HTTP-date, got {other:?}")
1902 }
1903 }
1904 }
1905
1906 #[tokio::test]
1907 async fn test_transient_on_500_with_status() {
1908 let mut server = Server::new_async().await;
1913 let _m = server
1914 .mock("GET", "/projects")
1915 .with_status(500)
1916 .with_body("Internal Server Error")
1917 .create_async()
1918 .await;
1919
1920 let client = make_client(&server);
1921 let err = client.projects().list().await.unwrap_err();
1922 assert!(
1923 matches!(
1924 err,
1925 AkribesError::Transient {
1926 status: Some(500),
1927 ..
1928 }
1929 ),
1930 "expected Transient with status=500, got {err:?}",
1931 );
1932 }
1933
1934 #[tokio::test]
1935 async fn test_transient_on_504_with_status() {
1936 let mut server = Server::new_async().await;
1940 let _m = server
1941 .mock("GET", "/projects")
1942 .with_status(504)
1943 .with_body("Gateway Timeout")
1944 .create_async()
1945 .await;
1946
1947 let client = make_client(&server);
1948 let err = client.projects().list().await.unwrap_err();
1949 assert!(
1950 matches!(
1951 err,
1952 AkribesError::Transient {
1953 status: Some(504),
1954 ..
1955 }
1956 ),
1957 "expected Transient with status=504, got {err:?}",
1958 );
1959 let b504 = AkribesError::recommended_backoff_ms(504).unwrap();
1962 let b500 = AkribesError::recommended_backoff_ms(500).unwrap();
1963 let b502 = AkribesError::recommended_backoff_ms(502).unwrap();
1964 let b503 = AkribesError::recommended_backoff_ms(503).unwrap();
1965 assert!(b504 > b500);
1966 assert!(b504 > b502);
1967 assert_eq!(b503, AkribesError::recommended_backoff_ms(429).unwrap());
1969 assert_eq!(b500, b502);
1971 assert_eq!(err.transient_status(), Some(504));
1972 }
1973
1974 #[test]
1977 fn test_scoped_shim_without_project_id() {
1978 let client = AkribesClient::builder("http://localhost:3001")
1982 .name("test-app")
1983 .id("test-id")
1984 .build();
1985
1986 assert!(matches!(
1987 client.scoped(),
1988 Err(AkribesError::MissingProjectId)
1989 ));
1990 let _ = client.project(1).scripts();
1992 }
1993
1994 #[tokio::test]
1997 async fn test_get_document() {
1998 let mut server = Server::new_async().await;
1999 let body = r#"{"id":"doc_abc","filename":"report.pdf","content_type":"application/pdf","size_bytes":1024,"content_hash":"abc123","conversion_status":"ready","conversion_error":null,"created_at":"2026-04-12T00:00:00Z"}"#;
2000 let _m = server
2001 .mock("GET", "/documents/doc_abc")
2002 .with_status(200)
2003 .with_header("content-type", "application/json")
2004 .with_body(body)
2005 .create_async()
2006 .await;
2007
2008 let client = make_authed_client(&server);
2009 let doc = client.executions().get_document("doc_abc").await.unwrap();
2010 let doc = doc.expect("should return Some");
2011 assert_eq!(doc.id, "doc_abc");
2012 assert_eq!(doc.filename, "report.pdf");
2013 assert_eq!(doc.conversion_status, "ready");
2014 }
2015
2016 #[tokio::test]
2017 async fn test_get_document_not_found() {
2018 let mut server = Server::new_async().await;
2019 let _m = server
2020 .mock("GET", "/documents/doc_missing")
2021 .with_status(404)
2022 .create_async()
2023 .await;
2024
2025 let client = make_authed_client(&server);
2026 let doc = client
2027 .executions()
2028 .get_document("doc_missing")
2029 .await
2030 .unwrap();
2031 assert!(doc.is_none());
2032 }
2033
2034 #[tokio::test]
2035 async fn test_get_document_markdown() {
2036 let mut server = Server::new_async().await;
2037 let _m = server
2038 .mock("GET", "/documents/doc_abc/markdown")
2039 .with_status(200)
2040 .with_header("content-type", "application/json")
2041 .with_body(r##"{"markdown":"# Hello World"}"##)
2042 .create_async()
2043 .await;
2044
2045 let client = make_authed_client(&server);
2046 let md = client
2047 .executions()
2048 .get_document_markdown("doc_abc")
2049 .await
2050 .unwrap();
2051 assert_eq!(md, "# Hello World");
2052 }
2053
2054 #[tokio::test]
2055 async fn test_get_document_url() {
2056 let mut server = Server::new_async().await;
2057 let presigned = "https://s3.example.com/documents/doc_abc/report.pdf?token=xyz";
2058 let _m = server
2059 .mock("GET", "/documents/doc_abc/content")
2060 .with_status(200)
2061 .with_header("location", presigned)
2062 .create_async()
2063 .await;
2064
2065 let client = make_authed_client(&server);
2066 let url = client
2067 .executions()
2068 .get_document_url("doc_abc")
2069 .await
2070 .unwrap();
2071 assert_eq!(url, presigned);
2072 }
2073
2074 #[tokio::test]
2075 async fn test_reconvert_document() {
2076 let mut server = Server::new_async().await;
2077 let _m = server
2078 .mock("POST", "/documents/doc_abc/convert")
2079 .with_status(200)
2080 .with_header("content-type", "application/json")
2081 .with_body(r#"{"status":"ready"}"#)
2082 .create_async()
2083 .await;
2084
2085 let client = make_authed_client(&server);
2086 let resp = client
2087 .executions()
2088 .reconvert_document("doc_abc")
2089 .await
2090 .unwrap();
2091 assert_eq!(resp["status"], "ready");
2092 }
2093
2094 #[tokio::test]
2097 async fn test_projects_list_scripts_flat() {
2098 let mut server = Server::new_async().await;
2099 let body = format!("[{}]", script_json());
2100 let _m = server
2101 .mock("GET", "/projects/7/scripts")
2102 .with_status(200)
2103 .with_header("content-type", "application/json")
2104 .with_body(&body)
2105 .create_async()
2106 .await;
2107
2108 let client = make_client(&server);
2109 let scripts = client.projects().list_scripts(7).await.unwrap();
2110 assert_eq!(scripts.len(), 1);
2111 assert_eq!(scripts[0].name, "my_script");
2112 }
2113
2114 #[tokio::test]
2115 async fn test_projects_move_script_flat() {
2116 let mut server = Server::new_async().await;
2117 let _m = server
2118 .mock("POST", "/projects/3/scripts/foo/move")
2119 .match_body(r#"{"target_project_id":9}"#)
2120 .with_status(200)
2121 .with_header("content-type", "application/json")
2122 .with_body(script_json())
2123 .create_async()
2124 .await;
2125
2126 let client = make_client(&server);
2127 let s = client.projects().move_script(3, "foo", 9).await.unwrap();
2128 assert_eq!(s.id, 5);
2129 }
2130
2131 #[tokio::test]
2132 async fn test_projects_rename_script_flat() {
2133 let mut server = Server::new_async().await;
2134 let _m = server
2135 .mock("PATCH", "/projects/3/scripts/old")
2136 .match_body(r#"{"new_name":"new"}"#)
2137 .with_status(204)
2138 .create_async()
2139 .await;
2140
2141 let client = make_client(&server);
2142 client
2143 .projects()
2144 .rename_script(3, "old", "new")
2145 .await
2146 .unwrap();
2147 }
2148
2149 #[tokio::test]
2150 async fn test_projects_delete_script_flat() {
2151 let mut server = Server::new_async().await;
2152 let _m = server
2153 .mock("DELETE", "/projects/3/scripts/foo")
2154 .with_status(204)
2155 .create_async()
2156 .await;
2157
2158 let client = make_client(&server);
2159 client.projects().delete_script(3, "foo").await.unwrap();
2160 }
2161
2162 #[tokio::test]
2163 async fn test_projects_duplicate_script_flat() {
2164 let mut server = Server::new_async().await;
2165 let _m = server
2166 .mock("POST", "/projects/3/scripts/foo/duplicate")
2167 .with_status(200)
2168 .with_header("content-type", "application/json")
2169 .with_body(script_json())
2170 .create_async()
2171 .await;
2172
2173 let client = make_client(&server);
2174 let s = client
2175 .projects()
2176 .duplicate_script(3, "foo", None)
2177 .await
2178 .unwrap();
2179 assert_eq!(s.name, "my_script");
2180 }
2181
2182 fn lock_json() -> &'static str {
2185 r#"{
2186 "id":42,
2187 "client_id":"c1",
2188 "client_name":"sdk",
2189 "script_name":"my_script",
2190 "channel":"production",
2191 "bound_version_id":3,
2192 "lifetime":"persistent",
2193 "drifted":false,
2194 "created_by":null,
2195 "created_at":"2026-04-01T00:00:00Z",
2196 "input_schema":"{}"
2197 }"#
2198 }
2199
2200 #[tokio::test]
2201 async fn test_list_locks_for_flat() {
2202 let mut server = Server::new_async().await;
2203 let body = format!("[{}]", lock_json());
2204 let _m = server
2205 .mock("GET", "/projects/7/scripts/my_script/locks")
2206 .with_status(200)
2207 .with_header("content-type", "application/json")
2208 .with_body(&body)
2209 .create_async()
2210 .await;
2211
2212 let client = make_client(&server);
2213 let locks = client
2217 .project(1)
2218 .registered_clients()
2219 .list_locks_for(7, "my_script")
2220 .await
2221 .unwrap();
2222 assert_eq!(locks.len(), 1);
2223 assert_eq!(locks[0].id, 42);
2224 }
2225
2226 #[tokio::test]
2227 async fn test_delete_lock_flat() {
2228 let mut server = Server::new_async().await;
2229 let _m = server
2230 .mock("DELETE", "/projects/7/scripts/my_script/locks/42")
2231 .with_status(204)
2232 .create_async()
2233 .await;
2234
2235 let client = make_client(&server);
2236 client
2237 .project(1)
2238 .registered_clients()
2239 .delete_lock(7, "my_script", 42)
2240 .await
2241 .unwrap();
2242 }
2243
2244 #[tokio::test]
2245 async fn test_update_lock_flat() {
2246 let mut server = Server::new_async().await;
2247 let _m = server
2248 .mock("PATCH", "/projects/7/scripts/my_script/locks/42/rebind")
2249 .match_body(r#"{"version_id":11}"#)
2250 .with_status(200)
2251 .with_header("content-type", "application/json")
2252 .with_body(lock_json())
2253 .create_async()
2254 .await;
2255
2256 let client = make_client(&server);
2257 let lock = client
2258 .project(1)
2259 .registered_clients()
2260 .update_lock(7, "my_script", 42, Some(11))
2261 .await
2262 .unwrap();
2263 assert_eq!(lock.id, 42);
2264 assert_eq!(lock.bound_version_id, Some(3));
2265 }
2266}