use std::collections::BTreeSet;
use std::io::Write;
use std::process::{Command, Stdio};
use serde_json::json;
use harn_serve::adapters::acp::{
ACP_PROMPT_ERROR_DATA_SCHEMA, HARN_AGENT_EVENT_KINDS, HARN_AGENT_EVENT_METHOD,
HARN_CONTENT_EXTENSION_FIELDS, HARN_PROMPT_RESULT_EXTENSION_FIELDS,
HARN_PROVIDER_CATALOG_METHOD, HARN_SESSION_UPDATE_EXTENSIONS,
HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS,
};
use harn_vm::llm::receipts::{
TOOL_CALL_RECEIPT_SCHEMA_ARTIFACT, TOOL_CALL_RECEIPT_SCHEMA_VERSION, TOOL_CALL_RECEIPT_STATUSES,
};
use harn_vm::orchestration::SESSION_VIEW_QUERY_METHOD;
use harn_vm::session_timeline::{
SESSION_TIMELINE_QUERY_METHOD, SESSION_TIMELINE_SUBSCRIBE_METHOD,
SESSION_TIMELINE_UNSUBSCRIBE_METHOD,
};
use super::activity::ActivityVocabulary;
use super::connector_setup::ConnectorSetupVocabulary;
use super::constants::*;
use super::external_action::ExternalActionVocabulary;
use super::go::*;
use super::manifest::*;
use super::python::*;
use super::rust::*;
use super::support::*;
use super::swift::*;
use super::typescript::*;
use super::values::*;
use super::*;
#[rustfmt::skip]
#[path = "../../../../../spec/protocol-artifacts/harn-protocol.rs"]
mod generated_rust_binding;
mod external_action_roundtrip;
mod llm_outcome_vocabulary;
mod open_vocabulary_projection;
mod plan;
mod prepared_session;
mod session_recap;
mod session_update_payloads;
fn protocol_source() -> ProtocolArtifactSource {
ProtocolArtifactSource::from_anchor(std::path::Path::new(env!("CARGO_MANIFEST_DIR")))
.expect("harn-cli is compiled from the Harn workspace")
}
#[test]
fn runtime_wire_values_reach_generated_bindings() {
let common: BTreeSet<String> = ACP_AGENT_METHODS
.iter()
.chain(HARN_SESSION_UPDATE_EXTENSIONS.iter())
.chain(HARN_AGENT_EVENT_KINDS.iter())
.map(|value| (*value).to_owned())
.chain([HARN_PROVIDER_CATALOG_METHOD.to_owned()])
.collect();
assert!(!common.is_empty());
for (language, artifact) in [
("Rust", generate_rust_for_tests()),
("Swift", generate_swift_for_tests()),
("TypeScript", generate_typescript_for_tests()),
("Python", generate_python()),
("Go", generate_go()),
] {
let mut values = common.clone();
if matches!(language, "Swift" | "Python" | "Go") {
values.extend(worker_status_values());
}
if language == "Rust" {
values.extend(
ACP_DISPATCHED_METHODS
.iter()
.chain(ACP_TRANSPORT_CONTROL_METHODS.iter())
.chain(HARN_SESSION_TIMELINE_METHODS.iter())
.chain(ACP_CLIENT_METHODS.iter())
.chain(HARN_CONTENT_EXTENSION_FIELDS.iter())
.chain(HARN_PROMPT_RESULT_EXTENSION_FIELDS.iter())
.map(|value| (*value).to_owned()),
);
values.extend(all_acp_session_updates());
}
if matches!(language, "Rust" | "TypeScript") {
values.extend(
HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS
.iter()
.map(|value| (*value).to_owned()),
);
}
if language == "Swift" {
values.extend(
tool_kind_values()
.into_iter()
.chain(tool_call_status_values())
.chain(tool_call_error_category_values())
.chain(tool_mutation_status_values())
.chain(
TOOL_CALL_RECEIPT_STATUSES
.iter()
.map(|value| (*value).to_owned()),
),
);
}
if matches!(language, "Swift" | "TypeScript") {
values.extend(
agent_terminal_class_values()
.into_iter()
.chain(agent_terminal_kind_values()),
);
}
for value in &values {
assert!(
artifact.contains(value),
"{language} omitted wire value {value}"
);
}
}
}
#[test]
fn tool_annotations_project_typed_completion_evidence_roles_to_every_binding() {
assert_eq!(
completion_evidence_role_values(),
vec![
"observation".to_string(),
"mutation".to_string(),
"verification".to_string(),
]
);
for (binding, type_declaration, field_declaration) in [
(
generate_typescript_for_tests(),
"export type HarnCompletionEvidenceRole =",
"completion_evidence_role?: HarnCompletionEvidenceRole",
),
(
generate_swift_for_tests(),
"public enum HarnCompletionEvidenceRole:",
"public var completionEvidenceRole: HarnCompletionEvidenceRole?",
),
(
generate_python(),
"class HarnCompletionEvidenceRole(str, Enum):",
"completion_evidence_role: Optional[HarnCompletionEvidenceRole] = None",
),
(
generate_go(),
"type HarnCompletionEvidenceRole string",
"CompletionEvidenceRole *HarnCompletionEvidenceRole `json:\"completion_evidence_role,omitempty\"`",
),
(
generate_rust_for_tests(),
"pub enum HarnCompletionEvidenceRole {",
"pub completion_evidence_role: Option<HarnCompletionEvidenceRole>",
),
] {
assert!(
binding.contains(type_declaration),
"binding omitted typed completion-evidence vocabulary: {type_declaration}"
);
assert!(
binding.contains(field_declaration),
"binding omitted optional typed annotation field: {field_declaration}"
);
for role in completion_evidence_role_values() {
assert!(binding.contains(&role), "binding omitted role `{role}`");
}
}
}
#[test]
fn generated_bindings_preserve_compatibility_annotations() {
let ts = generate_typescript_for_tests();
assert!(ts.contains("export type JsonRpcId = number | string | null"));
assert!(ts.contains("export const MCP_PROTOCOL_VERSION = \"2026-07-28\""));
assert!(ts.contains("application_type: MCPOAuthApplicationType"));
assert!(ts.contains("server/discover"));
assert!(ts.contains("io.modelcontextprotocol/protocolVersion"));
assert!(ts.contains("MCP-Protocol-Version"));
assert!(ts.contains("ttlMs"));
assert!(ts.contains("cacheScope"));
assert!(ts.contains("sessionClose: \"session/close\""));
assert!(ts.contains("@deprecated Use session/close; session/stop will be removed"));
let swift = generate_swift_for_tests();
assert!(swift.contains("mcpProtocolVersion = \"2026-07-28\""));
assert!(swift.contains("case applicationType = \"application_type\""));
assert!(swift.contains("case protocolVersion = \"io.modelcontextprotocol/protocolVersion\""));
assert!(swift.contains("case protocolVersion = \"MCP-Protocol-Version\""));
assert!(swift.contains("case sessionClose = \"session/close\""));
assert!(swift.contains("@available(*, deprecated"));
assert!(swift.contains("public static let allCases: [Self]"));
assert!(swift.contains("public var id: HarnJsonRpcId"));
assert!(swift.contains("public init?(jsonObject: Any)"));
assert!(swift.contains("case let value as NSNumber: return jsonNumber(value)"));
assert!(swift.contains("CFGetTypeID(value) == CFBooleanGetTypeID()"));
assert!(swift.contains("public static func success(id: HarnJsonRpcId"));
assert!(ts.contains("harnPlanDocument?: HarnPlanDocument"));
assert!(ts.contains("content?: ACPToolCallContent[]"));
}
#[test]
fn swift_and_rust_publish_identical_acp_method_vocabularies() {
fn generated_json_string_array(artifact: &str, start: &str, end: &str) -> BTreeSet<String> {
let (_, after_start) = artifact
.split_once(start)
.unwrap_or_else(|| panic!("generated artifact missing {start:?}"));
let (body, _) = after_start
.split_once(end)
.unwrap_or_else(|| panic!("generated artifact missing {end:?} after {start:?}"));
body.lines()
.filter_map(|line| line.trim().strip_suffix(','))
.filter_map(|literal| serde_json::from_str::<String>(literal).ok())
.collect()
}
let swift = generate_swift_for_tests();
let rust = generate_rust_for_tests();
for vocabulary in acp_method_vocabularies() {
let (_, swift_enum) = swift
.split_once(&format!(
"public enum {}: String, Codable, Sendable, CaseIterable {{\n",
vocabulary.swift_enum_name
))
.unwrap_or_else(|| {
panic!(
"Swift artifact missing method vocabulary {}",
vocabulary.swift_enum_name
)
});
let swift_values = generated_json_string_array(
swift_enum,
" public static let allCases: [Self] = [\n",
" ].map { Self(rawValue: $0)! }",
);
let rust_values = generated_json_string_array(
&rust,
&format!("pub const {}: &[&str] = &[\n", vocabulary.rust_slice_name),
"];",
);
assert_eq!(
swift_values, rust_values,
"Swift {} drifted from Rust {}",
vocabulary.swift_enum_name, vocabulary.rust_slice_name
);
}
assert!(swift.contains("case workflowPause = \"workflow/pause\""));
assert!(swift.contains("case workflowResume = \"workflow/resume\""));
}
#[test]
fn external_action_vocabulary_projects_to_every_supported_host() {
let vocabulary = ExternalActionVocabulary::load(&protocol_source()).unwrap();
let setup = ConnectorSetupVocabulary::load(&protocol_source()).unwrap();
let activity = ActivityVocabulary::load(&protocol_source()).unwrap();
let ts = generate_typescript(&vocabulary, &setup, &activity);
let swift = generate_swift(&vocabulary, &setup, &activity);
let rust = generate_rust(&vocabulary, &setup, &activity);
assert!(ts.contains("isExternalActionActivityStatusTerminal"));
assert!(ts.contains("canExternalActionActivityStatusAdvance"));
assert!(swift.contains("var isTerminal: Bool"));
assert!(swift.contains("func canAdvance(to next: Self) -> Bool"));
assert!(rust.contains("pub const fn is_terminal(self) -> bool"));
assert!(rust.contains("pub const fn can_advance_to(self, next: Self) -> bool"));
for value in vocabulary
.projections()
.iter()
.flat_map(|(_, _, values)| *values)
{
for (host, generated) in [("TypeScript", &ts), ("Swift", &swift), ("Rust", &rust)] {
assert!(
generated.contains(value),
"{host} projection omitted external-action value `{value}`"
);
}
}
}
#[test]
fn generic_permission_activity_projects_to_every_supported_host() {
let actions = ExternalActionVocabulary::load(&protocol_source()).unwrap();
let setup = ConnectorSetupVocabulary::load(&protocol_source()).unwrap();
let activity = ActivityVocabulary::load(&protocol_source()).unwrap();
let generated = [
(
"TypeScript",
generate_typescript(&actions, &setup, &activity),
),
("Swift", generate_swift(&actions, &setup, &activity)),
("Rust", generate_rust(&actions, &setup, &activity)),
];
for value in activity
.kinds
.iter()
.chain(&activity.permission_outcomes)
.chain(&activity.permission_deciders)
.chain(&activity.permission_policy_layers)
.chain(&activity.permission_policy_outcomes)
.chain(&activity.permission_grant_scopes)
.chain(&activity.permission_grant_expiries)
{
for (host, artifact) in &generated {
assert!(
artifact.contains(value),
"{host} projection omitted activity value `{value}`"
);
}
}
for (_, artifact) in &generated {
assert!(artifact.contains("harn.tool_permission_decision.v1"));
assert!(artifact.contains("policy_evaluations"));
}
}
#[test]
fn adding_external_action_values_updates_all_host_projections() {
let vocabulary = ExternalActionVocabulary {
records: ExternalActionVocabulary::load_for_tests().records,
outcomes: vec!["confirmed".into(), "future_outcome".into()],
receipt_statuses: vec!["confirmed".into(), "future_status".into()],
next_actions: vec!["none".into(), "future_action".into()],
environments: vec!["test".into(), "future_environment".into()],
authorization_methods: vec!["manual".into(), "future_authorization".into()],
authentication_assurances: vec!["session".into(), "future_assurance".into()],
disclosure_sources: vec!["user_profile".into(), "future_source".into()],
error_kinds: vec!["invalid_intent".into(), "future_error".into()],
protected_field_classes: vec!["legal_identity".into(), "future_field".into()],
passenger_genders: vec!["m".into(), "future_gender".into()],
activity_statuses: vec!["proposed".into(), "future_activity".into()],
progress_activity_statuses: vec!["proposed".into()],
terminal_activity_statuses: vec!["future_activity".into()],
policy_layers: vec!["user_policy".into(), "future_layer".into()],
policy_evaluation_outcomes: vec!["allowed".into(), "future_evaluation".into()],
decision_outcomes: vec!["approved".into(), "future_decision".into()],
deciders: vec!["person".into(), "future_decider".into()],
reconciliation_statuses: vec!["not_needed".into(), "future_reconciliation".into()],
};
let setup = ConnectorSetupVocabulary::load(&protocol_source()).unwrap();
let activity = ActivityVocabulary::load(&protocol_source()).unwrap();
for generated in [
generate_typescript(&vocabulary, &setup, &activity),
generate_swift(&vocabulary, &setup, &activity),
generate_rust(&vocabulary, &setup, &activity),
] {
for future_value in [
"future_outcome",
"future_status",
"future_action",
"future_environment",
"future_authorization",
"future_assurance",
"future_source",
"future_error",
"future_field",
"future_gender",
"future_activity",
"future_layer",
"future_evaluation",
"future_decision",
"future_decider",
"future_reconciliation",
] {
assert!(generated.contains(future_value));
}
}
}
#[test]
fn complete_external_action_activity_projects_without_sensitive_values() {
let actions = ExternalActionVocabulary::load(&protocol_source()).unwrap();
let setup = ConnectorSetupVocabulary::load(&protocol_source()).unwrap();
let activity = ActivityVocabulary::load(&protocol_source()).unwrap();
for generated in [
generate_typescript(&actions, &setup, &activity),
generate_swift(&actions, &setup, &activity),
generate_rust(&actions, &setup, &activity),
] {
for required in [
"harn.external_action_activity.v1",
"harn.external_action_receipt.v1",
] {
assert!(
generated.contains(required),
"projection omitted {required}"
);
}
for forbidden in [
"protected_values",
"reusable_grant",
"credential_value",
"provider_payload",
] {
assert!(
!generated.contains(forbidden),
"projection made sensitive field `{forbidden}` representable"
);
}
}
}
#[test]
fn connector_setup_vocabulary_projects_to_every_supported_host() {
let actions = ExternalActionVocabulary::load(&protocol_source()).unwrap();
let setup = ConnectorSetupVocabulary::load(&protocol_source()).unwrap();
let activity = ActivityVocabulary::load(&protocol_source()).unwrap();
assert_eq!(
setup.stages,
crate::commands::connect::setup_events::CONNECTOR_SETUP_STAGES
);
assert_eq!(
setup.statuses,
crate::commands::connect::setup_events::CONNECTOR_SETUP_STATUSES
);
assert_eq!(
setup.interactions,
crate::commands::connect::setup_events::CONNECTOR_SETUP_INTERACTIONS
);
assert_eq!(
setup.configuration_fields,
crate::package::ConnectorSetupConfigurationField::WIRE_VALUES
);
assert_eq!(
setup.error_codes,
crate::commands::connect::setup_events::CONNECTOR_SETUP_ERROR_CODES
);
let generated = [
(
"TypeScript",
generate_typescript(&actions, &setup, &activity),
),
("Swift", generate_swift(&actions, &setup, &activity)),
("Rust", generate_rust(&actions, &setup, &activity)),
];
for value in setup
.stages
.iter()
.chain(&setup.statuses)
.chain(&setup.interactions)
.chain(&setup.configuration_fields)
.chain(&setup.error_codes)
{
for (host, artifact) in &generated {
assert!(
artifact.contains(value),
"{host} projection omitted connector-setup value `{value}`"
);
}
}
}
#[test]
fn generated_rust_external_action_enum_rejects_unknown_values() {
let unknown = serde_json::from_str::<generated_rust_binding::HarnExternalActionOutcome>(
"\"future_terminal\"",
);
assert!(unknown.is_err());
let rejected =
serde_json::from_str::<generated_rust_binding::HarnExternalActionOutcome>("\"rejected\"")
.unwrap();
assert_eq!(rejected.as_str(), "rejected");
}
#[test]
fn generated_external_action_progress_is_monotonic() {
use generated_rust_binding::HarnExternalActionActivityStatus as Status;
assert!(Status::Proposed.can_advance_to(Status::ApprovalPending));
assert!(Status::Proposed.can_advance_to(Status::DispatchPending));
assert!(Status::DispatchPending.can_advance_to(Status::ReconciliationRequired));
assert!(Status::DispatchPending.can_advance_to(Status::Confirmed));
assert!(!Status::DispatchPending.can_advance_to(Status::ApprovalPending));
assert!(!Status::Confirmed.can_advance_to(Status::ReconciliationRequired));
assert!(Status::Confirmed.can_advance_to(Status::Confirmed));
}
#[test]
fn swift_case_name_emits_valid_identifiers() {
assert_eq!(swift_case_name("private"), "`private`");
assert_eq!(swift_case_name("public"), "`public`");
assert_eq!(swift_case_name("class"), "`class`");
assert_eq!(swift_case_name("application_type"), "applicationType");
assert_eq!(swift_case_name("session/close"), "sessionClose");
assert_eq!(
swift_case_name("harn.acp.prompt_error.v1"),
"harnAcpPromptErrorV1"
);
assert_eq!(
swift_case_name("session:prompt-error"),
"sessionPromptError"
);
assert_eq!(swift_case_name("private_room"), "privateRoom");
let swift = generate_swift_for_tests();
assert!(
!swift.contains("case private = "),
"Swift artifact contains unescaped `case private = ...`"
);
assert!(
!swift.contains("case public = "),
"Swift artifact contains unescaped `case public = ...`"
);
assert!(swift.contains("case `private` = \"private\""));
assert!(swift.contains("case `public` = \"public\""));
}
#[test]
fn generated_rust_preserves_wire_constants_and_provenance() {
let rust = generate_rust_for_tests();
assert!(
rust.ends_with('\n'),
"Rust artifact must end with a newline"
);
assert!(
!rust.ends_with("\n\n"),
"Rust artifact must not emit a trailing blank line"
);
assert!(
rust.starts_with("// GENERATED by `harn dump-protocol-artifacts` - do not edit by hand."),
"Rust artifact missing provenance header"
);
use generated_rust_binding as binding;
for (actual, expected) in [
(binding::HARN_AGENT_EVENT_METHOD, HARN_AGENT_EVENT_METHOD),
(
binding::HARN_PROVIDER_CATALOG_METHOD,
HARN_PROVIDER_CATALOG_METHOD,
),
(binding::ACP_AGENT_METHOD_SESSION_PROMPT, "session/prompt"),
(
binding::HARN_CONTENT_EXTENSION_FIELD_PERMISSION_PREVIEW,
"permission_preview",
),
(
binding::HARN_CONTENT_EXTENSION_FIELD_VISIBLE_TEXT,
"visible_text",
),
(
binding::HARN_CONTENT_EXTENSION_FIELD_VISIBLE_DELTA,
"visible_delta",
),
(
binding::ACP_DISPATCHED_METHOD_HARN_HITL_RESPOND,
"harn.hitl.respond",
),
(binding::ACP_DISPATCHED_METHOD_AGENT_RESUME, "agent/resume"),
] {
assert_eq!(actual, expected);
}
}
#[test]
fn generated_bindings_expose_one_open_session_timeline_contract() {
let rust = generate_rust_for_tests();
let swift = generate_swift_for_tests();
let typescript = generate_typescript_for_tests();
let python = generate_python();
let go = generate_go();
for type_name in [
"HarnSessionTimelineCursor",
"HarnSessionTimelineQuery",
"HarnSessionTimelineReference",
"HarnSessionTimelineLink",
"HarnSessionTimelineNode",
"HarnSessionTimelineCoverage",
"HarnSessionTimelineSnapshot",
"HarnSessionTimelineUpdate",
] {
assert!(rust.contains(&format!("struct {type_name}")));
assert!(swift.contains(&format!("struct {type_name}")));
assert!(typescript.contains(&format!("interface {type_name}")));
assert!(python.contains(&format!("class {type_name}")));
assert!(go.contains(&format!("type {type_name} struct")));
}
assert!(rust.contains("pub kind: String"));
assert!(swift.contains("public var kind: String"));
assert!(typescript.contains("kind: string"));
}
#[test]
fn generated_rust_permission_shapes_round_trip() {
use generated_rust_binding::{
ACPPermissionOptionKind, ACPPermissionOutcome, ACPSessionRequestPermissionParams,
ACPSessionRequestPermissionResult,
};
let fixture: serde_json::Value = serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../conformance/protocols/fixtures/acp/session_request_permission.valid.json"
)))
.expect("canonical permission fixture parses");
let request = fixture["documents"][0]["params"].clone();
let decoded: ACPSessionRequestPermissionParams =
serde_json::from_value(request.clone()).expect("generated permission request decodes");
assert_eq!(decoded.session_id, "session-1");
assert_eq!(decoded.tool_call.tool_call_id, "tool-1");
assert_eq!(decoded.options[0].kind, ACPPermissionOptionKind::AllowOnce);
assert_eq!(
serde_json::to_value(decoded).expect("generated permission request encodes"),
request
);
let result = fixture["documents"][1]["result"].clone();
let decoded: ACPSessionRequestPermissionResult =
serde_json::from_value(result.clone()).expect("generated permission result decodes");
assert_eq!(
decoded.outcome,
ACPPermissionOutcome::Selected {
option_id: "allow".to_string()
}
);
assert_eq!(
serde_json::to_value(decoded).expect("generated permission result encodes"),
result
);
}
#[test]
fn generated_rust_agent_event_shapes_round_trip() {
use generated_rust_binding::{
HarnAgentEventKind, HarnAgentEventNotification, HarnAgentEventParams,
};
let fixture: serde_json::Value = serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../conformance/protocols/fixtures/acp/agent_event_ext_notifications.valid.json"
)))
.expect("canonical agent-event fixture parses");
let mut notification = fixture["documents"]
.as_array()
.expect("agent-event documents")
.iter()
.find(|document| document["params"]["kind"] == "iteration_end")
.expect("iteration_end fixture")
.clone();
notification["_harn"] = json!({"replayed": true});
let decoded: HarnAgentEventNotification =
serde_json::from_value(notification.clone()).expect("generated agent event decodes");
assert_eq!(decoded.params.kind, HarnAgentEventKind::IterationEnd);
assert_eq!(decoded.params.fields["iteration"], json!(0));
assert_eq!(decoded.fields["_harn"]["replayed"], json!(true));
assert_eq!(
serde_json::to_value(decoded).expect("generated agent event encodes"),
notification
);
let future = json!({
"sessionId": "session-2",
"kind": "future_agent_event",
"payload": {"kept": true}
});
let decoded: HarnAgentEventParams =
serde_json::from_value(future.clone()).expect("unknown agent event kind decodes");
assert_eq!(
decoded.kind,
HarnAgentEventKind::Other("future_agent_event".to_string())
);
assert_eq!(
serde_json::to_value(decoded).expect("unknown agent event kind encodes"),
future
);
}
#[test]
fn rust_const_name_sanitizes_wire_values() {
assert_eq!(
rust_const_name("ACP_AGENT_METHOD", "session/prompt"),
"ACP_AGENT_METHOD_SESSION_PROMPT"
);
assert_eq!(
rust_const_name("ACP_DISPATCHED_METHOD", "harn.workflow.signal"),
"ACP_DISPATCHED_METHOD_HARN_WORKFLOW_SIGNAL"
);
assert_eq!(
rust_const_name("ACP_CLIENT_METHOD", "fs/read_text_file"),
"ACP_CLIENT_METHOD_FS_READ_TEXT_FILE"
);
assert_eq!(rust_const_name("X", "/"), "X");
assert_eq!(rust_const_name("V", "2026-07-28"), "V_2026_07_28");
assert_eq!(rust_type_name("iteration_end"), "IterationEnd");
assert_eq!(rust_type_name("mcp_auth_required"), "McpAuthRequired");
}
#[test]
fn dispatched_acp_methods_match_artifact() {
let dispatch = protocol_source()
.read_text("crates/harn-serve/src/adapters/acp/dispatch.rs")
.expect("read acp adapter");
let body = dispatch
.split_once("match method.as_str() {")
.expect("dispatch match block")
.1
.split_once("\n _ => {")
.expect("dispatch wildcard arm")
.0;
let mut dispatched = BTreeSet::new();
for line in body.lines() {
let trimmed = line.trim();
if !trimmed.contains("=>") || !trimmed.starts_with('"') {
if trimmed.contains("=>") {
let method = dispatch_arm_constant_value(trimmed).unwrap_or_else(|| {
panic!(
"constant-based ACP dispatch arm is not resolved by the protocol artifact guard: {trimmed}"
)
});
dispatched.insert(method);
}
continue;
}
let arm = trimmed.split("=>").next().unwrap_or("");
for literal in arm.split('|') {
let name = literal.trim().trim_matches('"');
if !name.is_empty() {
dispatched.insert(name.to_string());
}
}
}
let published: BTreeSet<String> = ACP_DISPATCHED_METHODS
.iter()
.map(|m| m.to_string())
.collect();
assert_eq!(
published,
dispatched,
"ACP_DISPATCHED_METHODS is out of sync with the ACP adapter dispatch arms.\n\
missing from artifact: {:?}\n\
stale in artifact: {:?}",
dispatched.difference(&published).collect::<Vec<_>>(),
published.difference(&dispatched).collect::<Vec<_>>(),
);
}
fn dispatch_arm_constant_value(trimmed_arm: &str) -> Option<String> {
let name = trimmed_arm.split("=>").next()?.trim();
match name {
"HARN_PROVIDER_CATALOG_METHOD" => Some(HARN_PROVIDER_CATALOG_METHOD.to_string()),
"ACP_METHOD_SESSION_PLAN_DOCUMENT_MUTATE" => {
Some(ACP_METHOD_SESSION_PLAN_DOCUMENT_MUTATE.to_string())
}
"harn_vm::session_timeline::SESSION_TIMELINE_QUERY_METHOD" => {
Some(SESSION_TIMELINE_QUERY_METHOD.to_string())
}
"harn_vm::session_timeline::SESSION_TIMELINE_SUBSCRIBE_METHOD" => {
Some(SESSION_TIMELINE_SUBSCRIBE_METHOD.to_string())
}
"harn_vm::session_timeline::SESSION_TIMELINE_UNSUBSCRIBE_METHOD" => {
Some(SESSION_TIMELINE_UNSUBSCRIBE_METHOD.to_string())
}
"harn_vm::session_recap::SESSION_RECAP_QUERY_METHOD" => {
Some(harn_vm::session_recap::SESSION_RECAP_QUERY_METHOD.to_string())
}
"harn_vm::orchestration::SESSION_VIEW_QUERY_METHOD" => {
Some(SESSION_VIEW_QUERY_METHOD.to_string())
}
_ => None,
}
}
#[test]
fn transport_control_acp_methods_match_artifact() {
let sessions = protocol_source()
.read_text("crates/harn-serve/src/adapters/acp/sessions.rs")
.expect("read acp sessions");
let body = sessions
.split_once("pub(super) fn apply_session_budget_rearm")
.expect("budget rearm function")
.1
.split_once("\nfn rearm_dimension")
.expect("budget rearm function end")
.0;
let mut handled = BTreeSet::new();
for capture in regex::Regex::new(r#""([^"]+)""#)
.unwrap()
.captures_iter(body)
{
let value = capture.get(1).unwrap().as_str();
if value.starts_with("session/") {
handled.insert(value.to_string());
}
}
let published: BTreeSet<String> = ACP_TRANSPORT_CONTROL_METHODS
.iter()
.map(|m| m.to_string())
.collect();
assert_eq!(
published, handled,
"ACP_TRANSPORT_CONTROL_METHODS is out of sync with transport pre-dispatch control frames"
);
}
#[test]
fn generated_python_includes_harn_wire_vocabularies() {
let py = generate_python();
assert!(py.contains("class HarnPlanDocument(_HarnDataclass):"));
assert!(py.contains("MCP_PROTOCOL_VERSION: str = \"2026-07-28\""));
assert!(py.contains("MCP_REQUIRED_METADATA_KEYS: tuple"));
assert!(py.contains("class MCPDiscoverResult(_HarnDataclass):"));
assert!(py.contains("class MCPInputRequiredResult(_HarnDataclass):"));
assert!(py.contains("class MCPCacheScope(str, Enum):"));
assert!(py.contains("class ACPSessionUpdate(str, Enum):"));
assert!(py.contains("class HarnToolCallErrorCategory(str, Enum):"));
assert!(py.contains("class HarnToolMutationStatus(str, Enum):"));
assert!(py.contains("class HarnWorkerStatus(str, Enum):"));
assert!(py.contains("changedPaths: Optional[List[str]] = None"));
assert!(py.contains("data: Optional[JsonValue] = None"));
assert!(py.contains("mutationStatus: Optional[HarnToolMutationStatus] = None"));
assert!(py.contains("class ToolCallReceipt(_HarnDataclass):"));
assert!(py.contains("class ToolCallReceiptStatus(str, Enum):"));
assert!(py.contains("class _HarnDataclass:"));
assert!(py.contains("def is_request("));
assert!(py.contains("class HarnACPPromptErrorData(_HarnDataclass):"));
assert!(py.contains("class HarnACPPromptResult(_HarnDataclass):"));
assert!(py.contains("class HarnAgentTerminalOutcome(_HarnDataclass):"));
assert!(py.contains("class AgentTerminalClass(str, Enum):"));
assert!(py.contains("class AgentTerminalKind(str, Enum):"));
}
#[test]
fn generated_go_includes_harn_wire_vocabularies() {
let go = generate_go();
assert!(go.contains("type HarnPlanDocument struct {"));
assert!(go.contains("package harnprotocol"));
assert!(go.contains("const MCPProtocolVersion = \"2026-07-28\""));
assert!(go.contains("type MCPRequestMeta struct"));
assert!(go.contains("type MCPDiscoverResult struct"));
assert!(go.contains("type MCPInputRequiredResult struct"));
assert!(go.contains("MCPUnsupportedProtocolVersionErrorCode"));
assert!(go.contains("type JSONRPCID struct"));
assert!(go.contains("type HarnToolMutationStatus string"));
assert!(go.contains("ChangedPaths"));
assert!(go.contains("MutationStatus"));
assert!(go.contains("type ACPSessionUpdateNotification struct"));
assert!(go.contains("func IsRequest(envelope map[string]json.RawMessage)"));
assert!(go.contains("type HarnWorkerStatus string"));
assert!(go.contains("var HarnWorkerStatuses = []HarnWorkerStatus"));
assert!(go.contains("type ToolCallReceipt struct"));
assert!(go.contains("var ToolCallReceiptStatuses = []ToolCallReceiptStatus"));
assert!(go.contains("type HarnACPPromptErrorData struct"));
assert!(go.contains("type HarnACPPromptResult struct"));
assert!(go.contains("type HarnAgentTerminalOutcome struct"));
assert!(go.contains("var AgentTerminalClasses = []AgentTerminalClass"));
assert!(go.contains("var AgentTerminalKinds = []AgentTerminalKind"));
}
#[test]
fn acp_prompt_error_schema_matches_runtime_terminal_classes() {
let schema: serde_json::Value = serde_json::from_str(
&protocol_source()
.read_text("conformance/protocols/schemas/acp-session-update.schema.json")
.expect("read ACP schema"),
)
.expect("parse ACP schema");
assert_eq!(
schema["$defs"]["HarnPromptErrorData"]["properties"]["schema"]["const"],
json!(ACP_PROMPT_ERROR_DATA_SCHEMA)
);
assert_eq!(
schema["$defs"]["HarnPromptErrorData"]["properties"]["terminalClass"]["enum"],
json!(agent_terminal_class_values())
);
assert_eq!(
schema["$defs"]["AgentTerminalOutcome"]["properties"]["kind"]["enum"],
json!(agent_terminal_kind_values())
);
assert_eq!(
schema["$defs"]["AgentTerminalOutcome"]["properties"]["owner"]["enum"],
json!(agent_terminal_owner_values())
);
assert_eq!(
schema["$defs"]["AgentTerminalOutcome"]["properties"]["message"]["type"],
"string"
);
assert_eq!(
schema["$defs"]["AgentTerminalOutcome"]["properties"]["detail"]["type"],
"string"
);
}
#[test]
fn go_struct_field_formatter_aligns_long_generated_fields() {
let raw = "\
type Example struct {
\tA string `json:\"a\"`
\tMutationStatus *HarnToolMutationStatus `json:\"mutationStatus,omitempty\"`
\tRaw json.RawMessage `json:\"raw\"`
}
";
let formatted = "\
type Example struct {
\tA string `json:\"a\"`
\tMutationStatus *HarnToolMutationStatus `json:\"mutationStatus,omitempty\"`
\tRaw json.RawMessage `json:\"raw\"`
}
";
assert_eq!(format_go_struct_fields(raw), formatted);
}
#[test]
fn generated_go_artifact_is_gofmt_stable_when_gofmt_is_available() {
let go = generate_go_artifact().expect("generate Go artifact");
let mut child = match Command::new("gofmt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(error) => panic!("failed to spawn gofmt: {error}"),
};
child
.stdin
.as_mut()
.expect("gofmt stdin")
.write_all(go.as_bytes())
.expect("write generated Go to gofmt");
let output = child.wait_with_output().expect("wait for gofmt");
assert!(
output.status.success(),
"gofmt failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).expect("gofmt stdout utf8"),
go,
"generated Go protocol artifact must be gofmt-stable before it is written or checked"
);
}
#[test]
fn generated_go_artifact_fails_closed_without_gofmt() {
let tempdir = tempfile::tempdir().expect("temporary directory");
let missing_formatter = tempdir.path().join("missing-gofmt");
let error = format_go_source_with(generate_go(), &missing_formatter)
.expect_err("protocol generation must not treat missing formatting as success");
assert!(
error.contains("Go formatting is required to generate canonical protocol artifacts"),
"unexpected missing-gofmt error: {error}"
);
}
#[test]
fn generated_rust_artifact_is_rustfmt_stable() {
let source = protocol_source();
let artifacts = generate_artifacts(&source).expect("generate artifacts");
let rust = artifacts
.iter()
.find(|artifact| artifact.relative_path == "harn-protocol.rs")
.expect("generated Rust artifact")
.contents
.clone();
let reformatted = format_rust_source(rust.clone(), source.repo_root())
.expect("reformat generated Rust artifact");
assert_eq!(
reformatted, rust,
"generated Rust protocol artifact must be rustfmt-stable before it is written or checked"
);
}
#[test]
fn round_trip_fixture_matches_python_and_go_field_set() {
let fixture: serde_json::Value =
serde_json::from_str(&generate_round_trip_fixture().expect("fixture"))
.expect("fixture json");
assert_eq!(
fixture["envelopes"]["sessionUpdateNotification"]["params"]["update"]["sessionUpdate"],
json!("tool_call")
);
assert_eq!(
fixture["envelopes"]["planSessionUpdateNotification"]["params"]["update"]
["harnPlanDocument"]["schema_version"],
json!("harn.plan_document.v1")
);
assert_eq!(
fixture["envelopes"]["agentEventNotification"]["method"],
json!(HARN_AGENT_EVENT_METHOD)
);
assert_eq!(
fixture["harnProviderCatalogMethod"],
json!(HARN_PROVIDER_CATALOG_METHOD)
);
assert_eq!(
fixture["envelopes"]["agentEventNotification"]["params"]["kind"],
json!("composition_child_call")
);
assert_eq!(fixture["a2aTask"]["status"]["state"], json!("working"));
assert_eq!(
fixture["harnToolAnnotations"]["completion_evidence_role"],
json!("verification")
);
assert_eq!(
fixture["mcpDiscoverResult"]["supportedVersions"][0],
json!(MCP_PROTOCOL_VERSION)
);
assert_eq!(
fixture["mcpInputRequiredResult"]["resultType"],
json!(MCP_INPUT_REQUIRED_RESULT_TYPE)
);
assert_eq!(
fixture["mcpUnsupportedProtocolVersionError"]["error"]["code"],
json!(MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE)
);
assert_eq!(
fixture["envelopes"]["errorResponse"]["error"]["data"]["schema"],
json!(ACP_PROMPT_ERROR_DATA_SCHEMA)
);
assert_eq!(
fixture["envelopes"]["response"]["result"]["_meta"]["harn"]["terminal"]["kind"],
"policy_budget"
);
assert_eq!(fixture["toolCallReceipt"]["schema_version"], json!(1));
}
#[test]
fn manifest_advertises_python_and_go_bindings() {
let manifest: serde_json::Value =
serde_json::from_str(&generate_manifest(&protocol_source()).expect("manifest"))
.expect("manifest json");
assert!(manifest["bindings"]["python"]["artifact"].is_string());
assert!(manifest["bindings"]["go"]["artifact"].is_string());
assert!(manifest["bindings"]["go"]["modulePath"].is_string());
assert_eq!(
manifest["bindings"]["rust"]["artifact"],
json!("harn-protocol.rs")
);
assert_eq!(
manifest["bindings"]["rust"]["vendorPath"],
json!("protocol/src/generated.rs")
);
assert_eq!(
manifest["bindings"]["rust"]["dependencies"],
json!(["serde", "serde_json"])
);
assert_eq!(manifest["bindings"]["rust"]["stability"], json!("stable"));
assert_eq!(
manifest["acp"]["transportControlMethods"],
json!(ACP_TRANSPORT_CONTROL_METHODS)
);
assert_eq!(
manifest["acp"]["harnSessionTimelineMethods"],
json!(HARN_SESSION_TIMELINE_METHODS)
);
assert_eq!(
manifest["acp"]["promptResultExtensionFields"],
json!(HARN_PROMPT_RESULT_EXTENSION_FIELDS)
);
assert!(
manifest["acp"]["dispatchedMethods"]
.as_array()
.expect("dispatched methods")
.iter()
.any(|value| value == SESSION_TIMELINE_QUERY_METHOD),
"manifest missing timeline query in dispatchedMethods"
);
assert!(
manifest["acp"]["handledMethods"]
.as_array()
.expect("handled methods")
.iter()
.any(|value| value == "session/set_budget"),
"manifest missing session/set_budget in handledMethods"
);
assert_eq!(
manifest["bindings"]["typescript"]["stability"],
json!("stable")
);
assert_eq!(manifest["mcp"]["protocolVersion"], json!("2026-07-28"));
assert_eq!(
manifest["mcp"]["unsupportedProtocolVersionError"]["code"],
json!(MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE)
);
assert_eq!(
manifest["mcp"]["jsonSchemaDialect"],
json!(MCP_JSON_SCHEMA_2020_12_DIALECT)
);
assert_eq!(manifest["bindings"]["python"]["stability"], json!("stable"));
assert_eq!(manifest["bindings"]["go"]["stability"], json!("stable"));
assert_eq!(
manifest["acp"]["promptErrorDataSchema"],
json!(ACP_PROMPT_ERROR_DATA_SCHEMA)
);
assert_eq!(
manifest["acp"]["promptErrorTerminalClasses"],
json!(agent_terminal_class_values())
);
assert_eq!(
manifest["receipts"]["toolCallReceiptSchemaVersion"],
json!(TOOL_CALL_RECEIPT_SCHEMA_VERSION)
);
assert_eq!(
manifest["acp"]["deprecatedAgentMethods"]["session/stop"]["replacement"],
json!("session/close")
);
}
#[test]
fn generated_artifacts_publish_the_harn_tools_contract() {
let artifacts = generate_artifacts(&protocol_source()).expect("artifacts");
let schema = artifacts
.iter()
.find(|artifact| {
artifact.relative_path == harn_vm::tool_registry::TOOL_CATALOG_SCHEMA_ARTIFACT
})
.expect("harn-tools JSON Schema artifact");
let schema: serde_json::Value =
serde_json::from_str(&schema.contents).expect("harn-tools JSON Schema");
jsonschema::meta::validate(&schema)
.expect("harn-tools artifact must be a meta-valid Draft 2020-12 schema");
let artifact_validator =
jsonschema::draft202012::new(&schema).expect("harn-tools artifact validator");
let structurally_valid_catalog = json!({
"schema_version": "harn-tools/2.0",
"tools": [{
"name": "inspect",
"inputSchema": {"type": "object", "not": 7},
"governance": {"audiences": ["catalog"]},
"cli": {"command": ["inspect"], "hidden": false},
"deferLoading": false
}]
});
assert!(
artifact_validator.is_valid(&structurally_valid_catalog),
"the artifact validates the catalog envelope, keeps outputSchema optional, and leaves embedded Draft semantics to Harn"
);
assert!(
serde_json::from_value::<harn_vm::tool_registry::ToolCatalog>(structurally_valid_catalog)
.is_err(),
"Harn's semantic parser must reject the invalid embedded Draft schema"
);
let typescript = artifacts
.iter()
.find(|artifact| {
artifact.relative_path == harn_vm::tool_registry::TOOL_CATALOG_TYPESCRIPT_ARTIFACT
})
.expect("harn-tools TypeScript artifact");
assert!(typescript.contents.contains("export type ToolCatalog ="));
let manifest: serde_json::Value = serde_json::from_str(
&generate_manifest(&protocol_source()).expect("generate protocol manifest"),
)
.expect("parse protocol manifest");
assert_eq!(
manifest["bindings"]["harnToolsTypescript"]["schemaVersion"],
json!(harn_vm::tool_registry::TOOL_CATALOG_SCHEMA_VERSION)
);
assert!(manifest["schemas"].as_array().is_some_and(|schemas| {
schemas
.iter()
.any(|entry| entry["artifact"] == harn_vm::tool_registry::TOOL_CATALOG_SCHEMA_ARTIFACT)
}));
}
#[test]
fn generated_manifest_references_schema_artifacts() {
let manifest: serde_json::Value =
serde_json::from_str(&generate_manifest(&protocol_source()).expect("manifest"))
.expect("manifest json");
for schema in SCHEMA_COPIES {
assert!(
manifest["schemas"]
.as_array()
.expect("schema array")
.iter()
.any(|entry| entry["artifact"] == schema.artifact),
"manifest missing {}",
schema.artifact
);
}
assert!(
manifest["schemas"]
.as_array()
.expect("schema array")
.iter()
.any(|entry| entry["artifact"] == TOOL_CALL_RECEIPT_SCHEMA_ARTIFACT),
"manifest missing {TOOL_CALL_RECEIPT_SCHEMA_ARTIFACT}"
);
assert!(
manifest["schemas"]
.as_array()
.expect("schema array")
.iter()
.any(|entry| {
entry["artifact"] == harn_vm::llm::plan::PLAN_DOCUMENT_SCHEMA_ARTIFACT
}),
"manifest missing collaborative plan document schema"
);
}
#[test]
fn generated_artifacts_never_carry_the_crate_version() {
let artifacts = generate_artifacts(&protocol_source()).expect("artifacts");
let version = env!("CARGO_PKG_VERSION");
for artifact in &artifacts {
assert!(
!artifact.contents.contains(version),
"{} embeds the crate version {version}; a bump would restale it",
artifact.relative_path
);
}
let manifest = artifacts
.iter()
.find(|artifact| artifact.relative_path == "manifest.json")
.expect("manifest");
let manifest: serde_json::Value =
serde_json::from_str(&manifest.contents).expect("manifest JSON");
assert!(
manifest.get("artifactVersion").is_none(),
"manifest must not stamp an artifact version"
);
assert!(
manifest.get("schemaVersion").is_some(),
"manifest must still declare its schema version"
);
}
#[test]
fn agent_lifecycle_registry_owns_worker_and_a2a_projections() {
use harn_vm::agent_events::AgentLifecycleState;
assert_worker_lifecycle_parity();
assert_eq!(worker_status_values(), agent_lifecycle_state_values());
let a2a: BTreeSet<&str> = A2A_TASK_STATES.iter().copied().collect();
for state in AgentLifecycleState::ALL {
let Some(projected) = state.a2a_task_state() else {
continue;
};
if projected == "paused" {
continue;
}
assert!(
a2a.contains(projected),
"lifecycle state `{}` projects to A2A `{projected}` missing from A2A_TASK_STATES",
state.wire_name()
);
}
let manifest: serde_json::Value = serde_json::from_str(
&generate_manifest(&protocol_source()).expect("generate protocol manifest"),
)
.expect("parse protocol manifest");
assert_eq!(
manifest["acp"]["agentLifecycleStates"],
json!(agent_lifecycle_state_values())
);
assert_eq!(
manifest["acp"]["workerStatuses"],
json!(agent_lifecycle_state_values())
);
assert!(manifest["acp"]["agentLifecycleStateProjections"].is_array());
assert!(manifest["acp"]["agentLifecycleEvents"].is_array());
}
#[test]
fn committed_protocol_artifacts_match_generator() {
let source = protocol_source();
let artifacts = generate_artifacts(&source).expect("artifacts");
let output_root = source.repo_root().join("spec/protocol-artifacts");
for artifact in artifacts {
let path = output_root.join(&artifact.relative_path);
let on_disk = fs::read_to_string(&path).unwrap_or_else(|error| {
panic!(
"failed to read {}: {error}\n\
hint: run `make gen-protocol-artifacts` to regenerate.",
path.display()
)
});
assert_eq!(
normalize_line_endings(&on_disk),
normalize_line_endings(&artifact.contents),
"{} is stale. Run `make gen-protocol-artifacts` to regenerate.",
path.display()
);
}
}
#[test]
fn repo_root_is_discovered_at_runtime_for_relocated_cli() {
let checkout = tempfile::tempdir().expect("checkout");
fs::write(checkout.path().join("Cargo.toml"), "[workspace]\n").expect("workspace manifest");
let schemas = checkout.path().join("conformance/protocols/schemas");
fs::create_dir_all(&schemas).expect("protocol schemas");
let nested = checkout.path().join("crates/harn-cli");
fs::create_dir_all(&nested).expect("nested command directory");
assert_eq!(repo_root_from(&nested), Some(checkout.path().to_path_buf()));
}