use std::{fmt::Debug, fs, path::PathBuf};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use subc_protocol::{
manifest::{
Bindings, Concurrency, ExecutionMode, IdentityBinding, IdentityScope, ModuleManifest,
ProviderRole, StorageBinding, StorageKind, StorageScope, Tool, TrustTier,
},
session::{ModuleControlPush, ModuleControlRequest, ModuleControlResponse},
BindIdentity, ErrorBody, ModuleHelloAckBody, ModuleHelloBody, RouteTarget, PROTOCOL_VERSION,
};
#[test]
fn protocol_wire_shapes_match_golden_json_and_round_trip() {
assert_golden("bind_identity", &bind_identity());
assert_golden(
"route_target_tool_provider",
&RouteTarget::ToolProvider {
module_id: "aft-tools".to_string(),
},
);
assert_golden(
"route_target_management_surface",
&RouteTarget::ManagementSurface {
module_id: "memory-mc".to_string(),
},
);
assert_golden(
"route_target_internal_service",
&RouteTarget::InternalService {
module_id: "llm-runner".to_string(),
service_id: "llm".to_string(),
},
);
assert_golden("error_body", &error_body());
assert_golden("module_hello_body", &module_hello_body());
assert_golden("module_hello_ack_body", &module_hello_ack_body());
assert_golden(
"module_control_request_route_bind",
&module_control_request(),
);
assert_golden(
"module_control_response_route_bind_ack",
&ModuleControlResponse::RouteBindAck {},
);
assert_golden(
"module_control_push_route_status",
&ModuleControlPush::RouteStatus {
route_channel: 42,
status: "indexing".to_string(),
},
);
}
fn assert_golden<T>(name: &str, value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + Debug,
{
let actual = serde_json::to_value(value).unwrap();
let path = golden_path(name);
if std::env::var_os("UPDATE_GOLDEN").is_some() {
fs::write(
&path,
format!("{}\n", serde_json::to_string_pretty(&actual).unwrap()),
)
.unwrap();
}
let expected: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
actual, expected,
"golden JSON drift for {name}; rerun with UPDATE_GOLDEN=1 only after updating the TS mirror"
);
let decoded: T = serde_json::from_value(expected).unwrap();
assert_eq!(
&decoded, value,
"golden JSON no longer decodes to canonical Rust {name}"
);
}
fn golden_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("golden")
.join(format!("{name}.json"))
}
fn bind_identity() -> BindIdentity {
BindIdentity {
project_root: PathBuf::from("/tmp/subc/project"),
harness: "opencode".to_string(),
session: "session-0001".to_string(),
}
}
fn error_body() -> ErrorBody {
ErrorBody {
code: "config_divergence".to_string(),
message: "active config differs".to_string(),
}
}
fn module_hello_body() -> ModuleHelloBody {
ModuleHelloBody {
manifest: module_manifest("aft-tools"),
protocol_ver: PROTOCOL_VERSION,
control_ops: Some(vec!["route.bind".to_string(), "route.status".to_string()]),
launch_nonce: None,
}
}
fn module_hello_ack_body() -> ModuleHelloAckBody {
ModuleHelloAckBody {
negotiated_ver: PROTOCOL_VERSION,
subc_ops: vec![
"server.describe".to_string(),
"catalog.list".to_string(),
"route.open".to_string(),
"route.poll".to_string(),
],
subc_capabilities: vec!["manifest_registration_v1".to_string()],
storage: None,
}
}
fn module_control_request() -> ModuleControlRequest {
ModuleControlRequest::RouteBind {
route_channel: 42,
target: RouteTarget::ToolProvider {
module_id: "aft-tools".to_string(),
},
identity: bind_identity(),
}
}
fn module_manifest(module_id: &str) -> ModuleManifest {
ModuleManifest {
module_id: module_id.to_string(),
module_version: "1.2.3".to_string(),
protocol_ver: PROTOCOL_VERSION,
trust_tier: TrustTier::FirstParty,
provides: vec![ProviderRole::ToolProvider {
tools: vec![Tool {
name: "memory.read".to_string(),
execution_mode: ExecutionMode::Pure,
schema: serde_json::json!({"type": "object", "required": ["id"]}),
}],
identity_scope: vec![IdentityScope::Project, IdentityScope::Session],
concurrency: Concurrency::ModuleManaged,
emits_push: true,
sub_supervises: true,
}],
consumes: Vec::new(),
scheduled_tasks: Vec::new(),
bindings: Bindings {
storage: StorageBinding {
kind: StorageKind::Sqlite,
scope: StorageScope::Project,
owns_schema: true,
},
vault_grants: Vec::new(),
identity: IdentityBinding {
requires: vec![IdentityScope::Project],
optional: vec![IdentityScope::Session],
},
},
}
}