返回 CodeWhale
lens.rs
根目录 / crates / memory / tests / lens.rs
1 //! Native acceptance suite for the new lens. Requires a Rust toolchain.
2 use codewhale_memory::*;
3 use std::sync::{
4 Arc,
5 atomic::{AtomicI64, Ordering},
6 };
7 fn setup() -> (Store, Access, Scope, Scope) {
8 let scope = Scope::user("local", "owner").workspace("repo");
9 let session = scope.session("run");
10 let access = Access::operator(vec![scope.clone(), session.clone()]).unwrap();
11 (
12 Store::in_memory_with_clock(|| 1000).unwrap(),
13 access,
14 scope,
15 session,
16 )
17 }
18 fn draft(scope: &Scope, body: &str) -> Draft {
19 Draft::note(
20 scope.clone(),
21 "Whale",
22 body,
23 Evidence {
24 kind: SourceKind::User,
25 uri: "codewhale:input".into(),
26 locator: "explicit".into(),
27 sha256: None,
28 observed_at: 900,
29 },
30 )
31 }
32 fn active(s: &mut Store, a: &Access, scope: &Scope, key: &str, body: &str) -> Memory {
33 let c = s.capture(a, key, draft(scope, body)).unwrap().memory;
34 s.approve(a, &c.id, c.revision, None, &Snapshot::default())
35 .unwrap()
36 }
37 fn prepare(s: &Store, a: &Access, session: &Scope) -> lens::PreparedContext {
38 s.prepare_context(
39 a,
40 session,
41 "run",
42 "request",
43 &Recall::default(),
44 &ByteCounter,
45 &ContextBudget::default(),
46 )
47 .unwrap()
48 }
49 #[test]
50 fn lens_candidates_do_not_enter_a_context() {
51 let (mut s, a, n, se) = setup();
52 s.capture(&a, "c", draft(&n, "candidate")).unwrap();
53 let p = prepare(&s, &a, &se);
54 assert!(p.packet.selected.is_empty());
55 assert_eq!(
56 s.lens_snapshot(&a, Some("run"), None, 100, &Snapshot::default())
57 .unwrap()
58 .entries
59 .len(),
60 1
61 );
62 }
63 #[test]
64 fn preparation_is_not_dispatch() {
65 let (mut s, a, n, se) = setup();
66 active(&mut s, &a, &n, "a", "safe memory");
67 let p = prepare(&s, &a, &se);
68 assert_eq!(p.receipt.stage, "prepared");
69 assert!(p.receipt.dispatched_at.is_none());
70 }
71 #[test]
72 fn delivery_requires_the_engine_capability() {
73 let (mut s, a, n, se) = setup();
74 active(&mut s, &a, &n, "a", "safe memory");
75 let p = prepare(&s, &a, &se);
76 let model = Access::agent(vec![n, se]).unwrap();
77 assert!(matches!(
78 s.acknowledge_append(
79 &model,
80 &p.receipt.id,
81 &p.receipt.packet_hash,
82 1,
83 &Snapshot::default()
84 ),
85 Err(Error::Denied)
86 ));
87 }
88 #[test]
89 fn preparing_also_requires_the_engine_capability() {
90 let (s, _, n, se) = setup();
91 let model = Access::agent(vec![n, se.clone()]).unwrap();
92 assert!(matches!(
93 s.prepare_context(
94 &model,
95 &se,
96 "run",
97 "q",
98 &Recall::default(),
99 &ByteCounter,
100 &ContextBudget::default()
101 ),
102 Err(Error::Denied)
103 ));
104 }
105 #[test]
106 fn dispatch_cannot_skip_append() {
107 let (mut s, a, n, se) = setup();
108 active(&mut s, &a, &n, "a", "safe memory");
109 let p = prepare(&s, &a, &se);
110 assert!(matches!(
111 s.acknowledge_dispatch(
112 &a,
113 &p.receipt.id,
114 &p.receipt.packet_hash,
115 "transport",
116 &Snapshot::default()
117 ),
118 Err(Error::InvalidState)
119 ));
120 }
121 #[test]
122 fn packet_hash_is_checked() {
123 let (mut s, a, n, se) = setup();
124 active(&mut s, &a, &n, "a", "safe memory");
125 let p = prepare(&s, &a, &se);
126 assert!(matches!(
127 s.acknowledge_append(&a, &p.receipt.id, "wrong", 1, &Snapshot::default()),
128 Err(Error::RevisionConflict)
129 ));
130 }
131 #[test]
132 fn acknowledgement_is_idempotent() {
133 let (mut s, a, n, se) = setup();
134 active(&mut s, &a, &n, "a", "safe memory");
135 let p = prepare(&s, &a, &se);
136 for _ in 0..2 {
137 s.acknowledge_append(
138 &a,
139 &p.receipt.id,
140 &p.receipt.packet_hash,
141 1,
142 &Snapshot::default(),
143 )
144 .unwrap();
145 s.acknowledge_dispatch(
146 &a,
147 &p.receipt.id,
148 &p.receipt.packet_hash,
149 "transport",
150 &Snapshot::default(),
151 )
152 .unwrap();
153 }
154 assert_eq!(
155 s.context_receipts(&a, Some("run"), &Snapshot::default())
156 .unwrap()[0]
157 .stage,
158 "dispatched"
159 );
160 }
161 #[test]
162 fn conflicting_acknowledgement_is_rejected() {
163 let (mut s, a, n, se) = setup();
164 active(&mut s, &a, &n, "a", "safe memory");
165 let p = prepare(&s, &a, &se);
166 s.acknowledge_append(
167 &a,
168 &p.receipt.id,
169 &p.receipt.packet_hash,
170 1,
171 &Snapshot::default(),
172 )
173 .unwrap();
174 assert!(matches!(
175 s.acknowledge_append(
176 &a,
177 &p.receipt.id,
178 &p.receipt.packet_hash,
179 2,
180 &Snapshot::default()
181 ),
182 Err(Error::IdempotencyConflict)
183 ));
184 }
185 #[test]
186 fn preparation_reuses_same_request() {
187 let (mut s, a, n, se) = setup();
188 active(&mut s, &a, &n, "a", "safe memory");
189 assert_eq!(
190 prepare(&s, &a, &se).receipt.id,
191 prepare(&s, &a, &se).receipt.id
192 );
193 }
194 #[test]
195 fn changed_query_cannot_reuse_request() {
196 let (mut s, a, n, se) = setup();
197 active(&mut s, &a, &n, "a", "safe memory");
198 prepare(&s, &a, &se);
199 assert!(matches!(
200 s.prepare_context(
201 &a,
202 &se,
203 "run",
204 "request",
205 &Recall {
206 query: "changed".into(),
207 ..Default::default()
208 },
209 &ByteCounter,
210 &ContextBudget::default()
211 ),
212 Err(Error::IdempotencyConflict)
213 ));
214 }
215 #[test]
216 fn workspace_scope_cannot_pretend_to_be_a_session() {
217 let (s, a, n, _) = setup();
218 assert!(
219 s.prepare_context(
220 &a,
221 &n,
222 "run",
223 "q",
224 &Recall::default(),
225 &ByteCounter,
226 &ContextBudget::default()
227 )
228 .is_err()
229 );
230 }
231 #[test]
232 fn suppressed_memory_is_not_packed() {
233 let (mut s, a, n, se) = setup();
234 let m = active(&mut s, &a, &n, "a", "safe memory");
235 s.set_preferences(&a, &m.id, 0, false, true).unwrap();
236 assert!(prepare(&s, &a, &se).packet.selected.is_empty());
237 }
238 #[test]
239 fn pin_includes_an_off_query_record() {
240 let (mut s, a, n, se) = setup();
241 let m = active(&mut s, &a, &n, "a", "xylophone convention");
242 s.set_preferences(&a, &m.id, 0, true, false).unwrap();
243 let p = s
244 .prepare_context(
245 &a,
246 &se,
247 "run",
248 "q",
249 &Recall {
250 query: "unrelated".into(),
251 ..Default::default()
252 },
253 &ByteCounter,
254 &ContextBudget::default(),
255 )
256 .unwrap();
257 assert!(p.packet.selected.iter().any(|r| r.id == m.id));
258 }
259 #[test]
260 fn pin_does_not_promote_a_candidate() {
261 let (mut s, a, n, se) = setup();
262 let m = s.capture(&a, "a", draft(&n, "candidate")).unwrap().memory;
263 s.set_preferences(&a, &m.id, 0, true, false).unwrap();
264 assert!(prepare(&s, &a, &se).packet.selected.is_empty());
265 }
266 #[test]
267 fn preference_retries_converge() {
268 let (mut s, a, n, _) = setup();
269 let m = active(&mut s, &a, &n, "a", "safe memory");
270 assert_eq!(
271 s.set_preferences(&a, &m.id, 0, true, false)
272 .unwrap()
273 .revision,
274 1
275 );
276 assert_eq!(
277 s.set_preferences(&a, &m.id, 0, true, false)
278 .unwrap()
279 .revision,
280 1
281 );
282 }
283 #[test]
284 fn preference_conflict_does_not_change_flags() {
285 let (mut s, a, n, _) = setup();
286 let m = active(&mut s, &a, &n, "a", "safe memory");
287 s.set_preferences(&a, &m.id, 0, true, false).unwrap();
288 assert!(s.set_preferences(&a, &m.id, 0, false, true).is_err());
289 assert!(s.preferences(&a, &m.id).unwrap().pinned);
290 }
291 #[test]
292 fn preference_counter_cannot_overflow() {
293 let (mut s, a, n, _) = setup();
294 let m = active(&mut s, &a, &n, "a", "safe memory");
295 assert!(s.set_preferences(&a, &m.id, i64::MAX, false, true).is_err());
296 }
297 #[test]
298 fn suppression_blocks_preflight_but_does_not_rewrite_receipts() {
299 let (mut s, a, n, se) = setup();
300 let m = active(&mut s, &a, &n, "a", "safe memory");
301 let p = prepare(&s, &a, &se);
302 s.set_preferences(&a, &m.id, 0, false, true).unwrap();
303 assert!(
304 s.preflight_context(
305 &a,
306 &p.receipt.id,
307 &p.receipt.packet_hash,
308 &Snapshot::default()
309 )
310 .is_err()
311 );
312 s.acknowledge_append(
313 &a,
314 &p.receipt.id,
315 &p.receipt.packet_hash,
316 1,
317 &Snapshot::default(),
318 )
319 .unwrap();
320 assert_eq!(
321 s.context_receipts(&a, Some("run"), &Snapshot::default())
322 .unwrap()[0]
323 .stage,
324 "appended"
325 );
326 }
327 #[test]
328 fn changed_knowledge_marks_old_context_invalid() {
329 let (mut s, a, n, se) = setup();
330 let old = active(&mut s, &a, &n, "a", "old convention");
331 let p = prepare(&s, &a, &se);
332 s.acknowledge_append(
333 &a,
334 &p.receipt.id,
335 &p.receipt.packet_hash,
336 1,
337 &Snapshot::default(),
338 )
339 .unwrap();
340 let new = s
341 .capture(&a, "b", draft(&n, "new convention"))
342 .unwrap()
343 .memory;
344 s.supersede(
345 &a,
346 (&old.id, old.revision),
347 (&new.id, new.revision),
348 None,
349 &Snapshot::default(),
350 )
351 .unwrap();
352 let c = s
353 .context_receipts(&a, Some("run"), &Snapshot::default())
354 .unwrap();
355 assert_eq!(c[0].stage, "appended");
356 assert!(c[0].invalidated_ids.contains(&old.id));
357 }
358 #[test]
359 fn forgetting_removes_dependent_context() {
360 let (mut s, a, n, se) = setup();
361 let m = active(&mut s, &a, &n, "a", "safe memory");
362 prepare(&s, &a, &se);
363 s.forget(&a, &m.id, m.revision).unwrap();
364 assert!(
365 s.context_receipts(&a, Some("run"), &Snapshot::default())
366 .unwrap()
367 .is_empty()
368 );
369 }
370 #[test]
371 fn metadata_feed_never_contains_memory_text() {
372 let (mut s, a, n, se) = setup();
373 active(&mut s, &a, &n, "a", "the unrepeated private phrase");
374 prepare(&s, &a, &se);
375 let p = s.event_page(&a, 0, 200).unwrap();
376 let encoded = serde_json::to_string(&p).unwrap();
377 assert!(!encoded.contains("unrepeated private phrase"));
378 assert!(encoded.contains("memory.context_prepared"));
379 }
380 #[test]
381 fn events_use_existing_whalesong_category() {
382 let (mut s, a, n, _) = setup();
383 active(&mut s, &a, &n, "a", "safe memory");
384 let p = s.event_page(&a, 0, 200).unwrap();
385 assert!(!p.events.is_empty());
386 assert!(
387 p.events
388 .iter()
389 .all(|e| e["category"] == "memory" && e["schemaVersion"] == 1)
390 );
391 }
392 #[test]
393 fn event_feed_is_scope_filtered() {
394 let (mut s, a, n, _) = setup();
395 active(&mut s, &a, &n, "a", "safe memory");
396 let other = Access::readonly(vec![n.workspace("other")]).unwrap();
397 assert!(s.event_page(&other, 0, 200).unwrap().events.is_empty());
398 }
399 #[test]
400 fn numeric_alias_is_stable() {
401 let (mut s, a, n, _) = setup();
402 let m = active(&mut s, &a, &n, "a", "safe memory");
403 let id = s.numeric_id(&a, &m.id).unwrap();
404 assert_eq!(s.get_numeric(&a, id).unwrap().id, m.id);
405 }
406 #[test]
407 fn forgotten_alias_is_not_reused() {
408 let (mut s, a, n, _) = setup();
409 let m = active(&mut s, &a, &n, "a", "old text");
410 let old = s.numeric_id(&a, &m.id).unwrap();
411 s.forget(&a, &m.id, m.revision).unwrap();
412 let m = active(&mut s, &a, &n, "b", "new text");
413 assert!(s.numeric_id(&a, &m.id).unwrap() > old);
414 assert!(matches!(s.get_numeric(&a, old), Err(Error::NotFound)));
415 }
416 #[test]
417 fn numeric_alias_does_not_grant_scope() {
418 let (mut s, a, n, _) = setup();
419 let m = active(&mut s, &a, &n, "a", "safe memory");
420 let alias = s.numeric_id(&a, &m.id).unwrap();
421 let other = Access::readonly(vec![n.workspace("other")]).unwrap();
422 assert!(matches!(s.get_numeric(&other, alias), Err(Error::NotFound)));
423 }
424 #[test]
425 fn knowledge_time_does_not_leak_later_approval() {
426 let clock = Arc::new(AtomicI64::new(1000));
427 let c = clock.clone();
428 let mut s = Store::in_memory_with_clock(move || c.load(Ordering::SeqCst)).unwrap();
429 let n = Scope::user("local", "owner");
430 let a = Access::operator(vec![n.clone()]).unwrap();
431 let m = s.capture(&a, "q", draft(&n, "fact")).unwrap().memory;
432 clock.store(1100, Ordering::SeqCst);
433 s.approve(&a, &m.id, 1, None, &Snapshot::default()).unwrap();
434 assert_eq!(
435 s.memory_as_of(&a, &m.id, 1050, 1050).unwrap().memory.status,
436 Status::Candidate
437 );
438 assert_eq!(
439 s.memory_as_of(&a, &m.id, 1100, 1100).unwrap().memory.status,
440 Status::Active
441 );
442 }
443 #[test]
444 fn history_before_capture_is_not_fabricated() {
445 let (mut s, a, n, _) = setup();
446 let m = active(&mut s, &a, &n, "a", "safe memory");
447 assert!(matches!(
448 s.memory_as_of(&a, &m.id, 999, 999),
449 Err(Error::NotFound)
450 ));
451 }
452 #[test]
453 fn forgotten_history_cannot_be_read() {
454 let (mut s, a, n, _) = setup();
455 let m = active(&mut s, &a, &n, "a", "safe memory");
456 s.forget(&a, &m.id, m.revision).unwrap();
457 assert!(matches!(
458 s.memory_as_of(&a, &m.id, 1000, 1000),
459 Err(Error::NotFound)
460 ));
461 }
462 #[test]
463 fn exact_context_budget_accounts_for_utf8() {
464 let (mut s, a, n, se) = setup();
465 active(&mut s, &a, &n, "a", "鲸鱼 memory");
466 let p = prepare(&s, &a, &se);
467 assert_eq!(p.packet.used_units, p.packet.text.len());
468 assert_eq!(p.packet.unit, "utf8_bytes");
469 }
470
470 lines RUST