mod support;
use serde_json::Value;
use support::TestWorkspace;
fn assert_verify_failure(
source: &str,
expected_message: &str,
expected_location: &str,
expected_excerpt: &str,
) {
let workspace = TestWorkspace::new();
workspace.write_file("collection.hen", source);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(
output.stderr.contains(expected_message),
"stderr: {}",
output.stderr
);
assert!(
output.stderr.contains(expected_location),
"stderr: {}",
output.stderr
);
assert!(
output.stderr.contains(expected_excerpt),
"stderr: {}",
output.stderr
);
}
#[test]
fn verify_reports_requests_and_required_inputs() {
let workspace = TestWorkspace::new();
workspace.write_file(
"fragments/imported.hen",
r#"Imported request
POST https://example.com/imported/[[ token ]]
"#,
);
workspace.write_file(
"collection.hen",
r#"Verify Fixture
Verifies imported requests.
? region = [[ region = us-east-1 ]]
---
Root request
GET https://example.com/root
---
<< fragments/imported.hen
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("Verification passed"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[0] GET https://example.com/root"),
"stdout: {}",
output.stdout
);
assert!(
output
.stdout
.contains("[1] POST https://example.com/imported/[[ token ]]"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("Required inputs"),
"stdout: {}",
output.stdout
);
assert!(output.stdout.contains("token"), "stdout: {}", output.stdout);
assert!(
output.stdout.contains("region (default: us-east-1)"),
"stdout: {}",
output.stdout
);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
}
#[test]
fn verify_reports_available_environments() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Environment Verify Fixture
$ API_ORIGIN = https://api.example.com
env local
$ API_ORIGIN = http://localhost:3000
env staging
$ API_ORIGIN = https://staging.example.com
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stdout.contains("Available environments"), "stdout: {}", output.stdout);
assert!(output.stdout.contains("local"), "stdout: {}", output.stdout);
assert!(output.stdout.contains("staging"), "stdout: {}", output.stdout);
}
#[test]
fn verify_outputs_json_report_with_available_environments() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Environment Verify Fixture
$ API_ORIGIN = https://api.example.com
env local
$ API_ORIGIN = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["ok"], true);
assert!(parsed["diagnostics"].as_array().is_some_and(|items| items.is_empty()));
assert_eq!(parsed["summary"]["availableEnvironments"][0], "local");
assert_eq!(parsed["availableEnvironments"][0], "local");
}
#[test]
fn verify_outputs_http_session_name_in_protocol_context() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"HTTP Session Verify Fixture
---
Login
session = web
POST https://example.com/login
^ & status == 200
---
Load profile
session = web
GET https://example.com/profile
^ & status == 200
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["requests"][0]["protocol"], "http");
assert_eq!(parsed["requests"][0]["protocolContext"]["sessionName"], "web");
assert_eq!(parsed["requests"][1]["protocol"], "http");
assert_eq!(parsed["requests"][1]["protocolContext"]["sessionName"], "web");
}
#[test]
fn verify_outputs_oauth_auth_profile_in_protocol_context() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = OAuth Verify Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
access_token -> $API_ACCESS_TOKEN
---
Get profile
auth = api
GET https://example.com/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["requests"][0]["protocolContext"]["authProfile"], "api");
}
#[test]
fn verify_rejects_unknown_oauth_profile_reference() {
assert_verify_failure(
r#"name = Unknown OAuth Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
---
Get profile
auth = missing
GET https://example.com/profile
"#,
"request references unknown OAuth profile 'missing'",
"9:1",
"auth = missing",
);
}
#[test]
fn verify_outputs_json_error_report_with_structured_parse_diagnostics() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Unknown OAuth Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
---
Get profile
auth = missing
GET https://example.com/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["ok"], false);
assert_eq!(parsed["summary"]["requests"][0]["url"], "https://example.com/profile");
assert_eq!(parsed["summary"]["requests"][0]["protocolContext"]["authProfile"], "missing");
assert_eq!(parsed["error"]["kind"], "Parse");
assert_eq!(parsed["error"]["summary"], "Failed to parse hen file");
assert_eq!(
parsed["diagnostics"][0]["code"],
"unknown_oauth_profile"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["code"],
"unknown_oauth_profile"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["phase"],
"validate"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["source"],
"hen.parser"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["message"],
"request references unknown OAuth profile 'missing'."
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["line"]
.is_u64()
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]
["character"]
.is_u64()
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["range"]["end"]["line"]
.is_u64()
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["range"]["end"]["character"]
.is_u64()
);
assert!(
parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.is_some_and(|items| items.is_empty())
);
assert_eq!(
parsed["error"]["diagnostics"][0]["symbol"]["kind"],
"oauthProfile"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["symbol"]["name"],
"missing"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["symbol"]["role"],
"reference"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["expectedKinds"][0],
"oauthProfile"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["availableNames"][0],
"api"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["kind"],
"replaceRange"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["label"],
"Replace with 'api'"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["text"],
"api"
);
assert!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["range"]["start"]["line"]
.is_u64()
);
}
#[test]
fn verify_outputs_json_error_report_with_duplicate_environment_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Duplicate Environment Fixture
$ API_ORIGIN = https://api.example.com
env local
$ API_ORIGIN = http://localhost:3000
env local
$ API_ORIGIN = http://127.0.0.1:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "duplicate_environment");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "environment");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "local");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["duplicateName"], "local");
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
4
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert_eq!(related[0]["message"], "First environment 'local' is declared here.");
assert_eq!(related[0]["location"]["range"]["start"]["line"], 2);
assert_eq!(related[0]["location"]["range"]["start"]["character"], 4);
}
#[test]
fn verify_outputs_json_error_report_with_duplicate_oauth_profile_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Duplicate OAuth Fixture
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
oauth api
grant = client_credentials
issuer = https://login2.example.com
client_id = secret.env("CLIENT_ID")
client_secret = secret.env("CLIENT_SECRET")
---
Get profile
auth = api
GET https://example.com/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "duplicate_oauth_profile");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "oauthProfile");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "api");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["duplicateName"], "api");
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
6
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert_eq!(related[0]["message"], "First OAuth profile 'api' is declared here.");
assert_eq!(related[0]["location"]["range"]["start"]["character"], 6);
}
#[test]
fn verify_outputs_json_error_report_with_unknown_environment_variable_suggestions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Unknown Environment Variable Fixture
$ API_ORIGIN = https://api.example.com
$ API_TOKEN = top-secret
env local
$ API_BASE = http://localhost:3000
---
Get profile
GET {{ API_ORIGIN }}/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["diagnostics"][0]["code"], "unknown_environment_variable");
assert_eq!(parsed["diagnostics"][0]["symbol"]["kind"], "variable");
assert_eq!(parsed["diagnostics"][0]["symbol"]["name"], "API_BASE");
assert_eq!(parsed["diagnostics"][0]["data"]["environmentName"], "local");
assert_eq!(parsed["diagnostics"][0]["data"]["expectedKinds"][0], "scalarVariable");
assert_eq!(parsed["diagnostics"][0]["data"]["availableNames"][0], "API_ORIGIN");
assert_eq!(parsed["diagnostics"][0]["data"]["availableNames"][1], "API_TOKEN");
assert_eq!(parsed["diagnostics"][0]["suggestions"][0]["kind"], "replaceRange");
assert_eq!(parsed["diagnostics"][0]["suggestions"][0]["text"], "API_ORIGIN");
assert_eq!(
parsed["diagnostics"][0]["location"]["range"]["start"]["character"],
2
);
}
#[test]
fn verify_outputs_json_error_report_with_preprocess_diagnostics() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Missing Import Fixture
---
<< fragments/missing.hen
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "fragment_import_io");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "preprocess");
assert_eq!(parsed["error"]["diagnostics"][0]["source"], "hen.preprocess");
assert!(
parsed["error"]["diagnostics"][0]["message"]
.as_str()
.is_some_and(|value| value.contains("Failed to read import"))
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["line"],
4
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
3
);
assert!(
parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.is_some_and(|items| items.is_empty())
);
}
#[test]
fn verify_outputs_json_error_report_with_nested_preprocess_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Nested Missing Import Fixture
---
<< fragments/first.hen
"#,
);
workspace.write_file("fragments/first.hen", "<< second/missing.hen\n");
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "fragment_import_io");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "preprocess");
assert!(
parsed["error"]["diagnostics"][0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["line"],
4
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert!(
related[0]["message"]
.as_str()
.is_some_and(|value| value.starts_with("Imported file '") && value.ends_with("fragments/first.hen' failed here."))
);
assert!(
related[0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("fragments/first.hen"))
);
assert_eq!(related[0]["location"]["range"]["start"]["line"], 0);
assert_eq!(related[0]["location"]["range"]["start"]["character"], 3);
}
#[test]
fn verify_outputs_json_error_report_with_preprocess_cycle_diagnostics() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Import Cycle Fixture
---
<< fragments/first.hen
"#,
);
workspace.write_file("fragments/first.hen", "<< second.hen\n");
workspace.write_file("fragments/second.hen", "<< first.hen\n");
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "fragment_import_cycle");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "preprocess");
assert!(
parsed["error"]["diagnostics"][0]["message"]
.as_str()
.is_some_and(|value| value.contains("Fragment import cycle detected") && value.contains("first.hen") && value.contains("second.hen"))
);
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "fragment");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["role"], "import");
assert!(
parsed["error"]["diagnostics"][0]["symbol"]["name"]
.as_str()
.is_some_and(|value| value.ends_with("first.hen"))
);
assert!(
parsed["error"]["diagnostics"][0]["data"]["cycleMembers"]
.as_array()
.is_some_and(|items| items.len() == 3)
);
assert!(
parsed["error"]["diagnostics"][0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 2);
assert!(
related[0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("fragments/first.hen"))
);
assert!(
related[1]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("fragments/second.hen"))
);
assert_eq!(related[0]["location"]["range"]["start"]["character"], 3);
assert_eq!(related[1]["location"]["range"]["start"]["character"], 3);
}
#[test]
fn verify_outputs_json_error_report_with_unknown_request_dependency_suggestions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Unknown Dependency
description = Ensure request dependency diagnostics include structured suggestions.
---
Seed user
GET https://example.com/users
---
Fetch profile
> requires: Seed usr
GET https://example.com/profile
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "unknown_dependency");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "validate");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "request");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "Seed usr");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["expectedKinds"][0], "request");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["ownerName"], "Fetch profile");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["symbolName"], "Seed usr");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["availableNames"][0], "Seed user");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["kind"], "replaceRange");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["text"], "Seed user");
assert_eq!(parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["line"], 7);
assert_eq!(parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"], 12);
assert_eq!(parsed["error"]["diagnostics"][0]["location"]["range"]["end"]["character"], 20);
}
#[test]
fn verify_outputs_json_error_report_with_protocol_diagnostics() {
let workspace = TestWorkspace::new();
let cases = [
(
r#"name = Verify GraphQL Protocol Diagnostic
description = Ensure protocol directive mismatches use protocol diagnostics.
---
Load profile
operation = GetProfile
POST https://example.com/graphql
"#,
"graphql_protocol_required",
"GraphQL directives require 'protocol = graphql'",
serde_json::json!({
"protocol": "graphql",
"expectedProtocol": "graphql",
"requiredDirectives": ["protocol"],
"directiveFamilies": ["graphql"],
}),
),
(
r#"name = Verify Unsupported Protocol Diagnostic
description = Ensure unsupported protocol values expose supported replacements.
---
Broken request
protocol = ftp
GET https://example.com
"#,
"unsupported_protocol",
"unsupported protocol 'ftp' (supported: http, graphql, mcp, sse, ws)",
serde_json::json!({
"directiveName": "protocol",
"invalidValue": "ftp",
"supportedValues": ["http", "graphql", "mcp", "sse", "ws"],
}),
),
(
r#"name = Verify GraphQL Form Fields Diagnostic
description = Ensure unsupported GraphQL form fields expose cleanup targets.
---
Load profile
protocol = graphql
POST https://example.com/graphql
~ userId = abc123
~~~graphql
query {
viewer {
id
}
}
~~~
"#,
"graphql_form_fields_unsupported",
"GraphQL requests do not support form fields",
serde_json::json!({
"protocol": "graphql",
"cleanupTargets": ["formFields"],
}),
),
(
r#"name = Verify GraphQL Missing Document Diagnostic
description = Ensure missing GraphQL documents expose insertion metadata.
---
Fetch products
protocol = graphql
POST https://example.com/graphql
"#,
"graphql_missing_document",
"GraphQL requests require a ~~~graphql document block",
serde_json::json!({
"protocol": "graphql",
"requiredBlocks": ["graphqlDocument"],
"supportedBlockTypes": ["graphql"],
"insertBlockTitle": "Insert GraphQL document block",
"replacementOpeningFence": "~~~graphql",
"replacementBodyText": "query {\n field\n}",
}),
),
(
r#"name = Verify MCP Protocol Diagnostic
description = Ensure missing MCP call directives use protocol diagnostics.
---
Initialize session
protocol = mcp
POST https://example.com/mcp
"#,
"mcp_missing_call",
"MCP requests require 'call = ...'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"supportedValues": ["initialize", "tools/list", "resources/list", "tools/call"],
}),
),
(
r#"name = Verify MCP Body Unsupported Diagnostic
description = Ensure unsupported MCP body or form inputs expose cleanup targets.
---
Initialize session
protocol = mcp
call = initialize
POST https://example.com/mcp
~~~json
{}
~~~
"#,
"mcp_body_unsupported",
"MCP-over-HTTP requests do not support explicit body or content type blocks",
serde_json::json!({
"protocol": "mcp",
"cleanupTargets": ["bodyBlock", "formFields"],
}),
),
(
r#"name = Verify Unsupported MCP Call Diagnostic
description = Ensure unsupported MCP calls keep the structured replacement choices.
---
Initialize session
protocol = mcp
call = tools/run
POST https://example.com/mcp
"#,
"unsupported_mcp_call",
"unsupported MCP call 'tools/run' (supported: initialize, tools/list, resources/list, tools/call)",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"supportedValues": ["initialize", "tools/list", "resources/list", "tools/call"],
}),
),
(
r#"name = Verify GraphQL Method Diagnostic
description = Ensure GraphQL method errors expose the expected method.
---
Load profile
protocol = graphql
GET https://example.com/graphql
~~~graphql
query {
viewer {
id
}
}
~~~
"#,
"graphql_requires_post",
"GraphQL requests currently require POST",
serde_json::json!({
"protocol": "graphql",
"expectedMethod": "POST",
}),
),
(
r#"name = Verify SSE Method Diagnostic
description = Ensure SSE method errors expose the expected method.
---
Open stream
protocol = sse
session = prices
POST https://example.com/prices/stream
"#,
"sse_requires_get",
"SSE requests currently require GET",
serde_json::json!({
"protocol": "sse",
"expectedMethod": "GET",
}),
),
(
r#"name = Verify SSE Missing Session Diagnostic
description = Ensure missing SSE sessions expose a directive replacement stub.
---
Open stream
protocol = sse
GET https://example.com/prices/stream
"#,
"sse_missing_session",
"SSE requests require 'session = ...'",
serde_json::json!({
"protocol": "sse",
"requiredDirectives": ["session"],
"directiveName": "session",
"replacementValue": "exampleSession",
}),
),
(
r#"name = Verify SSE Body Unsupported Diagnostic
description = Ensure unsupported SSE body blocks expose cleanup targets.
---
Open stream
protocol = sse
session = prices
GET https://example.com/prices/stream
~~~json
{}
~~~
"#,
"sse_body_unsupported",
"SSE requests do not support explicit body or content type blocks",
serde_json::json!({
"protocol": "sse",
"cleanupTargets": ["bodyBlock"],
}),
),
(
r#"name = Verify SSE Missing Within Diagnostic
description = Ensure missing SSE within directives expose a directive replacement stub.
---
Receive stream
protocol = sse
session = prices
receive
GET https://example.com/prices/stream
"#,
"sse_missing_within",
"SSE receive steps require 'within = ...'",
serde_json::json!({
"protocol": "sse",
"requiredDirectives": ["within"],
"requiredAction": "receive",
"directiveName": "within",
"replacementValue": "30s",
}),
),
(
r#"name = Verify Session Protocol Conflict Diagnostic
description = Ensure shared sessions expose the established protocol for repair.
---
Open stream
protocol = sse
session = shared
GET https://example.com/prices/stream
---
Open socket
protocol = ws
session = shared
GET wss://example.com/prices/ws
"#,
"session_protocol_conflict",
"session 'shared' already uses protocol 'sse', so this step cannot use protocol 'ws'",
serde_json::json!({
"sessionName": "shared",
"expectedProtocol": "sse",
"conflictingProtocol": "ws",
}),
),
(
r#"name = Verify MCP Tool Arguments Call Diagnostic
description = Ensure tool and arguments on the wrong MCP call expose the required call.
---
Connect session
protocol = mcp
call = initialize
tool = generateNumbers
POST https://example.com/mcp
"#,
"mcp_tool_arguments_invalid_for_call",
"'tool' and 'arguments' are only valid with 'call = tools/call'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"requiredCall": "tools/call",
}),
),
(
r#"name = Verify MCP Initialize Overrides Call Diagnostic
description = Ensure initialize overrides on the wrong MCP call expose the required call.
---
Connect session
protocol = mcp
call = tools/call
client_name = hen
tool = generateNumbers
POST https://example.com/mcp
"#,
"mcp_initialize_overrides_invalid_for_call",
"initialize override directives are only valid with 'call = initialize'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"requiredCall": "initialize",
}),
),
(
r#"name = Verify MCP Tools List Directive Conflict Diagnostic
description = Ensure tools/list conflicts expose concrete replacement calls.
---
List tools
protocol = mcp
call = tools/list
tool = generateNumbers
client_name = hen
POST https://example.com/mcp
"#,
"mcp_tools_list_directives_unsupported",
"'call = tools/list' does not accept tool/arguments or initialize override directives; use 'call = tools/call' or 'call = initialize'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"supportedValues": ["tools/call", "initialize"],
}),
),
(
r#"name = Verify MCP Resources List Directive Conflict Diagnostic
description = Ensure resources/list conflicts expose the required replacement call.
---
List resources
protocol = mcp
call = resources/list
client_name = hen
POST https://example.com/mcp
"#,
"mcp_resources_list_directives_unsupported",
"'call = resources/list' does not accept initialize override directives; use 'call = initialize'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["call"],
"requiredCall": "initialize",
}),
),
(
r#"name = Verify WebSocket Send Receive Conflict Diagnostic
description = Ensure WebSocket send/receive conflicts expose the conflicting directives.
---
Exchange message
protocol = ws
session = chat
send = json
receive
GET wss://example.com/ws
"#,
"ws_send_receive_conflict",
"WebSocket requests cannot combine 'send = ...' with 'receive'",
serde_json::json!({
"protocol": "ws",
"conflictingDirectives": ["send", "receive"],
}),
),
(
r#"name = Verify MCP Missing Tool Diagnostic
description = Ensure missing MCP tool directives expose a directive replacement stub.
---
Call tool
protocol = mcp
call = tools/call
POST https://example.com/mcp
"#,
"mcp_missing_tool",
"'call = tools/call' requires 'tool = ...'",
serde_json::json!({
"protocol": "mcp",
"requiredDirectives": ["tool"],
"requiredCall": "tools/call",
"directiveName": "tool",
"replacementValue": "exampleTool",
}),
),
(
r#"name = Verify Unsupported WebSocket Send Kind Diagnostic
description = Ensure unsupported WebSocket send kinds expose replacement options.
---
Exchange message
protocol = ws
session = chat
send = binary
GET wss://example.com/ws
"#,
"unsupported_ws_send_kind",
"unsupported WebSocket send kind 'binary' (supported: text, json)",
serde_json::json!({
"protocol": "ws",
"directiveName": "send",
"invalidValue": "binary",
"supportedValues": ["text", "json"],
}),
),
(
r#"name = Verify WebSocket Form Fields Diagnostic
description = Ensure unsupported WebSocket form fields expose cleanup targets.
---
Exchange message
protocol = ws
session = chat
GET wss://example.com/ws
~ userId = abc123
"#,
"ws_form_fields_unsupported",
"WebSocket requests do not support form fields",
serde_json::json!({
"protocol": "ws",
"cleanupTargets": ["formFields"],
}),
),
(
r#"name = Verify WebSocket Body Unsupported Diagnostic
description = Ensure unsupported WebSocket body blocks expose cleanup targets.
---
Receive message
protocol = ws
session = chat
receive
within = 1s
GET wss://example.com/ws
~~~json
{}
~~~
"#,
"ws_body_unsupported",
"WebSocket receive steps do not support explicit body or content type blocks",
serde_json::json!({
"protocol": "ws",
"cleanupTargets": ["bodyBlock"],
}),
),
(
r#"name = Verify WebSocket Missing Session Diagnostic
description = Ensure missing WebSocket sessions expose a directive replacement stub.
---
Open socket
protocol = ws
GET wss://example.com/ws
"#,
"ws_missing_session",
"WebSocket requests require 'session = ...'",
serde_json::json!({
"protocol": "ws",
"requiredDirectives": ["session"],
"directiveName": "session",
"replacementValue": "exampleSession",
}),
),
(
r#"name = Verify WebSocket Missing Body Diagnostic
description = Ensure missing WebSocket send bodies expose insertion metadata.
---
Send message
protocol = ws
session = chat
send = json
GET wss://example.com/ws
"#,
"ws_missing_body",
"WebSocket send kind 'json' requires a body block",
serde_json::json!({
"protocol": "ws",
"requiredBlocks": ["body"],
"requiredDirectives": ["send"],
"sendKind": "json",
"insertBlockTitle": "Insert WebSocket JSON body block",
"replacementOpeningFence": "~~~json",
"replacementBodyText": "{\n \"type\": \"message\"\n}",
}),
),
(
r#"name = Verify WebSocket Missing Within Diagnostic
description = Ensure missing WebSocket within directives expose a directive replacement stub.
---
Receive message
protocol = ws
session = chat
receive
GET wss://example.com/ws
"#,
"ws_missing_within",
"WebSocket receive steps require 'within = ...'",
serde_json::json!({
"protocol": "ws",
"requiredDirectives": ["within"],
"requiredAction": "receive",
"directiveName": "within",
"replacementValue": "30s",
}),
),
(
r#"name = Verify Within Requires Receive Diagnostic
description = Ensure within directives without receive expose flag insertion metadata.
---
Receive stream
protocol = sse
session = prices
within = 30s
GET https://example.com/prices/stream
"#,
"within_requires_receive",
"'within = ...' is only valid with 'receive'",
serde_json::json!({
"requiredDirectives": ["receive"],
"requiredFlagDirective": "receive",
"anchorDirectiveNames": ["within"],
}),
),
(
r#"name = Verify Conflicting WebSocket Send Kind Diagnostic
description = Ensure conflicting WebSocket send kinds expose the body-implied replacement.
---
Exchange message
protocol = ws
session = chat
send = json
GET wss://example.com/ws
~~~text
hello
~~~
"#,
"conflicting_ws_send_kind",
"WebSocket send kind 'json' conflicts with body block type 'text'",
serde_json::json!({
"protocol": "ws",
"directiveName": "send",
"currentValue": "json",
"expectedValue": "text",
"bodyContentType": "text",
}),
),
(
r#"name = Verify Unsupported WebSocket Body Block Type Diagnostic
description = Ensure unsupported WebSocket body block types expose supported fence replacements.
---
Exchange message
protocol = ws
session = chat
GET wss://example.com/ws
~~~xml
<message />
~~~
"#,
"unsupported_ws_body_content_type",
"unsupported WebSocket body block type 'xml' (supported: plain ~~~, ~~~text, ~~~text/plain, ~~~json, or ~~~application/json)",
serde_json::json!({
"protocol": "ws",
"blockKind": "body",
"invalidValue": "xml",
"supportedBlockTypes": ["text", "json"],
}),
),
(
r#"name = Verify Invalid WebSocket JSON Payload Diagnostic
description = Ensure invalid WebSocket JSON payloads expose a replacement body.
---
Send message
protocol = ws
session = chat
send = json
GET wss://example.com/ws
~~~json
{
~~~
"#,
"invalid_ws_json_payload",
"invalid WebSocket JSON payload: EOF while parsing an object at line 2 column 0",
serde_json::json!({
"protocol": "ws",
"blockKind": "body",
"replacementBodyText": "{\n \"type\": \"message\"\n}",
}),
),
(
r#"name = Verify Invalid GraphQL Variables Diagnostic
description = Ensure invalid GraphQL variables expose the replacement directive target.
---
Get user
protocol = graphql
variables = {
POST https://example.com/graphql
~~~graphql
query ($id: ID!) {
user(id: $id) { id }
}
~~~
"#,
"invalid_graphql_variables_json",
"invalid GraphQL variables JSON: EOF while parsing an object at line 1 column 1",
serde_json::json!({
"protocol": "graphql",
"directiveName": "variables",
"replacementValue": "{}",
}),
),
(
r#"name = Verify Invalid MCP Arguments Diagnostic
description = Ensure invalid MCP arguments JSON exposes the replacement directive target.
---
Call tool
protocol = mcp
call = tools/call
tool = sample
arguments = {
POST https://example.com/mcp
"#,
"invalid_mcp_json",
"invalid MCP arguments JSON: EOF while parsing an object at line 1 column 1",
serde_json::json!({
"protocol": "mcp",
"directiveName": "arguments",
"replacementValue": "{}",
}),
),
(
r#"name = Verify MCP Capabilities Object Diagnostic
description = Ensure MCP capabilities object diagnostics expose the replacement directive target.
---
Initialize session
protocol = mcp
call = initialize
capabilities = []
POST https://example.com/mcp
"#,
"invalid_mcp_json_object",
"MCP capabilities must be a JSON object",
serde_json::json!({
"protocol": "mcp",
"directiveName": "capabilities",
"replacementValue": "{}",
}),
),
];
for (content, expected_code, expected_message, expected_data) in cases {
workspace.write_file("collection.hen", content);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], expected_code);
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "validate");
assert_eq!(parsed["error"]["diagnostics"][0]["source"], "hen.protocol");
assert_eq!(parsed["error"]["diagnostics"][0]["message"], expected_message);
assert_eq!(parsed["error"]["diagnostics"][0]["data"], expected_data);
}
}
#[test]
fn verify_skips_invalid_ws_json_payload_when_placeholders_remain_unresolved() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Unresolved WebSocket JSON Placeholder
description = Ensure unresolved placeholders do not fail verify-time JSON validation.
$ BODY_ID = {{UNKNOWN_ID}}
---
Send message
protocol = ws
session = chat
send = json
GET wss://example.com/ws
~~~json
{
"id": {{BODY_ID}}
}
~~~
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["ok"], true);
assert_eq!(parsed["diagnostics"], serde_json::json!([]));
}
#[test]
fn verify_outputs_json_error_report_with_duplicate_declaration_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Duplicate Declaration
description = Ensure duplicate declaration names include the original definition.
schema User {
id: UUID
}
scalar User = string
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "duplicate_declaration_name");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "validate");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "scalar");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "User");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["role"], "declaration");
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert_eq!(related[0]["message"], "First declaration of 'User' is here.");
assert!(
related[0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
let primary_line = parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["line"]
.as_u64()
.expect("primary line should be numeric");
let related_line = related[0]["location"]["range"]["start"]["line"]
.as_u64()
.expect("related line should be numeric");
assert_ne!(primary_line, related_line);
}
#[test]
fn verify_does_not_execute_shell_substitutions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"Shell Verify
$ shell_value = $(touch side-effect.txt)
---
Shell request
GET https://example.com/{{ shell_value }}
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
!workspace.root().join("side-effect.txt").exists(),
"verify executed a shell substitution"
);
}
#[test]
fn verify_accepts_local_secret_references_without_loading_them() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Secret Verify Fixture
$ API_TOKEN = secret.env("HEN_TEST_VERIFY_TOKEN")
$ CLIENT_ID = secret.file("./missing/client_id.txt")
---
Get profile
GET https://example.com/profile
* Authorization = Bearer {{ API_TOKEN }}
* X-Client-Id = {{ CLIENT_ID }}
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output.stdout.contains("Verification passed"), "stdout: {}", output.stdout);
}
#[test]
fn verify_rejects_unsupported_secret_provider() {
assert_verify_failure(
r#"name = Unsupported Secret Provider Fixture
$ API_TOKEN = secret.keychain("service/account")
---
Get profile
GET https://example.com/profile
"#,
"unsupported provider 'keychain'",
"2:1",
"$ API_TOKEN = secret.keychain(\"service/account\")",
);
}
#[test]
fn verify_accepts_websocket_authoring_slice() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"WebSocket Verify
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
~~~json
{"type":"hello"}
~~~
---
Receive reply
session = chat
receive
within = 2s
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("[0] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[1] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[2] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
}
#[test]
fn verify_accepts_websocket_exchange_authoring_slice() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"WebSocket Exchange Verify
---
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
within = 2s
~~~json
{"type":"hello"}
~~~
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("[0] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
assert!(
output.stdout.contains("[1] GET wss://example.com/chat"),
"stdout: {}",
output.stdout
);
}
#[test]
fn verify_reports_parse_failures_without_running_requests() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"Broken Verify
---
Broken assertion
GET https://example.com
^ & body =~ /foo/
"#,
);
let output = workspace.run_hen(["verify", "collection.hen"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("No valid operator found in assertion"));
}
#[test]
fn verify_reports_unknown_schema_reference_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Unknown Reference
description = Ensure unknown schema references point at the owning declaration.
schema User {
profile: Profile
}
---
Get user
GET https://example.com/users/1
"#,
"User references unknown validation target Profile",
"--> 3:1",
"schema User {",
);
}
#[test]
fn verify_outputs_json_error_report_with_unknown_schema_reference_suggestions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Unknown Reference
description = Ensure declaration-time unknown references include structured suggestions.
schema Account {
id: UUID
}
schema User {
profile: Profile
}
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "unknown_validation_target");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["availableNames"][0], "Account");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["availableNames"][1], "User");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["kind"], "replaceRange");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["text"], "Account");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["range"]["start"]["character"], 9);
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["range"]["end"]["character"], 16);
}
#[test]
fn verify_outputs_json_error_report_with_schema_cycle_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Schema Cycle
description = Ensure schema cycles include related declaration locations.
schema User {
team: Team
}
schema Team {
owner: User
}
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "circular_schema_reference");
assert_eq!(parsed["error"]["diagnostics"][0]["phase"], "validate");
assert!(
parsed["error"]["diagnostics"][0]["message"]
.as_str()
.is_some_and(|value| value.contains("circular reference") && value.contains("User") && value.contains("Team"))
);
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "schema");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["role"], "declaration");
assert!(
parsed["error"]["diagnostics"][0]["symbol"]["name"]
.as_str()
.is_some_and(|value| value == "User" || value == "Team")
);
assert!(
parsed["error"]["diagnostics"][0]["data"]["cyclePath"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item == "User") && items.iter().any(|item| item == "Team"))
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert!(
related[0]["message"]
.as_str()
.is_some_and(|value| value.contains("participates in the cycle"))
);
assert!(
related[0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
}
#[test]
fn verify_reports_invalid_scalar_base_reference_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Invalid Scalar Base
description = Ensure schema-backed scalar bases point at the scalar declaration.
schema User {
id: UUID
}
scalar UserAlias = User
---
Get user
GET https://example.com/users/1
"#,
"scalar UserAlias cannot use schema User as a scalar base",
"--> 6:1",
"scalar UserAlias = User",
);
}
#[test]
fn verify_reports_unknown_schema_assertion_target_at_assertion_span() {
assert_verify_failure(
r#"name = Verify Unknown Assertion Target
description = Ensure schema assertions are validated during verify.
---
Get user
GET https://example.com/users/1
^ & body === MissingUser
"#,
"Unknown schema validation target 'MissingUser'",
"--> 6:1",
"^ & body === MissingUser",
);
}
#[test]
fn verify_outputs_json_error_report_with_unknown_schema_assertion_target_suggestions() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Unknown Assertion Target
description = Ensure schema assertion diagnostics include structured suggestions.
schema User {
id: UUID
}
---
Get user
GET https://example.com/users/1
^ & body === MissingUser
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(
parsed["error"]["diagnostics"][0]["code"],
"unknown_schema_validation_target"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["expectedKinds"][0],
"schema"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["expectedKinds"][1],
"scalar"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["availableNames"][0],
"User"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["kind"],
"replaceRange"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["text"],
"User"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["range"]["start"]["character"],
13
);
assert_eq!(
parsed["error"]["diagnostics"][0]["suggestions"][0]["range"]["end"]["character"],
24
);
}
#[test]
fn verify_reports_duplicate_declaration_names_at_second_declaration() {
assert_verify_failure(
r#"name = Verify Duplicate Declaration
description = Ensure duplicate declaration names point at the second definition.
schema User {
id: UUID
}
scalar User = string
---
Get user
GET https://example.com/users/1
"#,
"User is already defined",
"--> 6:1",
"scalar User = string",
);
}
#[test]
fn verify_reports_reserved_name_redefinitions_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Reserved Name
description = Ensure built-in names cannot be redefined.
scalar UUID = string
---
Get user
GET https://example.com/users/1
"#,
"UUID is reserved and cannot be redefined",
"--> 3:1",
"scalar UUID = string",
);
}
#[test]
fn verify_outputs_json_error_report_with_reserved_declaration_symbol_data() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Reserved Name
description = Ensure reserved declaration diagnostics include symbol metadata.
scalar UUID = string
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "reserved_declaration_name");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "scalar");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "UUID");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["reservedName"], "UUID");
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["reservedNamespace"],
"builtin"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
7
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["end"]["character"],
11
);
}
#[test]
fn verify_reports_invalid_scalar_predicate_combinations_at_declaration_span() {
assert_verify_failure(
r#"name = Verify Invalid Scalar Expression
description = Ensure invalid scalar base combinations point at the declaration.
scalar Broken = string & integer
---
Get user
GET https://example.com/users/1
"#,
"scalar expressions can only declare one base type",
"--> 3:17",
"scalar Broken = string & integer",
);
}
#[test]
fn verify_outputs_json_error_report_with_invalid_scalar_expression_symbol_data() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Invalid Scalar Expression
description = Ensure invalid scalar expression diagnostics include declaration metadata.
scalar Broken = string & integer
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "multiple_scalar_base_types");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "scalar");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "Broken");
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["declarationName"],
"Broken"
);
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["reason"], "multipleBaseTypes");
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
16
);
}
#[test]
fn verify_outputs_json_error_report_with_invalid_scalar_base_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Invalid Scalar Base
description = Ensure scalar base diagnostics include the referenced schema location.
schema User {
id: UUID
}
scalar AccountId = UUID
scalar UserAlias = User
---
Get user
GET https://example.com/users/1
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "invalid_scalar_base_reference");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "scalar");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "UserAlias");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["expectedKinds"][0], "scalar");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["availableNames"][0], "AccountId");
assert_eq!(parsed["error"]["diagnostics"][0]["data"]["referencedSchema"], "User");
assert_eq!(parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"], 19);
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["kind"], "replaceRange");
assert_eq!(parsed["error"]["diagnostics"][0]["suggestions"][0]["text"], "AccountId");
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert_eq!(related[0]["message"], "Schema 'User' is declared here.");
assert!(
related[0]["location"]["path"]
.as_str()
.is_some_and(|value| value.ends_with("collection.hen"))
);
}
#[test]
fn verify_reports_misplaced_declarations_after_requests_begin() {
assert_verify_failure(
r#"name = Verify Misplaced Declaration
description = Ensure declarations after requests begin get a targeted error.
---
Get user
GET https://example.com/users/1
schema User {
id: UUID
}
"#,
"schema and scalar declarations must appear before the first ---",
"--> 6:1",
"schema User {",
);
}
#[test]
fn verify_accepts_declaration_only_fragment_files() {
let workspace = TestWorkspace::new();
workspace.write_file(
"fragments/common_schema_types.hen",
r#"scalar ROLE = enum("admin", "member", "viewer")
schema TeamMember {
id: UUID
email: EMAIL
role: ROLE
}
"#,
);
let output = workspace.run_hen(["verify", "fragments/common_schema_types.hen"]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(
output.stdout.contains("Verification passed"),
"stdout: {}",
output.stdout
);
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
}
#[test]
fn verify_outputs_json_error_report_with_misplaced_declaration_related_information() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"name = Verify Misplaced Declaration
description = Ensure misplaced declaration diagnostics link back to the request boundary.
---
Get user
GET https://example.com/users/1
schema User {
id: UUID
}
"#,
);
let output = workspace.run_hen(["verify", "collection.hen", "--output", "json"]);
assert_eq!(output.status_code, 1, "stderr: {}", output.stderr);
let parsed: Value = serde_json::from_str(&output.stdout).expect("stdout should be valid json");
assert_eq!(parsed["error"]["diagnostics"][0]["code"], "misplaced_declaration");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["kind"], "schema");
assert_eq!(parsed["error"]["diagnostics"][0]["symbol"]["name"], "User");
assert_eq!(
parsed["error"]["diagnostics"][0]["data"]["declarationName"],
"User"
);
assert_eq!(
parsed["error"]["diagnostics"][0]["location"]["range"]["start"]["character"],
7
);
let related = parsed["error"]["diagnostics"][0]["relatedInformation"]
.as_array()
.expect("related information should be an array");
assert_eq!(related.len(), 1);
assert_eq!(related[0]["message"], "Requests begin here.");
assert_eq!(related[0]["location"]["range"]["start"]["line"], 2);
assert_eq!(related[0]["location"]["range"]["start"]["character"], 0);
}