@moejay/wrightty 0.0.0 → 0.1.1

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 (94) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +144 -0
  3. package/dist/client.d.ts +14 -0
  4. package/dist/client.js +83 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +8 -0
  7. package/dist/terminal.d.ts +48 -0
  8. package/dist/terminal.js +210 -0
  9. package/dist/types.d.ts +90 -0
  10. package/dist/types.js +3 -0
  11. package/package.json +38 -15
  12. package/.github/workflows/ci.yml +0 -90
  13. package/.github/workflows/release.yml +0 -177
  14. package/Cargo.lock +0 -2662
  15. package/Cargo.toml +0 -38
  16. package/PROTOCOL.md +0 -1351
  17. package/README.md +0 -386
  18. package/agents/ceo/AGENTS.md +0 -24
  19. package/agents/ceo/HEARTBEAT.md +0 -72
  20. package/agents/ceo/SOUL.md +0 -33
  21. package/agents/ceo/TOOLS.md +0 -3
  22. package/agents/founding-engineer/AGENTS.md +0 -44
  23. package/crates/wrightty/Cargo.toml +0 -43
  24. package/crates/wrightty/src/client_cmds.rs +0 -366
  25. package/crates/wrightty/src/discover.rs +0 -78
  26. package/crates/wrightty/src/main.rs +0 -100
  27. package/crates/wrightty/src/server.rs +0 -100
  28. package/crates/wrightty/src/term.rs +0 -338
  29. package/crates/wrightty-bridge-ghostty/Cargo.toml +0 -27
  30. package/crates/wrightty-bridge-ghostty/src/ghostty.rs +0 -422
  31. package/crates/wrightty-bridge-ghostty/src/lib.rs +0 -2
  32. package/crates/wrightty-bridge-ghostty/src/main.rs +0 -146
  33. package/crates/wrightty-bridge-ghostty/src/rpc.rs +0 -307
  34. package/crates/wrightty-bridge-kitty/Cargo.toml +0 -26
  35. package/crates/wrightty-bridge-kitty/src/kitty.rs +0 -269
  36. package/crates/wrightty-bridge-kitty/src/lib.rs +0 -2
  37. package/crates/wrightty-bridge-kitty/src/main.rs +0 -124
  38. package/crates/wrightty-bridge-kitty/src/rpc.rs +0 -304
  39. package/crates/wrightty-bridge-tmux/Cargo.toml +0 -26
  40. package/crates/wrightty-bridge-tmux/src/lib.rs +0 -2
  41. package/crates/wrightty-bridge-tmux/src/main.rs +0 -119
  42. package/crates/wrightty-bridge-tmux/src/rpc.rs +0 -291
  43. package/crates/wrightty-bridge-tmux/src/tmux.rs +0 -215
  44. package/crates/wrightty-bridge-wezterm/Cargo.toml +0 -26
  45. package/crates/wrightty-bridge-wezterm/src/lib.rs +0 -2
  46. package/crates/wrightty-bridge-wezterm/src/main.rs +0 -119
  47. package/crates/wrightty-bridge-wezterm/src/rpc.rs +0 -339
  48. package/crates/wrightty-bridge-wezterm/src/wezterm.rs +0 -190
  49. package/crates/wrightty-bridge-zellij/Cargo.toml +0 -27
  50. package/crates/wrightty-bridge-zellij/src/lib.rs +0 -2
  51. package/crates/wrightty-bridge-zellij/src/main.rs +0 -125
  52. package/crates/wrightty-bridge-zellij/src/rpc.rs +0 -328
  53. package/crates/wrightty-bridge-zellij/src/zellij.rs +0 -199
  54. package/crates/wrightty-client/Cargo.toml +0 -16
  55. package/crates/wrightty-client/src/client.rs +0 -254
  56. package/crates/wrightty-client/src/lib.rs +0 -2
  57. package/crates/wrightty-core/Cargo.toml +0 -21
  58. package/crates/wrightty-core/src/input.rs +0 -212
  59. package/crates/wrightty-core/src/lib.rs +0 -4
  60. package/crates/wrightty-core/src/screen.rs +0 -325
  61. package/crates/wrightty-core/src/session.rs +0 -249
  62. package/crates/wrightty-core/src/session_manager.rs +0 -77
  63. package/crates/wrightty-protocol/Cargo.toml +0 -13
  64. package/crates/wrightty-protocol/src/error.rs +0 -8
  65. package/crates/wrightty-protocol/src/events.rs +0 -138
  66. package/crates/wrightty-protocol/src/lib.rs +0 -4
  67. package/crates/wrightty-protocol/src/methods.rs +0 -321
  68. package/crates/wrightty-protocol/src/types.rs +0 -201
  69. package/crates/wrightty-server/Cargo.toml +0 -23
  70. package/crates/wrightty-server/src/lib.rs +0 -2
  71. package/crates/wrightty-server/src/main.rs +0 -65
  72. package/crates/wrightty-server/src/rpc.rs +0 -455
  73. package/crates/wrightty-server/src/state.rs +0 -39
  74. package/examples/basic_command.py +0 -53
  75. package/examples/interactive_tui.py +0 -86
  76. package/examples/record_session.py +0 -96
  77. package/install.sh +0 -81
  78. package/sdks/node/package-lock.json +0 -85
  79. package/sdks/node/package.json +0 -44
  80. package/sdks/node/src/client.ts +0 -94
  81. package/sdks/node/src/index.ts +0 -19
  82. package/sdks/node/src/terminal.ts +0 -258
  83. package/sdks/node/src/types.ts +0 -105
  84. package/sdks/node/tsconfig.json +0 -17
  85. package/sdks/python/README.md +0 -96
  86. package/sdks/python/pyproject.toml +0 -42
  87. package/sdks/python/wrightty/__init__.py +0 -6
  88. package/sdks/python/wrightty/cli.py +0 -210
  89. package/sdks/python/wrightty/client.py +0 -136
  90. package/sdks/python/wrightty/mcp_server.py +0 -434
  91. package/sdks/python/wrightty/terminal.py +0 -333
  92. package/skills/wrightty/SKILL.md +0 -261
  93. package/src/lib.rs +0 -1
  94. package/tests/integration_test.rs +0 -618
@@ -1,618 +0,0 @@
1
- use std::collections::HashMap;
2
- use std::time::Duration;
3
-
4
- use wrightty_client::WrighttyClient;
5
- use wrightty_protocol::methods::SessionCreateParams;
6
- use wrightty_protocol::types::{KeyInput, ScreenshotFormat};
7
- use wrightty_server::rpc::build_rpc_module;
8
- use wrightty_server::state::AppState;
9
-
10
- /// Start the server on a random available port and return the URL.
11
- async fn start_server() -> (String, jsonrpsee::server::ServerHandle) {
12
- let state = AppState::new(64);
13
- let module = build_rpc_module(state).unwrap();
14
-
15
- let server = jsonrpsee::server::Server::builder()
16
- .build("127.0.0.1:0")
17
- .await
18
- .unwrap();
19
-
20
- let addr = server.local_addr().unwrap();
21
- let handle = server.start(module);
22
-
23
- (format!("ws://{addr}"), handle)
24
- }
25
-
26
- fn default_session() -> SessionCreateParams {
27
- SessionCreateParams {
28
- shell: Some("/bin/sh".to_string()),
29
- args: vec![],
30
- cols: 80,
31
- rows: 24,
32
- env: HashMap::new(),
33
- cwd: None,
34
- }
35
- }
36
-
37
- // ─── Existing tests (kept) ───────────────────────────────────────────────────
38
-
39
- #[tokio::test]
40
- async fn test_create_session_and_read_screen() {
41
- let (url, _handle) = start_server().await;
42
- let client = WrighttyClient::connect(&url).await.unwrap();
43
-
44
- // Create a session
45
- let session_id = client
46
- .session_create(SessionCreateParams {
47
- shell: Some("/bin/sh".to_string()),
48
- args: vec![],
49
- cols: 80,
50
- rows: 24,
51
- env: HashMap::new(),
52
- cwd: None,
53
- })
54
- .await
55
- .unwrap();
56
-
57
- // Give the shell a moment to start and print its prompt
58
- tokio::time::sleep(Duration::from_millis(500)).await;
59
-
60
- // Send "echo hello" + Enter
61
- client
62
- .send_text(&session_id, "echo hello\n")
63
- .await
64
- .unwrap();
65
-
66
- // Wait for output to be processed
67
- tokio::time::sleep(Duration::from_millis(500)).await;
68
-
69
- // Read the screen
70
- let text = client.get_text(&session_id).await.unwrap();
71
- println!("Screen text:\n---\n{text}\n---");
72
-
73
- assert!(
74
- text.contains("hello"),
75
- "Expected 'hello' in screen output, got:\n{text}"
76
- );
77
-
78
- // Clean up
79
- client.session_destroy(&session_id).await.unwrap();
80
- }
81
-
82
- #[tokio::test]
83
- async fn test_send_keys() {
84
- let (url, _handle) = start_server().await;
85
- let client = WrighttyClient::connect(&url).await.unwrap();
86
-
87
- let session_id = client
88
- .session_create(SessionCreateParams {
89
- shell: Some("/bin/sh".to_string()),
90
- args: vec![],
91
- cols: 80,
92
- rows: 24,
93
- env: HashMap::new(),
94
- cwd: None,
95
- })
96
- .await
97
- .unwrap();
98
-
99
- tokio::time::sleep(Duration::from_millis(500)).await;
100
-
101
- // Send keystrokes using the shorthand format
102
- let keys: Vec<KeyInput> = vec![
103
- KeyInput::Shorthand("e".to_string()),
104
- KeyInput::Shorthand("c".to_string()),
105
- KeyInput::Shorthand("h".to_string()),
106
- KeyInput::Shorthand("o".to_string()),
107
- KeyInput::Shorthand(" ".to_string()),
108
- KeyInput::Shorthand("w".to_string()),
109
- KeyInput::Shorthand("o".to_string()),
110
- KeyInput::Shorthand("r".to_string()),
111
- KeyInput::Shorthand("l".to_string()),
112
- KeyInput::Shorthand("d".to_string()),
113
- KeyInput::Shorthand("Enter".to_string()),
114
- ];
115
-
116
- client.send_keys(&session_id, keys).await.unwrap();
117
-
118
- tokio::time::sleep(Duration::from_millis(500)).await;
119
-
120
- let text = client.get_text(&session_id).await.unwrap();
121
- println!("Screen text:\n---\n{text}\n---");
122
-
123
- assert!(
124
- text.contains("world"),
125
- "Expected 'world' in screen output, got:\n{text}"
126
- );
127
-
128
- client.session_destroy(&session_id).await.unwrap();
129
- }
130
-
131
- #[tokio::test]
132
- async fn test_resize() {
133
- let (url, _handle) = start_server().await;
134
- let client = WrighttyClient::connect(&url).await.unwrap();
135
-
136
- let session_id = client
137
- .session_create(SessionCreateParams {
138
- shell: Some("/bin/sh".to_string()),
139
- args: vec![],
140
- cols: 80,
141
- rows: 24,
142
- env: HashMap::new(),
143
- cwd: None,
144
- })
145
- .await
146
- .unwrap();
147
-
148
- // Check initial size
149
- let (cols, rows) = client.get_size(&session_id).await.unwrap();
150
- assert_eq!(cols, 80);
151
- assert_eq!(rows, 24);
152
-
153
- // Resize
154
- client.resize(&session_id, 120, 40).await.unwrap();
155
-
156
- // Verify new size
157
- let (cols, rows) = client.get_size(&session_id).await.unwrap();
158
- assert_eq!(cols, 120);
159
- assert_eq!(rows, 40);
160
-
161
- client.session_destroy(&session_id).await.unwrap();
162
- }
163
-
164
- #[tokio::test]
165
- async fn test_session_list() {
166
- let (url, _handle) = start_server().await;
167
- let client = WrighttyClient::connect(&url).await.unwrap();
168
-
169
- // No sessions initially
170
- let sessions = client.session_list().await.unwrap();
171
- assert!(sessions.is_empty());
172
-
173
- // Create two sessions
174
- let id1 = client
175
- .session_create(SessionCreateParams {
176
- shell: Some("/bin/sh".to_string()),
177
- args: vec![],
178
- cols: 80,
179
- rows: 24,
180
- env: HashMap::new(),
181
- cwd: None,
182
- })
183
- .await
184
- .unwrap();
185
-
186
- let id2 = client
187
- .session_create(SessionCreateParams {
188
- shell: Some("/bin/sh".to_string()),
189
- args: vec![],
190
- cols: 80,
191
- rows: 24,
192
- env: HashMap::new(),
193
- cwd: None,
194
- })
195
- .await
196
- .unwrap();
197
-
198
- let sessions = client.session_list().await.unwrap();
199
- assert_eq!(sessions.len(), 2);
200
-
201
- // Destroy one
202
- client.session_destroy(&id1).await.unwrap();
203
-
204
- let sessions = client.session_list().await.unwrap();
205
- assert_eq!(sessions.len(), 1);
206
- assert_eq!(sessions[0].session_id, id2);
207
-
208
- client.session_destroy(&id2).await.unwrap();
209
- }
210
-
211
- #[tokio::test]
212
- async fn test_get_info() {
213
- let (url, _handle) = start_server().await;
214
- let client = WrighttyClient::connect(&url).await.unwrap();
215
-
216
- let info = client.get_info().await.unwrap();
217
- assert_eq!(info.version, "0.1.0");
218
- assert_eq!(info.implementation, "wrightty-server");
219
- assert!(info.capabilities.supports_resize);
220
- assert!(info.capabilities.supports_session_create);
221
- }
222
-
223
- // ─── Screen domain ───────────────────────────────────────────────────────────
224
-
225
- #[tokio::test]
226
- async fn test_screen_get_contents_dimensions() {
227
- let (url, _handle) = start_server().await;
228
- let client = WrighttyClient::connect(&url).await.unwrap();
229
-
230
- let session_id = client.session_create(default_session()).await.unwrap();
231
- tokio::time::sleep(Duration::from_millis(300)).await;
232
-
233
- let contents = client.get_contents(&session_id).await.unwrap();
234
-
235
- // Screen dimensions match what we requested
236
- assert_eq!(contents.cols, 80);
237
- assert_eq!(contents.rows, 24);
238
-
239
- // Cell grid has the right shape
240
- assert_eq!(contents.cells.len(), 24, "expected 24 rows");
241
- for row in &contents.cells {
242
- assert_eq!(row.len(), 80, "expected 80 cols per row");
243
- }
244
-
245
- // Cursor is within bounds
246
- assert!(contents.cursor.row < 24);
247
- assert!(contents.cursor.col < 80);
248
-
249
- client.session_destroy(&session_id).await.unwrap();
250
- }
251
-
252
- #[tokio::test]
253
- async fn test_screen_get_contents_cell_data() {
254
- let (url, _handle) = start_server().await;
255
- let client = WrighttyClient::connect(&url).await.unwrap();
256
-
257
- let session_id = client.session_create(default_session()).await.unwrap();
258
- tokio::time::sleep(Duration::from_millis(500)).await;
259
-
260
- client.send_text(&session_id, "echo hi\n").await.unwrap();
261
- tokio::time::sleep(Duration::from_millis(500)).await;
262
-
263
- let contents = client.get_contents(&session_id).await.unwrap();
264
-
265
- // Flatten all chars and find "h","i" somewhere
266
- let all_chars: String = contents.cells.iter().flat_map(|row| row.iter().map(|c| c.char.as_str())).collect();
267
- assert!(all_chars.contains('h'), "expected 'h' in cell data");
268
- assert!(all_chars.contains('i'), "expected 'i' in cell data");
269
-
270
- // Each cell has valid width (0, 1, or 2)
271
- for row in &contents.cells {
272
- for cell in row {
273
- assert!(cell.width <= 2, "unexpected cell width: {}", cell.width);
274
- }
275
- }
276
-
277
- client.session_destroy(&session_id).await.unwrap();
278
- }
279
-
280
- #[tokio::test]
281
- async fn test_screen_get_scrollback_empty_initially() {
282
- let (url, _handle) = start_server().await;
283
- let client = WrighttyClient::connect(&url).await.unwrap();
284
-
285
- let session_id = client.session_create(default_session()).await.unwrap();
286
- tokio::time::sleep(Duration::from_millis(300)).await;
287
-
288
- // Fresh terminal has no scrollback
289
- let result = client.get_scrollback(&session_id, 100, 0).await.unwrap();
290
- assert_eq!(result.total_scrollback, 0);
291
- assert!(result.lines.is_empty());
292
-
293
- client.session_destroy(&session_id).await.unwrap();
294
- }
295
-
296
- #[tokio::test]
297
- async fn test_screen_get_scrollback_after_fill() {
298
- let (url, _handle) = start_server().await;
299
- let client = WrighttyClient::connect(&url).await.unwrap();
300
-
301
- // Small terminal so it scrolls quickly
302
- let session_id = client
303
- .session_create(SessionCreateParams {
304
- shell: Some("/bin/sh".to_string()),
305
- args: vec![],
306
- cols: 80,
307
- rows: 5,
308
- env: HashMap::new(),
309
- cwd: None,
310
- })
311
- .await
312
- .unwrap();
313
-
314
- tokio::time::sleep(Duration::from_millis(500)).await;
315
-
316
- // Print more lines than the terminal height so scrollback fills up
317
- for i in 0..10 {
318
- client
319
- .send_text(&session_id, &format!("echo line{i}\n"))
320
- .await
321
- .unwrap();
322
- tokio::time::sleep(Duration::from_millis(150)).await;
323
- }
324
-
325
- let result = client.get_scrollback(&session_id, 100, 0).await.unwrap();
326
- println!("Scrollback lines: {}", result.total_scrollback);
327
- println!("Returned: {:?}", result.lines.iter().map(|l| &l.text).collect::<Vec<_>>());
328
-
329
- // We should have some scrollback now
330
- assert!(result.total_scrollback > 0, "expected scrollback after filling terminal");
331
- // Line numbers in scrollback are negative
332
- for line in &result.lines {
333
- assert!(line.line_number < 0, "scrollback line_number should be negative, got {}", line.line_number);
334
- }
335
-
336
- client.session_destroy(&session_id).await.unwrap();
337
- }
338
-
339
- #[tokio::test]
340
- async fn test_screen_screenshot_text_format() {
341
- let (url, _handle) = start_server().await;
342
- let client = WrighttyClient::connect(&url).await.unwrap();
343
-
344
- let session_id = client.session_create(default_session()).await.unwrap();
345
- tokio::time::sleep(Duration::from_millis(500)).await;
346
-
347
- client.send_text(&session_id, "echo screenshot_test\n").await.unwrap();
348
- tokio::time::sleep(Duration::from_millis(500)).await;
349
-
350
- let result = client.screenshot(&session_id, ScreenshotFormat::Text).await.unwrap();
351
-
352
- assert!(
353
- matches!(result.format, ScreenshotFormat::Text),
354
- "format should be Text"
355
- );
356
- assert!(
357
- result.data.contains("screenshot_test"),
358
- "screenshot data should contain the echoed text, got: {}",
359
- result.data
360
- );
361
- // Text format has no pixel dimensions
362
- assert!(result.width.is_none());
363
- assert!(result.height.is_none());
364
-
365
- client.session_destroy(&session_id).await.unwrap();
366
- }
367
-
368
- #[tokio::test]
369
- async fn test_screen_screenshot_json_format() {
370
- let (url, _handle) = start_server().await;
371
- let client = WrighttyClient::connect(&url).await.unwrap();
372
-
373
- let session_id = client.session_create(default_session()).await.unwrap();
374
- tokio::time::sleep(Duration::from_millis(300)).await;
375
-
376
- let result = client.screenshot(&session_id, ScreenshotFormat::Json).await.unwrap();
377
-
378
- assert!(matches!(result.format, ScreenshotFormat::Json));
379
- assert_eq!(result.width, Some(80));
380
- assert_eq!(result.height, Some(24));
381
- // data is valid JSON
382
- let parsed: serde_json::Value = serde_json::from_str(&result.data).expect("JSON screenshot data should be valid JSON");
383
- assert!(parsed.is_array(), "JSON screenshot should be an array of rows");
384
-
385
- client.session_destroy(&session_id).await.unwrap();
386
- }
387
-
388
- #[tokio::test]
389
- async fn test_screen_wait_for_text_success() {
390
- let (url, _handle) = start_server().await;
391
- let client = WrighttyClient::connect(&url).await.unwrap();
392
-
393
- let session_id = client.session_create(default_session()).await.unwrap();
394
- tokio::time::sleep(Duration::from_millis(500)).await;
395
-
396
- // Send text in the background, then wait for it
397
- client.send_text(&session_id, "echo wait_target\n").await.unwrap();
398
-
399
- let result = client
400
- .wait_for_text(&session_id, "wait_target", false, 5000)
401
- .await
402
- .unwrap();
403
-
404
- assert!(result.found, "wait_for_text should have found 'wait_target'");
405
- assert!(!result.matches.is_empty());
406
- assert!(result.elapsed < 5000);
407
-
408
- client.session_destroy(&session_id).await.unwrap();
409
- }
410
-
411
- #[tokio::test]
412
- async fn test_screen_wait_for_text_timeout() {
413
- let (url, _handle) = start_server().await;
414
- let client = WrighttyClient::connect(&url).await.unwrap();
415
-
416
- let session_id = client.session_create(default_session()).await.unwrap();
417
- tokio::time::sleep(Duration::from_millis(300)).await;
418
-
419
- // Wait for text that will never appear
420
- let result = client
421
- .wait_for_text(&session_id, "this_text_will_never_appear_xyz", false, 300)
422
- .await
423
- .unwrap();
424
-
425
- assert!(!result.found, "wait_for_text should have timed out");
426
- assert!(result.matches.is_empty());
427
-
428
- client.session_destroy(&session_id).await.unwrap();
429
- }
430
-
431
- #[tokio::test]
432
- async fn test_screen_wait_for_text_regex() {
433
- let (url, _handle) = start_server().await;
434
- let client = WrighttyClient::connect(&url).await.unwrap();
435
-
436
- let session_id = client.session_create(default_session()).await.unwrap();
437
- tokio::time::sleep(Duration::from_millis(500)).await;
438
-
439
- client.send_text(&session_id, "echo regex123\n").await.unwrap();
440
-
441
- // Regex: match "regex" followed by digits
442
- let result = client
443
- .wait_for_text(&session_id, r"regex\d+", true, 5000)
444
- .await
445
- .unwrap();
446
-
447
- assert!(result.found, "regex wait should have matched");
448
- assert!(!result.matches.is_empty());
449
- assert!(result.matches[0].text.starts_with("regex"));
450
-
451
- client.session_destroy(&session_id).await.unwrap();
452
- }
453
-
454
- // ─── Input domain ────────────────────────────────────────────────────────────
455
-
456
- #[tokio::test]
457
- async fn test_input_send_text_multiple_commands() {
458
- let (url, _handle) = start_server().await;
459
- let client = WrighttyClient::connect(&url).await.unwrap();
460
-
461
- let session_id = client.session_create(default_session()).await.unwrap();
462
- tokio::time::sleep(Duration::from_millis(500)).await;
463
-
464
- // Send several commands
465
- client.send_text(&session_id, "echo first\n").await.unwrap();
466
- tokio::time::sleep(Duration::from_millis(200)).await;
467
- client.send_text(&session_id, "echo second\n").await.unwrap();
468
- tokio::time::sleep(Duration::from_millis(200)).await;
469
- client.send_text(&session_id, "echo third\n").await.unwrap();
470
- tokio::time::sleep(Duration::from_millis(300)).await;
471
-
472
- let text = client.get_text(&session_id).await.unwrap();
473
- println!("Screen:\n{text}");
474
-
475
- assert!(text.contains("first") || text.contains("second") || text.contains("third"),
476
- "At least one output line should be visible");
477
-
478
- client.session_destroy(&session_id).await.unwrap();
479
- }
480
-
481
- #[tokio::test]
482
- async fn test_input_send_keys_special_keys() {
483
- let (url, _handle) = start_server().await;
484
- let client = WrighttyClient::connect(&url).await.unwrap();
485
-
486
- let session_id = client.session_create(default_session()).await.unwrap();
487
- tokio::time::sleep(Duration::from_millis(500)).await;
488
-
489
- // Type a partial command then backspace to erase it, then type correct command
490
- let keys: Vec<KeyInput> = vec![
491
- KeyInput::Shorthand("e".to_string()),
492
- KeyInput::Shorthand("c".to_string()),
493
- KeyInput::Shorthand("x".to_string()), // typo
494
- KeyInput::Shorthand("Backspace".to_string()),
495
- KeyInput::Shorthand("h".to_string()),
496
- KeyInput::Shorthand("o".to_string()),
497
- KeyInput::Shorthand(" ".to_string()),
498
- KeyInput::Shorthand("k".to_string()),
499
- KeyInput::Shorthand("e".to_string()),
500
- KeyInput::Shorthand("y".to_string()),
501
- KeyInput::Shorthand("s".to_string()),
502
- KeyInput::Shorthand("Enter".to_string()),
503
- ];
504
-
505
- client.send_keys(&session_id, keys).await.unwrap();
506
- tokio::time::sleep(Duration::from_millis(500)).await;
507
-
508
- let text = client.get_text(&session_id).await.unwrap();
509
- println!("Screen:\n{text}");
510
-
511
- assert!(text.contains("keys"), "Expected 'keys' in output, got:\n{text}");
512
-
513
- client.session_destroy(&session_id).await.unwrap();
514
- }
515
-
516
- // ─── Error cases ─────────────────────────────────────────────────────────────
517
-
518
- #[tokio::test]
519
- async fn test_error_get_text_invalid_session() {
520
- let (url, _handle) = start_server().await;
521
- let client = WrighttyClient::connect(&url).await.unwrap();
522
-
523
- let result = client.get_text("nonexistent-session-id").await;
524
- assert!(result.is_err(), "should return error for invalid session ID");
525
- let err = result.unwrap_err().to_string();
526
- println!("Error: {err}");
527
- assert!(
528
- err.contains("1001") || err.contains("session not found"),
529
- "expected SESSION_NOT_FOUND error code 1001, got: {err}"
530
- );
531
- }
532
-
533
- #[tokio::test]
534
- async fn test_error_get_contents_invalid_session() {
535
- let (url, _handle) = start_server().await;
536
- let client = WrighttyClient::connect(&url).await.unwrap();
537
-
538
- let result = client.get_contents("nonexistent-session-id").await;
539
- assert!(result.is_err());
540
- let err = result.unwrap_err().to_string();
541
- assert!(
542
- err.contains("1001") || err.contains("session not found"),
543
- "expected SESSION_NOT_FOUND, got: {err}"
544
- );
545
- }
546
-
547
- #[tokio::test]
548
- async fn test_error_get_scrollback_invalid_session() {
549
- let (url, _handle) = start_server().await;
550
- let client = WrighttyClient::connect(&url).await.unwrap();
551
-
552
- let result = client.get_scrollback("nonexistent-session-id", 10, 0).await;
553
- assert!(result.is_err());
554
- let err = result.unwrap_err().to_string();
555
- assert!(
556
- err.contains("1001") || err.contains("session not found"),
557
- "expected SESSION_NOT_FOUND, got: {err}"
558
- );
559
- }
560
-
561
- #[tokio::test]
562
- async fn test_error_send_text_invalid_session() {
563
- let (url, _handle) = start_server().await;
564
- let client = WrighttyClient::connect(&url).await.unwrap();
565
-
566
- let result = client.send_text("nonexistent-session-id", "hello\n").await;
567
- assert!(result.is_err());
568
- let err = result.unwrap_err().to_string();
569
- assert!(
570
- err.contains("1001") || err.contains("session not found"),
571
- "expected SESSION_NOT_FOUND, got: {err}"
572
- );
573
- }
574
-
575
- #[tokio::test]
576
- async fn test_error_send_keys_invalid_session() {
577
- let (url, _handle) = start_server().await;
578
- let client = WrighttyClient::connect(&url).await.unwrap();
579
-
580
- let result = client
581
- .send_keys("nonexistent-session-id", vec![KeyInput::Shorthand("a".to_string())])
582
- .await;
583
- assert!(result.is_err());
584
- let err = result.unwrap_err().to_string();
585
- assert!(
586
- err.contains("1001") || err.contains("session not found"),
587
- "expected SESSION_NOT_FOUND, got: {err}"
588
- );
589
- }
590
-
591
- #[tokio::test]
592
- async fn test_error_destroy_nonexistent_session() {
593
- let (url, _handle) = start_server().await;
594
- let client = WrighttyClient::connect(&url).await.unwrap();
595
-
596
- let result = client.session_destroy("nonexistent-session-id").await;
597
- assert!(result.is_err());
598
- }
599
-
600
- #[tokio::test]
601
- async fn test_error_screenshot_unsupported_format() {
602
- let (url, _handle) = start_server().await;
603
- let client = WrighttyClient::connect(&url).await.unwrap();
604
-
605
- let session_id = client.session_create(default_session()).await.unwrap();
606
- tokio::time::sleep(Duration::from_millis(300)).await;
607
-
608
- // PNG/SVG not implemented — should return NOT_SUPPORTED error
609
- let result = client.screenshot(&session_id, ScreenshotFormat::Png).await;
610
- assert!(result.is_err(), "PNG screenshot should return an error");
611
- let err = result.unwrap_err().to_string();
612
- assert!(
613
- err.contains("1006") || err.contains("not supported"),
614
- "expected NOT_SUPPORTED error code 1006, got: {err}"
615
- );
616
-
617
- client.session_destroy(&session_id).await.unwrap();
618
- }