@@ -59,15 +59,27 @@ impl testcontainers::Image for SpiceDbImage {
5959
6060// ── Shared container ──────────────────────────────────────────
6161
62+ /// Holds the running container and its mapped port.
63+ /// The `Client` is NOT shared because each `#[tokio::test]` creates its
64+ /// own tokio runtime, and tonic `Channel` is tied to the runtime that
65+ /// created it. Sharing a single Channel across runtimes causes transport
66+ /// errors. Instead we share only the container and create a fresh Client
67+ /// per test invocation.
6268struct SharedSpiceDb {
6369 _container : ContainerAsync < SpiceDbImage > ,
64- client : Client ,
70+ port : u16 ,
71+ schema_written : bool ,
6572}
6673
6774static SPICEDB : OnceCell < Arc < SharedSpiceDb > > = OnceCell :: const_new ( ) ;
6875
69- async fn spicedb ( ) -> Arc < SharedSpiceDb > {
70- SPICEDB
76+ /// Returns a fresh `Client` connected to the shared SpiceDB container.
77+ /// The container is started once (lazily) and the schema is written on
78+ /// first access. Each call creates a new tonic Channel on the caller's
79+ /// runtime, avoiding cross-runtime transport errors.
80+ async fn spicedb ( ) -> Client {
81+ // Ensure container is started and schema is written (once)
82+ let shared = SPICEDB
7183 . get_or_init ( || async {
7284 let container = SpiceDbImage
7385 . start ( )
@@ -123,11 +135,19 @@ async fn spicedb() -> Arc<SharedSpiceDb> {
123135
124136 Arc :: new ( SharedSpiceDb {
125137 _container : container,
126- client,
138+ port,
139+ schema_written : true ,
127140 } )
128141 } )
142+ . await ;
143+
144+ assert ! ( shared. schema_written, "schema should have been written" ) ;
145+
146+ // Create a fresh client on the CURRENT runtime
147+ let endpoint = format ! ( "http://localhost:{}" , shared. port) ;
148+ Client :: new ( & endpoint, SPICEDB_TOKEN )
129149 . await
130- . clone ( )
150+ . expect ( "failed to create client for test" )
131151}
132152
133153const TEST_SCHEMA : & str = r#"
@@ -146,33 +166,32 @@ definition document {
146166
147167#[ tokio:: test]
148168async fn write_and_read_schema ( ) {
149- let db = spicedb ( ) . await ;
169+ let c = spicedb ( ) . await ;
150170
151- let ( schema_text, read_at) = db . client . read_schema ( ) . await . expect ( "read_schema failed" ) ;
171+ let ( schema_text, read_at) = c . read_schema ( ) . await . expect ( "read_schema failed" ) ;
152172 assert ! ( schema_text. contains( "definition document" ) ) ;
153173 assert ! ( !read_at. token( ) . is_empty( ) ) ;
154174}
155175
156176#[ tokio:: test]
157177async fn write_schema_empty_rejected ( ) {
158- let db = spicedb ( ) . await ;
159- let err = db . client . write_schema ( "" ) . await . unwrap_err ( ) ;
178+ let c = spicedb ( ) . await ;
179+ let err = c . write_schema ( "" ) . await . unwrap_err ( ) ;
160180 assert ! ( matches!( err, prescience:: Error :: InvalidArgument ( _) ) ) ;
161181}
162182
163183// ── Relationships ─────────────────────────────────────────────
164184
165185#[ tokio:: test]
166186async fn write_relationships_empty_rejected ( ) {
167- let db = spicedb ( ) . await ;
168- let err = db . client . write_relationships ( vec ! [ ] ) . await . unwrap_err ( ) ;
187+ let c = spicedb ( ) . await ;
188+ let err = c . write_relationships ( vec ! [ ] ) . await . unwrap_err ( ) ;
169189 assert ! ( matches!( err, prescience:: Error :: InvalidArgument ( _) ) ) ;
170190}
171191
172192#[ tokio:: test]
173193async fn write_and_check_permission ( ) {
174- let db = spicedb ( ) . await ;
175- let c = & db. client ;
194+ let c = spicedb ( ) . await ;
176195
177196 let token = c
178197 . write_relationships ( vec ! [ RelationshipUpdate :: create( Relationship :: new(
@@ -224,8 +243,7 @@ async fn write_and_check_permission() {
224243
225244#[ tokio:: test]
226245async fn read_relationships ( ) {
227- let db = spicedb ( ) . await ;
228- let c = & db. client ;
246+ let c = spicedb ( ) . await ;
229247
230248 let token = c
231249 . write_relationships ( vec ! [
@@ -268,8 +286,7 @@ async fn read_relationships() {
268286
269287#[ tokio:: test]
270288async fn lookup_resources ( ) {
271- let db = spicedb ( ) . await ;
272- let c = & db. client ;
289+ let c = spicedb ( ) . await ;
273290
274291 let token = c
275292 . write_relationships ( vec ! [
@@ -320,8 +337,7 @@ async fn lookup_resources() {
320337
321338#[ tokio:: test]
322339async fn lookup_subjects ( ) {
323- let db = spicedb ( ) . await ;
324- let c = & db. client ;
340+ let c = spicedb ( ) . await ;
325341
326342 let token = c
327343 . write_relationships ( vec ! [
@@ -364,8 +380,7 @@ async fn lookup_subjects() {
364380
365381#[ tokio:: test]
366382async fn delete_relationships ( ) {
367- let db = spicedb ( ) . await ;
368- let c = & db. client ;
383+ let c = spicedb ( ) . await ;
369384
370385 let token = c
371386 . write_relationships ( vec ! [ RelationshipUpdate :: create( Relationship :: new(
@@ -425,8 +440,7 @@ async fn delete_relationships() {
425440#[ cfg( feature = "watch" ) ]
426441#[ tokio:: test]
427442async fn watch_receives_updates ( ) {
428- let db = spicedb ( ) . await ;
429- let c = & db. client ;
443+ let c = spicedb ( ) . await ;
430444
431445 let mut stream = c
432446 . watch ( vec ! [ "document" ] )
@@ -464,8 +478,7 @@ async fn watch_receives_updates() {
464478async fn bulk_check_permissions ( ) {
465479 use prescience:: BulkCheckItem ;
466480
467- let db = spicedb ( ) . await ;
468- let c = & db. client ;
481+ let c = spicedb ( ) . await ;
469482
470483 let token = c
471484 . write_relationships ( vec ! [ RelationshipUpdate :: create( Relationship :: new(
0 commit comments