tokra 0.0.1.pre.1 → 0.0.1.pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSES/MIT-0.txt +16 -0
  3. data/REUSE.toml +6 -1
  4. data/Rakefile +2 -0
  5. data/Steepfile +2 -0
  6. data/clippy_exceptions.rb +75 -37
  7. data/doc/contributors/adr/004.md +63 -0
  8. data/doc/contributors/chats/002.md +177 -0
  9. data/doc/contributors/chats/003.md +2180 -0
  10. data/doc/contributors/chats/004.md +1992 -0
  11. data/doc/contributors/chats/005.md +1529 -0
  12. data/doc/contributors/plan/002.md +173 -0
  13. data/doc/contributors/plan/003.md +111 -0
  14. data/examples/verify_hello_world/index.html +15 -2
  15. data/examples/verify_ping_pong/app.rb +3 -1
  16. data/examples/verify_ping_pong/public/styles.css +36 -9
  17. data/examples/verify_ping_pong/views/layout.erb +1 -1
  18. data/examples/verify_rails_sqlite/.dockerignore +51 -0
  19. data/examples/verify_rails_sqlite/.gitattributes +9 -0
  20. data/examples/verify_rails_sqlite/.github/dependabot.yml +12 -0
  21. data/examples/verify_rails_sqlite/.github/workflows/ci.yml +124 -0
  22. data/examples/verify_rails_sqlite/.gitignore +35 -0
  23. data/examples/verify_rails_sqlite/.kamal/hooks/docker-setup.sample +3 -0
  24. data/examples/verify_rails_sqlite/.kamal/hooks/post-app-boot.sample +3 -0
  25. data/examples/verify_rails_sqlite/.kamal/hooks/post-deploy.sample +14 -0
  26. data/examples/verify_rails_sqlite/.kamal/hooks/post-proxy-reboot.sample +3 -0
  27. data/examples/verify_rails_sqlite/.kamal/hooks/pre-app-boot.sample +3 -0
  28. data/examples/verify_rails_sqlite/.kamal/hooks/pre-build.sample +51 -0
  29. data/examples/verify_rails_sqlite/.kamal/hooks/pre-connect.sample +47 -0
  30. data/examples/verify_rails_sqlite/.kamal/hooks/pre-deploy.sample +122 -0
  31. data/examples/verify_rails_sqlite/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  32. data/examples/verify_rails_sqlite/.kamal/secrets +20 -0
  33. data/examples/verify_rails_sqlite/.rubocop.yml +2 -0
  34. data/examples/verify_rails_sqlite/.ruby-version +1 -0
  35. data/examples/verify_rails_sqlite/Dockerfile +77 -0
  36. data/examples/verify_rails_sqlite/Gemfile +66 -0
  37. data/examples/verify_rails_sqlite/Gemfile.lock +563 -0
  38. data/examples/verify_rails_sqlite/README.md +41 -0
  39. data/examples/verify_rails_sqlite/Rakefile +6 -0
  40. data/examples/verify_rails_sqlite/app/assets/images/.keep +0 -0
  41. data/examples/verify_rails_sqlite/app/assets/stylesheets/application.css +469 -0
  42. data/examples/verify_rails_sqlite/app/controllers/application_controller.rb +12 -0
  43. data/examples/verify_rails_sqlite/app/controllers/concerns/.keep +0 -0
  44. data/examples/verify_rails_sqlite/app/controllers/todos_controller.rb +70 -0
  45. data/examples/verify_rails_sqlite/app/helpers/application_helper.rb +2 -0
  46. data/examples/verify_rails_sqlite/app/helpers/todos_helper.rb +2 -0
  47. data/examples/verify_rails_sqlite/app/javascript/application.js +3 -0
  48. data/examples/verify_rails_sqlite/app/javascript/controllers/application.js +9 -0
  49. data/examples/verify_rails_sqlite/app/javascript/controllers/hello_controller.js +7 -0
  50. data/examples/verify_rails_sqlite/app/javascript/controllers/index.js +4 -0
  51. data/examples/verify_rails_sqlite/app/jobs/application_job.rb +7 -0
  52. data/examples/verify_rails_sqlite/app/mailers/application_mailer.rb +4 -0
  53. data/examples/verify_rails_sqlite/app/models/application_record.rb +3 -0
  54. data/examples/verify_rails_sqlite/app/models/concerns/.keep +0 -0
  55. data/examples/verify_rails_sqlite/app/models/todo.rb +10 -0
  56. data/examples/verify_rails_sqlite/app/views/layouts/application.html.erb +24 -0
  57. data/examples/verify_rails_sqlite/app/views/layouts/mailer.html.erb +13 -0
  58. data/examples/verify_rails_sqlite/app/views/layouts/mailer.text.erb +1 -0
  59. data/examples/verify_rails_sqlite/app/views/pwa/manifest.json.erb +22 -0
  60. data/examples/verify_rails_sqlite/app/views/pwa/service-worker.js +26 -0
  61. data/examples/verify_rails_sqlite/app/views/todos/_form.html.erb +27 -0
  62. data/examples/verify_rails_sqlite/app/views/todos/_todo.html.erb +18 -0
  63. data/examples/verify_rails_sqlite/app/views/todos/_todo.json.jbuilder +2 -0
  64. data/examples/verify_rails_sqlite/app/views/todos/edit.html.erb +7 -0
  65. data/examples/verify_rails_sqlite/app/views/todos/index.html.erb +22 -0
  66. data/examples/verify_rails_sqlite/app/views/todos/index.json.jbuilder +1 -0
  67. data/examples/verify_rails_sqlite/app/views/todos/new.html.erb +7 -0
  68. data/examples/verify_rails_sqlite/app/views/todos/show.html.erb +23 -0
  69. data/examples/verify_rails_sqlite/app/views/todos/show.json.jbuilder +1 -0
  70. data/examples/verify_rails_sqlite/bin/brakeman +7 -0
  71. data/examples/verify_rails_sqlite/bin/bundler-audit +6 -0
  72. data/examples/verify_rails_sqlite/bin/ci +6 -0
  73. data/examples/verify_rails_sqlite/bin/dev +2 -0
  74. data/examples/verify_rails_sqlite/bin/docker-entrypoint +8 -0
  75. data/examples/verify_rails_sqlite/bin/importmap +4 -0
  76. data/examples/verify_rails_sqlite/bin/jobs +6 -0
  77. data/examples/verify_rails_sqlite/bin/kamal +16 -0
  78. data/examples/verify_rails_sqlite/bin/rails +4 -0
  79. data/examples/verify_rails_sqlite/bin/rake +4 -0
  80. data/examples/verify_rails_sqlite/bin/rubocop +8 -0
  81. data/examples/verify_rails_sqlite/bin/setup +35 -0
  82. data/examples/verify_rails_sqlite/bin/thrust +5 -0
  83. data/examples/verify_rails_sqlite/config/application.rb +27 -0
  84. data/examples/verify_rails_sqlite/config/boot.rb +4 -0
  85. data/examples/verify_rails_sqlite/config/bundler-audit.yml +5 -0
  86. data/examples/verify_rails_sqlite/config/cable.yml +17 -0
  87. data/examples/verify_rails_sqlite/config/cache.yml +16 -0
  88. data/examples/verify_rails_sqlite/config/ci.rb +24 -0
  89. data/examples/verify_rails_sqlite/config/credentials.yml.enc +1 -0
  90. data/examples/verify_rails_sqlite/config/database.yml +40 -0
  91. data/examples/verify_rails_sqlite/config/deploy.yml +119 -0
  92. data/examples/verify_rails_sqlite/config/environment.rb +5 -0
  93. data/examples/verify_rails_sqlite/config/environments/development.rb +84 -0
  94. data/examples/verify_rails_sqlite/config/environments/production.rb +99 -0
  95. data/examples/verify_rails_sqlite/config/environments/test.rb +53 -0
  96. data/examples/verify_rails_sqlite/config/importmap.rb +7 -0
  97. data/examples/verify_rails_sqlite/config/initializers/assets.rb +7 -0
  98. data/examples/verify_rails_sqlite/config/initializers/content_security_policy.rb +29 -0
  99. data/examples/verify_rails_sqlite/config/initializers/filter_parameter_logging.rb +8 -0
  100. data/examples/verify_rails_sqlite/config/initializers/inflections.rb +16 -0
  101. data/examples/verify_rails_sqlite/config/locales/en.yml +31 -0
  102. data/examples/verify_rails_sqlite/config/puma.rb +42 -0
  103. data/examples/verify_rails_sqlite/config/queue.yml +18 -0
  104. data/examples/verify_rails_sqlite/config/recurring.yml +15 -0
  105. data/examples/verify_rails_sqlite/config/routes.rb +15 -0
  106. data/examples/verify_rails_sqlite/config/storage.yml +27 -0
  107. data/examples/verify_rails_sqlite/config.ru +6 -0
  108. data/examples/verify_rails_sqlite/db/cable_schema.rb +11 -0
  109. data/examples/verify_rails_sqlite/db/cache_schema.rb +12 -0
  110. data/examples/verify_rails_sqlite/db/migrate/20260130080933_create_todos.rb +10 -0
  111. data/examples/verify_rails_sqlite/db/queue_schema.rb +129 -0
  112. data/examples/verify_rails_sqlite/db/schema.rb +20 -0
  113. data/examples/verify_rails_sqlite/db/seeds.rb +9 -0
  114. data/examples/verify_rails_sqlite/lib/tasks/.keep +0 -0
  115. data/examples/verify_rails_sqlite/log/.keep +0 -0
  116. data/examples/verify_rails_sqlite/public/400.html +135 -0
  117. data/examples/verify_rails_sqlite/public/404.html +135 -0
  118. data/examples/verify_rails_sqlite/public/406-unsupported-browser.html +135 -0
  119. data/examples/verify_rails_sqlite/public/422.html +135 -0
  120. data/examples/verify_rails_sqlite/public/500.html +135 -0
  121. data/examples/verify_rails_sqlite/public/icon.png +0 -0
  122. data/examples/verify_rails_sqlite/public/icon.svg +3 -0
  123. data/examples/verify_rails_sqlite/public/robots.txt +1 -0
  124. data/examples/verify_rails_sqlite/public/styles.css +469 -0
  125. data/examples/verify_rails_sqlite/script/.keep +0 -0
  126. data/examples/verify_rails_sqlite/storage/.keep +0 -0
  127. data/examples/verify_rails_sqlite/test/controllers/.keep +0 -0
  128. data/examples/verify_rails_sqlite/test/controllers/todos_controller_test.rb +48 -0
  129. data/examples/verify_rails_sqlite/test/fixtures/files/.keep +0 -0
  130. data/examples/verify_rails_sqlite/test/fixtures/todos.yml +9 -0
  131. data/examples/verify_rails_sqlite/test/helpers/.keep +0 -0
  132. data/examples/verify_rails_sqlite/test/integration/.keep +0 -0
  133. data/examples/verify_rails_sqlite/test/mailers/.keep +0 -0
  134. data/examples/verify_rails_sqlite/test/models/.keep +0 -0
  135. data/examples/verify_rails_sqlite/test/models/todo_test.rb +7 -0
  136. data/examples/verify_rails_sqlite/test/test_helper.rb +15 -0
  137. data/examples/verify_rails_sqlite/tmp/.keep +0 -0
  138. data/examples/verify_rails_sqlite/tmp/pids/.keep +0 -0
  139. data/examples/verify_rails_sqlite/tmp/storage/.keep +0 -0
  140. data/examples/verify_rails_sqlite/tokra.rb +42 -0
  141. data/examples/verify_rails_sqlite/vendor/.keep +0 -0
  142. data/examples/verify_rails_sqlite/vendor/javascript/.keep +0 -0
  143. data/ext/tokra/src/event_loop.rs +206 -0
  144. data/ext/tokra/src/events.rs +430 -0
  145. data/ext/tokra/src/lib.rs +52 -664
  146. data/ext/tokra/src/proxy.rs +142 -0
  147. data/ext/tokra/src/responders.rs +86 -0
  148. data/ext/tokra/src/user_event.rs +313 -0
  149. data/ext/tokra/src/webview.rs +194 -0
  150. data/ext/tokra/src/window.rs +92 -0
  151. data/lib/tokra/rack/handler.rb +37 -14
  152. data/lib/tokra/version.rb +1 -1
  153. data/rbs_exceptions.rb +12 -0
  154. data/sig/tokra.rbs +95 -1
  155. data/tasks/lint.rake +2 -2
  156. data/tasks/lint.rb +49 -0
  157. data/tasks/rust.rake +6 -23
  158. data/tasks/steep.rake +25 -3
  159. data/tasks/test.rake +1 -1
  160. metadata +143 -1
@@ -0,0 +1,142 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Proxy wrapper for cross-Ractor communication.
5
+ //!
6
+ //! This module provides the Ruby-facing `Tokra::Native::Proxy` class,
7
+ //! which wraps `tao::event_loop::EventLoopProxy<UserEvent>`.
8
+ //!
9
+ //! The Proxy is the ONLY Tokra native type that is genuinely thread-safe
10
+ //! and Ractor-shareable, as `EventLoopProxy` is designed to be `Send+Sync`.
11
+
12
+ use magnus::{method, prelude::*, Error, Ruby};
13
+ use tao::event_loop::EventLoopProxy;
14
+
15
+ use crate::runtime_error;
16
+ use crate::user_event::UserEvent;
17
+
18
+ /// Ruby wrapper for `tao::event_loop::EventLoopProxy<UserEvent>`.
19
+ ///
20
+ /// Thread-safe handle for waking up the event loop from Ractors.
21
+ /// This is the ONLY type that is genuinely designed to be Send+Sync safe by tao.
22
+ ///
23
+ /// This type uses `frozen_shareable` to be Ractor-shareable when frozen.
24
+ /// Ruby automatically freezes objects shared across Ractors.
25
+ #[derive(Clone)]
26
+ pub struct RbProxy {
27
+ inner: EventLoopProxy<UserEvent>,
28
+ }
29
+
30
+ impl RbProxy {
31
+ /// Create a new `RbProxy` from an `EventLoopProxy`.
32
+ #[must_use]
33
+ pub const fn new(inner: EventLoopProxy<UserEvent>) -> Self {
34
+ Self { inner }
35
+ }
36
+
37
+ /// Get a reference to the inner `EventLoopProxy`.
38
+ #[must_use]
39
+ pub const fn inner(&self) -> &EventLoopProxy<UserEvent> {
40
+ &self.inner
41
+ }
42
+
43
+ /// Wake up the event loop with a payload.
44
+ ///
45
+ /// This is the critical method for Worker Ractor → Main Ractor communication.
46
+ ///
47
+ /// # Errors
48
+ ///
49
+ /// Returns an error if the event loop has been closed.
50
+ pub fn wake_up(&self, payload: String) -> Result<(), Error> {
51
+ self.inner
52
+ .send_event(UserEvent::WakeUp(payload))
53
+ .map_err(|_| {
54
+ Error::new(
55
+ runtime_error(),
56
+ "Failed to send wake up event: event loop closed",
57
+ )
58
+ })
59
+ }
60
+
61
+ /// Send an HTTP response for a pending async protocol request.
62
+ ///
63
+ /// This is used by Ruby to respond to `tokra://` requests.
64
+ /// The `request_id` must match a pending `HttpRequest` event.
65
+ /// Headers should be an array of [name, value] pairs.
66
+ ///
67
+ /// # Errors
68
+ ///
69
+ /// Returns an error if the event loop has been closed.
70
+ pub fn respond(
71
+ &self,
72
+ request_id: u64,
73
+ status: u16,
74
+ headers: Vec<(String, String)>,
75
+ body: String,
76
+ ) -> Result<(), Error> {
77
+ self.inner
78
+ .send_event(UserEvent::HttpResponse {
79
+ request_id,
80
+ status,
81
+ headers,
82
+ body,
83
+ })
84
+ .map_err(|_| {
85
+ Error::new(
86
+ runtime_error(),
87
+ "Failed to send HTTP response: event loop closed",
88
+ )
89
+ })
90
+ }
91
+ }
92
+
93
+ // RbProxy is genuinely Send+Sync because EventLoopProxy<T> is Send+Sync
94
+ // This is NOT an unsafe impl - tao guarantees this.
95
+ // SAFETY note: Sync is required for frozen_shareable in Magnus.
96
+ // EventLoopProxy is explicitly designed to be used from any thread.
97
+
98
+ impl magnus::DataTypeFunctions for RbProxy {}
99
+
100
+ // SAFETY: RbProxy wraps EventLoopProxy which is Send+Sync by design.
101
+ // This impl enables the frozen_shareable flag which makes the Ruby object
102
+ // Ractor-shareable when frozen. Ruby automatically freezes shareable objects.
103
+ #[allow(unsafe_code)]
104
+ unsafe impl magnus::TypedData for RbProxy {
105
+ fn class(ruby: &magnus::Ruby) -> magnus::RClass {
106
+ use magnus::Class;
107
+ static CLASS: magnus::value::Lazy<magnus::RClass> = magnus::value::Lazy::new(|ruby| {
108
+ let tokra = ruby.define_module("Tokra").unwrap();
109
+ let native = tokra.define_module("Native").unwrap();
110
+ let class = native.define_class("Proxy", ruby.class_object()).unwrap();
111
+ class.undef_default_alloc_func();
112
+ class
113
+ });
114
+ ruby.get_inner(&CLASS)
115
+ }
116
+
117
+ fn data_type() -> &'static magnus::DataType {
118
+ static DATA_TYPE: magnus::DataType =
119
+ magnus::typed_data::DataTypeBuilder::<RbProxy>::new(c"Tokra::Native::Proxy")
120
+ .free_immediately()
121
+ .size()
122
+ .frozen_shareable()
123
+ .build();
124
+ &DATA_TYPE
125
+ }
126
+ }
127
+
128
+ /// Define the `Proxy` class methods on the given Ruby module.
129
+ ///
130
+ /// # Errors
131
+ ///
132
+ /// Returns an error if class or method definition fails.
133
+ pub fn define_class(ruby: &Ruby, native: magnus::RModule) -> Result<(), Error> {
134
+ use magnus::Class;
135
+ let proxy_class = native.define_class("Proxy", ruby.class_object())?;
136
+ // Prevent Ruby from allocating uninitialized Proxy objects
137
+ // This fixes the "undefining the allocator of T_DATA class" warning
138
+ proxy_class.undef_default_alloc_func();
139
+ proxy_class.define_method("wake_up", method!(RbProxy::wake_up, 1))?;
140
+ proxy_class.define_method("respond", method!(RbProxy::respond, 4))?;
141
+ Ok(())
142
+ }
@@ -0,0 +1,86 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Async protocol responder storage.
5
+ //!
6
+ //! This module manages pending HTTP responses for the tokra:// custom protocol.
7
+ //! When the `WebView` makes a request via the custom protocol, we store a responder
8
+ //! that will be used later when Ruby sends back an `HttpResponse` event.
9
+ //!
10
+ //! # Pattern Origin
11
+ //!
12
+ //! This pattern is adapted from Tauri's `register_asynchronous_uri_scheme_protocol`
13
+ //! in tauri/app.rs.
14
+
15
+ // SPDX-SnippetBegin
16
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
17
+ // SPDX-SnippetCopyrightText: 2020-2023 Tauri Programme within The Commons Conservancy
18
+ // SPDX-SnippetComment: Async responder pattern adapted from tauri/app.rs register_asynchronous_uri_scheme_protocol
19
+
20
+ use std::collections::HashMap;
21
+ use std::sync::atomic::{AtomicU64, Ordering};
22
+ use std::sync::Mutex;
23
+
24
+ use wry::RequestAsyncResponder;
25
+
26
+ /// Global counter for generating unique request IDs
27
+ static NEXT_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
28
+
29
+ /// Thread-safe storage for pending async responders
30
+ /// Maps `request_id` -> `RequestAsyncResponder`
31
+ static PENDING_RESPONDERS: std::sync::OnceLock<Mutex<HashMap<u64, RequestAsyncResponder>>> =
32
+ std::sync::OnceLock::new();
33
+
34
+ /// Get the global responders map, initializing if needed.
35
+ #[must_use]
36
+ pub fn get_responders() -> &'static Mutex<HashMap<u64, RequestAsyncResponder>> {
37
+ PENDING_RESPONDERS.get_or_init(|| Mutex::new(HashMap::new()))
38
+ }
39
+
40
+ /// Generate the next unique request ID.
41
+ #[must_use]
42
+ pub fn next_request_id() -> u64 {
43
+ NEXT_REQUEST_ID.fetch_add(1, Ordering::SeqCst)
44
+ }
45
+
46
+ // SPDX-SnippetEnd
47
+
48
+ // =============================================================================
49
+ // Unit Tests
50
+ // =============================================================================
51
+
52
+ #[cfg(test)]
53
+ mod tests {
54
+ use super::*;
55
+
56
+ #[test]
57
+ fn test_next_request_id_increments() {
58
+ let first = next_request_id();
59
+ let second = next_request_id();
60
+ assert_eq!(second, first + 1);
61
+ }
62
+
63
+ #[test]
64
+ fn test_next_request_id_is_unique_across_calls() {
65
+ let ids: Vec<u64> = (0..100).map(|_| next_request_id()).collect();
66
+ let unique: std::collections::HashSet<u64> = ids.iter().cloned().collect();
67
+ assert_eq!(ids.len(), unique.len(), "All IDs should be unique");
68
+ }
69
+
70
+ #[test]
71
+ fn test_get_responders_returns_same_instance() {
72
+ let first = get_responders();
73
+ let second = get_responders();
74
+ assert!(std::ptr::eq(first, second), "Should return same instance");
75
+ }
76
+
77
+ #[test]
78
+ fn test_get_responders_is_empty_initially() {
79
+ // Note: This may fail if other tests have added responders.
80
+ // The responders map is global, so this test verifies the HashMap exists.
81
+ let responders = get_responders();
82
+ let guard = responders.lock().unwrap();
83
+ // Just verify we can access it - can't guarantee empty due to other tests
84
+ let _ = guard.len();
85
+ }
86
+ }
@@ -0,0 +1,313 @@
1
+ // SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! User events for cross-thread/Ractor communication.
5
+ //!
6
+ //! This module defines the `UserEvent` enum that carries messages between
7
+ //! different parts of the Tokra runtime: from `WebView` to Ruby, from Ractors
8
+ //! to the main thread, and from Ruby back to pending protocol requests.
9
+
10
+ /// Custom event type for cross-thread/Ractor communication.
11
+ ///
12
+ /// This is the payload type for `tao::EventLoopProxy<UserEvent>`.
13
+ #[derive(Debug, Clone)]
14
+ pub enum UserEvent {
15
+ /// IPC message received from `WebView` JavaScript
16
+ IpcMessage(String),
17
+
18
+ /// HTTP request received from `WebView` (via custom protocol)
19
+ /// Contains: (`request_id`, method, uri, headers, body)
20
+ /// The `request_id` is used to correlate with the async response
21
+ HttpRequest {
22
+ request_id: u64,
23
+ method: String,
24
+ uri: String,
25
+ headers: Vec<(String, String)>,
26
+ body: String,
27
+ },
28
+
29
+ /// HTTP response from Ruby (for async protocol handling)
30
+ /// Contains: (`request_id`, status, headers, body)
31
+ HttpResponse {
32
+ request_id: u64,
33
+ status: u16,
34
+ headers: Vec<(String, String)>,
35
+ body: String,
36
+ },
37
+
38
+ /// Page load event (Started or Finished)
39
+ /// Matches Tauri's `on_page_load` API
40
+ PageLoad {
41
+ event: String, // "started" or "finished"
42
+ url: String,
43
+ },
44
+
45
+ /// Wake-up signal from a Ractor (via Proxy)
46
+ WakeUp(String),
47
+
48
+ /// Exit signal (from ctrlc handler)
49
+ Exit,
50
+ }
51
+
52
+ // =============================================================================
53
+ // Unit Tests
54
+ // =============================================================================
55
+
56
+ #[cfg(test)]
57
+ mod tests {
58
+ use super::*;
59
+
60
+ #[test]
61
+ fn test_user_event_ipc_message_stores_string() {
62
+ let event = UserEvent::IpcMessage("hello from JS".to_string());
63
+ match event {
64
+ UserEvent::IpcMessage(msg) => assert_eq!(msg, "hello from JS"),
65
+ _ => panic!("Expected IpcMessage variant"),
66
+ }
67
+ }
68
+
69
+ #[test]
70
+ fn test_user_event_ipc_message_preserves_unicode() {
71
+ let event = UserEvent::IpcMessage("こんにちは 🎌".to_string());
72
+ match event {
73
+ UserEvent::IpcMessage(msg) => assert_eq!(msg, "こんにちは 🎌"),
74
+ _ => panic!("Expected IpcMessage variant"),
75
+ }
76
+ }
77
+
78
+ #[test]
79
+ fn test_user_event_http_request_stores_all_fields() {
80
+ let headers = vec![
81
+ ("cookie".to_string(), "session=abc123".to_string()),
82
+ ("content-type".to_string(), "application/json".to_string()),
83
+ ];
84
+ let event = UserEvent::HttpRequest {
85
+ request_id: 42,
86
+ method: "POST".to_string(),
87
+ uri: "tokra://localhost/api".to_string(),
88
+ headers: headers.clone(),
89
+ body: r#"{"key":"value"}"#.to_string(),
90
+ };
91
+
92
+ match event {
93
+ UserEvent::HttpRequest {
94
+ request_id,
95
+ method,
96
+ uri,
97
+ headers: h,
98
+ body,
99
+ } => {
100
+ assert_eq!(request_id, 42);
101
+ assert_eq!(method, "POST");
102
+ assert_eq!(uri, "tokra://localhost/api");
103
+ assert_eq!(h.len(), 2);
104
+ assert_eq!(h[0].0, "cookie");
105
+ assert_eq!(h[0].1, "session=abc123");
106
+ assert_eq!(body, r#"{"key":"value"}"#);
107
+ }
108
+ _ => panic!("Expected HttpRequest variant"),
109
+ }
110
+ }
111
+
112
+ #[test]
113
+ fn test_user_event_http_response_stores_all_fields() {
114
+ let headers = vec![
115
+ ("content-type".to_string(), "text/html".to_string()),
116
+ ("x-custom".to_string(), "value".to_string()),
117
+ ];
118
+
119
+ let event = UserEvent::HttpResponse {
120
+ request_id: 99,
121
+ status: 200,
122
+ headers: headers.clone(),
123
+ body: "<h1>Hello</h1>".to_string(),
124
+ };
125
+
126
+ match event {
127
+ UserEvent::HttpResponse {
128
+ request_id,
129
+ status,
130
+ headers: h,
131
+ body,
132
+ } => {
133
+ assert_eq!(request_id, 99);
134
+ assert_eq!(status, 200);
135
+ assert_eq!(h.len(), 2);
136
+ assert_eq!(h[0].0, "content-type");
137
+ assert_eq!(h[0].1, "text/html");
138
+ assert_eq!(body, "<h1>Hello</h1>");
139
+ }
140
+ _ => panic!("Expected HttpResponse variant"),
141
+ }
142
+ }
143
+
144
+ #[test]
145
+ fn test_user_event_page_load_started() {
146
+ let event = UserEvent::PageLoad {
147
+ event: "started".to_string(),
148
+ url: "https://example.com/".to_string(),
149
+ };
150
+
151
+ match event {
152
+ UserEvent::PageLoad { event, url } => {
153
+ assert_eq!(event, "started");
154
+ assert_eq!(url, "https://example.com/");
155
+ }
156
+ _ => panic!("Expected PageLoad variant"),
157
+ }
158
+ }
159
+
160
+ #[test]
161
+ fn test_user_event_page_load_finished() {
162
+ let event = UserEvent::PageLoad {
163
+ event: "finished".to_string(),
164
+ url: "https://example.com/page".to_string(),
165
+ };
166
+
167
+ match event {
168
+ UserEvent::PageLoad { event, url } => {
169
+ assert_eq!(event, "finished");
170
+ assert_eq!(url, "https://example.com/page");
171
+ }
172
+ _ => panic!("Expected PageLoad variant"),
173
+ }
174
+ }
175
+
176
+ #[test]
177
+ fn test_user_event_wake_up_stores_payload() {
178
+ let event = UserEvent::WakeUp("worker response".to_string());
179
+ match event {
180
+ UserEvent::WakeUp(payload) => assert_eq!(payload, "worker response"),
181
+ _ => panic!("Expected WakeUp variant"),
182
+ }
183
+ }
184
+
185
+ #[test]
186
+ fn test_user_event_exit_variant_exists() {
187
+ let event = UserEvent::Exit;
188
+ assert!(matches!(event, UserEvent::Exit));
189
+ }
190
+
191
+ #[test]
192
+ fn test_user_event_clone_preserves_ipc_message() {
193
+ let original = UserEvent::IpcMessage("test".to_string());
194
+ let cloned = original.clone();
195
+
196
+ match (original, cloned) {
197
+ (UserEvent::IpcMessage(a), UserEvent::IpcMessage(b)) => {
198
+ assert_eq!(a, b);
199
+ }
200
+ _ => panic!("Clone should preserve variant"),
201
+ }
202
+ }
203
+
204
+ #[test]
205
+ fn test_user_event_clone_preserves_http_request() {
206
+ let original = UserEvent::HttpRequest {
207
+ request_id: 123,
208
+ method: "GET".to_string(),
209
+ uri: "tokra://test".to_string(),
210
+ headers: vec![("accept".to_string(), "text/html".to_string())],
211
+ body: "".to_string(),
212
+ };
213
+ let cloned = original.clone();
214
+
215
+ match cloned {
216
+ UserEvent::HttpRequest {
217
+ request_id,
218
+ headers,
219
+ ..
220
+ } => {
221
+ assert_eq!(request_id, 123);
222
+ assert_eq!(headers.len(), 1);
223
+ }
224
+ _ => panic!("Clone should preserve variant"),
225
+ }
226
+ }
227
+
228
+ #[test]
229
+ fn test_user_event_debug_format_ipc() {
230
+ let event = UserEvent::IpcMessage("debug test".to_string());
231
+ let debug_str = format!("{:?}", event);
232
+ assert!(debug_str.contains("IpcMessage"));
233
+ assert!(debug_str.contains("debug test"));
234
+ }
235
+
236
+ #[test]
237
+ fn test_user_event_debug_format_exit() {
238
+ let event = UserEvent::Exit;
239
+ let debug_str = format!("{:?}", event);
240
+ assert!(debug_str.contains("Exit"));
241
+ }
242
+
243
+ #[test]
244
+ fn test_http_request_id_can_be_zero() {
245
+ let event = UserEvent::HttpRequest {
246
+ request_id: 0,
247
+ method: "GET".to_string(),
248
+ uri: "tokra://test".to_string(),
249
+ headers: vec![],
250
+ body: "".to_string(),
251
+ };
252
+
253
+ match event {
254
+ UserEvent::HttpRequest { request_id, .. } => assert_eq!(request_id, 0),
255
+ _ => panic!("Expected HttpRequest"),
256
+ }
257
+ }
258
+
259
+ #[test]
260
+ fn test_http_request_id_can_be_max_u64() {
261
+ let event = UserEvent::HttpRequest {
262
+ request_id: u64::MAX,
263
+ method: "GET".to_string(),
264
+ uri: "tokra://test".to_string(),
265
+ headers: vec![],
266
+ body: "".to_string(),
267
+ };
268
+
269
+ match event {
270
+ UserEvent::HttpRequest { request_id, .. } => assert_eq!(request_id, u64::MAX),
271
+ _ => panic!("Expected HttpRequest"),
272
+ }
273
+ }
274
+
275
+ #[test]
276
+ fn test_http_response_status_boundary_low() {
277
+ let event = UserEvent::HttpResponse {
278
+ request_id: 1,
279
+ status: 100,
280
+ headers: vec![],
281
+ body: "".to_string(),
282
+ };
283
+
284
+ match event {
285
+ UserEvent::HttpResponse { status, .. } => assert_eq!(status, 100),
286
+ _ => panic!("Expected HttpResponse"),
287
+ }
288
+ }
289
+
290
+ #[test]
291
+ fn test_http_response_status_boundary_high() {
292
+ let event = UserEvent::HttpResponse {
293
+ request_id: 1,
294
+ status: 599,
295
+ headers: vec![],
296
+ body: "".to_string(),
297
+ };
298
+
299
+ match event {
300
+ UserEvent::HttpResponse { status, .. } => assert_eq!(status, 599),
301
+ _ => panic!("Expected HttpResponse"),
302
+ }
303
+ }
304
+
305
+ #[test]
306
+ fn test_empty_strings_are_preserved() {
307
+ let event = UserEvent::IpcMessage("".to_string());
308
+ match event {
309
+ UserEvent::IpcMessage(msg) => assert!(msg.is_empty()),
310
+ _ => panic!("Expected IpcMessage"),
311
+ }
312
+ }
313
+ }