use content_addressable::{canonical, ContentAddressable, ContentId};
use newt_interaction::{
binding::{validate_response, HandlerId, RegisteredAction, ResponderContext},
publish, HostMint, Refusal,
};
use newt_interaction::{
AssertionKind, Audience, ChoiceOption, Control, ControlId, ControlKind, ControlValue,
FeatureDemand, IdempotencyKey, InteractionDefinition, InteractionInstance, InteractionKind,
Nonce, OptionId, Provenance, Requirement, ResponderPolicy, ResponderProvenance, Response,
Revision, Scope, SecretRef, SemanticRole, Submission, SurfaceFeature,
};
use serde::Serialize;
use std::path::{Path, PathBuf};
fn vectors_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/interaction-vectors.json")
}
fn float_vectors_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/float-vectors.json")
}
fn invalid_vectors_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/interaction-vectors-invalid.json")
}
fn generate_invalid() -> Vec<Vector> {
let def = sample_definition();
let inst = sample_instance(&def);
let mismatches = [
(
"decision",
ControlValue::Text {
text: "not a choice".to_string(),
},
"control `decision` is kind=choice; a text value cannot answer it",
),
(
"remember",
ControlValue::Choice {
option: OptionId::new("deny").unwrap(),
},
"control `remember` is kind=toggle; a choice value cannot answer it",
),
(
"passphrase",
ControlValue::Toggle { on: true },
"control `passphrase` is kind=secret; a toggle value cannot answer it",
),
(
"no-such-field",
ControlValue::Toggle { on: true },
"control `no-such-field` does not exist in the definition",
),
];
let mut out = Vec::new();
for (control, value, why) in mismatches {
let mut v = vector(
&format!("response/invalid-{control}"),
"response",
&["definition", "instance"],
&sample_response(
&def,
&inst,
vec![Submission {
control: ControlId::new(control).unwrap(),
value,
}],
),
);
v.invalid_because = Some(why.to_string());
out.push(v);
}
out
}
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn from_hex(text: &str) -> Vec<u8> {
assert!(text.len().is_multiple_of(2), "hex must be byte-aligned");
(0..text.len())
.step_by(2)
.map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex digit"))
.collect()
}
#[derive(Serialize, serde::Deserialize, PartialEq, Eq, Debug, Clone)]
struct Vector {
name: String,
record: String,
json: serde_json::Value,
links: Vec<String>,
dagcbor_hex: String,
content_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
invalid_because: Option<String>,
}
fn vector<T: ContentAddressable + Serialize>(
name: &str,
record_type: &str,
links: &[&str],
value: &T,
) -> Vector {
let bytes = value.canonical_form().expect("canonical form");
Vector {
name: name.to_string(),
record: record_type.to_string(),
links: links.iter().map(|l| (*l).to_string()).collect(),
json: serde_json::to_value(value).expect("json"),
dagcbor_hex: to_hex(&bytes),
content_id: value.content_id().expect("content id").to_string(),
invalid_because: None,
}
}
fn choice_field(id: &str, options: &[(&str, SemanticRole, &str)]) -> Control {
Control {
id: ControlId::new(id).unwrap(),
kind: ControlKind::Choice {
options: options
.iter()
.map(|(oid, role, label)| ChoiceOption {
id: OptionId::new(*oid).unwrap(),
role: *role,
label: (*label).to_string(),
key: String::new(),
aliases: Vec::new(),
})
.collect(),
},
label: format!("{id} field"),
requirement: Requirement::Required,
}
}
fn field(id: &str, kind: ControlKind, requirement: Requirement) -> Control {
Control {
id: ControlId::new(id).unwrap(),
kind,
label: format!("{id} field"),
requirement,
}
}
fn sample_definition() -> InteractionDefinition {
let mut def = InteractionDefinition::new(
InteractionKind::Form,
"⊘ run_command wants to run `bash`",
vec![
choice_field(
"decision",
&[
("allow-once", SemanticRole::Allow, "allow once"),
("deny", SemanticRole::Deny, "deny (default)"),
],
),
field("reason", ControlKind::Text, Requirement::Optional),
field("remember", ControlKind::Toggle, Requirement::Optional),
field("passphrase", ControlKind::Secret, Requirement::Optional),
],
);
def.features = vec![
FeatureDemand {
feature: SurfaceFeature::new(SurfaceFeature::SECRET_INPUT).unwrap(),
requirement: Requirement::Required,
},
FeatureDemand {
feature: SurfaceFeature::new(SurfaceFeature::DIAGRAMS).unwrap(),
requirement: Requirement::Optional,
},
];
def
}
fn sample_instance(def: &InteractionDefinition) -> InteractionInstance {
InteractionInstance {
schema: newt_interaction::InstanceTag,
nonce: Nonce::new("1756200000000000000-0f4c1b2e").unwrap(),
definition: def.definition_id().unwrap(),
revision: Revision::FIRST,
ttl_ticks: 300,
scope: Scope {
workspace_key: "ws-abc".to_string(),
conversation_id: "conv-1".to_string(),
},
responder_policy: ResponderPolicy {
audiences: vec![Audience::Terminal, Audience::Web],
requires_assertion: true,
},
provenance: Provenance {
origin: "permission-gate".to_string(),
minted_tick: 42,
},
}
}
fn decision_answer() -> Submission {
Submission {
control: ControlId::new("decision").unwrap(),
value: ControlValue::Choice {
option: OptionId::new("deny").unwrap(),
},
}
}
fn sample_response(
def: &InteractionDefinition,
inst: &InteractionInstance,
values: Vec<Submission>,
) -> Response {
Response {
schema: newt_interaction::ResponseTag,
definition: def.definition_id().unwrap(),
instance: inst.instance_id().unwrap(),
revision: Revision::FIRST,
values,
idempotency_key: IdempotencyKey::new("first-try").unwrap(),
responder_provenance: ResponderProvenance {
kind: AssertionKind::SignedAssertion,
subject: "operator:example".to_string(),
audience: Audience::Web,
assertion: Some("assertion-handle-1".to_string()),
},
}
}
fn generate() -> Vec<Vector> {
let def = sample_definition();
let inst = sample_instance(&def);
let mut out = vec![
vector(
"definition/choice-with-feature-demands",
"definition",
&[],
&def,
),
vector(
"instance/two-audiences-assertion-required",
"instance",
&["definition"],
&inst,
),
];
for (name, control, value) in [
(
"choice",
"decision",
ControlValue::Choice {
option: OptionId::new("deny").unwrap(),
},
),
(
"text",
"reason",
ControlValue::Text {
text: "a typed answer".to_string(),
},
),
("toggle", "remember", ControlValue::Toggle { on: true }),
(
"secret-by-reference",
"passphrase",
ControlValue::Secret {
reference: SecretRef::new("vault-handle-9").unwrap(),
},
),
] {
let control = ControlId::new(control).unwrap();
let values = if control == decision_answer().control {
vec![Submission { control, value }]
} else {
vec![decision_answer(), Submission { control, value }]
};
out.push(vector(
&format!("response/value-{name}"),
"response",
&["definition", "instance"],
&sample_response(&def, &inst, values),
));
}
out
}
fn render(vectors: &[Vector]) -> String {
let mut text = serde_json::to_string_pretty(vectors).expect("render");
text.push('\n');
text
}
#[test]
fn the_committed_vectors_match_regeneration() {
let path = vectors_path();
let generated = render(&generate());
if std::env::var("NEWT_GOLDEN_UPDATE").is_ok() {
std::fs::create_dir_all(path.parent().unwrap()).expect("data dir");
std::fs::write(&path, &generated).expect("write vectors");
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{} is missing ({e}). Re-baseline deliberately with \
NEWT_GOLDEN_UPDATE=1, never by letting the test write it.",
path.display()
)
});
assert_eq!(
committed, generated,
"the committed vectors no longer match what this build produces. If \
that is intended, re-baseline with NEWT_GOLDEN_UPDATE=1 and say in \
the PR what changed on the wire — every non-Rust consumer reads \
these bytes."
);
}
#[test]
fn a_perturbed_vector_fails_the_byte_compare() {
let mut vectors = generate();
let original = render(&vectors);
let id = &mut vectors[0].content_id;
let last = id.pop().expect("non-empty id");
id.push(if last == 'a' { 'b' } else { 'a' });
assert_ne!(
render(&vectors),
original,
"perturbing a vector's content id did not change the rendered file — \
the comparison would pass over a corrupted vector"
);
}
#[test]
fn the_float_vectors_are_what_the_crate_encodes() {
for vector in committed(float_vectors_path()) {
let bytes = canonical::to_canonical_dagcbor(&vector.json).expect("canonical form");
assert_eq!(
to_hex(&bytes),
vector.dagcbor_hex,
"vector `{}`",
vector.name
);
let minted = ContentId::from_canonical_bytes(&bytes).to_string();
assert_eq!(minted, vector.content_id, "vector `{}`", vector.name);
}
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(
canonical::to_canonical_dagcbor(&bad).is_err(),
"{bad} must be refused"
);
}
}
#[test]
fn a_vector_id_is_reproducible_from_its_canonical_bytes() {
let committed: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(vectors_path()).expect("vectors file exists"),
)
.expect("vectors parse");
assert!(!committed.is_empty(), "no vectors to check");
for vector in &committed {
let bytes = from_hex(&vector.dagcbor_hex);
let minted = ContentId::from_canonical_bytes(&bytes);
assert_eq!(
minted.to_string(),
vector.content_id,
"vector `{}`: the recorded bytes do not mint the recorded id",
vector.name
);
let reencoded = match vector.record.as_str() {
"definition" => {
let record: InteractionDefinition =
canonical::from_canonical_dagcbor_checked(&bytes)
.expect("a committed vector decodes through the CHECKED door");
record.canonical_form().expect("re-encode")
}
"instance" => {
let record: InteractionInstance = canonical::from_canonical_dagcbor_checked(&bytes)
.expect("a committed vector decodes through the CHECKED door");
record.canonical_form().expect("re-encode")
}
"response" => {
let record: Response = canonical::from_canonical_dagcbor_checked(&bytes)
.expect("a committed vector decodes through the CHECKED door");
record.canonical_form().expect("re-encode")
}
other => panic!("vector `{}` names unknown record `{other}`", vector.name),
};
assert_eq!(
to_hex(&reencoded),
vector.dagcbor_hex,
"vector `{}`: decoding and re-encoding did not reproduce the bytes",
vector.name
);
for path in &vector.links {
let raw = vector.json.get(path).unwrap_or_else(|| {
panic!(
"vector `{}` declares link `{path}`, absent from its json",
vector.name
)
});
let text = raw
.as_str()
.unwrap_or_else(|| panic!("link `{path}` is not a string in json"));
let parsed: ContentId = text.parse().unwrap_or_else(|_| {
panic!(
"vector `{}`: link `{path}` is not a content id",
vector.name
)
});
assert_eq!(
parsed.to_string(),
text,
"vector `{}`: link `{path}` is not canonically rendered",
vector.name
);
}
}
}
#[test]
fn the_corpus_covers_every_record_and_value_variant() {
let committed: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(vectors_path()).expect("vectors file exists"),
)
.expect("vectors parse");
for record in ["definition", "instance", "response"] {
assert!(
committed.iter().any(|v| v.record == record),
"no vector covers the `{record}` record"
);
}
for variant in ["choice", "text", "toggle", "secret-by-reference"] {
assert!(
committed.iter().any(|v| v.name.ends_with(variant)),
"no vector covers the `{variant}` control value"
);
}
}
#[test]
fn decoding_then_encoding_reproduces_every_vector() {
let committed: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(vectors_path()).expect("vectors file exists"),
)
.expect("vectors parse");
assert!(!committed.is_empty(), "no vectors to check");
for vector in &committed {
let bytes = from_hex(&vector.dagcbor_hex);
let (reencoded, id) = match vector.record.as_str() {
"definition" => match newt_interaction::decode_definition(&bytes).unwrap() {
newt_interaction::Decoded::Known(r) => (
r.canonical_form().unwrap(),
r.content_id().unwrap().to_string(),
),
newt_interaction::Decoded::Unknown(_) => {
panic!("vector `{}` did not decode as known", vector.name)
}
},
"instance" => match newt_interaction::decode_instance(&bytes).unwrap() {
newt_interaction::Decoded::Known(r) => (
r.canonical_form().unwrap(),
r.content_id().unwrap().to_string(),
),
newt_interaction::Decoded::Unknown(_) => {
panic!("vector `{}` did not decode as known", vector.name)
}
},
"response" => match newt_interaction::decode_response(&bytes).unwrap() {
newt_interaction::Decoded::Known(r) => (
r.canonical_form().unwrap(),
r.content_id().unwrap().to_string(),
),
newt_interaction::Decoded::Unknown(_) => {
panic!("vector `{}` did not decode as known", vector.name)
}
},
other => panic!("vector `{}` names unknown record `{other}`", vector.name),
};
assert_eq!(
to_hex(&reencoded),
vector.dagcbor_hex,
"vector `{}`: decode then encode did not reproduce the bytes",
vector.name
);
assert_eq!(
id, vector.content_id,
"vector `{}`: the recomputed id differs from the recorded one",
vector.name
);
}
}
fn inconsistencies(corpus: &[Vector]) -> Vec<String> {
use newt_interaction::{ControlKind, Decoded};
let mut problems = Vec::new();
let mut definitions = std::collections::BTreeMap::new();
let mut instances = std::collections::BTreeMap::new();
for vector in corpus {
let bytes = from_hex(&vector.dagcbor_hex);
match vector.record.as_str() {
"definition" => {
if let Ok(Decoded::Known(d)) = newt_interaction::decode_definition(&bytes) {
definitions.insert(vector.content_id.clone(), d);
}
}
"instance" => {
if let Ok(Decoded::Known(i)) = newt_interaction::decode_instance(&bytes) {
instances.insert(vector.content_id.clone(), i);
}
}
_ => {}
}
}
for vector in corpus.iter().filter(|v| v.record == "response") {
let bytes = from_hex(&vector.dagcbor_hex);
let Ok(Decoded::Known(response)) = newt_interaction::decode_response(&bytes) else {
problems.push(format!("{}: does not decode as a response", vector.name));
continue;
};
let Some(definition) = definitions.get(&response.definition.to_string()) else {
problems.push(format!(
"{}: references definition {} which the corpus does not carry",
vector.name, response.definition
));
continue;
};
let Some(instance) = instances.get(&response.instance.to_string()) else {
problems.push(format!(
"{}: references instance {} which the corpus does not carry",
vector.name, response.instance
));
continue;
};
if instance.definition != response.definition {
problems.push(format!(
"{}: the instance offers a different definition than the response answers",
vector.name
));
}
if instance.revision != response.revision {
problems.push(format!(
"{}: response revision {:?} does not match the offer's {:?}",
vector.name, response.revision, instance.revision
));
}
for submission in &response.values {
let Some(control) = definition
.controls
.iter()
.find(|c| c.id == submission.control)
else {
problems.push(format!(
"{}: answers control `{}`, absent from the definition",
vector.name,
submission.control.as_str()
));
continue;
};
let matches = matches!(
(&control.kind, &submission.value),
(ControlKind::Choice { .. }, ControlValue::Choice { .. })
| (ControlKind::Text, ControlValue::Text { .. })
| (ControlKind::Toggle, ControlValue::Toggle { .. })
| (ControlKind::Secret, ControlValue::Secret { .. })
);
if !matches {
problems.push(format!(
"{}: control `{}` is {:?} but the value is a different kind",
vector.name,
submission.control.as_str(),
control.kind
));
}
if let (ControlKind::Choice { options }, ControlValue::Choice { option }) =
(&control.kind, &submission.value)
{
if !options.iter().any(|o| &o.id == option) {
problems.push(format!(
"{}: names option `{}`, which control `{}` does not offer",
vector.name,
option.as_str(),
submission.control.as_str()
));
}
}
}
}
problems
}
#[test]
fn the_positive_corpus_is_internally_consistent() {
let corpus: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(vectors_path()).expect("vectors file exists"),
)
.expect("vectors parse");
let problems = inconsistencies(&corpus);
assert!(
problems.is_empty(),
"the corpus is inconsistent: {problems:#?}"
);
assert!(
corpus.iter().all(|v| v.invalid_because.is_none()),
"the positive corpus carries an entry labelled invalid"
);
}
#[test]
fn the_checker_catches_every_labelled_invalid_vector() {
let mut corpus: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(vectors_path()).expect("vectors file exists"),
)
.expect("vectors parse");
let invalid: Vec<Vector> = serde_json::from_str(
&std::fs::read_to_string(invalid_vectors_path()).expect("invalid vectors file exists"),
)
.expect("invalid vectors parse");
assert!(!invalid.is_empty(), "no negative vectors to check");
for entry in &invalid {
assert!(
entry.invalid_because.is_some(),
"negative vector `{}` does not say why it is invalid",
entry.name
);
let mut one = corpus.clone();
one.push(entry.clone());
let problems = inconsistencies(&one);
assert!(
!problems.is_empty(),
"the checker accepted `{}`, which is labelled: {}",
entry.name,
entry.invalid_because.as_deref().unwrap_or("")
);
}
corpus.clear();
}
#[test]
fn the_committed_invalid_vectors_match_regeneration() {
let path = invalid_vectors_path();
let generated = render(&generate_invalid());
if std::env::var("NEWT_GOLDEN_UPDATE").is_ok() {
std::fs::create_dir_all(path.parent().unwrap()).expect("data dir");
std::fs::write(&path, &generated).expect("write vectors");
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{} is missing ({e}). Re-baseline deliberately with NEWT_GOLDEN_UPDATE=1.",
path.display()
)
});
assert_eq!(committed, generated);
}
mod schema_conformance {
use super::{inconsistencies, invalid_vectors_path, vectors_path, Vector};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
fn schema_path(record: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("schema/{record}.schema.json"))
}
fn validator_for(record: &str) -> jsonschema::Validator {
let path = schema_path(record);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| {
panic!(
"{} is missing ({e}). Regenerate deliberately: \
`cargo test -p newt-interaction --features schema`.",
path.display()
)
});
let schema: serde_json::Value =
serde_json::from_str(&text).expect("the published schema parses as JSON");
jsonschema::validator_for(&schema)
.unwrap_or_else(|e| panic!("{} is not a usable schema: {e}", path.display()))
}
fn load(path: &Path) -> Vec<Vector> {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("{} is missing ({e})", path.display()));
serde_json::from_str(&text).expect("vectors parse")
}
fn errors(validator: &jsonschema::Validator, document: &serde_json::Value) -> Vec<String> {
validator
.iter_errors(document)
.map(|e| {
let at = e.instance_path().to_string();
let at = if at.is_empty() { "/".to_string() } else { at };
format!("{at}: {e}")
})
.collect()
}
#[test]
fn every_positive_vector_validates_against_its_published_schema() {
let corpus = load(&vectors_path());
assert!(!corpus.is_empty(), "no vectors to validate");
let mut exercised = BTreeSet::new();
let mut problems = Vec::new();
for vector in &corpus {
exercised.insert(vector.record.clone());
for message in errors(&validator_for(&vector.record), &vector.json) {
problems.push(format!("{} [{}] {message}", vector.name, vector.record));
}
}
assert!(
problems.is_empty(),
"committed vectors violate their own published schema — serde and \
schemars disagree about a type: {problems:#?}"
);
let published: BTreeSet<String> = ["definition", "instance", "response"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(
exercised, published,
"the corpus stopped covering a published record type. Every schema \
we publish needs at least one vector proving it is inhabitable, or \
this test goes vacuous for the missing one."
);
}
#[test]
fn a_perturbed_document_is_rejected_for_a_schema_reason() {
let corpus = load(&vectors_path());
let definition = corpus
.iter()
.find(|v| v.record == "definition")
.expect("a definition vector");
let validator = validator_for("definition");
assert!(
errors(&validator, &definition.json).is_empty(),
"the unperturbed vector must validate, or the perturbations below \
prove nothing"
);
let mut wrong_tag = definition.json.clone();
wrong_tag["schema"] = serde_json::json!("newt.interaction.definition/v2");
let failures = errors(&validator, &wrong_tag);
assert!(
failures.iter().any(|f| f.contains("/schema")),
"a wrong version tag was accepted; the `const` pin is not enforced: \
{failures:#?}"
);
let mut bad_scalar = definition.json.clone();
bad_scalar["controls"][0]["id"] = serde_json::json!("has a space");
let failures = errors(&validator, &bad_scalar);
assert!(
failures.iter().any(|f| f.contains("/controls/0/id")),
"a ControlId with a space was accepted; the published `pattern` is \
not enforced: {failures:#?}"
);
let mut extra = definition.json.clone();
extra["unexpected"] = serde_json::json!(true);
assert!(
!errors(&validator, &extra).is_empty(),
"an unknown property was accepted; `additionalProperties: false` is \
not enforced"
);
}
#[test]
fn the_negative_corpus_is_schema_valid_yet_semantically_rejected() {
let positive = load(&vectors_path());
let negative = load(&invalid_vectors_path());
assert!(!negative.is_empty(), "no negative vectors to contrast");
for entry in &negative {
let schema_problems = errors(&validator_for(&entry.record), &entry.json);
assert!(
schema_problems.is_empty(),
"`{}` is meant to be structurally valid and semantically wrong, \
but the schema rejected it. Either the record shape drifted or \
this negative belongs in a different corpus: {schema_problems:#?}",
entry.name
);
let mut corpus = positive.clone();
corpus.push(entry.clone());
let semantic_problems = inconsistencies(&corpus);
assert!(
!semantic_problems.is_empty(),
"`{}` passed BOTH layers. It is labelled invalid ({}), so the \
semantic checker has a hole the schema cannot cover for it.",
entry.name,
entry.invalid_because.as_deref().unwrap_or("unlabelled")
);
}
}
}
#[test]
fn every_positive_response_vector_is_accepted_against_the_offer() {
let (definition, instance) = committed_offer();
let lifecycle = publish(&HostMint::assert_host_authority(), &instance, &definition)
.expect("the offer publishes");
let actions = corpus_actions();
let context = ResponderContext {
workspace_key: &instance.scope.workspace_key,
registered: &actions,
};
let mut checked = 0;
for vector in committed(vectors_path()) {
if vector.record != "response" {
continue;
}
let response: Response =
canonical::from_canonical_dagcbor_checked(&from_hex(&vector.dagcbor_hex))
.expect("a committed vector decodes");
validate_response(&definition, &instance, &lifecycle, &response, &context).unwrap_or_else(
|e| {
panic!(
"vector `{}` is published as valid but the protocol refuses it: {e}",
vector.name
)
},
);
checked += 1;
}
assert!(checked > 0, "no response vector was checked");
}
#[test]
fn every_invalid_vector_is_refused_by_that_same_path() {
let (definition, instance) = committed_offer();
let lifecycle = publish(&HostMint::assert_host_authority(), &instance, &definition)
.expect("the offer publishes");
let actions = corpus_actions();
let context = ResponderContext {
workspace_key: &instance.scope.workspace_key,
registered: &actions,
};
let mut checked = 0;
for vector in committed(invalid_vectors_path()) {
assert!(
vector.invalid_because.is_some(),
"vector `{}` sits in the invalid corpus without saying why",
vector.name
);
let response: Response =
canonical::from_canonical_dagcbor_checked(&from_hex(&vector.dagcbor_hex))
.expect("an invalid vector still decodes — it is well-formed, not valid");
let refusal = validate_response(&definition, &instance, &lifecycle, &response, &context)
.expect_err("the invalid corpus must be refused");
assert!(
matches!(
refusal,
Refusal::WrongControlType { .. } | Refusal::ExtraControl { .. }
),
"vector `{}` says `{}` but was refused as {refusal}",
vector.name,
vector.invalid_because.as_deref().unwrap_or("")
);
checked += 1;
}
assert!(
checked > 0,
"the invalid corpus is empty; nothing was refused"
);
}
fn committed(path: PathBuf) -> Vec<Vector> {
let vectors: Vec<Vector> =
serde_json::from_str(&std::fs::read_to_string(&path).expect("vectors file exists"))
.expect("vectors parse");
assert!(!vectors.is_empty(), "{} is empty", path.display());
vectors
}
fn committed_offer() -> (InteractionDefinition, InteractionInstance) {
let mut definition = None;
let mut instance = None;
for vector in committed(vectors_path()) {
let bytes = from_hex(&vector.dagcbor_hex);
match vector.record.as_str() {
"definition" => {
definition = Some(canonical::from_canonical_dagcbor_checked(&bytes).unwrap())
}
"instance" => {
instance = Some(canonical::from_canonical_dagcbor_checked(&bytes).unwrap())
}
_ => {}
}
}
(
definition.expect("a definition vector"),
instance.expect("an instance vector"),
)
}
fn corpus_actions() -> Vec<RegisteredAction> {
["allow-once", "deny"]
.into_iter()
.map(|option| RegisteredAction {
option: OptionId::new(option).unwrap(),
handler: HandlerId::new(format!("gate::{option}").replace('-', "_")).unwrap(),
audiences: vec![Audience::Terminal, Audience::Web],
})
.collect()
}