Validate the auth method id, use the standard error codes, tidy small ACP gaps
authenticate ignored methodId and opened a browser for anything. It now rejects an id the agent never offered with -32602, and reports a failed sign-in as -32603, since -32000 means authentication is required. An unknown session in prompt, set_mode and set_config_option is now -32002, matching load and resume. The final update of a command that ran in the client's terminal no longer re-sends the terminal after terminal/release. session/list rejects a relative cwd filter. An embedded image or audio blob is read as an image or audio input, and other blobs name their MIME type in the placeholder. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: siGit Code v1.6.6-acp <noreply@sigit.si>
Seto Elkahfi committed
Oct 10, 2026 at 17:08 UTC
657101c6ae052a5f18b57583da3d2f0f98e903b8
6 files changed
+441
-38
.agents/AGENTS.md
+2
-1
@@ -345,7 +345,8 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
345
the exception to the lock order: the dispatch loop runs them with neither lock, because
346
they touch only state keyed by session (`permissions`) and have to land mid-turn. They
347
check the id against `sessions` (`-32002` when unknown) and build the options from
348
- that session's own model (`config_options_for_session`), never `activate_session`. Tool
348
+ that session's own model (`config_options_for_session`), never `activate_session`. Unknown session ids are `-32002` (resource not found) everywhere,
349
+ and `authenticate` accepts only `AUTH_METHOD_ID`. Tool
350
execution, subagents included, still runs one session at a time.
351
- **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
352
Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
CHANGELOG.md
+18
@@ -20,6 +20,24 @@
20
ended. They now take no lock, are answered at once, and the pending call's
21
successors follow the new mode. Model and Inference changes still wait for
22
the turn.
23
+- **`authenticate` checks the method id.** An id that `initialize` never offered
24
+ is now rejected with `invalid_params` (`-32602`) instead of starting a
25
+ browser sign-in. A sign-in that fails reports `-32603`, since `-32000` means
26
+ authentication is required before a call.
27
+- **A session the agent does not know is `-32002`.** `session/prompt`,
28
+ `session/set_mode` and `session/set_config_option` answered `-32602` for an
29
+ unknown id; they now say resource not found, as `session/load` and
30
+ `session/resume` already did.
31
+- **A finished terminal command no longer names a released terminal.** The
32
+ closing `tool_call_update` of a command that ran in the editor's terminal
33
+ re-sent the terminal id after `terminal/release`. It now leaves the content
34
+ alone, so the embedded terminal stays as it was, and the result still goes out
35
+ as `rawOutput`.
36
+- **`session/list` rejects a relative `cwd` filter** with `-32602`, as
37
+ `session/new` does.
38
+- **An embedded binary resource is read when the model can read it.** An image
39
+ blob becomes an image input and an audio blob an audio input. Any other blob
40
+ keeps its placeholder, now with its MIME type.
41
- **A prompt that is only audio is answered.** A clip with no text or image used
42
to end the turn at once with nothing sent to the model, even though the agent
43
advertises audio.
src/main.rs
+53
-36
@@ -594,20 +594,13 @@ fn file_change_content(change: &tools::FileChange) -> ToolCallContent {
594
.into()
595
}
596
597
-/// The content of a finished tool call's card: the client terminal the
598
-/// command ran in, when it ran in one, which already shows its output (the
599
-/// full result text still travels as `raw_output`); otherwise the diff of the
600
-/// file it wrote, if it wrote one, then its result text.
601
-fn tool_result_content(
602
- output: &str,
603
- change: Option<&tools::FileChange>,
604
- terminal: Option<&str>,
605
-) -> Vec<ToolCallContent> {
606
- if let Some(terminal_id) = terminal {
607
- return vec![ToolCallContent::Terminal(Terminal::new(
608
- terminal_id.to_string(),
609
- ))];
610
- }
597
+/// The content of a finished tool call's card: the diff of the file it wrote,
598
+/// if it wrote one, then its result text. A command that ran in the client's
599
+/// terminal gets no content at all in its final update: the terminal is
600
+/// already embedded in the card and was released by then, so naming it again
601
+/// would point at an id the client no longer has. The result text still
602
+/// travels as `raw_output`.
603
+fn tool_result_content(output: &str, change: Option<&tools::FileChange>) -> Vec<ToolCallContent> {
604
change
605
.map(file_change_content)
606
.into_iter()
@@ -2196,6 +2189,9 @@ fn validate_session_roots(
2189
Ok(())
2190
}
2191
2192
+/// The id of the one auth method `initialize` advertises.
2193
+const AUTH_METHOD_ID: &str = "sigit";
2194
+
2195
// ── ACP handler implementations ───────────────────────────────────────────────
2196
2197
impl SiGitAgent {
@@ -2266,7 +2262,7 @@ impl SiGitAgent {
2262
// With an Agent method, clicking calls `authenticate`, which opens the
2263
// browser and finishes the sign-in itself.
2264
let auth_methods = vec![AuthMethod::Agent(
2269
- AuthMethodAgent::new("sigit", "Sign in to siGit Code")
2265
+ AuthMethodAgent::new(AUTH_METHOD_ID, "Sign in to siGit Code")
2266
.description("Opens sigit.si in your browser to authorize this device."),
2267
)];
2268
@@ -2345,6 +2341,16 @@ impl SiGitAgent {
2341
) -> agent_client_protocol::Result<AuthenticateResponse> {
2342
log::info!("authenticate: method={}", req.method_id.0);
2343
2344
+ if req.method_id.0.as_ref() != AUTH_METHOD_ID {
2345
+ return Err(agent_client_protocol::Error::new(
2346
+ -32602,
2347
+ format!(
2348
+ "unknown auth method {}; the only one offered is {AUTH_METHOD_ID}",
2349
+ req.method_id.0
2350
+ ),
2351
+ ));
2352
+ }
2353
+
2354
// An existing session clears the gate without sending anyone to a
2355
// browser they didn't ask for.
2356
if let Ok(email) = account::verify_session().await {
@@ -2360,7 +2366,7 @@ impl SiGitAgent {
2366
let flow = browser_auth::begin();
2367
if !flow.is_loopback() {
2368
return Err(agent_client_protocol::Error::new(
2363
- -32000,
2369
+ -32603,
2370
"Could not open a local port to receive the sign-in. \
2371
Run `sigit login` in a terminal instead."
2372
.to_string(),
@@ -2383,7 +2389,7 @@ impl SiGitAgent {
2389
Ok(AuthenticateResponse::default())
2390
}
2391
Err(reason) => Err(agent_client_protocol::Error::new(
2386
- -32000,
2392
+ -32603,
2393
format!(
2394
"Sign-in did not complete ({reason}). \
2395
Try again, or create an account at https://sigit.si."
@@ -2567,7 +2573,7 @@ impl SiGitAgent {
2573
2574
let Some(state) = self.sessions.lock().unwrap().get(&key).cloned() else {
2575
return Err(agent_client_protocol::Error::new(
2570
- -32602,
2576
+ -32002,
2577
format!(
2578
"unknown session {key}; create it with session/new or restore it with session/load"
2579
),
@@ -2827,6 +2833,14 @@ impl SiGitAgent {
2833
&self,
2834
args: ListSessionsRequest,
2835
) -> agent_client_protocol::Result<ListSessionsResponse> {
2836
+ if let Some(cwd) = args.cwd.as_ref()
2837
+ && !cwd.is_absolute()
2838
+ {
2839
+ return Err(agent_client_protocol::Error::new(
2840
+ -32602,
2841
+ format!("cwd must be an absolute path, got {}", cwd.display()),
2842
+ ));
2843
+ }
2844
let filter = args.cwd.clone();
2845
let sessions: Vec<SessionInfo> = session_store::list()
2846
.into_iter()
@@ -3334,7 +3348,22 @@ impl SiGitAgent {
3348
));
3349
}
3350
EmbeddedResourceResource::BlobResourceContents(blob) => {
3337
- parts.push(format!("[binary resource: {}]", blob.uri));
3351
+ let mime = blob.mime_type.clone().unwrap_or_default();
3352
+ if mime.starts_with("image/") {
3353
+ images.push(ImageInput {
3354
+ mime_type: mime,
3355
+ data: blob.blob.clone(),
3356
+ });
3357
+ } else if mime.starts_with("audio/") {
3358
+ audio.push(AudioInput {
3359
+ mime_type: mime,
3360
+ data: blob.blob.clone(),
3361
+ });
3362
+ } else if mime.is_empty() {
3363
+ parts.push(format!("[binary resource: {}]", blob.uri));
3364
+ } else {
3365
+ parts.push(format!("[binary resource: {} ({mime})]", blob.uri));
3366
+ }
3367
}
3368
_ => {
3369
log::debug!("ignoring unsupported embedded resource variant");
@@ -3897,12 +3926,10 @@ impl SiGitAgent {
3926
} else {
3927
ToolCallStatus::Failed
3928
})
3900
- .content(tool_result_content(
3901
- &output,
3902
- change.as_ref(),
3903
- terminal.as_deref(),
3904
- ))
3929
.raw_output(serde_json::Value::String(output.clone()));
3930
+ if terminal.is_none() {
3931
+ fields = fields.content(tool_result_content(&output, change.as_ref()));
3932
+ }
3933
// A denial at the prompt leaves the card titled with the
3934
// permission request's full arguments; put its own back.
3935
if !executed && announced_status == ToolCallStatus::Pending {
@@ -6836,7 +6863,7 @@ mod tests {
6863
.activate_session(&SessionId::new("not-opened"))
6864
.await
6865
.unwrap_err();
6839
- assert_eq!(error.code, agent_client_protocol::ErrorCode::InvalidParams);
6866
+ assert_eq!(i32::from(error.code), -32002);
6867
assert!(error.message.contains("session/new"));
6868
assert!(agent.active_session.lock().unwrap().is_none());
6869
}
@@ -8015,7 +8042,6 @@ mod tests {
8042
let content = serde_json::to_value(tool_result_content(
8043
"Edited file: /repo/src/lib.rs (4 bytes written)",
8044
Some(&change),
8018
- None,
8045
))
8046
.unwrap();
8047
assert_eq!(
@@ -8039,16 +8065,7 @@ mod tests {
8065
assert!(diff.get("oldText").is_none_or(serde_json::Value::is_null));
8066
8067
// Every other tool keeps the result text alone.
8042
- // A command that ran in the client's terminal keeps the terminal,
8043
- // which already shows its output.
8044
- let ran = serde_json::to_value(tool_result_content("Exit code 0:\nhi", None, Some("t-1")))
8045
- .unwrap();
8046
- assert_eq!(
8047
- ran,
8048
- serde_json::json!([{ "type": "terminal", "terminalId": "t-1" }])
8049
- );
8050
-
8051
- let plain = serde_json::to_value(tool_result_content("ok", None, None)).unwrap();
8068
+ let plain = serde_json::to_value(tool_result_content("ok", None)).unwrap();
8069
assert_eq!(plain.as_array().unwrap().len(), 1);
8070
assert_eq!(plain[0]["type"], "content");
8071
}
tests/acp_client_terminal.rs
+11
-1
@@ -457,7 +457,17 @@ fn run_command_runs_in_the_client_terminal_and_shows_it() {
457
.position(|update| update["status"] == "completed")
458
.expect("call_1 never completed");
459
assert!(shown < finished, "embedded after completion: {updates:?}");
460
- assert_eq!(updates[finished]["content"], terminal);
460
+ // The terminal was released by then, so the closing update does not name
461
+ // it again; the card keeps the one already embedded.
462
+ assert!(
463
+ updates[finished]["content"].is_null(),
464
+ "the final update must not re-send the released terminal: {}",
465
+ updates[finished]
466
+ );
467
+ assert_eq!(
468
+ updates[finished]["rawOutput"],
469
+ format!("Exit code 0:\n{EDITOR_OUTPUT}")
470
+ );
471
}
472
473
#[test]
tests/acp_embedded_context.rs
+77
@@ -272,3 +272,80 @@ fn an_embedded_resource_reaches_the_model_as_the_client_sent_it() {
272
drop(agent);
273
let _ = std::fs::remove_dir_all(&scratch);
274
}
275
+
276
+/// A binary resource is not a text placeholder when the model can read it:
277
+/// an image becomes an image input, anything unreadable says what it was.
278
+#[test]
279
+fn an_embedded_blob_is_an_image_input_or_names_its_type() {
280
+ let endpoint = start_fake_endpoint(vec![sse_text("done")]);
281
+ let scratch = std::env::temp_dir().join(format!("sigit_acp_blob_{}", std::process::id()));
282
+ let config_dir = scratch.join("config");
283
+ let project = scratch.join("project");
284
+ std::fs::create_dir_all(&config_dir).unwrap();
285
+ std::fs::create_dir_all(&project).unwrap();
286
+
287
+ let mut agent = spawn_agent(endpoint.port, &config_dir);
288
+ let id = agent.request(
289
+ "initialize",
290
+ json!({"protocolVersion": 1, "clientCapabilities": {}}),
291
+ );
292
+ agent.wait_for_response(id);
293
+ let id = agent.request("session/new", json!({"cwd": project, "mcpServers": []}));
294
+ let session_id = agent.wait_for_response(id)["result"]["sessionId"]
295
+ .as_str()
296
+ .expect("session id")
297
+ .to_string();
298
+
299
+ let id = agent.request(
300
+ "session/prompt",
301
+ json!({
302
+ "sessionId": session_id,
303
+ "prompt": [
304
+ {"type": "text", "text": "what are these?"},
305
+ {"type": "resource", "resource": {
306
+ "uri": "file:///shot.png",
307
+ "mimeType": "image/png",
308
+ "blob": "QUJDRA==",
309
+ }},
310
+ {"type": "resource", "resource": {
311
+ "uri": "file:///paper.pdf",
312
+ "mimeType": "application/pdf",
313
+ "blob": "JVBERi0=",
314
+ }},
315
+ ],
316
+ }),
317
+ );
318
+ let response = agent.wait_for_response(id);
319
+ assert_eq!(response["result"]["stopReason"], "end_turn");
320
+
321
+ let request = endpoint
322
+ .requests
323
+ .lock()
324
+ .unwrap()
325
+ .first()
326
+ .cloned()
327
+ .expect("the endpoint received a completion request");
328
+ let user = request["messages"]
329
+ .as_array()
330
+ .expect("messages")
331
+ .iter()
332
+ .rev()
333
+ .find(|message| message["role"] == "user")
334
+ .expect("a user message")
335
+ .to_string();
336
+ assert!(
337
+ user.contains("data:image/png;base64,QUJDRA=="),
338
+ "the image blob must reach the model as an image: {user}"
339
+ );
340
+ assert!(
341
+ user.contains("[binary resource: file:///paper.pdf (application/pdf)]"),
342
+ "an unreadable blob must say what it was: {user}"
343
+ );
344
+ assert!(
345
+ !user.contains("JVBERi0="),
346
+ "the pdf bytes must not be forwarded: {user}"
347
+ );
348
+
349
+ drop(agent);
350
+ let _ = std::fs::remove_dir_all(&scratch);
351
+}
tests/acp_error_codes.rs
new
+280
@@ -0,0 +1,280 @@
1
+//! The JSON-RPC error codes the agent answers with.
2
+//!
3
+//! ACP uses the standard codes: `-32602` for a request whose parameters are
4
+//! wrong (an auth method that was never offered, a relative `cwd`), `-32002`
5
+//! for a session id the agent does not know. `-32000` is reserved for
6
+//! "authentication required", so a sign-in that fails is `-32603`.
7
+
8
+use std::collections::VecDeque;
9
+use std::io::{BufRead, BufReader, Read, Write};
10
+use std::net::TcpListener;
11
+use std::process::{Child, ChildStdin, Command, Stdio};
12
+use std::sync::mpsc::{Receiver, channel};
13
+use std::sync::{Arc, Mutex};
14
+use std::time::{Duration, Instant};
15
+
16
+use serde_json::{Value, json};
17
+
18
+const TIMEOUT: Duration = Duration::from_secs(60);
19
+
20
+fn sse_body(events: &[Value]) -> String {
21
+ let mut body = String::new();
22
+ for event in events {
23
+ body.push_str("data: ");
24
+ body.push_str(&event.to_string());
25
+ body.push_str("\n\n");
26
+ }
27
+ body.push_str("data: [DONE]\n\n");
28
+ body
29
+}
30
+
31
+fn sse_text(text: &str) -> String {
32
+ sse_body(&[json!({"choices": [{"delta": {"content": text}}]})])
33
+}
34
+
35
+/// Serves one scripted SSE response per request and records each request body.
36
+// Shared harness; this file never reads the recorded requests.
37
+#[allow(dead_code)]
38
+struct FakeEndpoint {
39
+ port: u16,
40
+ requests: Arc<Mutex<Vec<Value>>>,
41
+}
42
+
43
+fn start_fake_endpoint(responses: Vec<String>) -> FakeEndpoint {
44
+ let listener = TcpListener::bind("127.0.0.1:0").expect("bind fake endpoint");
45
+ let port = listener.local_addr().unwrap().port();
46
+ let requests: Arc<Mutex<Vec<Value>>> = Arc::default();
47
+ let recorded = Arc::clone(&requests);
48
+ let queue = Mutex::new(VecDeque::from(responses));
49
+
50
+ std::thread::spawn(move || {
51
+ for stream in listener.incoming() {
52
+ let Ok(mut stream) = stream else { continue };
53
+ let mut reader = BufReader::new(match stream.try_clone() {
54
+ Ok(clone) => clone,
55
+ Err(_) => continue,
56
+ });
57
+ let mut content_length = 0usize;
58
+ loop {
59
+ let mut line = String::new();
60
+ if reader.read_line(&mut line).unwrap_or(0) == 0 {
61
+ break;
62
+ }
63
+ let line = line.trim();
64
+ if line.is_empty() {
65
+ break;
66
+ }
67
+ if let Some(length) = line.to_ascii_lowercase().strip_prefix("content-length:") {
68
+ content_length = length.trim().parse().unwrap_or(0);
69
+ }
70
+ }
71
+ let mut body = vec![0u8; content_length];
72
+ if reader.read_exact(&mut body).is_err() {
73
+ continue;
74
+ }
75
+ if let Ok(request) = serde_json::from_slice::<Value>(&body) {
76
+ recorded.lock().unwrap().push(request);
77
+ }
78
+ let payload = queue
79
+ .lock()
80
+ .unwrap()
81
+ .pop_front()
82
+ .unwrap_or_else(|| sse_text("out of scripted responses"));
83
+ let response = format!(
84
+ "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n\
85
+ content-length: {}\r\nconnection: close\r\n\r\n{}",
86
+ payload.len(),
87
+ payload
88
+ );
89
+ let _ = stream.write_all(response.as_bytes());
90
+ }
91
+ });
92
+
93
+ FakeEndpoint { port, requests }
94
+}
95
+
96
+// ── ACP client over the binary's stdio ──────────────────────────────────────
97
+
98
+struct AgentUnderTest {
99
+ child: Child,
100
+ stdin: ChildStdin,
101
+ incoming: Receiver<Value>,
102
+ next_id: u64,
103
+}
104
+
105
+fn spawn_agent(port: u16, config_dir: &std::path::Path) -> AgentUnderTest {
106
+ let mut child = Command::new(env!("CARGO_BIN_EXE_sigit"))
107
+ .env("OPENAI_BASE_URL", format!("http://127.0.0.1:{port}"))
108
+ .env("OPENAI_API_KEY", "test-key")
109
+ .env("SIGIT_MODEL", "scripted-model")
110
+ .env("SIGIT_CONFIG_DIR", config_dir)
111
+ .env("SIGIT_MCP", "off")
112
+ .env("SIGIT_PERMISSIONS", "allow")
113
+ .env_remove("SIGIT_LOCAL_INFERENCE")
114
+ .stdin(Stdio::piped())
115
+ .stdout(Stdio::piped())
116
+ .stderr(Stdio::null())
117
+ .spawn()
118
+ .expect("spawn sigit in ACP mode");
119
+
120
+ let stdout = child.stdout.take().unwrap();
121
+ let (message_tx, incoming) = channel();
122
+ std::thread::spawn(move || {
123
+ for line in BufReader::new(stdout).lines() {
124
+ let Ok(line) = line else { break };
125
+ if let Ok(message) = serde_json::from_str::<Value>(&line)
126
+ && message_tx.send(message).is_err()
127
+ {
128
+ break;
129
+ }
130
+ }
131
+ });
132
+
133
+ let stdin = child.stdin.take().unwrap();
134
+ AgentUnderTest {
135
+ child,
136
+ stdin,
137
+ incoming,
138
+ next_id: 0,
139
+ }
140
+}
141
+
142
+impl AgentUnderTest {
143
+ fn send(&mut self, message: Value) {
144
+ let mut line = message.to_string();
145
+ line.push('\n');
146
+ self.stdin
147
+ .write_all(line.as_bytes())
148
+ .expect("write to agent stdin");
149
+ self.stdin.flush().expect("flush agent stdin");
150
+ }
151
+
152
+ fn request(&mut self, method: &str, params: Value) -> u64 {
153
+ self.next_id += 1;
154
+ let id = self.next_id;
155
+ self.send(json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}));
156
+ id
157
+ }
158
+
159
+ /// Wait for the response to our request `id`, collecting the raw JSON of
160
+ /// every `session/update` notification that arrives before it.
161
+ fn wait_for_response_collecting_updates(&mut self, id: u64) -> (Value, String) {
162
+ let deadline = Instant::now() + TIMEOUT;
163
+ let mut updates = String::new();
164
+ loop {
165
+ let remaining = deadline.saturating_duration_since(Instant::now());
166
+ match self.incoming.recv_timeout(remaining) {
167
+ Ok(message) if message["id"] == id && message.get("method").is_none() => {
168
+ assert!(
169
+ message.get("error").is_none(),
170
+ "request {id} failed: {message}"
171
+ );
172
+ return (message, updates);
173
+ }
174
+ Ok(message) => {
175
+ if message["method"] == "session/update" {
176
+ updates.push_str(&message["params"].to_string());
177
+ updates.push('\n');
178
+ }
179
+ }
180
+ Err(_) => panic!("timed out waiting for response to request {id}"),
181
+ }
182
+ }
183
+ }
184
+
185
+ fn wait_for_response(&mut self, id: u64) -> Value {
186
+ self.wait_for_response_collecting_updates(id).0
187
+ }
188
+}
189
+
190
+impl Drop for AgentUnderTest {
191
+ fn drop(&mut self) {
192
+ let _ = self.child.kill();
193
+ let _ = self.child.wait();
194
+ }
195
+}
196
+
197
+impl AgentUnderTest {
198
+ /// The raw answer to request `id`, error or not.
199
+ fn answer(&mut self, id: u64) -> Value {
200
+ let deadline = Instant::now() + TIMEOUT;
201
+ loop {
202
+ let remaining = deadline.saturating_duration_since(Instant::now());
203
+ match self.incoming.recv_timeout(remaining) {
204
+ Ok(message) if message["id"] == id && message.get("method").is_none() => {
205
+ return message;
206
+ }
207
+ Ok(_) => {}
208
+ Err(_) => panic!("timed out waiting for the answer to request {id}"),
209
+ }
210
+ }
211
+ }
212
+}
213
+
214
+fn open() -> (AgentUnderTest, std::path::PathBuf, std::path::PathBuf) {
215
+ let endpoint = start_fake_endpoint(vec![]);
216
+ let scratch = std::env::temp_dir().join(format!(
217
+ "sigit_acp_codes_{}_{}",
218
+ std::process::id(),
219
+ endpoint.port
220
+ ));
221
+ let config_dir = scratch.join("config");
222
+ std::fs::create_dir_all(&config_dir).unwrap();
223
+ let mut agent = spawn_agent(endpoint.port, &config_dir);
224
+ let id = agent.request(
225
+ "initialize",
226
+ json!({"protocolVersion": 1, "clientCapabilities": {}}),
227
+ );
228
+ agent.wait_for_response(id);
229
+ (agent, scratch, config_dir)
230
+}
231
+
232
+#[test]
233
+fn an_auth_method_that_was_never_offered_is_invalid_params() {
234
+ let (mut agent, scratch, _) = open();
235
+ let id = agent.request("authenticate", json!({"methodId": "not-a-method"}));
236
+ let answer = agent.answer(id);
237
+ assert_eq!(answer["error"]["code"], -32602, "{answer}");
238
+ drop(agent);
239
+ let _ = std::fs::remove_dir_all(scratch);
240
+}
241
+
242
+#[test]
243
+fn a_session_the_agent_does_not_know_is_resource_not_found() {
244
+ let (mut agent, scratch, _) = open();
245
+ for (method, params) in [
246
+ (
247
+ "session/prompt",
248
+ json!({"sessionId": "nope", "prompt": [{"type": "text", "text": "hi"}]}),
249
+ ),
250
+ (
251
+ "session/set_mode",
252
+ json!({"sessionId": "nope", "modeId": "permission-mode-auto"}),
253
+ ),
254
+ (
255
+ "session/set_config_option",
256
+ json!({"sessionId": "nope", "configId": "sigit-permission-mode",
257
+ "value": "permission-mode-auto"}),
258
+ ),
259
+ ] {
260
+ let id = agent.request(method, params);
261
+ let answer = agent.answer(id);
262
+ assert_eq!(answer["error"]["code"], -32002, "{method}: {answer}");
263
+ }
264
+ drop(agent);
265
+ let _ = std::fs::remove_dir_all(scratch);
266
+}
267
+
268
+#[test]
269
+fn a_relative_cwd_filter_on_session_list_is_invalid_params() {
270
+ let (mut agent, scratch, _) = open();
271
+ let id = agent.request("session/list", json!({"cwd": "relative/dir"}));
272
+ let answer = agent.answer(id);
273
+ assert_eq!(answer["error"]["code"], -32602, "{answer}");
274
+
275
+ let id = agent.request("session/list", json!({}));
276
+ let answer = agent.answer(id);
277
+ assert!(answer.get("error").is_none(), "{answer}");
278
+ drop(agent);
279
+ let _ = std::fs::remove_dir_all(scratch);
280
+}