Skip to main content

max / shop

kitty-graphics: 15 tests Covers control field parsing (defaults, full set, missing G prefix, malformed input), all three formats (RGB/RGBA/PNG), chunk reassembly (single-shot, 2-chunk, 3-chunk, interleaved concurrent by both i= and I=), and delete dispatch. No bugs found — the parser is small and the code paths are direct. Workspace test count: 67 (21 shop-vt + 31 shop-grid + 15 kitty-graphics), 0 failures.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 01:02 UTC
Signed with PGP, not checked
Commit: b5dc7602b28b492e03969082e6ebd6067497b6d6
Parent: 34b75b2
1 file changed, +185 insertions, -0 deletions
@@ -248,3 +248,188 @@
248 248 into.no_cursor_move = true;
249 249 }
250 250 }
251 +
252 + #[cfg(test)]
253 + mod tests {
254 + use super::*;
255 + use base64::Engine;
256 +
257 + fn b64(bytes: &[u8]) -> String {
258 + B64.encode(bytes)
259 + }
260 +
261 + fn transmit(cmd: Command) -> (Control, Vec<u8>) {
262 + match cmd {
263 + Command::Transmit { control, payload } => (control, payload),
264 + other => panic!("expected Transmit, got {other:?}"),
265 + }
266 + }
267 +
268 + // ---- control-field parsing ---------------------------------------
269 +
270 + #[test]
271 + fn parses_default_action_and_missing_semicolon() {
272 + // No `;` means control-only, empty payload; default action is 'T'.
273 + let mut p = Parser::new();
274 + let (control, payload) = transmit(
275 + p.feed(b"Gf=32,s=1,v=1")
276 + .expect("no chunk pending — must dispatch"),
277 + );
278 + assert_eq!(control.action, 'T');
279 + assert_eq!(control.format, Some(Format::Rgba));
280 + assert_eq!(control.width_px, Some(1));
281 + assert_eq!(control.height_px, Some(1));
282 + assert!(payload.is_empty());
283 + }
284 +
285 + #[test]
286 + fn parses_full_control_fields() {
287 + let mut p = Parser::new();
288 + let body = format!("Ga=T,f=100,t=d,i=42,I=7,p=3,s=100,v=50,c=8,r=4,C=1,q=1;{}", b64(b"data"));
289 + let (c, payload) = transmit(p.feed(body.as_bytes()).unwrap());
290 + assert_eq!(c.action, 'T');
291 + assert_eq!(c.format, Some(Format::Png));
292 + assert_eq!(c.medium, Some(Medium::Direct));
293 + assert_eq!(c.id, Some(42));
294 + assert_eq!(c.number, Some(7));
295 + assert_eq!(c.placement, Some(3));
296 + assert_eq!(c.width_px, Some(100));
297 + assert_eq!(c.height_px, Some(50));
298 + assert_eq!(c.cell_cols, Some(8));
299 + assert_eq!(c.cell_rows, Some(4));
300 + assert!(c.no_cursor_move);
301 + assert_eq!(c.quiet, 1);
302 + assert_eq!(payload, b"data");
303 + }
304 +
305 + #[test]
306 + fn tolerates_missing_g_prefix() {
307 + // `Parser::feed` strips a leading `G` if present. Callers that
308 + // pre-stripped it should still work.
309 + let mut p = Parser::new();
310 + let body = format!("a=T,f=32,s=1,v=1;{}", b64(b"xyz"));
311 + let (control, payload) = transmit(p.feed(body.as_bytes()).unwrap());
312 + assert_eq!(control.action, 'T');
313 + assert_eq!(payload, b"xyz");
314 + }
315 +
316 + #[test]
317 + fn unknown_action_returns_none() {
318 + let mut p = Parser::new();
319 + assert!(p.feed(b"Ga=z,f=32;YWJj").is_none());
320 + }
321 +
322 + #[test]
323 + fn malformed_control_returns_none() {
324 + // No `=` in a field.
325 + let mut p = Parser::new();
326 + assert!(p.feed(b"Ga=T,broken;YWJj").is_none());
327 + }
328 +
329 + #[test]
330 + fn malformed_base64_returns_none() {
331 + let mut p = Parser::new();
332 + assert!(p.feed(b"Ga=T,f=32;this-is-not-base64!!!").is_none());
333 + }
334 +
335 + // ---- formats -----------------------------------------------------
336 +
337 + #[test]
338 + fn format_24_is_rgb() {
339 + let mut p = Parser::new();
340 + let body = format!("Ga=T,f=24,s=1,v=1;{}", b64(&[1, 2, 3]));
341 + let (c, _) = transmit(p.feed(body.as_bytes()).unwrap());
342 + assert_eq!(c.format, Some(Format::Rgb));
343 + }
344 +
345 + #[test]
346 + fn format_32_is_rgba() {
347 + let mut p = Parser::new();
348 + let body = format!("Ga=T,f=32,s=1,v=1;{}", b64(&[1, 2, 3, 4]));
349 + let (c, _) = transmit(p.feed(body.as_bytes()).unwrap());
350 + assert_eq!(c.format, Some(Format::Rgba));
351 + }
352 +
353 + #[test]
354 + fn format_100_is_png() {
355 + let mut p = Parser::new();
356 + let body = format!("Ga=T,f=100;{}", b64(b"\x89PNG"));
357 + let (c, _) = transmit(p.feed(body.as_bytes()).unwrap());
358 + assert_eq!(c.format, Some(Format::Png));
359 + }
360 +
361 + // ---- chunked reassembly ------------------------------------------
362 +
363 + #[test]
364 + fn single_chunk_no_m_dispatches_immediately() {
365 + let mut p = Parser::new();
366 + let body = format!("Ga=T,f=32,s=1,v=1;{}", b64(b"solo"));
367 + let (_, payload) = transmit(p.feed(body.as_bytes()).unwrap());
368 + assert_eq!(payload, b"solo");
369 + }
370 +
371 + #[test]
372 + fn two_chunks_reassemble_by_id() {
373 + let mut p = Parser::new();
374 + let a = format!("Ga=T,f=32,s=2,v=1,i=7,m=1;{}", b64(b"HEAD"));
375 + let b = format!("Gi=7,m=0;{}", b64(b"TAIL"));
376 + assert!(p.feed(a.as_bytes()).is_none(), "first chunk should not dispatch");
377 + let (c, payload) = transmit(p.feed(b.as_bytes()).unwrap());
378 + assert_eq!(c.id, Some(7));
379 + // format from the first chunk must persist through merge.
380 + assert_eq!(c.format, Some(Format::Rgba));
381 + assert_eq!(c.width_px, Some(2));
382 + assert_eq!(payload, b"HEADTAIL");
383 + }
384 +
385 + #[test]
386 + fn three_chunks_reassemble() {
387 + let mut p = Parser::new();
388 + let a = format!("Ga=T,f=32,i=1,m=1;{}", b64(b"AAA"));
389 + let b = format!("Gi=1,m=1;{}", b64(b"BBB"));
390 + let c = format!("Gi=1,m=0;{}", b64(b"CCC"));
391 + assert!(p.feed(a.as_bytes()).is_none());
392 + assert!(p.feed(b.as_bytes()).is_none());
393 + let (_, payload) = transmit(p.feed(c.as_bytes()).unwrap());
394 + assert_eq!(payload, b"AAABBBCCC");
395 + }
396 +
397 + #[test]
398 + fn concurrent_ids_do_not_cross_contaminate() {
399 + let mut p = Parser::new();
400 + // Interleave two independent chunked transmissions.
401 + let a1 = format!("Ga=T,f=32,i=1,m=1;{}", b64(b"HELLO"));
402 + let b1 = format!("Ga=T,f=32,i=2,m=1;{}", b64(b"WORLD"));
403 + let a2 = format!("Gi=1,m=0;{}", b64(b"!"));
404 + let b2 = format!("Gi=2,m=0;{}", b64(b"?"));
405 +
406 + assert!(p.feed(a1.as_bytes()).is_none());
407 + assert!(p.feed(b1.as_bytes()).is_none());
408 + let (ca, pa) = transmit(p.feed(a2.as_bytes()).unwrap());
409 + let (cb, pb) = transmit(p.feed(b2.as_bytes()).unwrap());
410 + assert_eq!(ca.id, Some(1));
411 + assert_eq!(cb.id, Some(2));
412 + assert_eq!(pa, b"HELLO!");
413 + assert_eq!(pb, b"WORLD?");
414 + }
415 +
416 + #[test]
417 + fn chunked_by_number_field() {
418 + // Use I= (client-assigned number) instead of i=.
419 + let mut p = Parser::new();
420 + let a = format!("Ga=T,f=32,I=99,m=1;{}", b64(b"XX"));
421 + let b = format!("GI=99,m=0;{}", b64(b"YY"));
422 + assert!(p.feed(a.as_bytes()).is_none());
423 + let (_, payload) = transmit(p.feed(b.as_bytes()).unwrap());
424 + assert_eq!(payload, b"XXYY");
425 + }
426 +
427 + // ---- delete ------------------------------------------------------
428 +
429 + #[test]
430 + fn delete_returns_delete_command() {
431 + let mut p = Parser::new();
432 + let cmd = p.feed(b"Ga=d,d=A").expect("must dispatch");
433 + assert!(matches!(cmd, Command::Delete { .. }));
434 + }
435 + }