返回 CodeWhale
protocol.rs
根目录 / crates / memory / tests / protocol.rs
1 use codewhale_memory::{protocol::ToolServer, *};
2 use serde_json::{Value, json};
3 fn server(readonly: bool) -> ToolServer {
4 let scope = Scope::user("local", "u").workspace("alpha");
5 let access = if readonly {
6 Access::readonly(vec![scope.clone()])
7 } else {
8 Access::operator(vec![scope.clone()])
9 }
10 .unwrap();
11 ToolServer::new(
12 Store::in_memory_with_clock(|| 1_750_000_000).unwrap(),
13 access,
14 scope,
15 None,
16 None,
17 )
18 .unwrap()
19 }
20 fn proposal() -> Value {
21 json!({"request_id":"p1","kind":"fact","title":"cache","body":"Cache reuse is a design goal.","source_uri":"codewhale://session/1/message/1"})
22 }
23 #[test]
24 fn review_and_forget_are_never_exposed_to_the_model() {
25 let s = server(false);
26 let names: Vec<_> = s
27 .tools()
28 .as_array()
29 .unwrap()
30 .iter()
31 .map(|t| t["name"].as_str().unwrap().to_owned())
32 .collect();
33 assert!(
34 !names
35 .iter()
36 .any(|s| s.contains("approve") || s.contains("forget") || s.contains("review"))
37 );
38 }
39 #[test]
40 fn readonly_schema_removes_mutation_tools() {
41 let mut s = server(true);
42 assert!(!s.tools().to_string().contains("memory_propose"));
43 assert!(matches!(
44 s.invoke("memory_propose", proposal()),
45 Err(Error::Denied)
46 ));
47 }
48 #[test]
49 fn proposal_cannot_choose_its_authority_or_scope() {
50 let mut s = server(false);
51 let mut args = proposal();
52 args["scope"] = json!({"tenant":"other"});
53 assert!(s.invoke("memory_propose", args).is_err());
54 }
55 #[test]
56 fn proposed_memory_stays_out_of_recall() {
57 let mut s = server(false);
58 let result = s.invoke("memory_propose", proposal()).unwrap();
59 assert_eq!(result["memory"]["status"], "candidate");
60 let result = s.invoke("memory_search", json!({"query":"cache"})).unwrap();
61 assert_eq!(result["hits"].as_array().unwrap().len(), 0);
62 }
63 #[test]
64 fn protocol_initializes_supported_compatibility_version() {
65 let mut s = server(false);
66 let r=s.handle(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}})).unwrap();
67 assert_eq!(r["result"]["protocolVersion"], "2025-11-25");
68 }
69 #[test]
70 fn unsupported_new_version_is_not_silently_claimed() {
71 let mut s = server(false);
72 let r=s.handle(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28"}})).unwrap();
73 assert_eq!(r["result"]["protocolVersion"], "2025-11-25");
74 }
75 #[test]
76 fn mutation_notifications_are_ignored() {
77 let mut s = server(false);
78 let r=s.handle(json!({"jsonrpc":"2.0","method":"tools/call","params":{"name":"memory_propose","arguments":proposal()}}));
79 assert!(r.is_none());
80 let r = s.invoke("memory_propose", proposal()).unwrap();
81 assert_eq!(r["created"], true);
82 }
83 #[test]
84 fn get_cannot_read_an_unavailable_id() {
85 let mut s = server(false);
86 assert!(matches!(
87 s.invoke("memory_get", json!({"id":"other-project-memory"})),
88 Err(Error::NotFound)
89 ));
90 }
91 #[test]
92 fn oversized_frame_is_rejected_without_processing() {
93 let mut s = server(false);
94 let bytes = vec![b'x'; policy::MAX_FRAME_BYTES + 1];
95 let mut out = Vec::new();
96 assert!(s.serve(std::io::Cursor::new(bytes), &mut out).is_err());
97 assert!(out.is_empty());
98 }
99 #[test]
100 fn invalid_json_does_not_crash_stream() {
101 let mut s = server(false);
102 let mut out = Vec::new();
103 s.serve(
104 std::io::Cursor::new(b"{broken\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}\n"),
105 &mut out,
106 )
107 .unwrap();
108 let text = String::from_utf8(out).unwrap();
109 let lines: Vec<Value> = text
110 .lines()
111 .map(|l| serde_json::from_str(l).unwrap())
112 .collect();
113 assert_eq!(lines.len(), 2);
114 assert_eq!(lines[0]["error"]["code"], -32700);
115 assert_eq!(lines[1]["id"], 2);
116 }
117 #[test]
118 fn denied_tool_call_has_a_tool_error_result() {
119 let mut s = server(false);
120 s.handle(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}));
121 let r=s.handle(json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"memory_forget","arguments":{"id":"x"}}})).unwrap();
122 assert_eq!(r["result"]["isError"], true);
123 }
124 #[test]
125 fn request_id_must_not_be_null_or_fractional() {
126 let mut s = server(false);
127 for id in [Value::Null, json!(1.5)] {
128 let r = s
129 .handle(json!({"jsonrpc":"2.0","id":id,"method":"ping"}))
130 .unwrap();
131 assert_eq!(r["error"]["code"], -32600);
132 }
133 }
134 #[test]
135 fn protocol_errors_do_not_echo_secret_payloads() {
136 let mut s = server(false);
137 s.handle(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}));
138 let mut p = proposal();
139 p["body"] = json!("API_KEY=abcdefghijklmnop123456");
140 let r=s.handle(json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"memory_propose","arguments":p}})).unwrap();
141 assert!(!r.to_string().contains("abcdefghijklmnop123456"));
142 assert_eq!(r["result"]["isError"], true);
143 }
144
145 #[test]
146 fn disabled_runtime_does_not_open_files_or_publish_tools() {
147 let directory = tempfile::tempdir().unwrap();
148 let database = directory
149 .path()
150 .join("not-created")
151 .join("memory-v2.sqlite3");
152 let scope = Scope::user("local", "u");
153 let result = codewhale_memory::runtime::open_tools(
154 codewhale_memory::runtime::RuntimeOptions {
155 enabled: false,
156 database: database.clone(),
157 workspace_root: None,
158 },
159 Access::agent(vec![scope.clone()]).unwrap(),
160 scope,
161 None,
162 )
163 .unwrap();
164 assert!(result.is_none());
165 assert!(!database.exists());
166 assert!(!database.parent().unwrap().exists());
167 }
168
168 lines RUST