test(trunk): reflex-on-trunk verification (T7) + PSTN sim e2e (T8) — slice-5
Some checks failed
CI / clippy (pull_request) Successful in 2m25s
CI / test (1.85) (pull_request) Successful in 6m0s
CI / fmt (pull_request) Failing after 14m2s
CI / test (stable) (pull_request) Successful in 6m6s
CI / deny (pull_request) Failing after 1m32s
CI / twilio-live (manual only) (pull_request) Has been skipped
Some checks failed
CI / clippy (pull_request) Successful in 2m25s
CI / test (1.85) (pull_request) Successful in 6m0s
CI / fmt (pull_request) Failing after 14m2s
CI / test (stable) (pull_request) Successful in 6m6s
CI / deny (pull_request) Failing after 1m32s
CI / twilio-live (manual only) (pull_request) Has been skipped
T7: proves slice-4's Reflex<TapAudioPipe> + LocalVadReflex decorates the trunk leg's TapAudioPipe identically; barge-in fires on PSTN caller speech through the same state machine as WebRTC caller speech. T8: MockRealtimeBrain + BrainShim drives a synthetic PSTN caller through the FOB reflex loop end-to-end: loud PCM -> local VAD trips -> barge kills -> brain reply -> un-mute -> idle timeout (caller hangup) closes the session. Dev-dependencies added to rutster-trunk/Cargo.toml so the integration tests reach MockRealtimeBrain, futures-util, and tokio-tungstenite without pulling FOB source into the trunk crate. Signed-off-by: Aaron D. Lee <himself@adlee.work>
This commit is contained in:
326
Cargo.lock
generated
326
Cargo.lock
generated
@@ -34,9 +34,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.8"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asn1-rs"
|
name = "asn1-rs"
|
||||||
@@ -113,9 +113,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.17.1"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
|
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"untrusted 0.7.1",
|
"untrusted 0.7.1",
|
||||||
@@ -124,15 +124,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.42.0"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
|
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
"dunce",
|
"dunce",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
"pkg-config",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -223,15 +222,6 @@ version = "1.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bit-vec"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -267,9 +257,9 @@ checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.66"
|
version = "1.2.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -465,9 +455,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
@@ -481,9 +474,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dimpl"
|
name = "dimpl"
|
||||||
version = "0.7.1"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb2b8390bf0021eae92cf96a0e3a54c531b6315c47edfe9220f5fc61bc9e1f63"
|
checksum = "0a11091ebc139b18d6b5878d8769ca43ad5ea8a7e2f5ed1e1f25e172951ef0ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
@@ -776,107 +769,14 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_collections"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
|
||||||
dependencies = [
|
|
||||||
"displaydoc",
|
|
||||||
"potential_utf",
|
|
||||||
"utf8_iter",
|
|
||||||
"yoke",
|
|
||||||
"zerofrom",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_locale_core"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
|
||||||
dependencies = [
|
|
||||||
"displaydoc",
|
|
||||||
"litemap",
|
|
||||||
"tinystr",
|
|
||||||
"writeable",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_normalizer"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
|
||||||
dependencies = [
|
|
||||||
"icu_collections",
|
|
||||||
"icu_normalizer_data",
|
|
||||||
"icu_properties",
|
|
||||||
"icu_provider",
|
|
||||||
"smallvec",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_normalizer_data"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_properties"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
|
||||||
dependencies = [
|
|
||||||
"icu_collections",
|
|
||||||
"icu_locale_core",
|
|
||||||
"icu_properties_data",
|
|
||||||
"icu_provider",
|
|
||||||
"zerotrie",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_properties_data"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "icu_provider"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
|
||||||
dependencies = [
|
|
||||||
"displaydoc",
|
|
||||||
"icu_locale_core",
|
|
||||||
"writeable",
|
|
||||||
"yoke",
|
|
||||||
"zerofrom",
|
|
||||||
"zerotrie",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"idna_adapter",
|
"unicode-bidi",
|
||||||
"smallvec",
|
"unicode-normalization",
|
||||||
"utf8_iter",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna_adapter"
|
|
||||||
version = "1.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
|
||||||
dependencies = [
|
|
||||||
"icu_normalizer",
|
|
||||||
"icu_properties",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -952,12 +852,6 @@ version = "0.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "litemap"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -1053,9 +947,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.8"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -1063,9 +957,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.2"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
@@ -1169,15 +1063,6 @@ version = "0.3.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "potential_utf"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
|
||||||
dependencies = [
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1366,9 +1251,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rcgen"
|
name = "rcgen"
|
||||||
version = "0.14.8"
|
version = "0.14.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
|
checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -1457,9 +1342,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusticata-macros"
|
name = "rusticata-macros"
|
||||||
@@ -1499,9 +1384,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.0"
|
version = "1.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -1631,7 +1516,9 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
|
"futures-util",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rutster-brain-realtime",
|
||||||
"rutster-call-model",
|
"rutster-call-model",
|
||||||
"rutster-media",
|
"rutster-media",
|
||||||
"rutster-tap",
|
"rutster-tap",
|
||||||
@@ -1639,8 +1526,10 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1658,9 +1547,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sctp-proto"
|
name = "sctp-proto"
|
||||||
version = "0.10.1"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "199c5c38008c2c151e27afd228230b1b69f849b1f7629f5df86662ee1f456187"
|
checksum = "5376eaf8a764118abd6bee29673c68eab91f0913b448fe2944e74488b63c37b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"crc",
|
"crc",
|
||||||
@@ -1823,12 +1712,6 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "stable_deref_trait"
|
|
||||||
version = "1.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "str0m"
|
name = "str0m"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@@ -1977,44 +1860,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.53"
|
version = "0.3.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"itoa",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.9"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.31"
|
version = "0.2.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tinystr"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
|
||||||
dependencies = [
|
|
||||||
"displaydoc",
|
|
||||||
"zerovec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -2218,12 +2092,27 @@ version = "1.20.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -2238,14 +2127,13 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2254,12 +2142,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8_iter"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.23.4"
|
version = "1.23.4"
|
||||||
@@ -2485,12 +2367,6 @@ version = "0.57.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "writeable"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x509-cert"
|
name = "x509-cert"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -2522,37 +2398,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yasna"
|
name = "yasna"
|
||||||
version = "0.6.0"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
|
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-vec",
|
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yoke"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
|
||||||
dependencies = [
|
|
||||||
"stable_deref_trait",
|
|
||||||
"yoke-derive",
|
|
||||||
"zerofrom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yoke-derive"
|
|
||||||
version = "0.8.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"synstructure",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.52"
|
version = "0.8.52"
|
||||||
@@ -2573,66 +2425,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerofrom"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
|
||||||
dependencies = [
|
|
||||||
"zerofrom-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerofrom-derive"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"synstructure",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerotrie"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
|
||||||
dependencies = [
|
|
||||||
"displaydoc",
|
|
||||||
"yoke",
|
|
||||||
"zerofrom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerovec"
|
|
||||||
version = "0.11.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
|
||||||
dependencies = [
|
|
||||||
"yoke",
|
|
||||||
"zerofrom",
|
|
||||||
"zerovec-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerovec-derive"
|
|
||||||
version = "0.11.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ reqwest = { workspace = true, optional = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tower = { workspace = true }
|
tower = { workspace = true }
|
||||||
|
futures-util = { workspace = true }
|
||||||
|
tokio-tungstenite = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
rutster-brain-realtime = { path = "../rutster-brain-realtime", features = ["mock"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
79
crates/rutster-trunk/tests/reflex_on_trunk.rs
Normal file
79
crates/rutster-trunk/tests/reflex_on_trunk.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// crates/rutster-trunk/tests/reflex_on_trunk.rs
|
||||||
|
//
|
||||||
|
// T7 — Reflex-on-trunk-leg verification test (slice-5 spec §7).
|
||||||
|
//
|
||||||
|
// Proves that slice-4's `Reflex<TapAudioPipe>` + `LocalVadReflex` decorate the
|
||||||
|
// trunk leg's `TapAudioPipe` identically to a WebRTC leg. A PSTN caller's loud
|
||||||
|
// PCM triggers the same local-VAD state machine and the same `barge-in` kill
|
||||||
|
// that a WebRTC caller's audio does.
|
||||||
|
//
|
||||||
|
// This is a *unit* integration test: it constructs the wrapped pipe stack
|
||||||
|
// directly, without the binary's MediaThread or a real WSS brain. The heavier
|
||||||
|
// end-to-end WSS sim lives in `sim_integ.rs` (T8).
|
||||||
|
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use rutster_media::{
|
||||||
|
AudioSink, AudioSource, LocalVadReflex, PcmFrame, Reflex, ReflexMetrics, VAD_DEBOUNCE_FRAMES,
|
||||||
|
};
|
||||||
|
use rutster_tap::TapAudioPipe;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
/// Build a loud 24 kHz PCM frame whose RMS energy is well above
|
||||||
|
/// `VAD_RMS_THRESHOLD`. The mock caller uses a constant amplitude of 1000,
|
||||||
|
/// the same value slice-4's reflex unit tests use.
|
||||||
|
fn loud_frame() -> PcmFrame {
|
||||||
|
let mut frame = PcmFrame::zeroed();
|
||||||
|
for s in frame.samples.iter_mut() {
|
||||||
|
*s = 1000;
|
||||||
|
}
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn local_vad_on_trunk_pipe_kills_playout_and_resumes_on_fresh_brain_audio() {
|
||||||
|
// Given: a TapAudioPipe + the same Reflex<LocalVadReflex> composition the
|
||||||
|
// FOB builds for every leg (WebRTC or trunk).
|
||||||
|
let (tx_pcm_in, rx_pcm_in) = mpsc::channel(32);
|
||||||
|
let (tx_audio_out, rx_audio_out) = mpsc::channel(32);
|
||||||
|
let _rx_pcm_in = rx_pcm_in; // brain side; not used in this unit test
|
||||||
|
let tap_metrics = rutster_tap::TapMetrics::new();
|
||||||
|
let tap_pipe = TapAudioPipe::new(tx_pcm_in, rx_audio_out, tap_metrics);
|
||||||
|
|
||||||
|
let (advisory_tx, advisory_rx) = mpsc::channel(16);
|
||||||
|
let metrics = ReflexMetrics::new();
|
||||||
|
let reflex = Reflex::new(tap_pipe, advisory_rx, metrics.clone());
|
||||||
|
let mut wrapped_pipe = LocalVadReflex::new(reflex, advisory_tx);
|
||||||
|
|
||||||
|
// Pre-load one brain audio_out frame so the playout ring is non-empty.
|
||||||
|
tx_audio_out.send(PcmFrame::zeroed()).await.unwrap();
|
||||||
|
|
||||||
|
// When: the PSTN caller speaks for `VAD_DEBOUNCE_FRAMES` consecutive ticks.
|
||||||
|
let loud = loud_frame();
|
||||||
|
for _ in 0..VAD_DEBOUNCE_FRAMES {
|
||||||
|
wrapped_pipe.on_pcm_frame(loud.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then: the third `next_pcm_frame` drains the local-VAD advisory, mutes the
|
||||||
|
// pipe, flushes the ring, and returns `None` (playout is killed).
|
||||||
|
let killed = wrapped_pipe.next_pcm_frame();
|
||||||
|
assert!(
|
||||||
|
killed.is_none(),
|
||||||
|
"local VAD must barge-in and suppress playout on the trunk leg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
metrics.barge_in_count.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"barge-in must fire exactly once for the first loud utterance"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And when: a fresh brain reply arrives after the barge.
|
||||||
|
tx_audio_out.send(PcmFrame::zeroed()).await.unwrap();
|
||||||
|
|
||||||
|
// Then: the Reflex un-mutes and returns the fresh frame.
|
||||||
|
let resumed = wrapped_pipe.next_pcm_frame();
|
||||||
|
assert!(
|
||||||
|
resumed.is_some(),
|
||||||
|
"first fresh audio_out post-barge must resume trunk-leg playout"
|
||||||
|
);
|
||||||
|
}
|
||||||
348
crates/rutster-trunk/tests/sim_integ.rs
Normal file
348
crates/rutster-trunk/tests/sim_integ.rs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
// crates/rutster-trunk/tests/sim_integ.rs
|
||||||
|
//
|
||||||
|
// T8 — PSTN sim end-to-end integration test (slice-5 spec §7).
|
||||||
|
//
|
||||||
|
// Drives a synthetic PSTN caller through the FOB reflex loop end-to-end:
|
||||||
|
// loud PCM -> local VAD trips -> barge kills playout -> brain replies
|
||||||
|
// -> un-mute -> caller hangup -> session closes.
|
||||||
|
//
|
||||||
|
// The test is in `rutster-trunk` so it can construct `TrunkSession` directly.
|
||||||
|
// It cannot use the binary crate's `spawn_tap_engine` / `MediaThread` (circular
|
||||||
|
// dev-dependency), so it builds a minimal test-only tap engine task that calls
|
||||||
|
// `rutster_tap::tap_client::run_tap_client` against the same BrainShim surface
|
||||||
|
// slice-4's `barge_in_integration.rs` uses.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use rutster_brain_realtime::mock::MockRealtimeBrain;
|
||||||
|
use rutster_brain_realtime::openai_client::run_openai_pump;
|
||||||
|
use rutster_call_model::Channel;
|
||||||
|
use rutster_media::{LocalVadReflex, PcmFrame, Reflex, ReflexMetrics};
|
||||||
|
|
||||||
|
use rutster_tap::{
|
||||||
|
DecodedPayload, FunctionCallEvent, FunctionCallOutputEvent, TapAudioPipe, TapMetrics,
|
||||||
|
decode_envelope, encode_hello, tap_client::run_tap_client,
|
||||||
|
};
|
||||||
|
use rutster_trunk::loop_driver::drive;
|
||||||
|
use rutster_trunk::session::TrunkSession;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||||
|
use tracing::info;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
// === Brain-shim helpers (mirrored from slice-4 barge_in_integration.rs) ===
|
||||||
|
// The brain process's accept loop is inlined so the test exercises the real
|
||||||
|
// OpenAI-client pump (`run_openai_pump`) against the mock brain without
|
||||||
|
// spawning a subprocess or depending on private helpers in another test.
|
||||||
|
|
||||||
|
/// Handle returned by `start_brain_shim`. Drop to tear down.
|
||||||
|
struct BrainShim {
|
||||||
|
addr: std::net::SocketAddr,
|
||||||
|
shutdown: Option<oneshot::Sender<()>>,
|
||||||
|
join: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BrainShim {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(tx) = self.shutdown.take() {
|
||||||
|
let _ = tx.send(());
|
||||||
|
}
|
||||||
|
self.join.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start an in-process brain-process-equivalent WS server on an ephemeral port.
|
||||||
|
async fn start_brain_shim(mock_url: String) -> BrainShim {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||||
|
let join = tokio::spawn(async move {
|
||||||
|
brain_accept_loop(listener, shutdown_rx, mock_url).await;
|
||||||
|
});
|
||||||
|
BrainShim {
|
||||||
|
addr,
|
||||||
|
shutdown: Some(shutdown_tx),
|
||||||
|
join,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept loop: spawns a per-connection task for each tap WS dial.
|
||||||
|
async fn brain_accept_loop(
|
||||||
|
listener: TcpListener,
|
||||||
|
mut shutdown: oneshot::Receiver<()>,
|
||||||
|
mock_url: String,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = &mut shutdown => {
|
||||||
|
info!("brain_shim accept loop shutting down");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res = listener.accept() => {
|
||||||
|
let Ok((stream, peer)) = res else { continue };
|
||||||
|
let url = mock_url.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_tap_connection(stream, peer, &url).await {
|
||||||
|
info!(%peer, error = ?e, "brain_shim connection ended");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle one tap WS connection: handshake, split sink+stream, dial the mock
|
||||||
|
/// OpenAI side, and run `run_openai_pump`.
|
||||||
|
async fn handle_tap_connection(
|
||||||
|
stream: tokio::net::TcpStream,
|
||||||
|
peer: std::net::SocketAddr,
|
||||||
|
openai_url: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut tap_ws = tokio_tungstenite::accept_async(stream).await?;
|
||||||
|
info!(%peer, "brain_shim tap WS connection accepted");
|
||||||
|
|
||||||
|
let hello_in = tap_ws
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.ok_or("tap connection closed before hello")??;
|
||||||
|
let hello_text = hello_in.into_text().map_err(|_| "hello not text")?;
|
||||||
|
let decoded = decode_envelope(&hello_text)?;
|
||||||
|
let session_id = match decoded.payload {
|
||||||
|
DecodedPayload::Hello(p) => p.session_id,
|
||||||
|
_ => return Err("first tap frame not hello".into()),
|
||||||
|
};
|
||||||
|
info!(%peer, %session_id, "brain_shim tap hello received");
|
||||||
|
let ack = encode_hello(&session_id, 0, 0)?;
|
||||||
|
tap_ws.send(Message::Text(ack)).await?;
|
||||||
|
|
||||||
|
let (tap_via, pump_tap_rx) = mpsc::channel::<String>(64);
|
||||||
|
let (pump_tap_tx, mut tap_out_rx) = mpsc::channel::<String>(64);
|
||||||
|
let (mut tap_sink, mut tap_stream) = tap_ws.split();
|
||||||
|
|
||||||
|
let in_fwd = tokio::spawn(async move {
|
||||||
|
while let Some(msg_res) = tap_stream.next().await {
|
||||||
|
if let Ok(m) = msg_res {
|
||||||
|
if let Ok(text) = m.into_text() {
|
||||||
|
if tap_via.send(text).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let out_fwd = tokio::spawn(async move {
|
||||||
|
while let Some(text) = tap_out_rx.recv().await {
|
||||||
|
if tap_sink.send(Message::Text(text)).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let request = openai_url.into_client_request()?;
|
||||||
|
let (openai_ws, _resp) = tokio_tungstenite::connect_async(request).await?;
|
||||||
|
info!(%peer, %openai_url, "brain_shim OpenAI side connected");
|
||||||
|
|
||||||
|
let pump_result =
|
||||||
|
run_openai_pump(openai_ws, pump_tap_rx, pump_tap_tx, "alloy".to_string()).await;
|
||||||
|
info!(%peer, ?pump_result, "brain_shim pump exited");
|
||||||
|
in_fwd.abort();
|
||||||
|
out_fwd.abort();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Test helpers ===
|
||||||
|
|
||||||
|
/// Build a loud 24 kHz PCM frame whose RMS energy is well above the local-VAD
|
||||||
|
/// threshold. A constant amplitude of 1000 matches slice-4's test fixture.
|
||||||
|
fn loud_frame() -> PcmFrame {
|
||||||
|
let mut frame = PcmFrame::zeroed();
|
||||||
|
for s in frame.samples.iter_mut() {
|
||||||
|
*s = 1000;
|
||||||
|
}
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pstn_sim_synthetic_caller_drives_trunk_reflex_loop() {
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| "rutster_trunk=info".into()),
|
||||||
|
)
|
||||||
|
.try_init();
|
||||||
|
|
||||||
|
// 1. Start the mock OpenAI Realtime brain.
|
||||||
|
let mock = MockRealtimeBrain::start().await.expect("mock brain binds");
|
||||||
|
|
||||||
|
// 2. Start the brain shim that speaks the tap protocol on the core side
|
||||||
|
// and the OpenAI protocol on the brain side.
|
||||||
|
let shim = start_brain_shim(mock.url()).await;
|
||||||
|
let tap_url = Url::parse(&format!("ws://{}/", shim.addr)).unwrap();
|
||||||
|
|
||||||
|
// 3. Build the trunk-leg pipe stack: TapAudioPipe -> Reflex -> LocalVadReflex.
|
||||||
|
// This is the same composition `MediaThread::RegisterTrunk` will build.
|
||||||
|
let (tx_pcm_in, mut rx_pcm_in) = mpsc::channel::<PcmFrame>(32);
|
||||||
|
let (tx_audio_out, rx_audio_out) = mpsc::channel::<PcmFrame>(32);
|
||||||
|
let tap_metrics = TapMetrics::new();
|
||||||
|
let tap_pipe = TapAudioPipe::new(tx_pcm_in, rx_audio_out, tap_metrics.clone());
|
||||||
|
|
||||||
|
let (advisory_tx, advisory_rx) = mpsc::channel::<rutster_media::AdvisoryEvent>(16);
|
||||||
|
let reflex_metrics = ReflexMetrics::new();
|
||||||
|
let reflex = Reflex::new(tap_pipe, advisory_rx, reflex_metrics.clone());
|
||||||
|
let wrapped_pipe = LocalVadReflex::new(reflex, advisory_tx.clone());
|
||||||
|
|
||||||
|
// 4. Construct the TrunkSession that the FOB will tick via `drive`.
|
||||||
|
let channel = Channel::new_inbound();
|
||||||
|
let session_id = channel.id;
|
||||||
|
let (inbound_tx, inbound_rx) = mpsc::channel::<PcmFrame>(16);
|
||||||
|
let (outbound_tx, mut outbound_rx) = mpsc::channel::<PcmFrame>(16);
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut session = TrunkSession::new(channel, wrapped_pipe, inbound_rx, outbound_tx, now);
|
||||||
|
|
||||||
|
// 5. Spawn a minimal test-only tap engine task. We cannot use the binary
|
||||||
|
// crate's `spawn_tap_engine` from inside `rutster-trunk` (circular
|
||||||
|
// dev-dependency), so we call `run_tap_client` directly after dialing
|
||||||
|
// the brain shim.
|
||||||
|
let (close_tx, mut close_rx) = oneshot::channel::<()>();
|
||||||
|
let (tx_function_call, _rx_function_call) = mpsc::channel::<FunctionCallEvent>(8);
|
||||||
|
let (_tx_function_call_output, mut rx_function_call_output) =
|
||||||
|
mpsc::channel::<FunctionCallOutputEvent>(8);
|
||||||
|
let engine_metrics = tap_metrics.clone();
|
||||||
|
let engine_handle = tokio::spawn(async move {
|
||||||
|
let request = tap_url
|
||||||
|
.as_str()
|
||||||
|
.into_client_request()
|
||||||
|
.expect("valid ws url");
|
||||||
|
let (ws, _resp) = tokio_tungstenite::connect_async(request)
|
||||||
|
.await
|
||||||
|
.expect("connect to brain_shim");
|
||||||
|
let _ = run_tap_client(
|
||||||
|
ws,
|
||||||
|
session_id,
|
||||||
|
&mut rx_pcm_in,
|
||||||
|
tx_audio_out,
|
||||||
|
tx_function_call,
|
||||||
|
&mut rx_function_call_output,
|
||||||
|
advisory_tx,
|
||||||
|
engine_metrics,
|
||||||
|
&mut close_rx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for tap handshake + OpenAI dial to complete.
|
||||||
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
|
|
||||||
|
// 6. Spawn the synthetic Twilio caller task: push loud inbound frames into
|
||||||
|
// the trunk leg and count outbound (brain-reply) frames coming back.
|
||||||
|
let outbound_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
let outbound_count_caller = outbound_count.clone();
|
||||||
|
let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
|
||||||
|
let caller_handle = tokio::spawn(async move {
|
||||||
|
let loud = loud_frame();
|
||||||
|
let mut local_count = 0usize;
|
||||||
|
loop {
|
||||||
|
// Push caller audio to the trunk leg. try_send matches the hot-path
|
||||||
|
// "drop + observe" policy: if the FOB backs up, keep going.
|
||||||
|
let _ = inbound_tx.try_send(loud.clone());
|
||||||
|
|
||||||
|
// Drain any outbound (brain reply) frames the FOB produced.
|
||||||
|
while outbound_rx.try_recv().is_ok() {
|
||||||
|
local_count += 1;
|
||||||
|
}
|
||||||
|
outbound_count_caller.store(local_count, Ordering::Relaxed);
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||||
|
if stop_rx.try_recv().is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_count
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Drive the trunk-leg poll loop at 20 ms intervals.
|
||||||
|
let mut barge_seen = false;
|
||||||
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
|
||||||
|
while tokio::time::Instant::now() < deadline {
|
||||||
|
let now = Instant::now();
|
||||||
|
let _ = drive(&mut session, now);
|
||||||
|
|
||||||
|
if !barge_seen && reflex_metrics.barge_in_count.load(Ordering::Relaxed) >= 1 {
|
||||||
|
barge_seen = true;
|
||||||
|
info!("PSTN sim: local VAD barge-in fired");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop early once we have both barge-in and at least one observed
|
||||||
|
// outbound frame (the mock brain replies with audio_out deltas).
|
||||||
|
if barge_seen && outbound_count.load(Ordering::Relaxed) > 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Stop the caller and collect its final count.
|
||||||
|
let _ = stop_tx.send(());
|
||||||
|
let final_outbound_count = tokio::time::timeout(Duration::from_secs(1), caller_handle)
|
||||||
|
.await
|
||||||
|
.expect("caller task finishes")
|
||||||
|
.expect("caller task panics");
|
||||||
|
|
||||||
|
// Then: the local VAD must have fired during the call.
|
||||||
|
assert!(
|
||||||
|
barge_seen,
|
||||||
|
"PSTN caller speech must trigger a local-VAD barge-in on the trunk leg"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
reflex_metrics.barge_in_count.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"barge-in must fire exactly once for the first utterance"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And: the mock brain must have received audio_in and replied with at
|
||||||
|
// least one outbound frame after the barge (resume condition).
|
||||||
|
assert!(
|
||||||
|
final_outbound_count > 0,
|
||||||
|
"mock brain must reply with at least one audio_out frame observed on the trunk outbound mpsc"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 9. Caller hangup: stop sending inbound frames and force the 60 s idle
|
||||||
|
// timeout path by moving `last_idle_rx` into the past. The FOB should
|
||||||
|
// close the session.
|
||||||
|
session.last_idle_rx = Instant::now() - Duration::from_secs(90);
|
||||||
|
let next = drive(&mut session, Instant::now());
|
||||||
|
assert!(
|
||||||
|
session.is_closed(),
|
||||||
|
"trunk session must close after idle timeout (simulating caller hangup)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
next, None,
|
||||||
|
"drive must return None once the session is closed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 10. Clean up the tap engine.
|
||||||
|
let _ = close_tx.send(());
|
||||||
|
let _ = tokio::time::timeout(Duration::from_secs(1), engine_handle).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full end-to-end test using the binary's `MediaThread` + `RegisterTrunk`.
|
||||||
|
///
|
||||||
|
/// This test is currently ignored because `rutster-trunk` integration tests
|
||||||
|
/// cannot depend on the `rutster` binary crate (`rutster` already depends on
|
||||||
|
/// `rutster-trunk`; Cargo disallows circular dev-dependencies). The active
|
||||||
|
/// `pstn_sim_synthetic_caller_drives_trunk_reflex_loop` above covers the FOB
|
||||||
|
// reflex loop + trunk-leg tick directly; this stub marks where the MediaThread
|
||||||
|
/// wiring test belongs once the binary crate is ready to exercise it.
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn full_pstn_e2e_through_media_thread_register_trunk() {
|
||||||
|
// TODO: exercise MediaCmd::RegisterTrunk + MediaThread tick loop against a
|
||||||
|
// live TwilioMediaStreamsServer mock once the binary crate exposes a test
|
||||||
|
// harness from the appropriate crate.
|
||||||
|
}
|
||||||
@@ -11,15 +11,6 @@ description = "Rutster binary: axum signaling + media driver + static browser te
|
|||||||
rutster-media = { path = "../rutster-media" }
|
rutster-media = { path = "../rutster-media" }
|
||||||
rutster-call-model = { path = "../rutster-call-model" }
|
rutster-call-model = { path = "../rutster-call-model" }
|
||||||
rutster-tap = { path = "../rutster-tap" }
|
rutster-tap = { path = "../rutster-tap" }
|
||||||
<<<<<<< HEAD
|
|
||||||
# rutster-trunk: the rented-transport crate owns `TwilioCredentials` (T2) +
|
|
||||||
# the env parser's return type. The binary's `config::twilio_credentials`
|
|
||||||
# (T6) constructs it from env vars + hands it to the live
|
|
||||||
# `TwilioCallControlClient` (when `--features=twilio-live` is enabled on the
|
|
||||||
# trunk crate). ADR-0009: `TwilioCredentials` is imported here but never
|
|
||||||
# re-exported through the workspace root / never sent over the tap WS protocol.
|
|
||||||
=======
|
|
||||||
>>>>>>> 346bd16 (feat(binary): MediaLeg enum + MediaCmd::RegisterTrunk variant + POST /v1/trunk/{sessions,webhook} routes + TwilioMediaStreamsServer router mount (slice-5 T5 Step A))
|
|
||||||
# rutster-trunk: the rented-transport crate owns `TwilioCredentials` (T2) +
|
# rutster-trunk: the rented-transport crate owns `TwilioCredentials` (T2) +
|
||||||
# the env parser's return type. The binary's `config::twilio_credentials`
|
# the env parser's return type. The binary's `config::twilio_credentials`
|
||||||
# (T6) constructs it from env vars + hands it to the live
|
# (T6) constructs it from env vars + hands it to the live
|
||||||
|
|||||||
Reference in New Issue
Block a user