From 0d588fb3d9e9a4b52189e21327c5469bafa2be51 Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Sun, 5 Jul 2026 22:30:15 +0200 Subject: [PATCH] test: cover all protocol roundtrips --- crates/geth-control/src/lib.rs | 670 +++++++++++++++++++++++++++ docs/production-readiness-roadmap.md | 8 +- 2 files changed, 674 insertions(+), 4 deletions(-) diff --git a/crates/geth-control/src/lib.rs b/crates/geth-control/src/lib.rs index f538d75..f565a30 100644 --- a/crates/geth-control/src/lib.rs +++ b/crates/geth-control/src/lib.rs @@ -1809,6 +1809,676 @@ pub fn decode_overlay_wire_response(line: &str) -> Result( + "ControlRequest", + CONTROL_REQUEST_VARIANTS, + decode_request, + encode_request, + ); + roundtrip_declared_variants::( + "ControlResponse", + CONTROL_RESPONSE_VARIANTS, + decode_response, + encode_response, + ); + roundtrip_declared_variants::( + "PeerControlRequest", + PEER_CONTROL_REQUEST_VARIANTS, + decode_peer_request, + encode_peer_request, + ); + roundtrip_declared_variants::( + "PeerControlResponse", + PEER_CONTROL_RESPONSE_VARIANTS, + decode_peer_response, + encode_peer_response, + ); + roundtrip_declared_variants::( + "PipeWireRequest", + PIPE_WIRE_REQUEST_VARIANTS, + decode_pipe_wire_request, + encode_pipe_wire_request, + ); + roundtrip_declared_variants::( + "PipeWireResponse", + PIPE_WIRE_RESPONSE_VARIANTS, + decode_pipe_wire_response, + encode_pipe_wire_response, + ); + roundtrip_declared_variants::( + "OverlayWireRequest", + OVERLAY_WIRE_REQUEST_VARIANTS, + decode_overlay_wire_request, + encode_overlay_wire_request, + ); + roundtrip_declared_variants::( + "OverlayWireResponse", + OVERLAY_WIRE_RESPONSE_VARIANTS, + decode_overlay_wire_response, + encode_overlay_wire_response, + ); + } + + fn roundtrip_declared_variants( + enum_name: &str, + expected_count: usize, + decode: fn(&str) -> Result, + encode: fn(&T) -> Result, + ) where + T: std::fmt::Debug + PartialEq, + { + let variants = declared_enum_variants(enum_name); + assert_eq!(variants.len(), expected_count, "{enum_name} variant count"); + + for variant in variants { + let sample = protocol_sample(&variant); + let encoded_sample = serde_json::to_string(&sample).expect("sample json"); + let decoded = decode(&encoded_sample).unwrap_or_else(|err| { + panic!("{enum_name}::{} sample decodes: {err}", variant.name) + }); + let encoded = encode(&decoded).unwrap_or_else(|err| { + panic!("{enum_name}::{} sample encodes: {err}", variant.name) + }); + let decoded_again = decode(&encoded).unwrap_or_else(|err| { + panic!( + "{enum_name}::{} encoded sample decodes: {err}", + variant.name + ) + }); + assert_eq!( + decoded_again, decoded, + "{enum_name}::{} roundtrips through line codec", + variant.name + ); + } + } + + #[derive(Debug)] + struct EnumVariant { + name: String, + body: VariantBody, + } + + #[derive(Debug)] + enum VariantBody { + Unit, + Tuple(String), + Struct(Vec<(String, String)>), + } + + fn declared_enum_variants(enum_name: &str) -> Vec { + let source = include_str!("lib.rs"); + let needle = format!("pub enum {enum_name}"); + let start = source.find(&needle).expect("enum exists"); + let after_name = &source[start + needle.len()..]; + let open = after_name.find('{').expect("enum opens"); + let block = enum_block(&after_name[open..]); + split_top_level(block) + .into_iter() + .filter_map(|decl| parse_variant_decl(decl.trim())) + .collect() + } + + fn enum_block(input: &str) -> &str { + let mut depth = 0_i32; + let mut start = None; + for (idx, ch) in input.char_indices() { + match ch { + '{' => { + depth += 1; + if start.is_none() { + start = Some(idx + 1); + } + } + '}' => { + depth -= 1; + if depth == 0 { + return &input[start.expect("block start")..idx]; + } + } + _ => {} + } + } + panic!("unterminated enum block"); + } + + fn split_top_level(input: &str) -> Vec<&str> { + let mut parts = Vec::new(); + let mut start = 0; + let mut brace_depth = 0_i32; + let mut paren_depth = 0_i32; + let mut angle_depth = 0_i32; + for (idx, ch) in input.char_indices() { + match ch { + '{' => brace_depth += 1, + '}' => brace_depth -= 1, + '(' => paren_depth += 1, + ')' => paren_depth -= 1, + '<' => angle_depth += 1, + '>' => angle_depth -= 1, + ',' if brace_depth == 0 && paren_depth == 0 && angle_depth == 0 => { + parts.push(&input[start..idx]); + start = idx + 1; + } + _ => {} + } + } + if start < input.len() { + parts.push(&input[start..]); + } + parts + } + + fn parse_variant_decl(decl: &str) -> Option { + let decl = decl.trim(); + if decl.is_empty() || decl.starts_with("#[") { + return None; + } + let name_len = decl + .find(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_')) + .unwrap_or(decl.len()); + let name = decl[..name_len].to_owned(); + let rest = decl[name_len..].trim(); + let body = if rest.starts_with('{') { + let fields = split_top_level(enum_block(rest)) + .into_iter() + .filter_map(|field| { + let (name, ty) = field.trim().split_once(':')?; + Some((name.trim().to_owned(), ty.trim().to_owned())) + }) + .collect(); + VariantBody::Struct(fields) + } else if let Some(tuple) = rest + .strip_prefix('(') + .and_then(|rest| rest.split(')').next()) + { + VariantBody::Tuple(tuple.trim().to_owned()) + } else { + VariantBody::Unit + }; + Some(EnumVariant { name, body }) + } + + fn protocol_sample(variant: &EnumVariant) -> Value { + let mut object = Map::new(); + object.insert("type".to_owned(), Value::String(kebab_case(&variant.name))); + match &variant.body { + VariantBody::Unit => {} + VariantBody::Tuple(ty) => { + let Value::Object(fields) = sample_for_type(ty) else { + panic!("tuple variant {} must use object sample", variant.name); + }; + object.extend(fields); + } + VariantBody::Struct(fields) => { + for (name, ty) in fields { + object.insert(name.clone(), sample_for_type(ty)); + } + } + } + Value::Object(object) + } + + fn kebab_case(name: &str) -> String { + let mut out = String::new(); + for (idx, ch) in name.chars().enumerate() { + if ch.is_ascii_uppercase() { + if idx > 0 { + out.push('-'); + } + out.push(ch.to_ascii_lowercase()); + } else { + out.push(ch); + } + } + out + } + + fn sample_for_type(ty: &str) -> Value { + let ty = compact_type(ty); + if let Some(inner) = ty + .strip_prefix("Option<") + .and_then(|ty| ty.strip_suffix('>')) + { + let _ = inner; + return Value::Null; + } + if ty.starts_with("Vec<") { + return json!([]); + } + if let Some(inner) = ty.strip_prefix("Box<").and_then(|ty| ty.strip_suffix('>')) { + return sample_for_type(inner); + } + + match ty.as_str() { + "String" | "PathBuf" => json!("sample"), + "bool" => json!(true), + "usize" | "u64" | "u32" | "u16" | "i64" => json!(1), + "BlobHash" => json!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + "StatusResponse" => json!({ + "home": "/tmp/geth-home", + "socket": "/tmp/geth.sock", + "agent_id": "agent:local", + "node_id": "node:local", + "daemon_started_at_ms": 1, + "daemon_uptime_seconds": 1, + "store_schema_version": 1, + "store_current_schema_version": 1, + "store_journal_mode": "wal", + "store_synchronous": "normal", + "store_status": "ok", + "store_note": "sample", + "iroh_enabled": true, + "endpoint_id": "endpoint:local", + "iroh_relay_mode": "default", + "iroh_local_discovery": true, + "iroh": "enabled", + "native_backends": [] + }), + "NodeIdResponse" => json!({ + "agent_id": "agent:local", + "node_id": "node:local", + "endpoint_id": "endpoint:local" + }), + "KeychainStatusResponse" => json!({ + "initialized": true, + "admin_keys": 1, + "signatures": 1, + "verified_signatures": 1, + "failed_signatures": 0, + "users": 1, + "devices": 1, + "nodes": 1 + }), + "KeychainFetchImportReport" => json!({ + "ops_imported": 1, + "signatures_imported": 1, + "invalid_ops_rejected": 0 + }), + "ResourceDescriptor" => json!({ + "id": "resource:sample", + "kind": "kv", + "name": "sample", + "authority": {"kind": "local"}, + "local_role": "owner", + "replication": "local-only", + "retention": "keep", + "status": "active" + }), + "PeerCard" => peer_card_json(), + "DiscoveredPeer" => json!({ + "card": peer_card_json(), + "discovered_at": 1, + "source": "manual", + "trust_state": "candidate-only" + }), + "OverlayPlan" => json!({ + "name": "home", + "resource": "resource:overlay:home", + "cidr": "172.22.0.0/24", + "alpn": "/geth/overlay/1", + "capabilities": [], + "discovery": "manual", + "runtime": "planned", + "security": [], + "implementation_notes": [] + }), + "OverlayJoinPlan" => json!({ + "plan": sample_for_type("OverlayPlan"), + "network": sample_for_type("OverlayNetworkStatus"), + "enabled": true, + "note": "sample" + }), + "OverlayNetworkStatus" => json!({ + "name": "home", + "resource": "resource:overlay:home", + "cidr": "172.22.0.0/24", + "state": "joined", + "virtual_ip": "172.22.0.2", + "peers": [], + "note": "sample" + }), + "OverlayInterfacePlan" => json!({ + "name": "home", + "platform": "linux", + "interface_name": "geth-home", + "cidr": "172.22.0.0/24", + "virtual_ip": "172.22.0.2", + "requires_privileges": true, + "commands": [], + "notes": [] + }), + "OverlayRuntimeStatus" => json!({ + "name": "home", + "interface_name": "geth-home", + "virtual_ip": "172.22.0.2", + "cidr": "172.22.0.0/24", + "mtu": 1280, + "started_at_ms": 1, + "packets_from_tun": 0, + "packets_to_tun": 0, + "packets_to_peers": 0, + "last_error": null, + "note": "sample" + }), + "OverlayPacket" => json!({ + "id": "overlay-packet:sample", + "network": "home", + "source_node": "node:local", + "destination_node": "node:peer", + "packet_base64": "AA==", + "size_bytes": 1, + "received_at_ms": 1, + "note": "sample" + }), + "FileRoot" => json!({ + "id": "file-root:sample", + "resource": "resource:cas-root:sample", + "name": "sample", + "path": "/tmp/sample", + "latest_tree": null, + "updated_at_ms": 1 + }), + "FileRootScan" => json!({ + "root": sample_for_type("FileRoot"), + "tree": { + "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "size_bytes": 1 + }, + "changes": [], + "note": "sample" + }), + "FileConflict" => json!({ + "id": "file-conflict:sample", + "root": "sample", + "resource": "resource:cas-root:sample", + "path": "file.txt", + "kind": "concurrent-edit", + "status": "open", + "base_tree": null, + "local_tree": null, + "remote_tree": null, + "detail": "sample", + "resolution": null, + "resolution_note": null, + "created_at_ms": 1, + "resolved_at_ms": null + }), + "KeychainOp" => json!({ + "id": "auth-op:sample", + "created_at": 1, + "kind": {"kind": "keychain-init"} + }), + "KeychainOpSignature" => json!({ + "op_id": "auth-op:sample", + "signer": "key:sample", + "signer_public_key": "public", + "namespace": "geth.keychain.v1@geth.local", + "signature": [1], + "created_at": 1 + }), + "KeychainAllowedSigner" => json!({ + "key": "key:sample", + "principal": "admin", + "public_key": "public", + "valid_after_ms": null, + "valid_before_ms": null + }), + "KeychainSigchainReport" => json!({ + "ops": 1, + "signatures": 1, + "accepted_ops": 1, + "rejected_ops": 0, + "active_admin_keys": 1, + "accepted_head": null, + "note": "sample" + }), + "KeychainSigchainEntry" => json!({ + "op": sample_for_type("KeychainOp"), + "signatures": [] + }), + "KeychainCheckpoint" => json!({ + "version": 1, + "profile": { + "keychain_signature_namespace": "geth.keychain.v1@geth.local", + "node_enrollment_request_namespace": "geth.node-enrollment-request.v1@geth.local", + "default_admin_principal": "geth-admin" + }, + "base_url": "https://example.invalid", + "head": null, + "ops": 1, + "signatures": 1, + "sigchain_bytes": 1, + "sigchain_hash": "hash", + "allowed_signers_hash": "hash", + "reduced_view_hash": "hash", + "generated_at": 1 + }), + "AuthExplanation" => json!({ + "subject": "node:peer", + "resource": "resource:sample", + "capability": "sample.read", + "allowed": true, + "reason": "sample", + "evaluated_ops": 1, + "diagnostics": [] + }), + "AuthOp" => json!({ + "id": "auth-op:sample", + "resource": "resource:sample", + "created_at": 1, + "kind": {"kind": "resource-create"} + }), + "AuthOpSignature" => json!({ + "op_id": "auth-op:sample", + "signer": "key:sample", + "signer_public_key": "public", + "namespace": "geth.auth.v1@geth.local", + "signature": [1], + "created_at": 1 + }), + "NodeRecord" => json!({ + "id": "node:peer", + "device": "device:peer", + "name": "peer", + "endpoints": [] + }), + "NodeEnrollmentRequest" => json!({ + "id": "auth-op:enroll", + "requester_node": "node:peer", + "requester_agent": "agent:peer", + "requester_agent_public_key": "public", + "requested_node_name": "peer", + "requested_capabilities": [], + "endpoint_id": null, + "reason": null, + "status": "pending", + "created_at": 1, + "provenance": null + }), + "ResourceMasterSecret" => json!({ + "id": "secret:sample", + "resource": "resource:sample", + "epoch": 1, + "created_at": 1 + }), + "BearerAccess" => json!({ + "secret": "bearer:sample", + "token": null, + "token_hash": "hash", + "resource": "resource:sample", + "capabilities": ["sample.read"], + "expires_at": null, + "may_delegate": false + }), + "BearerChallenge" => json!({ + "resource": "resource:sample", + "capabilities": ["sample.read"], + "nonce": "nonce", + "issued_at": 1 + }), + "BearerProof" => json!({ + "secret": "bearer:sample", + "resource": "resource:sample", + "capabilities": ["sample.read"], + "nonce": "nonce", + "response": "response" + }), + "SshCertRequest" => json!({ + "id": "ssh-cert-request:sample", + "requester_node": "node:peer", + "public_key": "ssh-ed25519 AAAA sample", + "public_key_fingerprint": "SHA256:sample", + "cert_kind": "user", + "principals": [], + "requested_validity": null, + "renewal_of": null, + "reason": null, + "status": "pending", + "created_at": 1, + "provenance": null + }), + "SshCertApproval" => json!({ + "request_id": "ssh-cert-request:sample", + "approved_by_node": "node:local", + "ca_key_path": "/tmp/ca", + "key_id": "geth", + "valid_for": "+1h", + "serial": null, + "output_path": null, + "signing_command": [], + "signed": false, + "certificate_id": null, + "note": "sample" + }), + "SshCertificateRecord" => json!({ + "id": "ssh-cert:sample", + "request_id": "ssh-cert-request:sample", + "certificate": "cert", + "certificate_fingerprint": "SHA256:sample", + "imported_at": 1, + "provenance": null + }), + "SshRevocationEntry" => json!({ + "id": "ssh-revocation:sample", + "kind": "public-key", + "target": "ssh-ed25519 AAAA sample", + "reason": null, + "created_at": 1, + "published": false, + "provenance": null + }), + "DbResource" => json!({ + "id": "db:sample", + "resource": "resource:db:sample", + "name": "sample", + "path": "/tmp/sample.sqlite", + "path_exists": true, + "size_bytes": 1, + "schema_metadata": "schema", + "crsqlite_changes": { + "available": false, + "change_count": null, + "max_db_version": null, + "columns": [], + "error": null + }, + "sync_status": "local" + }), + "CrSqliteChangeBatch" => json!({ + "schema_metadata": "schema", + "max_db_version": null, + "changes": [] + }), + "KvResource" => json!({ + "id": "kv:sample", + "resource": "resource:kv:sample", + "name": "sample", + "sync_status": "local" + }), + "KvEntry" => json!({ + "store": "kv:sample", + "key": "key", + "value": "value" + }), + "DocumentResource" => json!({ + "id": "document:sample", + "resource": "resource:document:sample", + "name": "sample", + "sync_status": "local", + "state_bytes": 2 + }), + "DocumentState" => json!({ + "document": sample_for_type("DocumentResource"), + "state_json": "{}", + "updated_at": 1 + }), + "PubsubMessage" => json!({ + "topic": "topic:sample", + "message": "sample", + "published_at": 1 + }), + "PipeListener" => json!({ + "id": "pipe:sample", + "name": "sample", + "listened_at": 1, + "note": "sample" + }), + "PipeConnection" => json!({ + "target": "sample", + "connected_at": 1, + "local_listener_found": true, + "note": "sample" + }), + "PipeMessage" => json!({ + "pipe": "sample", + "data_base64": "AA==", + "received_at": 1, + "source_node": null, + "note": "sample" + }), + "SshProxyConnection" => json!({ + "target_node": "node:peer", + "connected_at": 1, + "local_sshd_target": "127.0.0.1:22", + "admin_shell_available": false, + "note": "sample" + }), + other => panic!("missing protocol sample for type {other}"), + } + } + + fn compact_type(ty: &str) -> String { + ty.chars().filter(|ch| !ch.is_whitespace()).collect() + } + + fn peer_card_json() -> Value { + json!({ + "node_id": "node:peer", + "agent_id": "agent:peer", + "endpoints": [], + "issued_at": 1, + "signature": { + "namespace": "geth.peer-card.v1@geth.local", + "signer": "agent:peer", + "public_key": "public", + "signature": "signature" + } + }) + } #[test] fn control_request_response_serialization_roundtrip() { diff --git a/docs/production-readiness-roadmap.md b/docs/production-readiness-roadmap.md index 5cfbce1..19ebeec 100644 --- a/docs/production-readiness-roadmap.md +++ b/docs/production-readiness-roadmap.md @@ -101,10 +101,10 @@ and downstream projects. failures. - `[x]` Fixture updates require intentional review. -- `[~]` Expand protocol roundtrip tests. +- `[x]` Expand protocol roundtrip tests. Acceptance criteria: - - `[ ]` Every `ControlRequest` and `ControlResponse` variant roundtrips. - - `[ ]` Every `PeerControlRequest` and `PeerControlResponse` variant + - `[x]` Every `ControlRequest` and `ControlResponse` variant roundtrips. + - `[x]` Every `PeerControlRequest` and `PeerControlResponse` variant roundtrips. - `[x]` Pipe and overlay wire protocol variants roundtrip. - `[x]` Unknown or malformed protocol inputs fail safely. @@ -338,7 +338,7 @@ Goal: prove the system works as an actual base layer before broader use. 1. `[x]` Finish Phase 0. 2. `[~]` Refactor `geth-node` into daemon subsystems. -3. `[ ]` Add stable contract and golden JSON tests. +3. `[x]` Add stable contract and golden JSON tests. 4. `[ ]` Harden store migrations and backup. 5. `[ ]` Complete security-boundary test coverage. 6. `[ ]` Replace prototype private CAS cryptography.