|
1 | 1 | package dev.axme.sdk; |
2 | 2 |
|
3 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 4 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
4 | 5 | import static org.junit.jupiter.api.Assertions.assertThrows; |
5 | 6 | import static org.junit.jupiter.api.Assertions.assertTrue; |
6 | 7 |
|
7 | 8 | import com.fasterxml.jackson.core.type.TypeReference; |
8 | 9 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 10 | +import java.util.List; |
9 | 11 | import java.util.Map; |
10 | 12 | import okhttp3.mockwebserver.MockResponse; |
11 | 13 | import okhttp3.mockwebserver.MockWebServer; |
@@ -429,4 +431,133 @@ void organizationRoutingDeliveryAndBillingEndpointsAreReachable() throws Excepti |
429 | 431 | assertEquals("/v1/billing/invoices?org_id=org_1&workspace_id=ws_1&status=open", server.takeRequest().getPath()); |
430 | 432 | assertEquals("/v1/billing/invoices/inv_1", server.takeRequest().getPath()); |
431 | 433 | } |
| 434 | + |
| 435 | + @Test |
| 436 | + void observeCollectsEventsAndStopsAtTerminal() throws Exception { |
| 437 | + // First poll: two non-terminal events |
| 438 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 439 | + "{\"ok\":true,\"events\":[" |
| 440 | + + "{\"seq\":1,\"event_type\":\"intent.created\",\"status\":\"PENDING\"}," |
| 441 | + + "{\"seq\":2,\"event_type\":\"intent.dispatched\",\"status\":\"IN_PROGRESS\"}" |
| 442 | + + "]}")); |
| 443 | + // Second poll: empty (triggers sleep + re-poll) |
| 444 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 445 | + "{\"ok\":true,\"events\":[]}")); |
| 446 | + // Third poll: terminal event |
| 447 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 448 | + "{\"ok\":true,\"events\":[" |
| 449 | + + "{\"seq\":3,\"event_type\":\"intent.completed\",\"status\":\"COMPLETED\"}" |
| 450 | + + "]}")); |
| 451 | + |
| 452 | + List<Map<String, Object>> events = client.observe("it_obs_1", |
| 453 | + new ObserveOptions(0, 0.05, null)); |
| 454 | + |
| 455 | + assertEquals(3, events.size()); |
| 456 | + assertEquals("intent.created", events.get(0).get("event_type")); |
| 457 | + assertEquals("intent.dispatched", events.get(1).get("event_type")); |
| 458 | + assertEquals("intent.completed", events.get(2).get("event_type")); |
| 459 | + assertEquals("COMPLETED", events.get(2).get("status")); |
| 460 | + |
| 461 | + // Verify since parameter advances: first call since=0, third since=2 |
| 462 | + RecordedRequest req1 = server.takeRequest(); |
| 463 | + assertEquals("/v1/intents/it_obs_1/events?since=0", req1.getPath()); |
| 464 | + RecordedRequest req2 = server.takeRequest(); |
| 465 | + assertEquals("/v1/intents/it_obs_1/events?since=2", req2.getPath()); |
| 466 | + RecordedRequest req3 = server.takeRequest(); |
| 467 | + assertEquals("/v1/intents/it_obs_1/events?since=2", req3.getPath()); |
| 468 | + } |
| 469 | + |
| 470 | + @Test |
| 471 | + void observeStopsOnEventTypeAlone() throws Exception { |
| 472 | + // Terminal via event_type only (no status field) |
| 473 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 474 | + "{\"ok\":true,\"events\":[" |
| 475 | + + "{\"seq\":1,\"event_type\":\"intent.failed\"}" |
| 476 | + + "]}")); |
| 477 | + |
| 478 | + List<Map<String, Object>> events = client.observe("it_obs_2", |
| 479 | + new ObserveOptions(0, 0.05, null)); |
| 480 | + |
| 481 | + assertEquals(1, events.size()); |
| 482 | + assertEquals("intent.failed", events.get(0).get("event_type")); |
| 483 | + } |
| 484 | + |
| 485 | + @Test |
| 486 | + void observeStopsOnStatusAlone() throws Exception { |
| 487 | + // Terminal via status only (no event_type field) |
| 488 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 489 | + "{\"ok\":true,\"events\":[" |
| 490 | + + "{\"seq\":1,\"status\":\"CANCELED\"}" |
| 491 | + + "]}")); |
| 492 | + |
| 493 | + List<Map<String, Object>> events = client.observe("it_obs_3", |
| 494 | + new ObserveOptions(0, 0.05, null)); |
| 495 | + |
| 496 | + assertEquals(1, events.size()); |
| 497 | + assertEquals("CANCELED", events.get(0).get("status")); |
| 498 | + } |
| 499 | + |
| 500 | + @Test |
| 501 | + void observeTimesOut() { |
| 502 | + // Enqueue enough empty responses to keep polling |
| 503 | + for (int i = 0; i < 20; i++) { |
| 504 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 505 | + "{\"ok\":true,\"events\":[]}")); |
| 506 | + } |
| 507 | + |
| 508 | + AxmeTimeoutException ex = assertThrows(AxmeTimeoutException.class, () -> |
| 509 | + client.observe("it_timeout", new ObserveOptions(0, 0.05, 0.1))); |
| 510 | + |
| 511 | + assertEquals("it_timeout", ex.getIntentId()); |
| 512 | + assertTrue(ex.getTimeoutSeconds() > 0); |
| 513 | + } |
| 514 | + |
| 515 | + @Test |
| 516 | + void waitForReturnsTerminalEvent() throws Exception { |
| 517 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 518 | + "{\"ok\":true,\"events\":[" |
| 519 | + + "{\"seq\":1,\"event_type\":\"intent.created\",\"status\":\"PENDING\"}" |
| 520 | + + "]}")); |
| 521 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 522 | + "{\"ok\":true,\"events\":[" |
| 523 | + + "{\"seq\":2,\"event_type\":\"intent.timed_out\",\"status\":\"TIMED_OUT\"}" |
| 524 | + + "]}")); |
| 525 | + |
| 526 | + Map<String, Object> terminal = client.waitFor("it_wait_1", |
| 527 | + new ObserveOptions(0, 0.05, null)); |
| 528 | + |
| 529 | + assertNotNull(terminal); |
| 530 | + assertEquals("TIMED_OUT", terminal.get("status")); |
| 531 | + assertEquals("intent.timed_out", terminal.get("event_type")); |
| 532 | + } |
| 533 | + |
| 534 | + @Test |
| 535 | + void observeWithSinceParameter() throws Exception { |
| 536 | + // Start from since=5, verify first request uses since=5 |
| 537 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 538 | + "{\"ok\":true,\"events\":[" |
| 539 | + + "{\"seq\":6,\"event_type\":\"intent.completed\",\"status\":\"COMPLETED\"}" |
| 540 | + + "]}")); |
| 541 | + |
| 542 | + List<Map<String, Object>> events = client.observe("it_since", |
| 543 | + new ObserveOptions(5, 0.05, null)); |
| 544 | + |
| 545 | + assertEquals(1, events.size()); |
| 546 | + RecordedRequest req = server.takeRequest(); |
| 547 | + assertEquals("/v1/intents/it_since/events?since=5", req.getPath()); |
| 548 | + } |
| 549 | + |
| 550 | + @Test |
| 551 | + void observeWithNullOptionsUsesDefaults() throws Exception { |
| 552 | + server.enqueue(new MockResponse().setResponseCode(200).setBody( |
| 553 | + "{\"ok\":true,\"events\":[" |
| 554 | + + "{\"seq\":1,\"event_type\":\"intent.completed\",\"status\":\"COMPLETED\"}" |
| 555 | + + "]}")); |
| 556 | + |
| 557 | + List<Map<String, Object>> events = client.observe("it_null_opts", null); |
| 558 | + |
| 559 | + assertEquals(1, events.size()); |
| 560 | + RecordedRequest req = server.takeRequest(); |
| 561 | + assertEquals("/v1/intents/it_null_opts/events?since=0", req.getPath()); |
| 562 | + } |
432 | 563 | } |
0 commit comments