-
Notifications
You must be signed in to change notification settings - Fork 1.5k
test(reborn): E-TRIGGERED-SUBMIT enabler — triggered-turn submit seam #5516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1006ac3
test(reborn): E-TRIGGERED-SUBMIT enabler — triggered-turn submit seam
henrypark133 95adba2
fix(reborn): address bot review comments on E-TRIGGERED-SUBMIT seam
henrypark133 600420b
fix(reborn): address human review comments on E-TRIGGERED-SUBMIT seam
henrypark133 f3236b8
fix(reborn): match content: prefix convention for synthetic content_ref
henrypark133 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| //! Reborn integration-test framework — E-TRIGGERED-SUBMIT driving test. | ||
| //! | ||
| //! Proves the trusted-trigger submission seam end-to-end: this exercises the | ||
| //! real `TrustedTriggerFireSubmitter` (via | ||
| //! `RebornIntegrationHarness::submit_triggered_turn`), proving that | ||
| //! `TurnOriginKind::ScheduledTrigger` propagates all the way into the | ||
| //! persisted run state observable at the coordinator boundary | ||
| //! (`TurnCoordinator::get_run_state`). | ||
| //! | ||
| //! This is a smoke slice, not the exhaustive matrix: it asserts only that the | ||
| //! submission is accepted and that the resulting run state carries the | ||
| //! scheduled-trigger origin. It does not drive the scripted model — the | ||
| //! triggered turn executes under its own resolved scope, which intentionally | ||
| //! has no registered gateway, so this test asserts submission + persisted | ||
| //! `product_context` only, not turn completion. The exhaustive origin/delivery | ||
| //! matrix (surface types, adapters, delivery routing) is separate later | ||
| //! coverage under C-TRIGGERED-ORIGIN / C-TRIGGERED-DELIVERY. | ||
|
|
||
| // The support tree is large and shared; a single-test file exercises only a | ||
| // slice of it, so suppress dead-code warnings on the includes (matches | ||
| // `reborn_qa_recorded_behavior.rs`). | ||
| #[allow(dead_code)] | ||
| #[path = "support/reborn/mod.rs"] | ||
| mod reborn_support; | ||
| #[allow(dead_code)] | ||
| mod support; | ||
|
|
||
| use reborn_support::builder::RebornIntegrationHarness; | ||
|
|
||
| #[tokio::test] | ||
| async fn triggered_submit_carries_scheduled_trigger_origin() { | ||
| let harness = RebornIntegrationHarness::test_default() | ||
| .build() | ||
| .await | ||
| .expect("harness builds"); | ||
|
|
||
| let submission = harness | ||
| .submit_triggered_turn("run the scheduled reminder") | ||
| .await | ||
| .expect("triggered submit accepted"); | ||
|
|
||
| let state = harness | ||
| .coordinator | ||
| .get_run_state(ironclaw_turns::GetRunStateRequest { | ||
| scope: submission.turn_scope, | ||
| run_id: submission.run_id, | ||
| }) | ||
| .await | ||
| .expect("run state readable at the coordinator boundary"); | ||
|
|
||
| assert_eq!( | ||
| state.product_context.map(|context| context.origin), | ||
| Some(ironclaw_turns::TurnOriginKind::ScheduledTrigger), | ||
| "a turn submitted through the trusted-trigger submitter must carry \ | ||
| TurnOriginKind::ScheduledTrigger, proving the real trusted-trigger \ | ||
| origin wire is exercised end to end", | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| //! E-TRIGGERED-SUBMIT enabler seam: submit a turn through the REAL | ||
| //! `TrustedTriggerFireSubmitter` so it carries a genuine | ||
| //! `TurnOriginKind::ScheduledTrigger` origin, end to end. | ||
| //! | ||
| //! [`RebornIntegrationHarness::submit_triggered_turn`] builds a synthetic | ||
| //! `TriggerFire` + `TriggerMaterializedPrompt::for_fire` (test ctor) and hands | ||
| //! them to the production `trusted_trigger_fire_submitter` — it does NOT fake | ||
| //! or re-implement origin tagging itself. That submitter runs over a fresh, | ||
| //! per-call `InMemoryConversationServices` dedicated to the trigger path (the | ||
| //! same conversation-services type production's own local-dev build wires for | ||
| //! this exact purpose), while the harness's REAL shared `coordinator` is | ||
| //! passed through unchanged, so the submitted run lands in the same turn | ||
| //! store/scheduler as every other harness turn. | ||
|
|
||
| // Shared integration-test support: not every binary that mounts the | ||
| // `reborn_support` tree consumes this module — its symbols read as dead there | ||
| // under the all-features `-D warnings` lane. Module-level allow matches | ||
| // `builder.rs`/`assertions.rs`/`session_thread.rs`. | ||
| #![allow(dead_code)] | ||
|
|
||
| use std::sync::Arc; | ||
|
|
||
| use chrono::{TimeZone, Utc}; | ||
| use ironclaw_conversations::{ | ||
| AdapterInstallationId, AdapterKind, ExternalActorRef, InMemoryConversationServices, | ||
| trusted_trigger_fire_submitter, | ||
| }; | ||
| use ironclaw_triggers::{ | ||
| TRIGGER_TRUSTED_ADAPTER_INSTALLATION_ID, TRIGGER_TRUSTED_ADAPTER_KIND, | ||
| TRIGGER_TRUSTED_EXTERNAL_ACTOR_NAMESPACE, TriggerFire, TriggerFireIdentity, TriggerId, | ||
| TriggerInboundContentRef, TriggerMaterializedPrompt, TrustedTriggerFireSubmitOutcome, | ||
| TrustedTriggerSubmitRequest, | ||
| }; | ||
| use ironclaw_turns::{TurnRunId, TurnScope}; | ||
|
|
||
| use super::builder::RebornIntegrationHarness; | ||
|
|
||
| // `builder.rs`'s `HarnessResult` is module-private; every sibling file that | ||
|
henrypark133 marked this conversation as resolved.
|
||
| // needs the alias (`assertions.rs`, `harness.rs`, `harness_mcp.rs`) declares | ||
| // its own identical copy rather than reaching across the module boundary. | ||
| type HarnessResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; | ||
|
|
||
| /// Far-future, deterministic fire slot — no wall-clock flake, matches the | ||
| /// style already used in `tests/reborn_group_triggers/scenario_verbs_lifecycle.rs`. | ||
| fn triggered_fire_slot() -> chrono::DateTime<chrono::Utc> { | ||
| Utc.with_ymd_and_hms(2999, 1, 1, 0, 0, 0).unwrap() | ||
| } | ||
|
|
||
| /// Result of a successful `submit_triggered_turn` call. | ||
| pub(crate) struct TriggeredSubmission { | ||
| pub(crate) run_id: TurnRunId, | ||
| /// The trigger's OWN resolved scope, as returned by the real submitter. | ||
| /// The trusted-submit contract forbids re-deriving binding keys from a | ||
| /// `TriggerFire` (see `ironclaw_triggers::trusted_submit` docs), so callers | ||
| /// must read this back rather than reconstruct it. | ||
| pub(crate) turn_scope: TurnScope, | ||
| } | ||
|
|
||
| impl RebornIntegrationHarness { | ||
| /// Submit a turn through the REAL `TrustedTriggerFireSubmitter` so it carries | ||
| /// a genuine `TurnOriginKind::ScheduledTrigger` origin, end to end (E-TRIGGERED-SUBMIT). | ||
| /// | ||
| /// Builds a synthetic `TriggerFire` + `TriggerMaterializedPrompt::for_fire` (test | ||
| /// ctor) and hands them to the production `trusted_trigger_fire_submitter`, over a | ||
| /// fresh, per-call `InMemoryConversationServices` dedicated to the trigger path — | ||
| /// the same conversation-services type production's own local-dev build wires for | ||
| /// this exact purpose (`ironclaw_reborn_composition::runtime.rs`, | ||
| /// `build_trigger_poller_services_from_conversation_services`), NOT the harness's | ||
| /// unrelated direct-chat product-workflow binding service (a different axis, even | ||
| /// in real production). The harness's REAL shared `coordinator` is passed through | ||
| /// unchanged, so the submitted run lands in the same turn store/scheduler as every | ||
| /// other harness turn. | ||
| /// | ||
| /// The submitted run then executes autonomously on the background scheduler; no | ||
|
henrypark133 marked this conversation as resolved.
|
||
| /// scripted model gateway is registered for the trigger's own resolved scope, so it | ||
| /// fails benignly on a scope-miss (`ScopeRegistryGateway`'s `ConfigurationError` | ||
| /// sentinel). `product_context` (carrying the origin) is persisted synchronously at | ||
| /// submit time, before that later failure — this seam and its driving test assert | ||
| /// only on that submit-time state, not on anything after. Driving a triggered run | ||
| /// to model completion, or asserting behavior across that later failure, is | ||
| /// C-TRIGGERED-DELIVERY, not this seam. | ||
| pub(crate) async fn submit_triggered_turn( | ||
| &self, | ||
| prompt: &str, | ||
| ) -> HarnessResult<TriggeredSubmission> { | ||
| let tenant_id = self.binding.tenant_id.clone(); | ||
| let creator_user_id = self.binding.actor_user_id.clone(); | ||
| let fire_slot = triggered_fire_slot(); | ||
| let fire = TriggerFire { | ||
| identity: TriggerFireIdentity::new(tenant_id.clone(), TriggerId::new(), fire_slot), | ||
| creator_user_id: creator_user_id.clone(), | ||
| agent_id: self.binding.agent_id.clone(), | ||
| project_id: self.binding.project_id.clone(), | ||
| prompt: prompt.to_string(), | ||
| }; | ||
| let content_ref = TriggerInboundContentRef::new(format!( | ||
| "content:triggered-submit:{}", | ||
| fire.identity.external_event_id().as_str() | ||
| ))?; | ||
|
henrypark133 marked this conversation as resolved.
|
||
| let materialized_prompt = TriggerMaterializedPrompt::for_fire(&fire, content_ref); | ||
| let request = | ||
| TrustedTriggerSubmitRequest::new_for_test(fire, materialized_prompt, fire_slot); | ||
|
|
||
| // Pre-pair the trigger's canonical external actor — mirrors | ||
| // `TriggerTrustedInboundBinding::for_fire`'s own derivation exactly and mirrors | ||
| // production's pre-seed requirement (`resolve_actor` hard-fails | ||
| // `BindingRequired` without it, on both trusted and untrusted resolve paths). | ||
| // Uses `try_pair_external_actor` (not the infallible `pair_external_actor` | ||
| // wrapper) so a pairing failure surfaces here, at the seam boundary, instead | ||
| // of resurfacing later as an indirect binding-resolution error. | ||
| let conversations = InMemoryConversationServices::default(); | ||
| conversations | ||
| .try_pair_external_actor( | ||
| tenant_id, | ||
| AdapterKind::new(TRIGGER_TRUSTED_ADAPTER_KIND)?, | ||
| AdapterInstallationId::new(TRIGGER_TRUSTED_ADAPTER_INSTALLATION_ID)?, | ||
| ExternalActorRef::new( | ||
| TRIGGER_TRUSTED_EXTERNAL_ACTOR_NAMESPACE, | ||
| creator_user_id.as_str(), | ||
| )?, | ||
| creator_user_id, | ||
| ) | ||
| .await?; | ||
|
|
||
| let submitter = trusted_trigger_fire_submitter( | ||
| conversations.clone(), | ||
| conversations, | ||
| Arc::clone(&self.coordinator), | ||
| ); | ||
| match submitter.submit_trusted_trigger_fire(request).await? { | ||
| TrustedTriggerFireSubmitOutcome::Accepted { | ||
| run_id, turn_scope, .. | ||
| } => Ok(TriggeredSubmission { run_id, turn_scope }), | ||
| TrustedTriggerFireSubmitOutcome::Replayed { .. } => { | ||
| Err("first triggered submit unexpectedly replayed".into()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.