opencode_rs 0.7.0

Rust SDK for OpenCode (HTTP-first hybrid with SSE streaming)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! HTTP endpoint integration tests.
//!
//! Tests that verify typed HTTP responses against a live opencode server.
//!
//! TODO(3): Add error case tests (invalid session IDs, malformed payloads, missing fields)
//! to verify error responses deserialize correctly.

use super::create_test_client;
use super::should_run;
use opencode_rs::types::message::PromptPart;
use opencode_rs::types::message::PromptRequest;

/// Test session CRUD with typed responses.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_crud_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session - returns typed Session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    assert!(!session.id.is_empty(), "Session should have ID");

    // Get session - returns typed Session
    let fetched = client
        .sessions()
        .get(&session.id)
        .await
        .expect("Failed to get session");

    assert_eq!(fetched.id, session.id, "Session IDs should match");

    // List sessions - returns Vec<Session>
    // Note: Session list may have race conditions with filesystem, so we just verify we can call it
    match client.sessions().list().await {
        Ok(sessions) => {
            println!("Listed {} sessions", sessions.len());
            // Verify structure when sessions exist (can't check specific IDs due to timing)
            if let Some(first) = sessions.first() {
                assert!(!first.id.is_empty(), "Session should have ID");
            }
        }
        Err(e) => {
            // List may fail in some configurations
            println!("List sessions: {e:?}");
        }
    }

    // Delete session
    client
        .sessions()
        .delete(&session.id)
        .await
        .expect("Failed to delete session");
}

/// Test prompt with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_prompt_typed_response() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Send prompt - returns typed PromptResponse
    let response = client
        .messages()
        .prompt(
            &session.id,
            &PromptRequest {
                parts: vec![PromptPart::Text {
                    text: "Say hello".to_string(),
                    synthetic: None,
                    ignored: None,
                    metadata: None,
                }],
                message_id: None,
                model: None,
                agent: None,
                no_reply: Some(true), // Don't wait for reply
                system: None,
                variant: None,
            },
        )
        .await
        .expect("Failed to send prompt");

    // PromptResponse has typed fields
    // status and message_id are optional but should deserialize
    println!("Prompt response status: {:?}", response.status);
    println!("Prompt response message_id: {:?}", response.message_id);

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}

/// Test providers list with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_providers_list_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // List providers - returns ProviderListResponse with all/default/connected
    let response = client
        .providers()
        .list()
        .await
        .expect("Failed to list providers");

    // Verify typed fields in the 'all' array
    for provider in &response.all {
        assert!(!provider.id.is_empty(), "Provider should have ID");
        assert!(!provider.name.is_empty(), "Provider should have name");
        println!(
            "Provider: {} ({}) - {:?} models",
            provider.name,
            provider.id,
            provider.models.len()
        );
    }

    // Verify we have proper default and connected data
    println!(
        "Response: {} providers, {} defaults, {} connected",
        response.all.len(),
        response.default.len(),
        response.connected.len()
    );
}

/// Test MCP status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_mcp_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get MCP status - returns typed McpStatus
    let status = client
        .mcp()
        .status()
        .await
        .expect("Failed to get MCP status");

    // Verify typed fields
    println!("MCP servers: {:?}", status.servers.len());
    for server in &status.servers {
        assert!(!server.name.is_empty(), "MCP server should have name");
        println!("  Server: {} - {:?}", server.name, server.status);
    }
}

/// Test LSP status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_lsp_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get LSP status - returns Vec<LspServerStatus>
    let servers = client.misc().lsp().await.expect("Failed to get LSP status");

    // Verify we got a response (may be empty if no LSP servers configured)
    println!("LSP servers: {} configured", servers.len());
    for server in &servers {
        println!("  {} ({}): {:?}", server.name, server.id, server.status);
    }
}

/// Test formatter status with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_formatter_status_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get formatter status - returns Vec<FormatterInfo>
    let formatters = client
        .misc()
        .formatter()
        .await
        .expect("Failed to get formatter status");

    // Verify we got a response (may be empty if no formatters configured)
    println!("Formatters: {} configured", formatters.len());
    for fmt in &formatters {
        println!(
            "  {} - enabled: {}, extensions: {:?}",
            fmt.name, fmt.enabled, fmt.extensions
        );
    }
}

/// Test `OpenAPI` doc with typed response.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_openapi_doc_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Get OpenAPI doc - returns typed OpenApiDoc
    let doc = client
        .misc()
        .doc()
        .await
        .expect("Failed to get OpenAPI doc");

    // Verify it's a valid OpenAPI document
    assert!(doc.spec.is_object(), "Doc should be a JSON object");
    assert!(
        doc.spec.get("openapi").is_some() || doc.spec.get("swagger").is_some(),
        "Should be an OpenAPI/Swagger document"
    );
}

/// Test find endpoints with typed responses.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_find_endpoints_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Find text - returns typed FindResponse (uses 'pattern' param)
    match client.find().text("fn").await {
        Ok(text_results) => {
            println!("Text search: got response");
            let _ = text_results.results;
        }
        Err(e) => {
            // May fail if ripgrep not available or no files to search
            println!("Text search not available: {e:?}");
        }
    }

    // Find files - returns typed FindResponse (uses 'query' param)
    match client.find().files("Cargo").await {
        Ok(file_results) => {
            println!("File search: got response");
            let _ = file_results.results;
        }
        Err(e) => {
            println!("File search not available: {e:?}");
        }
    }

    // Find symbols - returns typed FindResponse (currently returns empty)
    match client.find().symbols("main").await {
        Ok(symbol_results) => {
            println!("Symbol search: got response");
            let _ = symbol_results.results;
        }
        Err(e) => {
            println!("Symbol search not available: {e:?}");
        }
    }
}

/// Test message list with typed Part deserialization.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_message_parts_typed() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Send a prompt
    let _ = client
        .messages()
        .prompt(
            &session.id,
            &PromptRequest {
                parts: vec![PromptPart::Text {
                    text: "Hello".to_string(),
                    synthetic: None,
                    ignored: None,
                    metadata: None,
                }],
                message_id: None,
                model: None,
                agent: None,
                no_reply: Some(true),
                system: None,
                variant: None,
            },
        )
        .await;

    // List messages - should have typed Parts
    let messages = client
        .messages()
        .list(&session.id)
        .await
        .expect("Failed to list messages");

    for message in &messages {
        println!("Message {} has {} parts", message.id(), message.parts.len());
        for part in &message.parts {
            // Parts should deserialize to typed enum variants
            match part {
                opencode_rs::types::Part::Text { text, .. } => {
                    let preview: String = text.chars().take(50).collect();
                    println!("  Text part: {preview}...");
                }
                opencode_rs::types::Part::Tool { tool, state, .. } => {
                    println!(
                        "  Tool part: {} - state: {:?}",
                        tool,
                        state.as_ref().map(opencode_rs::types::ToolState::status)
                    );
                }
                _ => {
                    println!("  Other part type");
                }
            }
        }
    }

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}

/// Test session with permission ruleset.
#[tokio::test]
#[ignore = "requires: opencode serve"]
async fn test_session_permission_ruleset() {
    if !should_run() {
        return;
    }

    let client = create_test_client().await;

    // Create session - permission should deserialize as Ruleset if present
    let session = client
        .sessions()
        .create(&Default::default())
        .await
        .expect("Failed to create session");

    // Get the session and check permission field
    let fetched = client
        .sessions()
        .get(&session.id)
        .await
        .expect("Failed to get session");

    // Permission may or may not be set
    if let Some(permission) = &fetched.permission {
        println!("Session has {} permission rules", permission.len());
        for rule in permission {
            println!(
                "  Rule: {} {} {:?}",
                rule.permission, rule.pattern, rule.action
            );
        }
    }

    // Clean up
    let _ = client.sessions().delete(&session.id).await;
}