Skip to main content

shape_vm/
remote.rs

1//! Per-function remote execution support
2//!
3//! This module provides the types and executor for transferring function
4//! execution to another machine. The design sends the full compiled
5//! `BytecodeProgram` + a "call this function with these args" message,
6//! running it on a full Shape VM on the remote side.
7//!
8//! # Architecture
9//!
10//! ```text
11//! Layer 4: @remote / @distributed annotations    (Shape stdlib — user-defined policy)
12//! Layer 3: RemoteCallRequest/Response            (this module)
13//! Layer 2: shape-wire codec (MessagePack)        (encode_message / decode_message)
14//! Layer 1: Transport (TCP/QUIC/Unix socket)      (user-provided, pluggable)
15//! ```
16//!
17//! Layer 0 (the foundation): Full Shape VM on both sides, same `BytecodeProgram`,
18//! same `Executor`.
19//!
20//! # Closure semantics
21//!
22//! Upvalues (SharedCell-backed shared captures or Phase D typed-pointer
23//! captures alike) become **value copies** on serialization. If the remote
24//! side mutates a captured variable, the sender doesn't see it. This is the
25//! correct semantic for distributed computing — a **send-copy** model.
26
27use serde::{Deserialize, Serialize};
28use shape_runtime::snapshot::{SerializableVMValue, SnapshotStore};
29use shape_runtime::type_schema::TypeSchemaRegistry;
30
31use shape_wire::WireValue;
32
33use crate::bytecode::{BytecodeProgram, FunctionBlob, FunctionHash, Program};
34
35// `execute_inner` / `execute_inner_with_runtimes` previously called
36// `VirtualMachine::new` / `load_program` / `populate_module_objects` /
37// `execute_*` / and round-tripped each argument and return value through
38// `serializable_to_nanboxed_with_layouts` / `nanboxed_to_serializable`.
39// Both round-trip helpers are deleted (see `crates/shape-runtime/src/snapshot.rs:649`
40// "The slot-(de)serialization functions ... were deleted in Phase 2b") and
41// their replacement is a kind-threaded `slot_to_serializable(bits, kind, store)`
42// pair scheduled for the Phase-2c snapshot rebuild (ADR-006 §2.7.4). The
43// execute paths are stubbed at the entry below; the rebuild lands the
44// kind-threaded serializer + a `Vec<KindedSlot>` arg pipeline together.
45//
46// Imports `VMConfig` / `VirtualMachine` are pulled in lazily inside
47// `execute_remote_call*` so the file still compiles when those are the
48// only consumers and the `execute_inner*` bodies are stubbed.
49
50/// Request to execute a function on a remote VM.
51///
52/// Contains everything needed to call a function: the full compiled program
53/// (cacheable by `program_hash`), function identity, serialized arguments,
54/// and optional closure captures.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RemoteCallRequest {
57    /// The full compiled program. After the first transfer, the remote
58    /// side caches by `program_hash` and subsequent calls only need args.
59    pub program: BytecodeProgram,
60
61    /// Function to call by name (for named functions).
62    pub function_name: String,
63
64    /// Function to call by ID (for closures that have no user-facing name).
65    /// Takes precedence over `function_name` when `Some`.
66    pub function_id: Option<u16>,
67
68    /// Function to call by content hash (canonical identity).
69    ///
70    /// Preferred over name-based lookup when present. This avoids ambiguity
71    /// when multiple modules define functions with the same name.
72    #[serde(default)]
73    pub function_hash: Option<FunctionHash>,
74
75    /// Serialized arguments to the function.
76    pub arguments: Vec<SerializableVMValue>,
77
78    /// Closure upvalues, if calling a closure. These are value-copied from
79    /// the sender's upvalue slots regardless of the local storage class
80    /// (SharedCell, typed frame-pointer capture, or inline scalar).
81    pub upvalues: Option<Vec<SerializableVMValue>>,
82
83    /// Type schema registry — sent separately because `BytecodeProgram`
84    /// has `#[serde(skip)]` on its registry (it's populated at compile time).
85    pub type_schemas: TypeSchemaRegistry,
86
87    /// Content hash of the program for caching. If the remote side has
88    /// already seen this hash, it can skip deserializing the program.
89    pub program_hash: [u8; 32],
90
91    /// Minimal content-addressed blobs for the called function and its
92    /// transitive dependencies. When present, the callee can reconstruct
93    /// a `Program` from these blobs instead of deserializing the full
94    /// `BytecodeProgram`, dramatically reducing payload size.
95    #[serde(default)]
96    pub function_blobs: Option<Vec<(FunctionHash, FunctionBlob)>>,
97}
98
99/// Response from a remote function execution.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RemoteCallResponse {
102    /// The function's return value, or an error message.
103    pub result: Result<SerializableVMValue, RemoteCallError>,
104}
105
106/// Error from remote execution.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RemoteCallError {
109    /// Human-readable error message.
110    pub message: String,
111    /// Optional error kind for programmatic handling.
112    pub kind: RemoteErrorKind,
113}
114
115/// Classification of remote execution errors.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum RemoteErrorKind {
118    /// Function not found in the program.
119    FunctionNotFound,
120    /// Argument deserialization failed.
121    ArgumentError,
122    /// Runtime error during execution.
123    RuntimeError,
124    /// Module function required on the remote side is missing.
125    MissingModuleFunction,
126}
127
128impl std::fmt::Display for RemoteCallError {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(f, "{:?}: {}", self.kind, self.message)
131    }
132}
133
134impl std::error::Error for RemoteCallError {}
135
136// ---------------------------------------------------------------------------
137// Wire message envelope (Phase 2: blob negotiation)
138// ---------------------------------------------------------------------------
139
140/// Envelope for all wire protocol messages.
141///
142/// Wraps the existing `RemoteCallRequest`/`RemoteCallResponse` with negotiation
143/// and sidecar message types for bandwidth optimization on persistent connections.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub enum WireMessage {
146    /// Offer function blob hashes to check what the remote already has.
147    BlobNegotiation(BlobNegotiationRequest),
148    /// Reply with the subset of offered hashes that are already cached.
149    BlobNegotiationReply(BlobNegotiationResponse),
150    /// A remote function call (may have blobs stripped if negotiation occurred).
151    Call(RemoteCallRequest),
152    /// Response to a remote function call.
153    CallResponse(RemoteCallResponse),
154    /// A large blob sent as a separate message before the call (Phase 3).
155    Sidecar(BlobSidecar),
156
157    // --- Execution server messages (V2) ---
158    /// Execute Shape source code on the server.
159    Execute(ExecuteRequest),
160    /// Response to an Execute request.
161    ExecuteResponse(ExecuteResponse),
162    /// Validate Shape source code (parse + type-check) without executing.
163    Validate(ValidateRequest),
164    /// Response to a Validate request.
165    ValidateResponse(ValidateResponse),
166    /// Authenticate with the server (required for non-localhost).
167    Auth(AuthRequest),
168    /// Response to an Auth request.
169    AuthResponse(AuthResponse),
170    /// Execute a Shape file on the server.
171    ExecuteFile(ExecuteFileRequest),
172    /// Execute a Shape project (shape.toml) on the server.
173    ExecuteProject(ExecuteProjectRequest),
174    /// Validate a Shape file or project (parse + type-check) without executing.
175    ValidatePath(ValidatePathRequest),
176    /// Ping the server for liveness / capability discovery.
177    Ping(PingRequest),
178    /// Pong reply with server info.
179    Pong(ServerInfo),
180}
181
182/// Ping request (empty payload for wire format consistency).
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct PingRequest {}
185
186/// Request to check which function blobs the remote side already has cached.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct BlobNegotiationRequest {
189    /// Content hashes of function blobs the caller wants to send.
190    pub offered_hashes: Vec<FunctionHash>,
191}
192
193/// Response indicating which offered blobs are already cached on the remote side.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct BlobNegotiationResponse {
196    /// Subset of offered hashes that the remote already has in its blob cache.
197    pub known_hashes: Vec<FunctionHash>,
198}
199
200/// A large binary payload sent as a separate message before the call request.
201///
202/// Used for splitting large BlobRef-backed values (DataTables, TypedArrays, etc.)
203/// out of the main serialized payload.
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct BlobSidecar {
206    pub sidecar_id: u32,
207    pub data: Vec<u8>,
208}
209
210// ---------------------------------------------------------------------------
211// Execution server message types (V2)
212// ---------------------------------------------------------------------------
213
214/// Request to execute Shape source code on the server.
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ExecuteRequest {
217    /// Shape source code to execute.
218    pub code: String,
219    /// Client-assigned request ID for correlation.
220    pub request_id: u64,
221}
222
223/// Response from executing Shape source code.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ExecuteResponse {
226    /// The request ID this response corresponds to.
227    pub request_id: u64,
228    /// Whether execution completed successfully.
229    pub success: bool,
230    /// Structured return value from execution.
231    pub value: WireValue,
232    /// Print/log output captured during execution (NOT the return value).
233    pub stdout: Option<String>,
234    /// Error message (if execution failed).
235    pub error: Option<String>,
236    /// Pre-rendered Content terminal representation (if value is Content).
237    #[serde(skip_serializing_if = "Option::is_none", default)]
238    pub content_terminal: Option<String>,
239    /// Pre-rendered Content HTML representation (if value is Content).
240    #[serde(skip_serializing_if = "Option::is_none", default)]
241    pub content_html: Option<String>,
242    /// Diagnostics (parse errors, type errors, warnings).
243    pub diagnostics: Vec<WireDiagnostic>,
244    /// Execution metrics (if available).
245    pub metrics: Option<ExecutionMetrics>,
246    /// Structured print output with rendered strings (MsgPack-serialized).
247    #[serde(skip_serializing_if = "Option::is_none", default)]
248    pub print_output: Option<Vec<shape_wire::print_result::WirePrintResult>>,
249}
250
251/// Request to validate Shape source code without executing it.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct ValidateRequest {
254    /// Shape source code to validate.
255    pub code: String,
256    /// Client-assigned request ID for correlation.
257    pub request_id: u64,
258}
259
260/// Response from validating Shape source code.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ValidateResponse {
263    /// The request ID this response corresponds to.
264    pub request_id: u64,
265    /// Whether the code is valid (no errors).
266    pub success: bool,
267    /// Diagnostics (parse errors, type errors, warnings).
268    pub diagnostics: Vec<WireDiagnostic>,
269}
270
271/// Request to execute a Shape file on the server.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ExecuteFileRequest {
274    /// Absolute path to the .shape file.
275    pub path: String,
276    /// Optional working directory (defaults to file's parent).
277    pub cwd: Option<String>,
278    /// Client-assigned request ID for correlation.
279    pub request_id: u64,
280}
281
282/// Request to execute a Shape project (shape.toml) on the server.
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct ExecuteProjectRequest {
285    /// Absolute path to the project directory (must contain shape.toml).
286    pub project_dir: String,
287    /// Client-assigned request ID for correlation.
288    pub request_id: u64,
289}
290
291/// Request to validate a Shape file or project without executing.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ValidatePathRequest {
294    /// Path to a .shape file or a project directory (containing shape.toml).
295    pub path: String,
296    /// Client-assigned request ID for correlation.
297    pub request_id: u64,
298}
299
300/// Authentication request for non-localhost connections.
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct AuthRequest {
303    /// Bearer token for authentication.
304    pub token: String,
305}
306
307/// Authentication response.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct AuthResponse {
310    /// Whether authentication succeeded.
311    pub authenticated: bool,
312    /// Error message if authentication failed.
313    pub error: Option<String>,
314}
315
316/// Server information returned in Pong responses.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ServerInfo {
319    /// Shape language version.
320    pub shape_version: String,
321    /// Wire protocol version.
322    pub wire_protocol: u32,
323    /// Server capabilities (e.g., "execute", "validate", "call", "blob-negotiation").
324    pub capabilities: Vec<String>,
325}
326
327/// A diagnostic message (error, warning, info).
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct WireDiagnostic {
330    /// Severity: "error", "warning", "info".
331    pub severity: String,
332    /// Human-readable diagnostic message.
333    pub message: String,
334    /// Source line number (1-indexed), if available.
335    pub line: Option<u32>,
336    /// Source column number (1-indexed), if available.
337    pub column: Option<u32>,
338}
339
340/// Execution performance metrics.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct ExecutionMetrics {
343    /// Number of VM instructions executed.
344    pub instructions_executed: u64,
345    /// Wall-clock time in milliseconds.
346    pub wall_time_ms: u64,
347    /// Peak memory usage in bytes.
348    pub memory_bytes_peak: u64,
349}
350
351// ---------------------------------------------------------------------------
352// Per-connection blob cache (Phase 2)
353// ---------------------------------------------------------------------------
354
355/// Per-connection cache of function blobs received from a remote peer.
356///
357/// Content hashes make stale entries harmless (same hash = same content),
358/// so no invalidation protocol is needed. LRU eviction bounds memory usage.
359pub struct RemoteBlobCache {
360    blobs: std::collections::HashMap<FunctionHash, FunctionBlob>,
361    /// Access order for LRU eviction (most recently used at the end).
362    order: Vec<FunctionHash>,
363    /// Maximum number of entries before LRU eviction kicks in.
364    max_entries: usize,
365}
366
367impl RemoteBlobCache {
368    /// Create a new blob cache with the given capacity.
369    pub fn new(max_entries: usize) -> Self {
370        Self {
371            blobs: std::collections::HashMap::new(),
372            order: Vec::new(),
373            max_entries,
374        }
375    }
376
377    /// Default cache with 4096 entry capacity.
378    pub fn default_cache() -> Self {
379        Self::new(4096)
380    }
381
382    /// Insert a blob, evicting the least recently used entry if at capacity.
383    pub fn insert(&mut self, hash: FunctionHash, blob: FunctionBlob) {
384        if self.blobs.contains_key(&hash) {
385            // Move to end (most recently used)
386            self.order.retain(|h| h != &hash);
387            self.order.push(hash);
388            return;
389        }
390
391        // Evict LRU if at capacity
392        while self.blobs.len() >= self.max_entries && !self.order.is_empty() {
393            let evicted = self.order.remove(0);
394            self.blobs.remove(&evicted);
395        }
396
397        self.blobs.insert(hash, blob);
398        self.order.push(hash);
399    }
400
401    /// Look up a cached blob by hash, updating access order.
402    pub fn get(&mut self, hash: &FunctionHash) -> Option<&FunctionBlob> {
403        if self.blobs.contains_key(hash) {
404            self.order.retain(|h| h != hash);
405            self.order.push(*hash);
406            self.blobs.get(hash)
407        } else {
408            None
409        }
410    }
411
412    /// Check if a hash is cached without updating access order.
413    pub fn contains(&self, hash: &FunctionHash) -> bool {
414        self.blobs.contains_key(hash)
415    }
416
417    /// Return all cached hashes.
418    pub fn known_hashes(&self) -> Vec<FunctionHash> {
419        self.blobs.keys().copied().collect()
420    }
421
422    /// Return the subset of `offered` hashes that are in the cache.
423    pub fn filter_known(&self, offered: &[FunctionHash]) -> Vec<FunctionHash> {
424        offered
425            .iter()
426            .filter(|h| self.blobs.contains_key(h))
427            .copied()
428            .collect()
429    }
430
431    /// Number of cached entries.
432    pub fn len(&self) -> usize {
433        self.blobs.len()
434    }
435
436    /// Whether the cache is empty.
437    pub fn is_empty(&self) -> bool {
438        self.blobs.is_empty()
439    }
440
441    /// Insert all blobs from a set, typically received from a remote call.
442    pub fn insert_blobs(&mut self, blobs: &[(FunctionHash, FunctionBlob)]) {
443        for (hash, blob) in blobs {
444            self.insert(*hash, blob.clone());
445        }
446    }
447}
448
449/// Build a minimal set of function blobs for a function hash and its
450/// transitive dependencies from a content-addressed `Program`.
451///
452/// Returns `None` if the program has no content-addressed representation
453/// or the entry hash is not present in the function store.
454pub fn build_minimal_blobs_by_hash(
455    program: &BytecodeProgram,
456    entry_hash: FunctionHash,
457) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
458    let ca = program.content_addressed.as_ref()?;
459    if !ca.function_store.contains_key(&entry_hash) {
460        return None;
461    }
462
463    // Compute transitive closure of dependencies
464    let mut needed: std::collections::HashSet<FunctionHash> = std::collections::HashSet::new();
465    let mut queue = vec![entry_hash];
466    while let Some(hash) = queue.pop() {
467        if needed.insert(hash) {
468            if let Some(blob) = ca.function_store.get(&hash) {
469                for dep in &blob.dependencies {
470                    if !needed.contains(dep) {
471                        queue.push(*dep);
472                    }
473                }
474            }
475        }
476    }
477
478    // Collect the minimal blob set
479    let blobs: Vec<(FunctionHash, FunctionBlob)> = needed
480        .into_iter()
481        .filter_map(|hash| {
482            ca.function_store
483                .get(&hash)
484                .map(|blob| (hash, blob.clone()))
485        })
486        .collect();
487
488    Some(blobs)
489}
490
491/// Backwards-compatible name-based wrapper around `build_minimal_blobs_by_hash`.
492///
493/// If multiple blobs share the same name, this returns `None` to avoid
494/// ambiguous, potentially incorrect dependency selection.
495pub fn build_minimal_blobs(
496    program: &BytecodeProgram,
497    fn_name: &str,
498) -> Option<Vec<(FunctionHash, FunctionBlob)>> {
499    let ca = program.content_addressed.as_ref()?;
500    let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
501        if blob.name == fn_name {
502            Some(*hash)
503        } else {
504            None
505        }
506    });
507    let first = matches.next()?;
508    if matches.next().is_some() {
509        return None;
510    }
511    build_minimal_blobs_by_hash(program, first)
512}
513
514/// Build a minimal `Program` from function blobs and an explicit entry hash.
515///
516/// Used on the callee side to reconstruct a `Program` from blobs received in
517/// a `RemoteCallRequest`.
518pub fn program_from_blobs_by_hash(
519    blobs: Vec<(FunctionHash, FunctionBlob)>,
520    entry_hash: FunctionHash,
521    source: &BytecodeProgram,
522) -> Option<Program> {
523    let function_store: std::collections::HashMap<FunctionHash, FunctionBlob> =
524        blobs.into_iter().collect();
525    if !function_store.contains_key(&entry_hash) {
526        return None;
527    }
528
529    Some(Program {
530        entry: entry_hash,
531        function_store,
532        top_level_locals_count: source.top_level_locals_count,
533        top_level_local_storage_hints: source.top_level_local_storage_hints.clone(),
534        module_binding_names: source.module_binding_names.clone(),
535        module_binding_storage_hints: source.module_binding_storage_hints.clone(),
536        function_local_storage_hints: source.function_local_storage_hints.clone(),
537        top_level_frame: source.top_level_frame.clone(),
538        top_level_local_concrete_types: source.top_level_local_concrete_types.clone(),
539        function_local_concrete_types: source.function_local_concrete_types.clone(),
540        function_return_concrete_types: source.function_return_concrete_types.clone(),
541        monomorphized_method_call_sites: source.monomorphized_method_call_sites.clone(),
542        value_call_return_concrete_types:
543            source.value_call_return_concrete_types.clone(),
544        operator_trait_dispatch_sites:
545            source.operator_trait_dispatch_sites.clone(),
546        data_schema: source.data_schema.clone(),
547        type_schema_registry: source.type_schema_registry.clone(),
548        trait_method_symbols: source.trait_method_symbols.clone(),
549        foreign_functions: source.foreign_functions.clone(),
550        native_struct_layouts: source.native_struct_layouts.clone(),
551        debug_info: source.debug_info.clone(),
552        // Closure spec §14.6 (H6.5): propagate the per-name layout side-
553        // table. Remote-stream origins that lack the side-table fail
554        // hard at the VM producer; there is no legacy fallback.
555        closure_function_layouts_by_name: source
556            .content_addressed
557            .as_ref()
558            .map(|ca| ca.closure_function_layouts_by_name.clone())
559            .unwrap_or_default(),
560        // ADR-006 §2.7.24 Q25.C: propagate trait-object vtables from
561        // the source BytecodeProgram so remote-streamed programs can
562        // dispatch dyn method calls.
563        trait_vtables: source.trait_vtables.clone(),
564        // R8 W8 Cluster A surface-and-stop flag propagation.
565        has_imported_const_inline: source.has_imported_const_inline,
566        // R8 W9 B1 W17-marshal-return surface-and-stop flag propagation.
567        has_w17_marshal_residual: source.has_w17_marshal_residual,
568    })
569}
570
571/// Backwards-compatible name-based wrapper around `program_from_blobs_by_hash`.
572pub fn program_from_blobs(
573    blobs: Vec<(FunctionHash, FunctionBlob)>,
574    fn_name: &str,
575    source: &BytecodeProgram,
576) -> Option<Program> {
577    let mut matches = blobs.iter().filter_map(|(hash, blob)| {
578        if blob.name == fn_name {
579            Some(*hash)
580        } else {
581            None
582        }
583    });
584    let entry = matches.next()?;
585    if matches.next().is_some() {
586        return None;
587    }
588    program_from_blobs_by_hash(blobs, entry, source)
589}
590
591/// Execute a remote call request on this machine.
592///
593/// This is the entry point for the receiving side. It:
594/// 1. Reconstructs the `BytecodeProgram` and populates its `TypeSchemaRegistry`
595/// 2. Creates a full `VirtualMachine` with the program
596/// 3. Materializes serialized arguments as kinded VM slots
597/// 4. Calls the function by name or ID
598/// 5. Converts the result back to `SerializableVMValue`
599///
600/// The `store` is used for `SerializableVMValue` ↔ slot conversion
601/// (needed for `BlobRef`-backed values like DataTable).
602///
603/// Phase-2c deferral: the slot/serializable round-trip is currently
604/// stubbed (see `execute_inner` body). The dispatch entry continues to
605/// exist so callers compile through the deferral; invocation surfaces
606/// the gap at runtime.
607pub fn execute_remote_call(
608    request: RemoteCallRequest,
609    store: &SnapshotStore,
610) -> RemoteCallResponse {
611    match execute_inner(request, store) {
612        Ok(value) => RemoteCallResponse { result: Ok(value) },
613        Err(err) => RemoteCallResponse { result: Err(err) },
614    }
615}
616
617/// Execute a remote call with pre-loaded language runtime extensions.
618///
619/// `language_runtimes` maps language IDs (e.g. "python") to pre-loaded
620/// runtime handles. The server loads these once at startup from installed
621/// extensions. The bytecode carries foreign function source text; the
622/// runtime on the server compiles and executes it.
623pub fn execute_remote_call_with_runtimes(
624    request: RemoteCallRequest,
625    store: &SnapshotStore,
626    language_runtimes: &std::collections::HashMap<
627        String,
628        std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
629    >,
630) -> RemoteCallResponse {
631    match execute_inner_with_runtimes(request, store, language_runtimes) {
632        Ok(value) => RemoteCallResponse { result: Ok(value) },
633        Err(err) => RemoteCallResponse { result: Err(err) },
634    }
635}
636
637fn execute_inner(
638    request: RemoteCallRequest,
639    store: &SnapshotStore,
640) -> Result<SerializableVMValue, RemoteCallError> {
641    // T1-host-tier-marshal-rebuild (ADR-006 §2.7.4, R8 2026-05-23):
642    // kind-threaded marshal protocol via
643    // `shape_runtime::snapshot::serializable_to_slot` (in) +
644    // `shape_runtime::snapshot::slot_to_serializable` (out). Each arg's
645    // expected kind is read from the callee's `frame_descriptor.slots`
646    // (i.e. the per-slot proven `NativeKind` per ADR-006 §2.7.5.1 — no
647    // `Unknown` placeholder). The return-kind is read from the callee's
648    // `frame_descriptor.return_kind`.
649    //
650    // Per the §0.A.iv supervisor ruling, frame-descriptor absence
651    // produces a structured `RemoteCallError` (no silent-degrade): a
652    // remote call cannot proceed if the callee has no proven param
653    // kinds, because the marshal protocol cannot pick an in-arm.
654    run_remote_call(request, store, None)
655}
656
657fn execute_inner_with_runtimes(
658    request: RemoteCallRequest,
659    store: &SnapshotStore,
660    language_runtimes: &std::collections::HashMap<
661        String,
662        std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
663    >,
664) -> Result<SerializableVMValue, RemoteCallError> {
665    // Same path as `execute_inner` plus the foreign-function language-
666    // runtime hookup. T1-host-tier-marshal-rebuild covers the marshal
667    // protocol; the language-runtime registration is forwarded through
668    // `run_remote_call` so the VM picks up the runtimes before invoking
669    // the callee.
670    run_remote_call(request, store, Some(language_runtimes))
671}
672
673/// Shared marshal+dispatch core for `execute_inner` /
674/// `execute_inner_with_runtimes`. Per ADR-006 §2.7.4 the protocol is:
675///
676/// 1. Reconstruct the `BytecodeProgram` (full payload or from blobs).
677/// 2. Build a `VirtualMachine`, load the program, populate module objects.
678/// 3. Resolve the callee (hash → id → name precedence).
679/// 4. Read the callee's per-slot `NativeKind` from `frame_descriptor.slots`
680///    and the return kind from `frame_descriptor.return_kind`.
681/// 5. Materialize each `SerializableVMValue` arg into a `KindedSlot` via
682///    `serializable_to_slot(arg, expected_kind, store)`.
683/// 6. Invoke the callee through the kinded ABI (`execute_function_by_id`).
684/// 7. Project the returned `KindedSlot` to `SerializableVMValue` via
685///    `slot_to_serializable(bits, kind, store)`.
686///
687/// Closure upvalue marshal (`request.upvalues` and `execute_closure`) is
688/// NOT covered by T1: the per-capture kind track lives on the closure
689/// header (ADR-006 §2.7.8 / Q10 cell-storage parallel-kind), which is
690/// rebuilt in a downstream sub-cluster. The body surfaces this with a
691/// structured error rather than silently dispatching.
692fn run_remote_call(
693    request: RemoteCallRequest,
694    store: &SnapshotStore,
695    language_runtimes: Option<
696        &std::collections::HashMap<
697            String,
698            std::sync::Arc<shape_runtime::plugins::language_runtime::PluginLanguageRuntime>,
699        >,
700    >,
701) -> Result<SerializableVMValue, RemoteCallError> {
702    use crate::executor::{VMConfig, VirtualMachine};
703    use shape_runtime::snapshot::{serializable_to_slot, slot_to_serializable};
704    use shape_value::{KindedSlot, ValueSlot};
705
706    // Forward the language_runtimes registration through the VM hookup
707    // path. The current VM API does not expose a register-language-
708    // runtime entry-point; consumers that need foreign-function support
709    // through the remote-call boundary should pre-load runtimes via the
710    // extension pipeline before dispatch. T1 stages the parameter
711    // surface so downstream W17-foreign-ffi can wire it up.
712    let _ = language_runtimes;
713
714    // Step 1: reconstruct the program. If function_blobs are supplied,
715    // build a content-addressed Program; otherwise use the full payload.
716    let mut program: BytecodeProgram = request.program;
717    program.type_schema_registry = request.type_schemas;
718
719    if let (Some(blobs), Some(entry_hash)) =
720        (request.function_blobs.clone(), request.function_hash)
721    {
722        if let Some(ca) = program_from_blobs_by_hash(blobs, entry_hash, &program) {
723            program.content_addressed = Some(ca);
724        }
725    }
726
727    // Closures need per-capture kinded marshal (ADR-006 §2.7.8 / Q10);
728    // T1 does not cover that path. Surface-and-stop rather than
729    // dispatch a possibly-wrong call.
730    if request.upvalues.is_some() {
731        return Err(RemoteCallError {
732            message: "remote closure dispatch requires upvalue kind track \
733                      (ADR-006 §2.7.8 / Q10 cell-storage parallel-kind) — \
734                      not yet wired through the remote-call boundary"
735                .to_string(),
736            kind: RemoteErrorKind::RuntimeError,
737        });
738    }
739
740    // Step 2: build VM and load program.
741    let mut vm = VirtualMachine::new(VMConfig::default());
742    vm.load_program(program);
743    vm.populate_module_objects();
744
745    // Step 3: resolve callee. function_hash (canonical) > function_id > name.
746    let func_id: u16 = if let Some(hash) = request.function_hash {
747        vm.program
748            .function_blob_hashes
749            .iter()
750            .position(|h| *h == Some(hash))
751            .map(|p| p as u16)
752            .or_else(|| request.function_id)
753            .or_else(|| {
754                vm.program
755                    .functions
756                    .iter()
757                    .position(|f| f.name == request.function_name)
758                    .map(|p| p as u16)
759            })
760            .ok_or_else(|| RemoteCallError {
761                message: format!(
762                    "function not found by hash; name='{}', id={:?}",
763                    request.function_name, request.function_id,
764                ),
765                kind: RemoteErrorKind::FunctionNotFound,
766            })?
767    } else if let Some(id) = request.function_id {
768        id
769    } else {
770        vm.program
771            .functions
772            .iter()
773            .position(|f| f.name == request.function_name)
774            .map(|p| p as u16)
775            .ok_or_else(|| RemoteCallError {
776                message: format!("function '{}' not found", request.function_name),
777                kind: RemoteErrorKind::FunctionNotFound,
778            })?
779    };
780
781    // Step 4: pick per-arg expected kinds from the callee's frame
782    // descriptor. ADR-006 §2.7.5.1: a present FunctionBlob has every
783    // slot's NativeKind proven — no Unknown placeholder.
784    let function = vm
785        .program
786        .functions
787        .get(func_id as usize)
788        .ok_or_else(|| RemoteCallError {
789            message: format!("function_id {} out of range", func_id),
790            kind: RemoteErrorKind::FunctionNotFound,
791        })?;
792
793    let arity = function.arity as usize;
794    if request.arguments.len() != arity {
795        return Err(RemoteCallError {
796            message: format!(
797                "argument count mismatch for function '{}': expected {}, got {}",
798                function.name,
799                arity,
800                request.arguments.len(),
801            ),
802            kind: RemoteErrorKind::ArgumentError,
803        });
804    }
805
806    let frame_desc = function.frame_descriptor.clone();
807    let arg_kinds: Vec<shape_value::NativeKind> = if let Some(ref fd) = frame_desc {
808        if fd.slots.len() < arity {
809            return Err(RemoteCallError {
810                message: format!(
811                    "function '{}' frame_descriptor has {} slots but arity is {}",
812                    function.name,
813                    fd.slots.len(),
814                    arity,
815                ),
816                kind: RemoteErrorKind::ArgumentError,
817            });
818        }
819        fd.slots.iter().take(arity).copied().collect()
820    } else if arity == 0 {
821        Vec::new()
822    } else {
823        return Err(RemoteCallError {
824            message: format!(
825                "function '{}' has no frame_descriptor — cannot derive \
826                 per-arg NativeKind for marshal protocol (ADR-006 §2.7.5.1)",
827                function.name,
828            ),
829            kind: RemoteErrorKind::ArgumentError,
830        });
831    };
832
833    let return_kind = frame_desc.as_ref().and_then(|fd| fd.return_kind);
834    let function_name_owned = function.name.clone();
835    let _ = function; // release the borrow before moving vm into call
836
837    // Step 5: marshal each SerializableVMValue → KindedSlot per
838    // expected_kind. `serializable_to_slot` allocates strong-count
839    // shares for heap-kinded args; each share transfers into the
840    // callee's frame via `execute_function_by_id`'s share-neutral
841    // call-helper (per cluster-1.5 fix).
842    let mut args: Vec<KindedSlot> = Vec::with_capacity(arity);
843    for (idx, sv) in request.arguments.iter().enumerate() {
844        let expected = arg_kinds[idx];
845        let (bits, kind) = serializable_to_slot(sv, expected, store).map_err(|e| {
846            RemoteCallError {
847                message: format!(
848                    "arg {} marshal failure (expected kind {:?}): {}",
849                    idx, expected, e,
850                ),
851                kind: RemoteErrorKind::ArgumentError,
852            }
853        })?;
854        args.push(KindedSlot::new(ValueSlot::from_raw(bits), kind));
855    }
856
857    // Step 6: dispatch.
858    let result = vm
859        .execute_function_by_id(func_id, args, None)
860        .map_err(|e| RemoteCallError {
861            message: format!(
862                "remote execution of '{}' failed: {:?}",
863                function_name_owned, e,
864            ),
865            kind: RemoteErrorKind::RuntimeError,
866        })?;
867
868    // Step 7: project the returned KindedSlot → SerializableVMValue.
869    // The slot owns one strong-count share that we must release after
870    // serialization (slot_to_serializable does NOT consume the share —
871    // it borrows). `KindedSlot::Drop` retires it at scope exit.
872    let (bits, kind) = (result.slot.raw(), result.kind);
873    // Cross-check: if the program declared a top-level return_kind, the
874    // returned slot's kind must agree. Mismatch is a structured error
875    // rather than a silent reinterpretation (ADR-006 §2.7.7 / Q9).
876    if let Some(declared) = return_kind {
877        if kind != declared {
878            return Err(RemoteCallError {
879                message: format!(
880                    "function '{}' returned kind {:?} but frame_descriptor \
881                     declared return_kind {:?}",
882                    function_name_owned, kind, declared,
883                ),
884                kind: RemoteErrorKind::RuntimeError,
885            });
886        }
887    }
888    let serialized = slot_to_serializable(bits, kind, store).map_err(|e| RemoteCallError {
889        message: format!("return-value marshal failure: {}", e),
890        kind: RemoteErrorKind::RuntimeError,
891    })?;
892    // `result` drops here, retiring the strong-count share via
893    // `KindedSlot::Drop` (ADR-006 §2.7.6 / Q8).
894    drop(result);
895    Ok(serialized)
896}
897
898/// Compute a SHA-256 hash of a `BytecodeProgram` for caching.
899///
900/// Remote VMs can cache programs by this hash, avoiding re-transfer
901/// of the same program on repeated calls.
902pub fn program_hash(program: &BytecodeProgram) -> [u8; 32] {
903    use sha2::{Digest, Sha256};
904    let bytes =
905        rmp_serde::to_vec_named(program).expect("BytecodeProgram serialization should not fail");
906    let hash = Sha256::digest(&bytes);
907    let mut out = [0u8; 32];
908    out.copy_from_slice(&hash);
909    out
910}
911
912/// Create a minimal stub program containing only metadata (no instructions/constants/functions).
913///
914/// Used by `build_call_request` and `build_closure_call_request` when content-addressed
915/// blobs are available, to reduce payload size.
916fn create_stub_program(program: &BytecodeProgram) -> BytecodeProgram {
917    let mut stub = BytecodeProgram::default();
918    stub.type_schema_registry = program.type_schema_registry.clone();
919    // Carry enough content-addressed metadata for program_from_blobs()
920    if let Some(ref ca) = program.content_addressed {
921        stub.content_addressed = Some(Program {
922            entry: ca.entry,
923            function_store: std::collections::HashMap::new(),
924            top_level_locals_count: ca.top_level_locals_count,
925            top_level_local_storage_hints: ca.top_level_local_storage_hints.clone(),
926            module_binding_names: ca.module_binding_names.clone(),
927            module_binding_storage_hints: ca.module_binding_storage_hints.clone(),
928            function_local_storage_hints: ca.function_local_storage_hints.clone(),
929            top_level_frame: ca.top_level_frame.clone(),
930            top_level_local_concrete_types: ca.top_level_local_concrete_types.clone(),
931            function_local_concrete_types: ca.function_local_concrete_types.clone(),
932            function_return_concrete_types: ca.function_return_concrete_types.clone(),
933            monomorphized_method_call_sites: ca.monomorphized_method_call_sites.clone(),
934            value_call_return_concrete_types:
935                ca.value_call_return_concrete_types.clone(),
936            operator_trait_dispatch_sites:
937                ca.operator_trait_dispatch_sites.clone(),
938            data_schema: ca.data_schema.clone(),
939            type_schema_registry: ca.type_schema_registry.clone(),
940            trait_method_symbols: ca.trait_method_symbols.clone(),
941            foreign_functions: ca.foreign_functions.clone(),
942            native_struct_layouts: ca.native_struct_layouts.clone(),
943            debug_info: ca.debug_info.clone(),
944            closure_function_layouts_by_name: ca
945                .closure_function_layouts_by_name
946                .clone(),
947            trait_vtables: ca.trait_vtables.clone(),
948            // R8 W8 Cluster A surface-and-stop flag propagation.
949            has_imported_const_inline: ca.has_imported_const_inline,
950            // R8 W9 B1 W17-marshal-return surface-and-stop flag propagation.
951            has_w17_marshal_residual: ca.has_w17_marshal_residual,
952        });
953    }
954    // Copy top-level metadata needed by program_from_blobs
955    stub.top_level_locals_count = program.top_level_locals_count;
956    stub.top_level_local_storage_hints = program.top_level_local_storage_hints.clone();
957    stub.module_binding_names = program.module_binding_names.clone();
958    stub.module_binding_storage_hints = program.module_binding_storage_hints.clone();
959    stub.function_local_storage_hints = program.function_local_storage_hints.clone();
960    stub.data_schema = program.data_schema.clone();
961    stub.trait_method_symbols = program.trait_method_symbols.clone();
962    stub.foreign_functions = program.foreign_functions.clone();
963    stub.native_struct_layouts = program.native_struct_layouts.clone();
964    stub.debug_info = program.debug_info.clone();
965    stub.function_blob_hashes = program.function_blob_hashes.clone();
966    stub
967}
968
969/// Perform blob negotiation before sending a call request.
970///
971/// Creates a `BlobNegotiationRequest` with the hashes from the blob set,
972/// checks which blobs the remote already has (via the provided cache as a
973/// local stand-in), and returns the set of known hashes that can be stripped
974/// from the outgoing request.
975///
976/// In a real transport scenario the `BlobNegotiationRequest` would be sent
977/// over the wire and the `BlobNegotiationResponse` received from the remote.
978/// Currently this performs the negotiation locally against the provided cache.
979///
980/// # Example flow
981/// ```text
982/// 1. Caller builds blob set for function
983/// 2. negotiate_blobs() → BlobNegotiationRequest with offered hashes
984/// 3. Remote replies with BlobNegotiationResponse (known_hashes)
985/// 4. Caller strips known blobs from the request
986/// ```
987pub fn negotiate_blobs(
988    blobs: &[(FunctionHash, FunctionBlob)],
989    remote_cache: &RemoteBlobCache,
990) -> BlobNegotiationResponse {
991    let request = BlobNegotiationRequest {
992        offered_hashes: blobs.iter().map(|(h, _)| *h).collect(),
993    };
994    // TODO: Wire this to actual transport — currently performs negotiation
995    // locally against the provided cache. In production, `request` would be
996    // serialized, sent over the wire, and the response deserialized.
997    handle_negotiation(&request, remote_cache)
998}
999
1000/// Build a `RemoteCallRequest` for a named function, with blob negotiation.
1001///
1002/// Performs a negotiation step against the provided `remote_cache` to discover
1003/// which blobs the remote already has, then strips those from the request.
1004/// If `remote_cache` is `None`, sends all blobs (no negotiation).
1005pub fn build_call_request_with_negotiation(
1006    program: &BytecodeProgram,
1007    function_name: &str,
1008    arguments: Vec<SerializableVMValue>,
1009    remote_cache: Option<&RemoteBlobCache>,
1010) -> RemoteCallRequest {
1011    let mut request = build_call_request(program, function_name, arguments);
1012
1013    if let (Some(cache), Some(blobs)) = (remote_cache, &mut request.function_blobs) {
1014        let response = negotiate_blobs(blobs, cache);
1015        let known_set: std::collections::HashSet<FunctionHash> =
1016            response.known_hashes.into_iter().collect();
1017        blobs.retain(|(hash, _)| !known_set.contains(hash));
1018    }
1019
1020    request
1021}
1022
1023/// Build a `RemoteCallRequest` for a named function.
1024///
1025/// Convenience function that handles program hashing and type schema extraction.
1026/// When the program has content-addressed blobs, automatically computes the
1027/// minimal transitive closure and attaches it to the request.
1028pub fn build_call_request(
1029    program: &BytecodeProgram,
1030    function_name: &str,
1031    arguments: Vec<SerializableVMValue>,
1032) -> RemoteCallRequest {
1033    let hash = program_hash(program);
1034    let function_id = program
1035        .functions
1036        .iter()
1037        .position(|f| f.name == function_name)
1038        .map(|id| id as u16);
1039    let function_hash = function_id
1040        .and_then(|fid| {
1041            program
1042                .function_blob_hashes
1043                .get(fid as usize)
1044                .copied()
1045                .flatten()
1046        })
1047        .or_else(|| {
1048            program.content_addressed.as_ref().and_then(|ca| {
1049                let mut matches = ca.function_store.iter().filter_map(|(hash, blob)| {
1050                    if blob.name == function_name {
1051                        Some(*hash)
1052                    } else {
1053                        None
1054                    }
1055                });
1056                let first = matches.next()?;
1057                if matches.next().is_some() {
1058                    None
1059                } else {
1060                    Some(first)
1061                }
1062            })
1063        });
1064    let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1065
1066    // When content-addressed blobs are available, send a minimal stub program
1067    // instead of the full BytecodeProgram to reduce payload size.
1068    let request_program = if blobs.is_some() {
1069        create_stub_program(program)
1070    } else {
1071        program.clone()
1072    };
1073
1074    RemoteCallRequest {
1075        program: request_program,
1076        function_name: function_name.to_string(),
1077        function_id,
1078        function_hash,
1079        arguments,
1080        upvalues: None,
1081        type_schemas: program.type_schema_registry.clone(),
1082        program_hash: hash,
1083        function_blobs: blobs,
1084    }
1085}
1086
1087/// Build a `RemoteCallRequest` for a closure.
1088///
1089/// Serializes the closure's captured upvalues alongside the function call.
1090/// When the closure's function has a matching content-addressed blob, sends
1091/// the minimal blob set instead of the full program.
1092pub fn build_closure_call_request(
1093    program: &BytecodeProgram,
1094    function_id: u16,
1095    arguments: Vec<SerializableVMValue>,
1096    upvalues: Vec<SerializableVMValue>,
1097) -> RemoteCallRequest {
1098    let hash = program_hash(program);
1099
1100    let function_hash = program
1101        .function_blob_hashes
1102        .get(function_id as usize)
1103        .copied()
1104        .flatten();
1105    let blobs = function_hash.and_then(|h| build_minimal_blobs_by_hash(program, h));
1106
1107    RemoteCallRequest {
1108        program: if blobs.is_some() {
1109            create_stub_program(program)
1110        } else {
1111            program.clone()
1112        },
1113        function_name: String::new(),
1114        function_id: Some(function_id),
1115        function_hash,
1116        arguments,
1117        upvalues: Some(upvalues),
1118        type_schemas: program.type_schema_registry.clone(),
1119        program_hash: hash,
1120        function_blobs: blobs,
1121    }
1122}
1123
1124/// Build a `RemoteCallRequest` that strips function blobs the remote already has.
1125///
1126/// Like `build_call_request`, but takes a set of hashes the remote is known to
1127/// have cached (from a prior `BlobNegotiationResponse`). Blobs with matching
1128/// hashes are omitted from `function_blobs`, reducing payload size.
1129pub fn build_call_request_negotiated(
1130    program: &BytecodeProgram,
1131    function_name: &str,
1132    arguments: Vec<SerializableVMValue>,
1133    known_hashes: &[FunctionHash],
1134) -> RemoteCallRequest {
1135    let mut request = build_call_request(program, function_name, arguments);
1136
1137    // Strip blobs the remote already has
1138    if let Some(ref mut blobs) = request.function_blobs {
1139        let known_set: std::collections::HashSet<FunctionHash> =
1140            known_hashes.iter().copied().collect();
1141        blobs.retain(|(hash, _)| !known_set.contains(hash));
1142    }
1143
1144    request
1145}
1146
1147/// Handle a blob negotiation request on the server side.
1148///
1149/// Returns the subset of offered hashes that are present in the cache.
1150pub fn handle_negotiation(
1151    request: &BlobNegotiationRequest,
1152    cache: &RemoteBlobCache,
1153) -> BlobNegotiationResponse {
1154    BlobNegotiationResponse {
1155        known_hashes: cache.filter_known(&request.offered_hashes),
1156    }
1157}
1158
1159// ---------------------------------------------------------------------------
1160// Wire message dispatch (V1 + V2 handlers)
1161// ---------------------------------------------------------------------------
1162
1163/// Handle a `WireMessage` by dispatching to the appropriate handler.
1164///
1165/// V1 messages (BlobNegotiation, Call, CallResponse, Sidecar) are fully handled.
1166/// V2 messages (Execute, Validate, Auth, Ping, file/project operations) return
1167/// stub error responses until the execution server is implemented.
1168pub fn handle_wire_message(
1169    msg: WireMessage,
1170    store: &SnapshotStore,
1171    cache: &mut RemoteBlobCache,
1172) -> WireMessage {
1173    match msg {
1174        WireMessage::BlobNegotiation(req) => {
1175            let response = handle_negotiation(&req, cache);
1176            WireMessage::BlobNegotiationReply(response)
1177        }
1178        WireMessage::BlobNegotiationReply(_) => {
1179            // Client-side message — server should not receive this.
1180            // Return an error wrapped in an ExecuteResponse as a generic error channel.
1181            WireMessage::ExecuteResponse(ExecuteResponse {
1182                request_id: 0,
1183                success: false,
1184                value: WireValue::Null,
1185                stdout: None,
1186                error: Some("Unexpected BlobNegotiationReply on server side".to_string()),
1187                content_terminal: None,
1188                content_html: None,
1189                diagnostics: vec![],
1190                metrics: None,
1191                print_output: None,
1192            })
1193        }
1194        WireMessage::Call(req) => {
1195            // Cache any incoming blobs for future negotiation
1196            if let Some(ref blobs) = req.function_blobs {
1197                cache.insert_blobs(blobs);
1198            }
1199            let response = execute_remote_call(req, store);
1200            WireMessage::CallResponse(response)
1201        }
1202        WireMessage::CallResponse(_) => {
1203            // Client-side message — server should not receive this.
1204            WireMessage::ExecuteResponse(ExecuteResponse {
1205                request_id: 0,
1206                success: false,
1207                value: WireValue::Null,
1208                stdout: None,
1209                error: Some("Unexpected CallResponse on server side".to_string()),
1210                content_terminal: None,
1211                content_html: None,
1212                diagnostics: vec![],
1213                metrics: None,
1214                print_output: None,
1215            })
1216        }
1217        WireMessage::Sidecar(_sidecar) => {
1218            // Sidecars are buffered by the transport layer and reassembled
1219            // before the Call message is dispatched. If we receive one here,
1220            // it means the transport did not buffer it.
1221            WireMessage::ExecuteResponse(ExecuteResponse {
1222                request_id: 0,
1223                success: false,
1224                value: WireValue::Null,
1225                stdout: None,
1226                error: Some("Unexpected standalone Sidecar message".to_string()),
1227                content_terminal: None,
1228                content_html: None,
1229                diagnostics: vec![],
1230                metrics: None,
1231                print_output: None,
1232            })
1233        }
1234
1235        // --- V2 message stubs ---
1236
1237        WireMessage::Execute(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1238            request_id: req.request_id,
1239            success: false,
1240            value: WireValue::Null,
1241            stdout: None,
1242            error: Some("V2 Execute not yet implemented".to_string()),
1243            content_terminal: None,
1244            content_html: None,
1245            diagnostics: vec![WireDiagnostic {
1246                severity: "error".to_string(),
1247                message: "V2 Execute handler not yet implemented".to_string(),
1248                line: None,
1249                column: None,
1250            }],
1251            metrics: None,
1252            print_output: None,
1253        }),
1254        WireMessage::ExecuteResponse(_) => {
1255            // Client-side message — should not arrive at server.
1256            WireMessage::ExecuteResponse(ExecuteResponse {
1257                request_id: 0,
1258                success: false,
1259                value: WireValue::Null,
1260                stdout: None,
1261                error: Some("Unexpected ExecuteResponse on server side".to_string()),
1262                content_terminal: None,
1263                content_html: None,
1264                diagnostics: vec![],
1265                metrics: None,
1266                print_output: None,
1267            })
1268        }
1269        WireMessage::Validate(req) => WireMessage::ValidateResponse(ValidateResponse {
1270            request_id: req.request_id,
1271            success: false,
1272            diagnostics: vec![WireDiagnostic {
1273                severity: "error".to_string(),
1274                message: "V2 Validate handler not yet implemented".to_string(),
1275                line: None,
1276                column: None,
1277            }],
1278        }),
1279        WireMessage::ValidateResponse(_) => {
1280            WireMessage::ExecuteResponse(ExecuteResponse {
1281                request_id: 0,
1282                success: false,
1283                value: WireValue::Null,
1284                stdout: None,
1285                error: Some("Unexpected ValidateResponse on server side".to_string()),
1286                content_terminal: None,
1287                content_html: None,
1288                diagnostics: vec![],
1289                metrics: None,
1290                print_output: None,
1291            })
1292        }
1293        WireMessage::Auth(_req) => WireMessage::AuthResponse(AuthResponse {
1294            authenticated: false,
1295            error: Some("V2 Auth handler not yet implemented".to_string()),
1296        }),
1297        WireMessage::AuthResponse(_) => {
1298            WireMessage::ExecuteResponse(ExecuteResponse {
1299                request_id: 0,
1300                success: false,
1301                value: WireValue::Null,
1302                stdout: None,
1303                error: Some("Unexpected AuthResponse on server side".to_string()),
1304                content_terminal: None,
1305                content_html: None,
1306                diagnostics: vec![],
1307                metrics: None,
1308                print_output: None,
1309            })
1310        }
1311        WireMessage::ExecuteFile(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1312            request_id: req.request_id,
1313            success: false,
1314            value: WireValue::Null,
1315            stdout: None,
1316            error: Some("V2 ExecuteFile handler not yet implemented".to_string()),
1317            content_terminal: None,
1318            content_html: None,
1319            diagnostics: vec![WireDiagnostic {
1320                severity: "error".to_string(),
1321                message: "V2 ExecuteFile handler not yet implemented".to_string(),
1322                line: None,
1323                column: None,
1324            }],
1325            metrics: None,
1326            print_output: None,
1327        }),
1328        WireMessage::ExecuteProject(req) => WireMessage::ExecuteResponse(ExecuteResponse {
1329            request_id: req.request_id,
1330            success: false,
1331            value: WireValue::Null,
1332            stdout: None,
1333            error: Some("V2 ExecuteProject handler not yet implemented".to_string()),
1334            content_terminal: None,
1335            content_html: None,
1336            diagnostics: vec![WireDiagnostic {
1337                severity: "error".to_string(),
1338                message: "V2 ExecuteProject handler not yet implemented".to_string(),
1339                line: None,
1340                column: None,
1341            }],
1342            metrics: None,
1343            print_output: None,
1344        }),
1345        WireMessage::ValidatePath(req) => WireMessage::ValidateResponse(ValidateResponse {
1346            request_id: req.request_id,
1347            success: false,
1348            diagnostics: vec![WireDiagnostic {
1349                severity: "error".to_string(),
1350                message: "V2 ValidatePath handler not yet implemented".to_string(),
1351                line: None,
1352                column: None,
1353            }],
1354        }),
1355        WireMessage::Ping(_) => WireMessage::Pong(ServerInfo {
1356            shape_version: env!("CARGO_PKG_VERSION").to_string(),
1357            wire_protocol: shape_wire::WIRE_PROTOCOL_V2,
1358            capabilities: vec![
1359                "call".to_string(),
1360                "blob-negotiation".to_string(),
1361                "sidecar".to_string(),
1362            ],
1363        }),
1364        WireMessage::Pong(_) => {
1365            // Client-side message — should not arrive at server.
1366            WireMessage::ExecuteResponse(ExecuteResponse {
1367                request_id: 0,
1368                success: false,
1369                value: WireValue::Null,
1370                stdout: None,
1371                error: Some("Unexpected Pong on server side".to_string()),
1372                content_terminal: None,
1373                content_html: None,
1374                diagnostics: vec![],
1375                metrics: None,
1376                print_output: None,
1377            })
1378        }
1379    }
1380}
1381
1382// ---------------------------------------------------------------------------
1383// Phase 3B: Sidecar extraction and reassembly
1384// ---------------------------------------------------------------------------
1385
1386/// Minimum blob size (in bytes) to extract as a sidecar.
1387/// Blobs smaller than this are left inline in the serialized payload.
1388pub const SIDECAR_THRESHOLD: usize = 1024 * 1024; // 1 MB
1389
1390/// Extract large blobs from serialized arguments into sidecars.
1391///
1392/// Walks the `SerializableVMValue` tree recursively. Any `BlobRef` whose
1393/// backing `ChunkedBlob` exceeds `SIDECAR_THRESHOLD` bytes is replaced
1394/// with a `SidecarRef` and the raw data is collected into a `BlobSidecar`.
1395///
1396/// Returns the extracted sidecars. The `args` are modified in place.
1397pub fn extract_sidecars(
1398    args: &mut Vec<SerializableVMValue>,
1399    store: &SnapshotStore,
1400) -> Vec<BlobSidecar> {
1401    let mut sidecars = Vec::new();
1402    let mut next_id: u32 = 0;
1403    for arg in args.iter_mut() {
1404        extract_sidecars_recursive(arg, store, &mut sidecars, &mut next_id);
1405    }
1406    sidecars
1407}
1408
1409/// Extract the BlobRef from a SerializableVMValue if it carries one (non-mutating read).
1410fn get_blob_ref(value: &SerializableVMValue) -> Option<&shape_runtime::snapshot::BlobRef> {
1411    use shape_runtime::snapshot::SerializableVMValue as SV;
1412    match value {
1413        SV::DataTable(blob)
1414        | SV::TypedTable { table: blob, .. }
1415        | SV::RowView { table: blob, .. }
1416        | SV::ColumnRef { table: blob, .. }
1417        | SV::IndexedTable { table: blob, .. } => Some(blob),
1418        SV::TypedArray { blob, .. } | SV::Matrix { blob, .. } => Some(blob),
1419        _ => None,
1420    }
1421}
1422
1423fn extract_sidecars_recursive(
1424    value: &mut SerializableVMValue,
1425    store: &SnapshotStore,
1426    sidecars: &mut Vec<BlobSidecar>,
1427    next_id: &mut u32,
1428) {
1429    use shape_runtime::snapshot::SerializableVMValue as SV;
1430
1431    // First: check if this value carries a blob large enough to extract.
1432    // Capture metadata (TypedArray len, Matrix rows/cols) before replacing.
1433    let meta = match &*value {
1434        SV::TypedArray { len, .. } => (*len as u32, 0u32),
1435        SV::Matrix { rows, cols, .. } => (*rows, *cols),
1436        _ => (0, 0),
1437    };
1438    // Clone the blob info to avoid borrow conflicts with the later mutation.
1439    if let Some(blob) = get_blob_ref(value) {
1440        let blob_kind = blob.kind.clone();
1441        let blob_hash = blob.hash.clone();
1442        if let Some(sidecar) = try_extract_blob(blob, store, next_id) {
1443            let sidecar_id = sidecar.sidecar_id;
1444            sidecars.push(sidecar);
1445            *value = SV::SidecarRef {
1446                sidecar_id,
1447                blob_kind,
1448                original_hash: blob_hash,
1449                meta_a: meta.0,
1450                meta_b: meta.1,
1451            };
1452            return;
1453        }
1454    }
1455
1456    // Recursive descent into containers
1457    match value {
1458        SV::Array(items) => {
1459            for item in items.iter_mut() {
1460                extract_sidecars_recursive(item, store, sidecars, next_id);
1461            }
1462        }
1463        SV::HashMap { keys, values } => {
1464            for k in keys.iter_mut() {
1465                extract_sidecars_recursive(k, store, sidecars, next_id);
1466            }
1467            for v in values.iter_mut() {
1468                extract_sidecars_recursive(v, store, sidecars, next_id);
1469            }
1470        }
1471        SV::TypedObject { slot_data, .. } => {
1472            for slot in slot_data.iter_mut() {
1473                extract_sidecars_recursive(slot, store, sidecars, next_id);
1474            }
1475        }
1476        SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1477            extract_sidecars_recursive(inner, store, sidecars, next_id);
1478        }
1479        SV::TypeAnnotatedValue { value: inner, .. } => {
1480            extract_sidecars_recursive(inner, store, sidecars, next_id);
1481        }
1482        SV::Closure { upvalues, .. } => {
1483            for uv in upvalues.iter_mut() {
1484                extract_sidecars_recursive(uv, store, sidecars, next_id);
1485            }
1486        }
1487        SV::Enum(ev) => match &mut ev.payload {
1488            shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1489            shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1490                for item in items.iter_mut() {
1491                    extract_sidecars_recursive(item, store, sidecars, next_id);
1492                }
1493            }
1494            shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1495                for (_, v) in fields.iter_mut() {
1496                    extract_sidecars_recursive(v, store, sidecars, next_id);
1497                }
1498            }
1499        },
1500        SV::PrintResult(pr) => {
1501            for span in pr.spans.iter_mut() {
1502                if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1503                    raw_value,
1504                    format_params,
1505                    ..
1506                } = span
1507                {
1508                    extract_sidecars_recursive(raw_value, store, sidecars, next_id);
1509                    for (_, v) in format_params.iter_mut() {
1510                        extract_sidecars_recursive(v, store, sidecars, next_id);
1511                    }
1512                }
1513            }
1514        }
1515        SV::SimulationCall { params, .. } => {
1516            for (_, v) in params.iter_mut() {
1517                extract_sidecars_recursive(v, store, sidecars, next_id);
1518            }
1519        }
1520        SV::FunctionRef { closure, .. } => {
1521            if let Some(c) = closure {
1522                extract_sidecars_recursive(c, store, sidecars, next_id);
1523            }
1524        }
1525        SV::Range { start, end, .. } => {
1526            if let Some(s) = start {
1527                extract_sidecars_recursive(s, store, sidecars, next_id);
1528            }
1529            if let Some(e) = end {
1530                extract_sidecars_recursive(e, store, sidecars, next_id);
1531            }
1532        }
1533
1534        // Leaf types and blob carriers (handled above) — nothing more to do
1535        _ => {}
1536    }
1537}
1538
1539/// Try to extract a BlobRef's data as a sidecar if it exceeds the threshold.
1540fn try_extract_blob(
1541    blob: &shape_runtime::snapshot::BlobRef,
1542    store: &SnapshotStore,
1543    next_id: &mut u32,
1544) -> Option<BlobSidecar> {
1545    // Load the ChunkedBlob metadata to check total size
1546    let chunked: shape_runtime::snapshot::ChunkedBlob = store.get_struct(&blob.hash).ok()?;
1547    if chunked.total_len < SIDECAR_THRESHOLD {
1548        return None;
1549    }
1550
1551    // Load the raw data
1552    let data = shape_runtime::snapshot::load_chunked_bytes(&chunked, store).ok()?;
1553    let sidecar_id = *next_id;
1554    *next_id += 1;
1555
1556    Some(BlobSidecar { sidecar_id, data })
1557}
1558
1559/// Reassemble sidecars back into the serialized payload.
1560///
1561/// Walks the `SerializableVMValue` tree and replaces `SidecarRef` variants
1562/// with the original `BlobRef`, storing the sidecar data back into the
1563/// snapshot store.
1564pub fn reassemble_sidecars(
1565    args: &mut Vec<SerializableVMValue>,
1566    sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1567    store: &SnapshotStore,
1568) -> anyhow::Result<()> {
1569    for arg in args.iter_mut() {
1570        reassemble_recursive(arg, sidecars, store)?;
1571    }
1572    Ok(())
1573}
1574
1575fn reassemble_recursive(
1576    value: &mut SerializableVMValue,
1577    sidecars: &std::collections::HashMap<u32, BlobSidecar>,
1578    store: &SnapshotStore,
1579) -> anyhow::Result<()> {
1580    use shape_runtime::snapshot::{BlobRef, SerializableVMValue as SV};
1581
1582    match value {
1583        SV::SidecarRef {
1584            sidecar_id,
1585            blob_kind,
1586            original_hash: _,
1587            meta_a,
1588            meta_b,
1589        } => {
1590            let sidecar = sidecars
1591                .get(sidecar_id)
1592                .ok_or_else(|| anyhow::anyhow!("missing sidecar with id {}", sidecar_id))?;
1593            let meta_a = *meta_a;
1594            let meta_b = *meta_b;
1595
1596            // Store the sidecar data back into the snapshot store as chunked bytes,
1597            // then wrap in a ChunkedBlob struct and store that.
1598            let chunked = shape_runtime::snapshot::store_chunked_bytes(&sidecar.data, store)?;
1599            let hash = store.put_struct(&chunked)?;
1600
1601            let blob = BlobRef {
1602                hash,
1603                kind: blob_kind.clone(),
1604            };
1605            *value = match blob_kind {
1606                shape_runtime::snapshot::BlobKind::DataTable => SV::DataTable(blob),
1607                shape_runtime::snapshot::BlobKind::TypedArray(ek) => SV::TypedArray {
1608                    element_kind: *ek,
1609                    blob,
1610                    len: meta_a as usize,
1611                },
1612                shape_runtime::snapshot::BlobKind::Matrix => SV::Matrix {
1613                    blob,
1614                    rows: meta_a,
1615                    cols: meta_b,
1616                },
1617            };
1618        }
1619
1620        // Recursive descent (same structure as extract)
1621        SV::Array(items) => {
1622            for item in items.iter_mut() {
1623                reassemble_recursive(item, sidecars, store)?;
1624            }
1625        }
1626        SV::HashMap { keys, values } => {
1627            for k in keys.iter_mut() {
1628                reassemble_recursive(k, sidecars, store)?;
1629            }
1630            for v in values.iter_mut() {
1631                reassemble_recursive(v, sidecars, store)?;
1632            }
1633        }
1634        SV::TypedObject { slot_data, .. } => {
1635            for slot in slot_data.iter_mut() {
1636                reassemble_recursive(slot, sidecars, store)?;
1637            }
1638        }
1639        SV::Some(inner) | SV::Ok(inner) | SV::Err(inner) => {
1640            reassemble_recursive(inner, sidecars, store)?;
1641        }
1642        SV::TypeAnnotatedValue { value: inner, .. } => {
1643            reassemble_recursive(inner, sidecars, store)?;
1644        }
1645        SV::Closure { upvalues, .. } => {
1646            for uv in upvalues.iter_mut() {
1647                reassemble_recursive(uv, sidecars, store)?;
1648            }
1649        }
1650        SV::Enum(ev) => match &mut ev.payload {
1651            shape_runtime::snapshot::EnumPayloadSnapshot::Unit => {}
1652            shape_runtime::snapshot::EnumPayloadSnapshot::Tuple(items) => {
1653                for item in items.iter_mut() {
1654                    reassemble_recursive(item, sidecars, store)?;
1655                }
1656            }
1657            shape_runtime::snapshot::EnumPayloadSnapshot::Struct(fields) => {
1658                for (_, v) in fields.iter_mut() {
1659                    reassemble_recursive(v, sidecars, store)?;
1660                }
1661            }
1662        },
1663        SV::PrintResult(pr) => {
1664            for span in pr.spans.iter_mut() {
1665                if let shape_runtime::snapshot::PrintSpanSnapshot::Value {
1666                    raw_value,
1667                    format_params,
1668                    ..
1669                } = span
1670                {
1671                    reassemble_recursive(raw_value, sidecars, store)?;
1672                    for (_, v) in format_params.iter_mut() {
1673                        reassemble_recursive(v, sidecars, store)?;
1674                    }
1675                }
1676            }
1677        }
1678        SV::SimulationCall { params, .. } => {
1679            for (_, v) in params.iter_mut() {
1680                reassemble_recursive(v, sidecars, store)?;
1681            }
1682        }
1683        SV::FunctionRef { closure, .. } => {
1684            if let Some(c) = closure {
1685                reassemble_recursive(c, sidecars, store)?;
1686            }
1687        }
1688        SV::Range { start, end, .. } => {
1689            if let Some(s) = start {
1690                reassemble_recursive(s, sidecars, store)?;
1691            }
1692            if let Some(e) = end {
1693                reassemble_recursive(e, sidecars, store)?;
1694            }
1695        }
1696
1697        // Leaf types and blob-carrying variants (non-sidecar) — nothing to reassemble
1698        _ => {}
1699    }
1700    Ok(())
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705    use super::*;
1706    use crate::bytecode::{FunctionBlob, FunctionHash, Instruction, OpCode, Program};
1707    use crate::compiler::BytecodeCompiler;
1708    use shape_abi_v1::PermissionSet;
1709    use std::collections::HashMap;
1710
1711    /// Helper: compile Shape source to BytecodeProgram
1712    fn compile(source: &str) -> BytecodeProgram {
1713        let program = shape_ast::parser::parse_program(source).expect("parse failed");
1714        let compiler = BytecodeCompiler::new();
1715        compiler.compile(&program).expect("compile failed")
1716    }
1717
1718    /// Helper: create a temp SnapshotStore
1719    fn temp_store() -> SnapshotStore {
1720        let dir = std::env::temp_dir().join(format!("shape_remote_test_{}", std::process::id()));
1721        SnapshotStore::new(dir).expect("create snapshot store")
1722    }
1723
1724    fn mk_hash(tag: u8) -> FunctionHash {
1725        let mut bytes = [0u8; 32];
1726        bytes[0] = tag;
1727        FunctionHash(bytes)
1728    }
1729
1730    fn mk_blob(name: &str, hash: FunctionHash, dependencies: Vec<FunctionHash>) -> FunctionBlob {
1731        FunctionBlob {
1732            content_hash: hash,
1733            name: name.to_string(),
1734            arity: 0,
1735            param_names: Vec::new(),
1736            locals_count: 0,
1737            is_closure: false,
1738            captures_count: 0,
1739            is_async: false,
1740            ref_params: Vec::new(),
1741            ref_mutates: Vec::new(),
1742            mutable_captures: Vec::new(),
1743            frame_descriptor: None,
1744            instructions: vec![
1745                Instruction::simple(OpCode::PushNull),
1746                Instruction::simple(OpCode::ReturnValue),
1747            ],
1748            constants: Vec::new(),
1749            strings: Vec::new(),
1750            required_permissions: PermissionSet::pure(),
1751            dependencies,
1752            callee_names: Vec::new(),
1753            type_schemas: Vec::new(),
1754            foreign_dependencies: Vec::new(),
1755            source_map: Vec::new(),
1756        }
1757    }
1758
1759    // The pre-bulldozer end-to-end execute tests
1760    // (`test_remote_call_simple_function`, `test_remote_call_function_not_found`)
1761    // drove `execute_remote_call`, which is currently a phase-2c stub
1762    // (see `execute_inner` body). Re-author them once the kind-threaded
1763    // `slot_to_serializable` / `serializable_to_slot` round-trip lands
1764    // (ADR-006 §2.7.4 + addendum); both depend on the snapshot-side
1765    // rebuild plus a `Vec<KindedSlot>` arg pipeline through the VM
1766    // entrypoints.
1767
1768    #[test]
1769    fn test_program_hash_deterministic() {
1770        let bytecode = compile("function f(x) { x * 2 }");
1771        let hash1 = program_hash(&bytecode);
1772        let hash2 = program_hash(&bytecode);
1773        assert_eq!(hash1, hash2, "Same program should produce same hash");
1774    }
1775
1776    #[test]
1777    fn test_request_response_serialization_roundtrip() {
1778        let bytecode = compile("function double(x) { x * 2 }");
1779        let request =
1780            build_call_request(&bytecode, "double", vec![SerializableVMValue::Number(21.0)]);
1781
1782        // Encode → decode roundtrip via MessagePack
1783        let bytes = shape_wire::encode_message(&request).expect("encode request");
1784        let decoded: RemoteCallRequest =
1785            shape_wire::decode_message(&bytes).expect("decode request");
1786
1787        assert_eq!(decoded.function_name, "double");
1788        assert_eq!(decoded.arguments.len(), 1);
1789        assert_eq!(decoded.program_hash, request.program_hash);
1790    }
1791
1792    #[test]
1793    fn test_response_serialization_roundtrip() {
1794        let response = RemoteCallResponse {
1795            result: Ok(SerializableVMValue::String("hello".to_string())),
1796        };
1797
1798        let bytes = shape_wire::encode_message(&response).expect("encode response");
1799        let decoded: RemoteCallResponse =
1800            shape_wire::decode_message(&bytes).expect("decode response");
1801
1802        match decoded.result {
1803            Ok(SerializableVMValue::String(s)) => assert_eq!(s, "hello"),
1804            other => panic!("Expected Ok(String), got {:?}", other),
1805        }
1806    }
1807
1808    #[test]
1809    fn test_type_schema_registry_roundtrip() {
1810        use shape_runtime::type_schema::{FieldType, TypeSchemaRegistry};
1811
1812        let mut registry = TypeSchemaRegistry::new();
1813        registry.register_type(
1814            "Point",
1815            vec![
1816                ("x".to_string(), FieldType::F64),
1817                ("y".to_string(), FieldType::F64),
1818            ],
1819        );
1820
1821        let bytes = shape_wire::encode_message(&registry).expect("encode registry");
1822        let decoded: TypeSchemaRegistry =
1823            shape_wire::decode_message(&bytes).expect("decode registry");
1824
1825        assert!(decoded.has_type("Point"));
1826        let schema = decoded.get("Point").unwrap();
1827        assert_eq!(schema.field_count(), 2);
1828        assert_eq!(schema.field_offset("x"), Some(0));
1829        assert_eq!(schema.field_offset("y"), Some(8));
1830    }
1831
1832    #[test]
1833    fn test_build_minimal_blobs_rejects_ambiguous_function_name() {
1834        let h1 = mk_hash(1);
1835        let h2 = mk_hash(2);
1836        let blob1 = mk_blob("dup", h1, vec![]);
1837        let blob2 = mk_blob("dup", h2, vec![]);
1838
1839        let mut function_store = HashMap::new();
1840        function_store.insert(h1, blob1.clone());
1841        function_store.insert(h2, blob2.clone());
1842
1843        let mut program = BytecodeProgram::default();
1844        program.content_addressed = Some(Program {
1845            entry: h1,
1846            function_store,
1847            top_level_locals_count: 0,
1848            top_level_local_storage_hints: Vec::new(),
1849            module_binding_names: Vec::new(),
1850            module_binding_storage_hints: Vec::new(),
1851            function_local_storage_hints: Vec::new(),
1852            top_level_frame: None,
1853            top_level_local_concrete_types: Vec::new(),
1854            function_local_concrete_types: Vec::new(),
1855            function_return_concrete_types: Vec::new(),
1856            monomorphized_method_call_sites: HashMap::new(),
1857            value_call_return_concrete_types: HashMap::new(),
1858            operator_trait_dispatch_sites: HashMap::new(),
1859            data_schema: None,
1860            type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
1861            trait_method_symbols: HashMap::new(),
1862            foreign_functions: Vec::new(),
1863            native_struct_layouts: Vec::new(),
1864            debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
1865            closure_function_layouts_by_name: HashMap::new(),
1866            trait_vtables: HashMap::new(),
1867            has_imported_const_inline: false,
1868            has_w17_marshal_residual: false,
1869        });
1870
1871        assert!(
1872            build_minimal_blobs(&program, "dup").is_none(),
1873            "name-based selection must reject ambiguous function names"
1874        );
1875
1876        let by_hash = build_minimal_blobs_by_hash(&program, h2)
1877            .expect("hash-based selection should work with duplicate names");
1878        assert_eq!(by_hash.len(), 1);
1879        assert_eq!(by_hash[0].0, h2);
1880        assert_eq!(by_hash[0].1.name, "dup");
1881    }
1882
1883    #[test]
1884    fn test_program_from_blobs_by_hash_requires_entry_blob() {
1885        let h1 = mk_hash(1);
1886        let h_missing = mk_hash(9);
1887        let blob = mk_blob("f", h1, vec![]);
1888        let source = BytecodeProgram::default();
1889
1890        let reconstructed = program_from_blobs_by_hash(vec![(h1, blob)], h_missing, &source);
1891        assert!(
1892            reconstructed.is_none(),
1893            "reconstruction must fail when the requested entry hash is absent"
1894        );
1895    }
1896
1897    // ---- Phase 2: Blob negotiation tests ----
1898
1899    #[test]
1900    fn test_blob_cache_insert_and_get() {
1901        let mut cache = RemoteBlobCache::new(10);
1902        let h1 = mk_hash(1);
1903        let blob1 = mk_blob("f1", h1, vec![]);
1904
1905        cache.insert(h1, blob1.clone());
1906        assert_eq!(cache.len(), 1);
1907        assert!(cache.contains(&h1));
1908        assert_eq!(cache.get(&h1).unwrap().name, "f1");
1909    }
1910
1911    #[test]
1912    fn test_blob_cache_lru_eviction() {
1913        let mut cache = RemoteBlobCache::new(2);
1914        let h1 = mk_hash(1);
1915        let h2 = mk_hash(2);
1916        let h3 = mk_hash(3);
1917
1918        cache.insert(h1, mk_blob("f1", h1, vec![]));
1919        cache.insert(h2, mk_blob("f2", h2, vec![]));
1920        assert_eq!(cache.len(), 2);
1921
1922        // Insert h3 should evict h1 (least recently used)
1923        cache.insert(h3, mk_blob("f3", h3, vec![]));
1924        assert_eq!(cache.len(), 2);
1925        assert!(!cache.contains(&h1), "h1 should be evicted");
1926        assert!(cache.contains(&h2));
1927        assert!(cache.contains(&h3));
1928    }
1929
1930    #[test]
1931    fn test_blob_cache_access_updates_order() {
1932        let mut cache = RemoteBlobCache::new(2);
1933        let h1 = mk_hash(1);
1934        let h2 = mk_hash(2);
1935        let h3 = mk_hash(3);
1936
1937        cache.insert(h1, mk_blob("f1", h1, vec![]));
1938        cache.insert(h2, mk_blob("f2", h2, vec![]));
1939
1940        // Access h1 to make it recently used
1941        cache.get(&h1);
1942
1943        // Insert h3 should evict h2 (now least recently used)
1944        cache.insert(h3, mk_blob("f3", h3, vec![]));
1945        assert!(
1946            cache.contains(&h1),
1947            "h1 was accessed, should not be evicted"
1948        );
1949        assert!(!cache.contains(&h2), "h2 should be evicted");
1950        assert!(cache.contains(&h3));
1951    }
1952
1953    #[test]
1954    fn test_blob_cache_filter_known() {
1955        let mut cache = RemoteBlobCache::new(10);
1956        let h1 = mk_hash(1);
1957        let h2 = mk_hash(2);
1958        let h3 = mk_hash(3);
1959
1960        cache.insert(h1, mk_blob("f1", h1, vec![]));
1961        cache.insert(h3, mk_blob("f3", h3, vec![]));
1962
1963        let known = cache.filter_known(&[h1, h2, h3]);
1964        assert_eq!(known.len(), 2);
1965        assert!(known.contains(&h1));
1966        assert!(known.contains(&h3));
1967        assert!(!known.contains(&h2));
1968    }
1969
1970    #[test]
1971    fn test_handle_negotiation() {
1972        let mut cache = RemoteBlobCache::new(10);
1973        let h1 = mk_hash(1);
1974        let h2 = mk_hash(2);
1975        cache.insert(h1, mk_blob("f1", h1, vec![]));
1976
1977        let request = BlobNegotiationRequest {
1978            offered_hashes: vec![h1, h2],
1979        };
1980        let response = handle_negotiation(&request, &cache);
1981        assert_eq!(response.known_hashes.len(), 1);
1982        assert!(response.known_hashes.contains(&h1));
1983    }
1984
1985    #[test]
1986    fn test_build_call_request_negotiated_strips_known_blobs() {
1987        // Create a program with content-addressed blobs
1988        let h1 = mk_hash(1);
1989        let h2 = mk_hash(2);
1990        let blob1 = mk_blob("entry", h1, vec![h2]);
1991        let blob2 = mk_blob("helper", h2, vec![]);
1992
1993        let mut function_store = HashMap::new();
1994        function_store.insert(h1, blob1.clone());
1995        function_store.insert(h2, blob2.clone());
1996
1997        let mut program = BytecodeProgram::default();
1998        program.content_addressed = Some(Program {
1999            entry: h1,
2000            function_store,
2001            top_level_locals_count: 0,
2002            top_level_local_storage_hints: Vec::new(),
2003            module_binding_names: Vec::new(),
2004            module_binding_storage_hints: Vec::new(),
2005            function_local_storage_hints: Vec::new(),
2006            top_level_frame: None,
2007            top_level_local_concrete_types: Vec::new(),
2008            function_local_concrete_types: Vec::new(),
2009            function_return_concrete_types: Vec::new(),
2010            monomorphized_method_call_sites: HashMap::new(),
2011            value_call_return_concrete_types: HashMap::new(),
2012            operator_trait_dispatch_sites: HashMap::new(),
2013            data_schema: None,
2014            type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2015            trait_method_symbols: HashMap::new(),
2016            foreign_functions: Vec::new(),
2017            native_struct_layouts: Vec::new(),
2018            debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2019            closure_function_layouts_by_name: HashMap::new(),
2020            trait_vtables: HashMap::new(),
2021            has_imported_const_inline: false,
2022            has_w17_marshal_residual: false,
2023        });
2024        program.functions = vec![crate::bytecode::Function {
2025            name: "entry".to_string(),
2026            arity: 0,
2027            param_names: vec![],
2028            locals_count: 0,
2029            entry_point: 0,
2030            body_length: 0,
2031            is_closure: false,
2032            captures_count: 0,
2033            is_async: false,
2034            ref_params: vec![],
2035            ref_mutates: vec![],
2036            mutable_captures: vec![],
2037            frame_descriptor: None,
2038            osr_entry_points: vec![],
2039            mir_data: None,
2040        }];
2041        program.function_blob_hashes = vec![Some(h1)];
2042
2043        // First call: no known hashes -> all blobs sent
2044        let req1 = build_call_request_negotiated(&program, "entry", vec![], &[]);
2045        let blobs1 = req1.function_blobs.as_ref().unwrap();
2046        assert_eq!(blobs1.len(), 2, "first call should send all blobs");
2047
2048        // Second call: h2 is known -> only h1 sent
2049        let req2 = build_call_request_negotiated(&program, "entry", vec![], &[h2]);
2050        let blobs2 = req2.function_blobs.as_ref().unwrap();
2051        assert_eq!(blobs2.len(), 1, "second call should skip known blobs");
2052        assert_eq!(blobs2[0].0, h1);
2053    }
2054
2055    #[test]
2056    fn test_wire_message_serialization_roundtrip() {
2057        let msg = WireMessage::BlobNegotiation(BlobNegotiationRequest {
2058            offered_hashes: vec![mk_hash(1), mk_hash(2)],
2059        });
2060        let bytes = shape_wire::encode_message(&msg).expect("encode WireMessage");
2061        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode WireMessage");
2062        match decoded {
2063            WireMessage::BlobNegotiation(req) => {
2064                assert_eq!(req.offered_hashes.len(), 2);
2065            }
2066            _ => panic!("Expected BlobNegotiation"),
2067        }
2068    }
2069
2070    // ---- V2 execution server message tests ----
2071
2072    #[test]
2073    fn test_execute_request_roundtrip() {
2074        let msg = WireMessage::Execute(ExecuteRequest {
2075            code: "fn main() { 42 }".to_string(),
2076            request_id: 7,
2077        });
2078        let bytes = shape_wire::encode_message(&msg).expect("encode Execute");
2079        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Execute");
2080        match decoded {
2081            WireMessage::Execute(req) => {
2082                assert_eq!(req.code, "fn main() { 42 }");
2083                assert_eq!(req.request_id, 7);
2084            }
2085            _ => panic!("Expected Execute"),
2086        }
2087    }
2088
2089    #[test]
2090    fn test_execute_response_roundtrip() {
2091        let msg = WireMessage::ExecuteResponse(ExecuteResponse {
2092            request_id: 7,
2093            success: true,
2094            value: WireValue::Number(42.0),
2095            stdout: Some("hello\n".to_string()),
2096            error: None,
2097            content_terminal: None,
2098            content_html: None,
2099            diagnostics: vec![WireDiagnostic {
2100                severity: "warning".to_string(),
2101                message: "unused variable".to_string(),
2102                line: Some(1),
2103                column: Some(5),
2104            }],
2105            metrics: Some(ExecutionMetrics {
2106                instructions_executed: 100,
2107                wall_time_ms: 3,
2108                memory_bytes_peak: 4096,
2109            }),
2110            print_output: None,
2111        });
2112        let bytes = shape_wire::encode_message(&msg).expect("encode ExecuteResponse");
2113        let decoded: WireMessage =
2114            shape_wire::decode_message(&bytes).expect("decode ExecuteResponse");
2115        match decoded {
2116            WireMessage::ExecuteResponse(resp) => {
2117                assert_eq!(resp.request_id, 7);
2118                assert!(resp.success);
2119                assert!(matches!(resp.value, WireValue::Number(n) if n == 42.0));
2120                assert_eq!(resp.stdout.as_deref(), Some("hello\n"));
2121                assert!(resp.error.is_none());
2122                assert_eq!(resp.diagnostics.len(), 1);
2123                assert_eq!(resp.diagnostics[0].severity, "warning");
2124                assert_eq!(resp.diagnostics[0].line, Some(1));
2125                let m = resp.metrics.unwrap();
2126                assert_eq!(m.instructions_executed, 100);
2127                assert_eq!(m.wall_time_ms, 3);
2128            }
2129            _ => panic!("Expected ExecuteResponse"),
2130        }
2131    }
2132
2133    #[test]
2134    fn test_ping_pong_roundtrip() {
2135        let ping = WireMessage::Ping(PingRequest {});
2136        let bytes = shape_wire::encode_message(&ping).expect("encode Ping");
2137        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Ping");
2138        assert!(matches!(decoded, WireMessage::Ping(_)));
2139
2140        let pong = WireMessage::Pong(ServerInfo {
2141            shape_version: "0.1.3".to_string(),
2142            wire_protocol: 2,
2143            capabilities: vec!["execute".to_string(), "validate".to_string()],
2144        });
2145        let bytes = shape_wire::encode_message(&pong).expect("encode Pong");
2146        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Pong");
2147        match decoded {
2148            WireMessage::Pong(info) => {
2149                assert_eq!(info.shape_version, "0.1.3");
2150                assert_eq!(info.wire_protocol, 2);
2151                assert_eq!(info.capabilities.len(), 2);
2152            }
2153            _ => panic!("Expected Pong"),
2154        }
2155    }
2156
2157    #[test]
2158    fn test_auth_roundtrip() {
2159        let msg = WireMessage::Auth(AuthRequest {
2160            token: "secret-token".to_string(),
2161        });
2162        let bytes = shape_wire::encode_message(&msg).expect("encode Auth");
2163        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Auth");
2164        match decoded {
2165            WireMessage::Auth(req) => assert_eq!(req.token, "secret-token"),
2166            _ => panic!("Expected Auth"),
2167        }
2168
2169        let resp = WireMessage::AuthResponse(AuthResponse {
2170            authenticated: true,
2171            error: None,
2172        });
2173        let bytes = shape_wire::encode_message(&resp).expect("encode AuthResponse");
2174        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode AuthResponse");
2175        match decoded {
2176            WireMessage::AuthResponse(r) => {
2177                assert!(r.authenticated);
2178                assert!(r.error.is_none());
2179            }
2180            _ => panic!("Expected AuthResponse"),
2181        }
2182    }
2183
2184    #[test]
2185    fn test_validate_roundtrip() {
2186        let msg = WireMessage::Validate(ValidateRequest {
2187            code: "let x = 1".to_string(),
2188            request_id: 99,
2189        });
2190        let bytes = shape_wire::encode_message(&msg).expect("encode Validate");
2191        let decoded: WireMessage = shape_wire::decode_message(&bytes).expect("decode Validate");
2192        match decoded {
2193            WireMessage::Validate(req) => {
2194                assert_eq!(req.code, "let x = 1");
2195                assert_eq!(req.request_id, 99);
2196            }
2197            _ => panic!("Expected Validate"),
2198        }
2199
2200        let resp = WireMessage::ValidateResponse(ValidateResponse {
2201            request_id: 99,
2202            success: false,
2203            diagnostics: vec![WireDiagnostic {
2204                severity: "error".to_string(),
2205                message: "parse error".to_string(),
2206                line: None,
2207                column: None,
2208            }],
2209        });
2210        let bytes = shape_wire::encode_message(&resp).expect("encode ValidateResponse");
2211        let decoded: WireMessage =
2212            shape_wire::decode_message(&bytes).expect("decode ValidateResponse");
2213        match decoded {
2214            WireMessage::ValidateResponse(r) => {
2215                assert_eq!(r.request_id, 99);
2216                assert!(!r.success);
2217                assert_eq!(r.diagnostics.len(), 1);
2218            }
2219            _ => panic!("Expected ValidateResponse"),
2220        }
2221    }
2222
2223    #[test]
2224    fn test_ping_framing_roundtrip() {
2225        use shape_wire::transport::framing::{decode_framed, encode_framed};
2226
2227        let ping = WireMessage::Ping(PingRequest {});
2228        let mp = shape_wire::encode_message(&ping).expect("encode Ping");
2229        eprintln!("Ping msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
2230
2231        let framed = encode_framed(&mp);
2232        eprintln!("Framed bytes ({} bytes): {:02x?}", framed.len(), &framed);
2233
2234        let decompressed = decode_framed(&framed).expect("decode_framed");
2235        assert_eq!(mp, decompressed, "framing roundtrip should preserve bytes");
2236
2237        let decoded: WireMessage =
2238            shape_wire::decode_message(&decompressed).expect("decode Ping after framing");
2239        assert!(matches!(decoded, WireMessage::Ping(_)));
2240    }
2241
2242    #[test]
2243    fn test_execute_framing_roundtrip() {
2244        use shape_wire::transport::framing::{decode_framed, encode_framed};
2245
2246        let exec = WireMessage::Execute(ExecuteRequest {
2247            code: "42".to_string(),
2248            request_id: 1,
2249        });
2250        let mp = shape_wire::encode_message(&exec).expect("encode Execute");
2251        eprintln!("Execute msgpack bytes ({} bytes): {:02x?}", mp.len(), &mp);
2252
2253        let framed = encode_framed(&mp);
2254        let decompressed = decode_framed(&framed).expect("decode_framed");
2255        let decoded: WireMessage =
2256            shape_wire::decode_message(&decompressed).expect("decode Execute after framing");
2257        match decoded {
2258            WireMessage::Execute(req) => {
2259                assert_eq!(req.code, "42");
2260                assert_eq!(req.request_id, 1);
2261            }
2262            _ => panic!("Expected Execute"),
2263        }
2264    }
2265
2266    // ---- Phase 3B: Sidecar extraction tests ----
2267
2268    #[test]
2269    fn test_extract_sidecars_no_large_blobs() {
2270        let store = temp_store();
2271        let mut args = vec![
2272            SerializableVMValue::Int(42),
2273            SerializableVMValue::String("hello".to_string()),
2274            SerializableVMValue::Array(vec![
2275                SerializableVMValue::Number(1.0),
2276                SerializableVMValue::Number(2.0),
2277            ]),
2278        ];
2279        let sidecars = extract_sidecars(&mut args, &store);
2280        assert!(sidecars.is_empty(), "no large blobs → no sidecars");
2281        // Args should be unchanged
2282        assert!(matches!(args[0], SerializableVMValue::Int(42)));
2283    }
2284
2285    // Sidecar extraction/reassembly tests that constructed input via the
2286    // deleted `ValueWord::from_float_array` + `nanboxed_to_serializable`
2287    // pair (`test_extract_sidecars_large_typed_array`,
2288    // `test_reassemble_sidecars_roundtrip`,
2289    // `test_extract_sidecars_nested_in_array`) belong to the Phase-2c
2290    // typed-module-exports rebuild. Re-author once the kind-threaded
2291    // `slot_to_serializable` round-trip lands. Pure
2292    // `SerializableVMValue`-shaped sidecar coverage is preserved below
2293    // (`test_extract_sidecars_no_large_blobs`,
2294    // `test_sidecar_ref_serialization_roundtrip`) — those exercise
2295    // `extract_sidecars` / `reassemble_sidecars` without crossing the
2296    // slot boundary.
2297
2298    #[test]
2299    fn test_sidecar_ref_serialization_roundtrip() {
2300        use shape_runtime::hashing::HashDigest;
2301        use shape_runtime::snapshot::{BlobKind, TypedArrayElementKind};
2302
2303        let value = SerializableVMValue::SidecarRef {
2304            sidecar_id: 7,
2305            blob_kind: BlobKind::TypedArray(TypedArrayElementKind::F64),
2306            original_hash: HashDigest::from_hex("abc123"),
2307            meta_a: 1000,
2308            meta_b: 0,
2309        };
2310
2311        let bytes = shape_wire::encode_message(&value).expect("encode SidecarRef");
2312        let decoded: SerializableVMValue =
2313            shape_wire::decode_message(&bytes).expect("decode SidecarRef");
2314        match decoded {
2315            SerializableVMValue::SidecarRef { sidecar_id, .. } => {
2316                assert_eq!(sidecar_id, 7);
2317            }
2318            _ => panic!("Expected SidecarRef"),
2319        }
2320    }
2321
2322    // ---- Blob negotiation integration tests ----
2323
2324    #[test]
2325    fn test_negotiate_blobs_returns_known_hashes() {
2326        let h1 = mk_hash(1);
2327        let h2 = mk_hash(2);
2328        let h3 = mk_hash(3);
2329
2330        let mut cache = RemoteBlobCache::new(10);
2331        cache.insert(h1, mk_blob("f1", h1, vec![]));
2332        cache.insert(h3, mk_blob("f3", h3, vec![]));
2333
2334        let blobs = vec![
2335            (h1, mk_blob("f1", h1, vec![])),
2336            (h2, mk_blob("f2", h2, vec![])),
2337            (h3, mk_blob("f3", h3, vec![])),
2338        ];
2339        let response = negotiate_blobs(&blobs, &cache);
2340        assert_eq!(response.known_hashes.len(), 2);
2341        assert!(response.known_hashes.contains(&h1));
2342        assert!(response.known_hashes.contains(&h3));
2343        assert!(!response.known_hashes.contains(&h2));
2344    }
2345
2346    #[test]
2347    fn test_build_call_request_with_negotiation_strips_known() {
2348        let h1 = mk_hash(1);
2349        let h2 = mk_hash(2);
2350        let blob1 = mk_blob("entry", h1, vec![h2]);
2351        let blob2 = mk_blob("helper", h2, vec![]);
2352
2353        let mut function_store = HashMap::new();
2354        function_store.insert(h1, blob1.clone());
2355        function_store.insert(h2, blob2.clone());
2356
2357        let mut program = BytecodeProgram::default();
2358        program.content_addressed = Some(Program {
2359            entry: h1,
2360            function_store,
2361            top_level_locals_count: 0,
2362            top_level_local_storage_hints: Vec::new(),
2363            module_binding_names: Vec::new(),
2364            module_binding_storage_hints: Vec::new(),
2365            function_local_storage_hints: Vec::new(),
2366            top_level_frame: None,
2367            top_level_local_concrete_types: Vec::new(),
2368            function_local_concrete_types: Vec::new(),
2369            function_return_concrete_types: Vec::new(),
2370            monomorphized_method_call_sites: HashMap::new(),
2371            value_call_return_concrete_types: HashMap::new(),
2372            operator_trait_dispatch_sites: HashMap::new(),
2373            data_schema: None,
2374            type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2375            trait_method_symbols: HashMap::new(),
2376            foreign_functions: Vec::new(),
2377            native_struct_layouts: Vec::new(),
2378            debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2379            closure_function_layouts_by_name: HashMap::new(),
2380            trait_vtables: HashMap::new(),
2381            has_imported_const_inline: false,
2382            has_w17_marshal_residual: false,
2383        });
2384        program.functions = vec![crate::bytecode::Function {
2385            name: "entry".to_string(),
2386            arity: 0,
2387            param_names: vec![],
2388            locals_count: 0,
2389            entry_point: 0,
2390            body_length: 0,
2391            is_closure: false,
2392            captures_count: 0,
2393            is_async: false,
2394            ref_params: vec![],
2395            ref_mutates: vec![],
2396            mutable_captures: vec![],
2397            frame_descriptor: None,
2398            osr_entry_points: vec![],
2399            mir_data: None,
2400        }];
2401        program.function_blob_hashes = vec![Some(h1)];
2402
2403        // Cache has h2 -> negotiation should strip it
2404        let mut cache = RemoteBlobCache::new(10);
2405        cache.insert(h2, blob2.clone());
2406
2407        let req = build_call_request_with_negotiation(&program, "entry", vec![], Some(&cache));
2408        let blobs = req.function_blobs.as_ref().unwrap();
2409        assert_eq!(blobs.len(), 1, "should strip known blob h2");
2410        assert_eq!(blobs[0].0, h1, "only h1 should remain");
2411    }
2412
2413    #[test]
2414    fn test_build_call_request_with_negotiation_no_cache() {
2415        let h1 = mk_hash(1);
2416        let blob1 = mk_blob("entry", h1, vec![]);
2417
2418        let mut function_store = HashMap::new();
2419        function_store.insert(h1, blob1.clone());
2420
2421        let mut program = BytecodeProgram::default();
2422        program.content_addressed = Some(Program {
2423            entry: h1,
2424            function_store,
2425            top_level_locals_count: 0,
2426            top_level_local_storage_hints: Vec::new(),
2427            module_binding_names: Vec::new(),
2428            module_binding_storage_hints: Vec::new(),
2429            function_local_storage_hints: Vec::new(),
2430            top_level_frame: None,
2431            top_level_local_concrete_types: Vec::new(),
2432            function_local_concrete_types: Vec::new(),
2433            function_return_concrete_types: Vec::new(),
2434            monomorphized_method_call_sites: HashMap::new(),
2435            value_call_return_concrete_types: HashMap::new(),
2436            operator_trait_dispatch_sites: HashMap::new(),
2437            data_schema: None,
2438            type_schema_registry: shape_runtime::type_schema::TypeSchemaRegistry::new(),
2439            trait_method_symbols: HashMap::new(),
2440            foreign_functions: Vec::new(),
2441            native_struct_layouts: Vec::new(),
2442            debug_info: crate::bytecode::DebugInfo::new("<test>".to_string()),
2443            closure_function_layouts_by_name: HashMap::new(),
2444            trait_vtables: HashMap::new(),
2445            has_imported_const_inline: false,
2446            has_w17_marshal_residual: false,
2447        });
2448        program.functions = vec![crate::bytecode::Function {
2449            name: "entry".to_string(),
2450            arity: 0,
2451            param_names: vec![],
2452            locals_count: 0,
2453            entry_point: 0,
2454            body_length: 0,
2455            is_closure: false,
2456            captures_count: 0,
2457            is_async: false,
2458            ref_params: vec![],
2459            ref_mutates: vec![],
2460            mutable_captures: vec![],
2461            frame_descriptor: None,
2462            osr_entry_points: vec![],
2463            mir_data: None,
2464        }];
2465        program.function_blob_hashes = vec![Some(h1)];
2466
2467        // No cache -> all blobs sent
2468        let req = build_call_request_with_negotiation(&program, "entry", vec![], None);
2469        let blobs = req.function_blobs.as_ref().unwrap();
2470        assert_eq!(blobs.len(), 1, "all blobs should be sent when no cache");
2471    }
2472
2473    // ---- V2 handler stub tests ----
2474
2475    #[test]
2476    fn test_handle_wire_message_ping_returns_pong() {
2477        let store = temp_store();
2478        let mut cache = RemoteBlobCache::default_cache();
2479        let msg = WireMessage::Ping(PingRequest {});
2480        let response = handle_wire_message(msg, &store, &mut cache);
2481        match response {
2482            WireMessage::Pong(info) => {
2483                assert_eq!(info.wire_protocol, shape_wire::WIRE_PROTOCOL_V2);
2484                assert!(info.capabilities.contains(&"call".to_string()));
2485                assert!(info.capabilities.contains(&"blob-negotiation".to_string()));
2486            }
2487            _ => panic!("Expected Pong response"),
2488        }
2489    }
2490
2491    #[test]
2492    fn test_handle_wire_message_execute_returns_v2_stub() {
2493        let store = temp_store();
2494        let mut cache = RemoteBlobCache::default_cache();
2495        let msg = WireMessage::Execute(ExecuteRequest {
2496            code: "42".to_string(),
2497            request_id: 5,
2498        });
2499        let response = handle_wire_message(msg, &store, &mut cache);
2500        match response {
2501            WireMessage::ExecuteResponse(resp) => {
2502                assert_eq!(resp.request_id, 5);
2503                assert!(!resp.success);
2504                assert!(resp.error.as_ref().unwrap().contains("not yet implemented"));
2505            }
2506            _ => panic!("Expected ExecuteResponse"),
2507        }
2508    }
2509
2510    #[test]
2511    fn test_handle_wire_message_validate_returns_v2_stub() {
2512        let store = temp_store();
2513        let mut cache = RemoteBlobCache::default_cache();
2514        let msg = WireMessage::Validate(ValidateRequest {
2515            code: "let x = 1".to_string(),
2516            request_id: 10,
2517        });
2518        let response = handle_wire_message(msg, &store, &mut cache);
2519        match response {
2520            WireMessage::ValidateResponse(resp) => {
2521                assert_eq!(resp.request_id, 10);
2522                assert!(!resp.success);
2523                assert!(resp.diagnostics[0].message.contains("not yet implemented"));
2524            }
2525            _ => panic!("Expected ValidateResponse"),
2526        }
2527    }
2528
2529    #[test]
2530    fn test_handle_wire_message_auth_returns_v2_stub() {
2531        let store = temp_store();
2532        let mut cache = RemoteBlobCache::default_cache();
2533        let msg = WireMessage::Auth(AuthRequest {
2534            token: "test".to_string(),
2535        });
2536        let response = handle_wire_message(msg, &store, &mut cache);
2537        match response {
2538            WireMessage::AuthResponse(resp) => {
2539                assert!(!resp.authenticated);
2540                assert!(resp.error.as_ref().unwrap().contains("not yet implemented"));
2541            }
2542            _ => panic!("Expected AuthResponse"),
2543        }
2544    }
2545
2546    #[test]
2547    fn test_handle_wire_message_blob_negotiation() {
2548        let store = temp_store();
2549        let mut cache = RemoteBlobCache::new(10);
2550        let h1 = mk_hash(1);
2551        let h2 = mk_hash(2);
2552        cache.insert(h1, mk_blob("f1", h1, vec![]));
2553
2554        let msg = WireMessage::BlobNegotiation(BlobNegotiationRequest {
2555            offered_hashes: vec![h1, h2],
2556        });
2557        let response = handle_wire_message(msg, &store, &mut cache);
2558        match response {
2559            WireMessage::BlobNegotiationReply(resp) => {
2560                assert_eq!(resp.known_hashes.len(), 1);
2561                assert!(resp.known_hashes.contains(&h1));
2562            }
2563            _ => panic!("Expected BlobNegotiationReply"),
2564        }
2565    }
2566
2567    // Track A.2B: a closure payload serialised through the slot/serializable
2568    // round-trip and replayed through the receiver's
2569    // `closure_function_layouts` slice. The pre-bulldozer test
2570    // (`test_a2b_closure_arg_roundtrip_with_layouts`) constructed the
2571    // closure via deleted ValueWord constructors
2572    // (`from_f64` + `into_raw_bits` + `from_heap_value(HeapValue::ClosureRaw(_))`)
2573    // and round-tripped it through the deleted
2574    // `nanboxed_to_serializable` / `serializable_to_nanboxed_with_layouts`
2575    // pair. Re-author against the kind-threaded slot pipeline once the
2576    // Phase-2c snapshot rebuild lands (ADR-006 §2.7.4 + addendum). The
2577    // wire schema (`function_id: u32`, `type_id: u32`, `upvalues: Vec<…>`)
2578    // is preserved verbatim.
2579}