From 6b356cbaa6b94efd221706272e87a155f86573c3 Mon Sep 17 00:00:00 2001
From: Amit Prakash Ambasta <amit.prakash.ambasta@gmail.com>
Date: Sat, 28 Mar 2026 05:19:31 +0530
Subject: [PATCH] refactor: upgrade dependencies and migrate to owned fd types

- Bump nix from 0.27 to 0.31, thiserror from 1.0 to 2.0,
  rpassword from 5.0 to 7.4, and tokio from 1.5 to 1.50
- Replace RawFd usage with OwnedFd in terminal module and session
  worker, removing manual Drop impl and close() calls
- Pass borrowed fd references to nix::fcntl instead of raw ints
- Use libc::dup2 directly in term_connect_pipes for clarity
- Update agreety to use rpassword::prompt_password (renamed API)
- Add unit tests for scrambler module and greetd_ipc serialization
- Fix typos in doc comments (Graphivs, appropciate, ot)

Signed-off-by: Amit Prakash Ambasta <amit.prakash.ambasta@gmail.com>
---
 Cargo.lock                      | 390 +++++++++++++-------------------
 agreety/Cargo.toml              |   4 +-
 agreety/src/main.rs             |   4 +-
 fakegreet/Cargo.toml            |   4 +-
 greetd/Cargo.toml               |   4 +-
 greetd/src/main.rs              |  11 +-
 greetd/src/scrambler.rs         |  51 +++++
 greetd/src/session/interface.rs |   4 +-
 greetd/src/terminal/mod.rs      |  97 ++++----
 greetd_ipc/Cargo.toml           |   2 +-
 greetd_ipc/src/lib.rs           | 168 ++++++++++++++
 11 files changed, 446 insertions(+), 293 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f89b04a..4f8462e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,21 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "addr2line"
-version = "0.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+version = 4
 
 [[package]]
 name = "agreety"
@@ -31,53 +16,38 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.80"
+version = "0.1.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn",
 ]
 
-[[package]]
-name = "backtrace"
-version = "0.3.71"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
-dependencies = [
- "addr2line",
- "cc",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
-]
-
 [[package]]
 name = "bitflags"
-version = "2.5.0"
+version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
 
 [[package]]
 name = "bytes"
-version = "1.6.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
 
 [[package]]
-name = "cc"
-version = "1.0.95"
+name = "cfg-if"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
 
 [[package]]
-name = "cfg-if"
-version = "1.0.0"
+name = "cfg_aliases"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 [[package]]
 name = "enquote"
@@ -85,7 +55,17 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932"
 dependencies = [
- "thiserror",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -94,25 +74,19 @@ version = "0.10.3"
 dependencies = [
  "greetd_ipc",
  "serde",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
 ]
 
 [[package]]
 name = "getopts"
-version = "0.2.21"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
 dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "gimli"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
-
 [[package]]
 name = "greetd"
 version = "0.10.3"
@@ -127,7 +101,7 @@ dependencies = [
  "pam-sys",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
 ]
 
@@ -138,7 +112,7 @@ dependencies = [
  "async-trait",
  "serde",
  "serde_json",
- "thiserror",
+ "thiserror 2.0.18",
  "tokio",
 ]
 
@@ -148,62 +122,45 @@ version = "0.1.0"
 
 [[package]]
 name = "itoa"
-version = "1.0.11"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.183"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
 
 [[package]]
 name = "memchr"
-version = "2.7.2"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
-dependencies = [
- "adler",
-]
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
 
 [[package]]
 name = "mio"
-version = "0.8.11"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
 dependencies = [
  "libc",
  "wasi",
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "nix"
-version = "0.27.1"
+version = "0.31.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
 dependencies = [
  "bitflags",
  "cfg-if",
+ "cfg_aliases",
  "libc",
 ]
 
-[[package]]
-name = "object"
-version = "0.32.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "pam-sys"
 version = "0.5.6"
@@ -215,64 +172,73 @@ dependencies = [
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.14"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.81"
+version = "1.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.36"
+version = "1.0.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "rpassword"
-version = "5.0.1"
+version = "7.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
+checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
 dependencies = [
  "libc",
- "winapi",
+ "rtoolbox",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
-name = "rustc-demangle"
-version = "0.1.23"
+name = "rtoolbox"
+version = "0.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
 
 [[package]]
-name = "ryu"
-version = "1.0.17"
+name = "serde"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
 
 [[package]]
-name = "serde"
-version = "1.0.198"
+name = "serde_core"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.198"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -281,39 +247,42 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
 dependencies = [
  "itoa",
- "ryu",
+ "memchr",
  "serde",
+ "serde_core",
+ "zmij",
 ]
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.1"
+version = "1.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
 dependencies = [
+ "errno",
  "libc",
 ]
 
 [[package]]
 name = "socket2"
-version = "0.5.6"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
 dependencies = [
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "syn"
-version = "2.0.60"
+version = "2.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -322,18 +291,38 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.58"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.58"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -342,11 +331,10 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.37.0"
+version = "1.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
 dependencies = [
- "backtrace",
  "bytes",
  "libc",
  "mio",
@@ -354,14 +342,14 @@ dependencies = [
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
 name = "tokio-macros"
-version = "2.2.0"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -370,179 +358,121 @@ dependencies = [
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
 
 [[package]]
 name = "unicode-width"
-version = "0.1.11"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
 
 [[package]]
 name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
+version = "0.11.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
 [[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
+name = "windows-link"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
 
 [[package]]
 name = "windows-sys"
-version = "0.48.0"
+version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.48.5",
+ "windows-targets",
 ]
 
 [[package]]
 name = "windows-sys"
-version = "0.52.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets 0.52.5",
+ "windows-targets",
 ]
 
 [[package]]
-name = "windows-targets"
-version = "0.48.5"
+name = "windows-sys"
+version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
 dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
+ "windows-link",
 ]
 
 [[package]]
 name = "windows-targets"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.5",
- "windows_aarch64_msvc 0.52.5",
- "windows_i686_gnu 0.52.5",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
  "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.5",
- "windows_x86_64_gnu 0.52.5",
- "windows_x86_64_gnullvm 0.52.5",
- "windows_x86_64_msvc 0.52.5",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
 [[package]]
 name = "windows_i686_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.5"
+version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.5"
+name = "zmij"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/agreety/Cargo.toml b/agreety/Cargo.toml
index 76ceb35..1ee7169 100644
--- a/agreety/Cargo.toml
+++ b/agreety/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://git.sr.ht/~kennylevinsen/greetd/"
 [dependencies]
 greetd_ipc = { path = "../greetd_ipc", features = ["sync-codec"]}
 inish = { path = "../inish"}
-rpassword = "5.0"
+rpassword = "7.4"
 getopts = "0.2"
 enquote = "1.1"
-nix = "0.27"
+nix = "0.31"
diff --git a/agreety/src/main.rs b/agreety/src/main.rs
index 5ab294d..0b4199d 100644
--- a/agreety/src/main.rs
+++ b/agreety/src/main.rs
@@ -7,7 +7,7 @@ use std::{
 use enquote::unquote;
 use getopts::Options;
 use nix::sys::utsname::uname;
-use rpassword::prompt_password_stderr;
+use rpassword::prompt_password;
 
 use greetd_ipc::{codec::SyncCodec, AuthMessageType, ErrorType, Request, Response};
 
@@ -82,7 +82,7 @@ fn login(node: &str, cmd: &mut Option<String>) -> Result<LoginResult, Box<dyn st
             } => {
                 let response = match auth_message_type {
                     AuthMessageType::Visible => Some(prompt_stderr(&auth_message)?),
-                    AuthMessageType::Secret => Some(prompt_password_stderr(&auth_message)?),
+                    AuthMessageType::Secret => Some(prompt_password(&auth_message)?),
                     AuthMessageType::Info => {
                         eprintln!("info: {}", auth_message);
                         None
diff --git a/fakegreet/Cargo.toml b/fakegreet/Cargo.toml
index bba4b58..78aea97 100644
--- a/fakegreet/Cargo.toml
+++ b/fakegreet/Cargo.toml
@@ -10,5 +10,5 @@ repository = "https://git.sr.ht/~kennylevinsen/greetd/"
 [dependencies]
 serde = { version = "1.0", features = ["derive"] }
 greetd_ipc = { path = "../greetd_ipc", features = ["tokio-codec"] }
-tokio = { version = "1.5", features = ["process", "macros", "time", "net", "rt"] }
-thiserror = "1.0"
+tokio = { version = "1.50", features = ["process", "macros", "time", "net", "rt"] }
+thiserror = "2.0"
diff --git a/greetd/Cargo.toml b/greetd/Cargo.toml
index a9ac8d1..cff8ae8 100644
--- a/greetd/Cargo.toml
+++ b/greetd/Cargo.toml
@@ -11,7 +11,7 @@ repository = "https://git.sr.ht/~kennylevinsen/greetd/"
 debug = []
 
 [dependencies]
-nix = { version = "0.27", features = ["ioctl", "signal", "user", "fs", "mman"] }
+nix = { version = "0.31", features = ["ioctl", "signal", "user", "fs", "mman"] }
 pam-sys = "0.5.6"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
@@ -20,6 +20,6 @@ inish = { path = "../inish" }
 libc = "0.2"
 tokio = { version = "1", features = ["net", "sync", "macros", "signal", "rt", "io-util", "time"] }
 getopts = "0.2"
-thiserror = "1.0"
+thiserror = "2.0"
 async-trait = "0.1"
 enquote = "1.1"
diff --git a/greetd/src/main.rs b/greetd/src/main.rs
index 92a53d4..10ac12b 100644
--- a/greetd/src/main.rs
+++ b/greetd/src/main.rs
@@ -8,7 +8,7 @@ mod session;
 mod terminal;
 
 use std::os::unix::{
-    io::{FromRawFd, RawFd},
+    io::{FromRawFd, OwnedFd, RawFd},
     net::UnixDatagram,
 };
 
@@ -21,11 +21,12 @@ use tokio::task;
 use crate::{error::Error, session::worker};
 
 async fn session_worker_main(config: config::Config) -> Result<(), Error> {
-    let raw_fd = config.internal.session_worker as RawFd;
-    let mut cur_flags = FdFlag::from_bits_retain(fcntl(raw_fd, FcntlArg::F_GETFD)?);
+    // SAFETY: raw_fd was passed by our parent process as a valid, open fd number
+    let owned_fd = unsafe { OwnedFd::from_raw_fd(config.internal.session_worker as RawFd) };
+    let mut cur_flags = FdFlag::from_bits_retain(fcntl(&owned_fd, FcntlArg::F_GETFD)?);
     cur_flags.insert(FdFlag::FD_CLOEXEC);
-    fcntl(raw_fd, FcntlArg::F_SETFD(cur_flags))?;
-    let sock = unsafe { UnixDatagram::from_raw_fd(raw_fd) };
+    fcntl(&owned_fd, FcntlArg::F_SETFD(cur_flags))?;
+    let sock = UnixDatagram::from(owned_fd);
     worker::main(&sock)
 }
 
diff --git a/greetd/src/scrambler.rs b/greetd/src/scrambler.rs
index 2494ec7..5f05edb 100644
--- a/greetd/src/scrambler.rs
+++ b/greetd/src/scrambler.rs
@@ -40,3 +40,54 @@ impl Scrambler for CString {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn scramble_vec_overwrites_and_clears() {
+        let mut v: Vec<u8> = vec![1, 2, 3, 4, 5];
+        let cap = v.capacity();
+        v.scramble();
+        assert!(v.is_empty());
+        assert_eq!(v.capacity(), cap);
+    }
+
+    #[test]
+    fn scramble_string_zeros_bytes() {
+        let mut s = String::from("secret_password");
+        let ptr = s.as_ptr();
+        let original_len = s.len();
+        s.scramble();
+        assert!(s.is_empty());
+        // Verify the underlying memory was zeroed
+        let slice = unsafe { std::slice::from_raw_parts(ptr, original_len) };
+        assert!(slice.iter().all(|&b| b == 0));
+    }
+
+    #[test]
+    fn scramble_cstring_zeros_bytes() {
+        let mut cs = CString::new("hello").unwrap();
+        let ptr = cs.as_ptr();
+        cs.scramble();
+        // All bytes before the null terminator should be zero
+        for i in 0..5 {
+            assert_eq!(unsafe { *ptr.offset(i) }, 0);
+        }
+    }
+
+    #[test]
+    fn scramble_empty_string() {
+        let mut s = String::new();
+        s.scramble();
+        assert!(s.is_empty());
+    }
+
+    #[test]
+    fn scramble_empty_vec() {
+        let mut v: Vec<u8> = Vec::new();
+        v.scramble();
+        assert!(v.is_empty());
+    }
+}
diff --git a/greetd/src/session/interface.rs b/greetd/src/session/interface.rs
index 7e6c502..f366ec4 100644
--- a/greetd/src/session/interface.rs
+++ b/greetd/src/session/interface.rs
@@ -100,9 +100,9 @@ impl Session {
             UnixDatagram::pair().map_err(|e| format!("could not create pipe: {}", e))?;
 
         let raw_child = childfd.as_raw_fd();
-        let mut cur_flags = FdFlag::from_bits_retain(fcntl(raw_child, FcntlArg::F_GETFD)?);
+        let mut cur_flags = FdFlag::from_bits_retain(fcntl(&childfd, FcntlArg::F_GETFD)?);
         cur_flags.remove(FdFlag::FD_CLOEXEC);
-        fcntl(raw_child, FcntlArg::F_SETFD(cur_flags))?;
+        fcntl(&childfd, FcntlArg::F_SETFD(cur_flags))?;
 
         let cur_exe = std::env::current_exe()?;
         let bin = CString::new(cur_exe.to_str().expect("unable to get current exe name"))?;
diff --git a/greetd/src/terminal/mod.rs b/greetd/src/terminal/mod.rs
index 987f6cc..a6daeb6 100644
--- a/greetd/src/terminal/mod.rs
+++ b/greetd/src/terminal/mod.rs
@@ -4,9 +4,12 @@ use crate::error::Error;
 use nix::{
     fcntl::{open, OFlag},
     sys::stat::Mode,
-    unistd::{close, dup2, write},
+    unistd::write,
+};
+use std::{
+    ffi::CStr,
+    os::unix::io::{AsFd, AsRawFd, OwnedFd},
 };
-use std::{ffi::CStr, os::unix::io::RawFd};
 
 #[allow(dead_code)]
 pub enum KdMode {
@@ -24,23 +27,15 @@ impl KdMode {
 }
 
 pub struct Terminal {
-    fd: RawFd,
-    autoclose: bool,
+    fd: OwnedFd,
 }
 
-impl Drop for Terminal {
-    fn drop(&mut self) {
-        if self.autoclose {
-            close(self.fd).unwrap();
-        }
-    }
-}
-
-fn ttyname_r(fd: RawFd) -> Result<String, Error> {
+fn ttyname_r(fd: &OwnedFd) -> Result<String, Error> {
+    let raw = fd.as_raw_fd();
     let mut arr: [u8; 32] = [0; 32];
     let res = unsafe {
         libc::ttyname_r(
-            fd as libc::c_int,
+            raw as libc::c_int,
             &mut arr[0] as *mut u8 as *mut libc::c_char,
             31,
         )
@@ -65,33 +60,36 @@ impl Terminal {
             Mode::from_bits_truncate(0o666),
         );
         match res {
-            Ok(fd) => Ok(Terminal {
-                fd,
-                autoclose: true,
-            }),
-            Err(e) => return Err(format!("terminal: unable to open: {}", e).into()),
+            Ok(fd) => Ok(Terminal { fd }),
+            Err(e) => Err(format!("terminal: unable to open: {}", e).into()),
         }
     }
 
     /// Open the terminal from stdin
     pub fn stdin() -> Terminal {
-        Terminal {
-            fd: 0 as RawFd,
-            autoclose: false,
-        }
+        let stdin = std::io::stdin();
+        let fd = stdin
+            .as_fd()
+            .try_clone_to_owned()
+            .expect("failed to dup stdin");
+        Terminal { fd }
+    }
+
+    fn raw_fd(&self) -> i32 {
+        self.fd.as_raw_fd()
     }
 
     /// Returns the name of the TTY
     pub fn ttyname(&self) -> Result<String, Error> {
-        ttyname_r(self.fd)
+        ttyname_r(&self.fd)
     }
 
-    /// Set the kernel display to either graphics or text mode. Graphivs mode
+    /// Set the kernel display to either graphics or text mode. Graphics mode
     /// disables the kernel console on this VT, and also disables blanking
     /// between VT switches if both source and target VT is in graphics mode.
     pub fn kd_setmode(&self, mode: KdMode) -> Result<(), Error> {
         let mode = mode.to_const();
-        let ret = unsafe { ioctl::kd_setmode(self.fd, mode) };
+        let ret = unsafe { ioctl::kd_setmode(self.raw_fd(), mode) };
 
         if let Err(v) = ret {
             Err(format!("terminal: unable to set kernel display mode: {}", v).into())
@@ -102,10 +100,10 @@ impl Terminal {
 
     /// Switches to the specified VT and waits for completion of switch.
     fn vt_activate(&self, target_vt: usize) -> Result<(), Error> {
-        if let Err(v) = unsafe { ioctl::vt_activate(self.fd, target_vt as i32) } {
+        if let Err(v) = unsafe { ioctl::vt_activate(self.raw_fd(), target_vt as i32) } {
             return Err(format!("terminal: unable to activate: {}", v).into());
         }
-        if let Err(v) = unsafe { ioctl::vt_waitactive(self.fd, target_vt as i32) } {
+        if let Err(v) = unsafe { ioctl::vt_waitactive(self.raw_fd(), target_vt as i32) } {
             return Err(format!("terminal: unable to wait for activation: {}", v).into());
         }
         Ok(())
@@ -113,7 +111,7 @@ impl Terminal {
 
     /// Waits for specified VT to become active.
     pub fn vt_waitactive(&self, target_vt: usize) -> Result<(), Error> {
-        if let Err(v) = unsafe { ioctl::vt_waitactive(self.fd, target_vt as i32) } {
+        if let Err(v) = unsafe { ioctl::vt_waitactive(self.raw_fd(), target_vt as i32) } {
             return Err(format!("terminal: unable to wait for activation: {}", v).into());
         }
         Ok(())
@@ -128,7 +126,7 @@ impl Terminal {
             acqsig: 0,
             frsig: 0,
         };
-        let res = unsafe { ioctl::vt_setmode(self.fd, &mode) };
+        let res = unsafe { ioctl::vt_setmode(self.raw_fd(), &mode) };
 
         if let Err(v) = res {
             Err(format!("terminal: unable to set vt mode: {}", v).into())
@@ -154,10 +152,10 @@ impl Terminal {
                     frsig: 0,
                 },
             };
-            if let Err(v) = unsafe { ioctl::vt_setactivate(self.fd, &arg) } {
+            if let Err(v) = unsafe { ioctl::vt_setactivate(self.raw_fd(), &arg) } {
                 return Err(format!("terminal: unable to setactivate: {}", v).into());
             }
-            if let Err(v) = unsafe { ioctl::vt_waitactive(self.fd, target_vt as i32) } {
+            if let Err(v) = unsafe { ioctl::vt_waitactive(self.raw_fd(), target_vt as i32) } {
                 return Err(format!("terminal: unable to wait for activation: {}", v).into());
             }
         } else {
@@ -174,7 +172,7 @@ impl Terminal {
             v_signal: 0,
             v_state: 0,
         };
-        let res = unsafe { ioctl::vt_getstate(self.fd, &mut state as *mut ioctl::vt_state) };
+        let res = unsafe { ioctl::vt_getstate(self.raw_fd(), &mut state as *mut ioctl::vt_state) };
 
         if let Err(v) = res {
             Err(format!("terminal: unable to get current vt: {}", v).into())
@@ -190,7 +188,7 @@ impl Terminal {
     /// and use the VT before you get to it.
     pub fn vt_get_next(&self) -> Result<usize, Error> {
         let mut next_vt: i64 = 0;
-        let res = unsafe { ioctl::vt_openqry(self.fd, &mut next_vt as *mut i64) };
+        let res = unsafe { ioctl::vt_openqry(self.raw_fd(), &mut next_vt as *mut i64) };
 
         if let Err(v) = res {
             Err(format!("terminal: unable to get next vt: {}", v).into())
@@ -201,24 +199,29 @@ impl Terminal {
         }
     }
 
-    /// Hook up stdin, stdout and stderr of the current process ot this
-    /// terminal.
+    /// Hook up stdin, stdout and stderr of the current process to this
+    /// terminal. This is inherently unsafe as it globally redirects standard
+    /// I/O file descriptors.
     pub fn term_connect_pipes(&self) -> Result<(), Error> {
-        let res = dup2(self.fd, 0)
-            .and_then(|_| dup2(self.fd, 1))
-            .and_then(|_| dup2(self.fd, 2));
-
-        if let Err(v) = res {
-            Err(format!("terminal: unable to connect pipes: {}", v).into())
-        } else {
-            Ok(())
+        // SAFETY: dup2 to stdin/stdout/stderr has no safe Rust API — we're
+        // globally redirecting standard I/O fds which is an inherently unsafe
+        // OS operation, only done post-fork before exec.
+        for target_fd in [0, 1, 2] {
+            if unsafe { libc::dup2(self.raw_fd(), target_fd) } < 0 {
+                return Err(format!(
+                    "terminal: unable to connect pipes: dup2 failed for fd {}",
+                    target_fd
+                )
+                .into());
+            }
         }
+        Ok(())
     }
 
-    /// Clear this terminal by sending the appropciate escape codes to it. Only
+    /// Clear this terminal by sending the appropriate escape codes to it. Only
     /// affects text mode.
     pub fn term_clear(&self) -> Result<(), Error> {
-        let res = write(self.fd, b"\x1B[H\x1B[2J");
+        let res = write(&self.fd, b"\x1B[H\x1B[2J");
         if let Err(v) = res {
             Err(format!("terminal: unable to clear: {}", v).into())
         } else {
@@ -228,7 +231,7 @@ impl Terminal {
 
     // Forcibly take control of the terminal referred to by this fd.
     pub fn term_take_ctty(&self) -> Result<(), Error> {
-        let res = unsafe { ioctl::term_tiocsctty(self.fd, 1) };
+        let res = unsafe { ioctl::term_tiocsctty(self.raw_fd(), 1) };
 
         match res {
             Err(e) => Err(format!("terminal: unable to take controlling terminal: {}", e).into()),
diff --git a/greetd_ipc/Cargo.toml b/greetd_ipc/Cargo.toml
index 41b8736..ce265c7 100644
--- a/greetd_ipc/Cargo.toml
+++ b/greetd_ipc/Cargo.toml
@@ -22,4 +22,4 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 tokio = { version = "1", features = ["io-util"], optional = true }
 async-trait = { version = "0.1", optional = true }
-thiserror = { version = "1.0", optional = true }
+thiserror = { version = "2.0", optional = true }
diff --git a/greetd_ipc/src/lib.rs b/greetd_ipc/src/lib.rs
index 8155267..fbb8413 100644
--- a/greetd_ipc/src/lib.rs
+++ b/greetd_ipc/src/lib.rs
@@ -156,3 +156,171 @@ pub enum Response {
         auth_message: String,
     },
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn request_create_session_serialization() {
+        let req = Request::CreateSession {
+            username: "bob".to_string(),
+        };
+        let json = serde_json::to_string(&req).unwrap();
+        assert!(json.contains("\"type\":\"create_session\""));
+        assert!(json.contains("\"username\":\"bob\""));
+
+        let deserialized: Request = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Request::CreateSession { username } => assert_eq!(username, "bob"),
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn request_post_auth_message_response_serialization() {
+        let req = Request::PostAuthMessageResponse {
+            response: Some("hunter2".to_string()),
+        };
+        let json = serde_json::to_string(&req).unwrap();
+        assert!(json.contains("\"type\":\"post_auth_message_response\""));
+
+        let deserialized: Request = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Request::PostAuthMessageResponse { response } => {
+                assert_eq!(response, Some("hunter2".to_string()));
+            }
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn request_post_auth_message_response_none() {
+        let req = Request::PostAuthMessageResponse { response: None };
+        let json = serde_json::to_string(&req).unwrap();
+        let deserialized: Request = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Request::PostAuthMessageResponse { response } => assert!(response.is_none()),
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn request_start_session_serialization() {
+        let req = Request::StartSession {
+            cmd: vec!["sway".to_string()],
+            env: vec!["WAYLAND_DISPLAY=wayland-1".to_string()],
+        };
+        let json = serde_json::to_string(&req).unwrap();
+        assert!(json.contains("\"type\":\"start_session\""));
+
+        let deserialized: Request = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Request::StartSession { cmd, env } => {
+                assert_eq!(cmd, vec!["sway"]);
+                assert_eq!(env, vec!["WAYLAND_DISPLAY=wayland-1"]);
+            }
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn request_start_session_env_defaults_empty() {
+        let json = r#"{"type":"start_session","cmd":["sway"]}"#;
+        let deserialized: Request = serde_json::from_str(json).unwrap();
+        match deserialized {
+            Request::StartSession { cmd, env } => {
+                assert_eq!(cmd, vec!["sway"]);
+                assert!(env.is_empty());
+            }
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn request_cancel_session_serialization() {
+        let req = Request::CancelSession;
+        let json = serde_json::to_string(&req).unwrap();
+        assert!(json.contains("\"type\":\"cancel_session\""));
+
+        let deserialized: Request = serde_json::from_str(&json).unwrap();
+        assert!(matches!(deserialized, Request::CancelSession));
+    }
+
+    #[test]
+    fn response_success_serialization() {
+        let resp = Response::Success;
+        let json = serde_json::to_string(&resp).unwrap();
+        assert!(json.contains("\"type\":\"success\""));
+
+        let deserialized: Response = serde_json::from_str(&json).unwrap();
+        assert!(matches!(deserialized, Response::Success));
+    }
+
+    #[test]
+    fn response_error_serialization() {
+        let resp = Response::Error {
+            error_type: ErrorType::AuthError,
+            description: "bad password".to_string(),
+        };
+        let json = serde_json::to_string(&resp).unwrap();
+        assert!(json.contains("\"type\":\"error\""));
+        assert!(json.contains("\"error_type\":\"auth_error\""));
+
+        let deserialized: Response = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Response::Error {
+                error_type,
+                description,
+            } => {
+                assert!(matches!(error_type, ErrorType::AuthError));
+                assert_eq!(description, "bad password");
+            }
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn response_auth_message_serialization() {
+        let resp = Response::AuthMessage {
+            auth_message_type: AuthMessageType::Secret,
+            auth_message: "Password:".to_string(),
+        };
+        let json = serde_json::to_string(&resp).unwrap();
+        assert!(json.contains("\"type\":\"auth_message\""));
+        assert!(json.contains("\"auth_message_type\":\"secret\""));
+
+        let deserialized: Response = serde_json::from_str(&json).unwrap();
+        match deserialized {
+            Response::AuthMessage {
+                auth_message_type,
+                auth_message,
+            } => {
+                assert!(matches!(auth_message_type, AuthMessageType::Secret));
+                assert_eq!(auth_message, "Password:");
+            }
+            _ => panic!("wrong variant"),
+        }
+    }
+
+    #[test]
+    fn auth_message_types_serialization() {
+        for (variant, expected) in [
+            (AuthMessageType::Visible, "\"visible\""),
+            (AuthMessageType::Secret, "\"secret\""),
+            (AuthMessageType::Info, "\"info\""),
+            (AuthMessageType::Error, "\"error\""),
+        ] {
+            let json = serde_json::to_string(&variant).unwrap();
+            assert_eq!(json, expected);
+        }
+    }
+
+    #[test]
+    fn error_types_serialization() {
+        let json = serde_json::to_string(&ErrorType::Error).unwrap();
+        assert_eq!(json, "\"error\"");
+        let json = serde_json::to_string(&ErrorType::AuthError).unwrap();
+        assert_eq!(json, "\"auth_error\"");
+    }
+}
-- 
2.53.0

