From 5268a5b168024b9751c3a66a8dcf565f460d6de5 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sat, 22 Aug 2026 21:59:16 +0000 Subject: [PATCH 1/5] test: pin InterpreterCallerMeta v1 serde defaults, validate chain, length caps, TryFrom paths AMT g5-interpretercallermeta-v1 unit 1. The model was zero-covered; these tests discriminate serde defaults (desc/source/alias/signed_context/ caller_context/cells), the nested #[validate] chain down to ContextCell, the min/max length constraints, TryFrom bytes parse+validate, TryFrom meta item via unpack (deflate), deny_unknown_fields and the camelCase wire format. Co-Authored-By: Claude Fable 5 --- .../src/meta/types/interpreter_caller/v1.rs | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/crates/cli/src/meta/types/interpreter_caller/v1.rs b/crates/cli/src/meta/types/interpreter_caller/v1.rs index aa228bb6..80aabd95 100644 --- a/crates/cli/src/meta/types/interpreter_caller/v1.rs +++ b/crates/cli/src/meta/types/interpreter_caller/v1.rs @@ -159,3 +159,228 @@ pub struct ContextCell { #[validate] pub alias: Option, } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + use crate::meta::{ContentEncoding, ContentLanguage, ContentType, KnownMagic}; + + /// A fully-populated valid InterpreterCallerMeta JSON document. + fn valid_json() -> serde_json::Value { + serde_json::json!({ + "name": "Test Caller", + "abiName": "TestCaller", + "desc": "A caller for tests.", + "source": "https://github.com/rainlanguage/rain.metadata", + "alias": "test-caller", + "methods": [{ + "name": "Add Order", + "abiName": "addOrder", + "desc": "Adds an order.", + "inputs": [{ + "name": "Order", + "abiName": "order", + "desc": "The order.", + "path": "[0]" + }], + "expressions": [{ + "name": "Calculate", + "abiName": "calculateOrder", + "desc": "Calculates.", + "path": "[0].evaluableConfig", + "signedContext": true, + "callerContext": true, + "contextColumns": [{ + "name": "Base", + "desc": "Base column.", + "alias": "base", + "cells": [{ + "name": "Sender", + "desc": "The sender.", + "alias": "sender" + }] + }] + }] + }] + }) + } + + fn parse(v: &serde_json::Value) -> Result { + InterpreterCallerMeta::try_from(serde_json::to_vec(v).unwrap()) + } + + /// Omitted optional fields parse and take their documented defaults: + /// empty desc/source, no alias, both context flags false, no context + /// columns, no cells. + #[test] + fn test_serde_defaults() { + let v = serde_json::json!({ + "name": "Test Caller", + "abiName": "TestCaller", + "methods": [{ + "name": "Add Order", + "abiName": "addOrder", + "inputs": [{ + "name": "Order", + "abiName": "order", + "path": "[0]" + }], + "expressions": [ + { + "name": "Calculate", + "abiName": "calculateOrder", + "path": "[0].evaluableConfig", + "contextColumns": [{ + "name": "Base" + }] + }, + { + "name": "Handle", + "abiName": "handleOrder", + "path": "[1].evaluableConfig" + } + ] + }] + }); + let parsed = parse(&v).unwrap(); + assert_eq!(parsed.desc.value, ""); + assert_eq!(parsed.source.value, ""); + assert!(parsed.alias.is_none()); + let method = &parsed.methods[0]; + assert_eq!(method.desc.value, ""); + assert_eq!(method.inputs[0].desc.value, ""); + let expression = &method.expressions[0]; + assert_eq!(expression.desc.value, ""); + assert!(!expression.signed_context); + assert!(!expression.caller_context); + let column = &expression.context_columns[0]; + assert_eq!(column.desc.value, ""); + assert!(column.alias.is_none()); + assert!(column.cells.is_empty()); + assert!(method.expressions[1].context_columns.is_empty()); + } + + /// Invalid nested values fail validation at every depth of the + /// #[validate] chain: methods -> inputs and + /// methods -> expressions -> context_columns -> cells. + #[test] + fn test_nested_validate_chain() { + // Baseline sanity: the valid document parses and validates. + assert!(parse(&valid_json()).is_ok()); + + // A RainTitle must not begin with a space: " x" is invalid at + // any depth. + for pointer in [ + "/methods/0/name", + "/methods/0/inputs/0/name", + "/methods/0/expressions/0/name", + "/methods/0/expressions/0/contextColumns/0/name", + "/methods/0/expressions/0/contextColumns/0/cells/0/name", + ] { + let mut v = valid_json(); + *v.pointer_mut(pointer).unwrap() = serde_json::Value::String(" x".to_string()); + let err = parse(&v).unwrap_err(); + assert!( + matches!(err, Error::ValidationErrors(_)), + "expected validation error for {pointer}, got {err:?}" + ); + } + } + + /// methods and inputs require at least one element; context_columns + /// allows at most u8::MAX (255) elements. + #[test] + fn test_length_constraints() { + let mut v = valid_json(); + *v.pointer_mut("/methods").unwrap() = serde_json::json!([]); + assert!(matches!( + parse(&v).unwrap_err(), + Error::ValidationErrors(_) + )); + + let mut v = valid_json(); + *v.pointer_mut("/methods/0/inputs").unwrap() = serde_json::json!([]); + assert!(matches!( + parse(&v).unwrap_err(), + Error::ValidationErrors(_) + )); + + let column = serde_json::json!({ "name": "Col" }); + let mut v = valid_json(); + *v.pointer_mut("/methods/0/expressions/0/contextColumns") + .unwrap() = serde_json::Value::Array(vec![column.clone(); 255]); + assert!(parse(&v).is_ok()); + + let mut v = valid_json(); + *v.pointer_mut("/methods/0/expressions/0/contextColumns") + .unwrap() = serde_json::Value::Array(vec![column; 256]); + assert!(matches!( + parse(&v).unwrap_err(), + Error::ValidationErrors(_) + )); + } + + /// TryFrom> and TryFrom<&[u8]> validate after parsing: + /// syntactically-valid JSON with semantically-invalid values errors, + /// and valid documents round-trip with their values intact. + #[test] + fn test_try_from_bytes_validates() { + let mut invalid = valid_json(); + *invalid.pointer_mut("/name").unwrap() = serde_json::Value::String(" x".to_string()); + let bytes = serde_json::to_vec(&invalid).unwrap(); + + let err = InterpreterCallerMeta::try_from(bytes.clone()).unwrap_err(); + assert!(matches!(err, Error::ValidationErrors(_))); + let err = InterpreterCallerMeta::try_from(bytes.as_slice()).unwrap_err(); + assert!(matches!(err, Error::ValidationErrors(_))); + + let valid_bytes = serde_json::to_vec(&valid_json()).unwrap(); + let ok = InterpreterCallerMeta::try_from(valid_bytes.clone()).unwrap(); + assert_eq!(ok.name.value, "Test Caller"); + assert_eq!(ok.abi_name.value, "TestCaller"); + let ok = InterpreterCallerMeta::try_from(valid_bytes.as_slice()).unwrap(); + assert_eq!(ok.methods[0].abi_name.value, "addOrder"); + } + + /// TryFrom unpacks the payload per the item's + /// content encoding before parsing. + #[test] + fn test_try_from_meta_item_unpacks_encoding() { + let json_bytes = serde_json::to_vec(&valid_json()).unwrap(); + let item = RainMetaDocumentV1Item { + payload: serde_bytes::ByteBuf::from(ContentEncoding::Deflate.encode(&json_bytes)), + magic: KnownMagic::InterpreterCallerMetaV1, + content_type: ContentType::Json, + content_encoding: ContentEncoding::Deflate, + content_language: ContentLanguage::En, + schema: None, + }; + let parsed = InterpreterCallerMeta::try_from(item).unwrap(); + assert_eq!(parsed.name.value, "Test Caller"); + assert_eq!(parsed.methods.len(), 1); + } + + /// Unknown fields are rejected. + #[test] + fn test_deny_unknown_fields() { + let mut v = valid_json(); + v.as_object_mut() + .unwrap() + .insert("unknownField".to_string(), serde_json::json!(1)); + let err = parse(&v).unwrap_err(); + assert!(matches!(err, Error::SerdeJsonError(_))); + } + + /// Field names are camelCase on the wire; the snake_case spelling is + /// an unknown field. + #[test] + fn test_camel_case_wire_format() { + assert!(parse(&valid_json()).is_ok()); + + let mut v = valid_json(); + let obj = v.as_object_mut().unwrap(); + let abi = obj.remove("abiName").unwrap(); + obj.insert("abi_name".to_string(), abi); + assert!(parse(&v).is_err()); + } +} From f77ffdd8e8b3bc38c018eae4b11044f253f7bbe6 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sat, 22 Aug 2026 22:03:47 +0000 Subject: [PATCH 2/5] test: pin build subcommand item conversion, arity guards, file IO and output encoding AMT g5-interpretercallermeta-v1 unit 2. Discriminates normalize-then- encode in the BuildItem conversion (deflate of normalized vs raw payload), the four arity guard messages (which fire before any file IO), file reading into build items, and binary vs hex output encoding. Co-Authored-By: Claude Fable 5 --- crates/cli/src/cli/build.rs | 143 ++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/crates/cli/src/cli/build.rs b/crates/cli/src/cli/build.rs index cbf5044d..3cd9ede1 100644 --- a/crates/cli/src/cli/build.rs +++ b/crates/cli/src/cli/build.rs @@ -312,4 +312,147 @@ mod tests { Ok(()) } + + use clap::Parser; + use std::io::Write; + use super::{Build, build}; + + /// Conversion normalizes the payload for the item's magic and then + /// applies the content encoding. + #[test] + fn test_item_normalize_then_encode() -> anyhow::Result<()> { + // "[ ]" normalizes to "[]" for a solidity ABI, then deflates. + let build_item = BuildItem { + data: "[ ]".as_bytes().to_vec(), + magic: KnownMagic::SolidityAbiV2, + content_type: ContentType::Json, + content_encoding: ContentEncoding::Deflate, + content_language: ContentLanguage::En, + }; + let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?; + assert_eq!( + meta_document.payload.as_ref(), + ContentEncoding::Deflate.encode("[]".as_bytes()) + ); + + // Un-normalizable data is rejected. + let invalid_item = BuildItem { + data: "not json".as_bytes().to_vec(), + ..build_item + }; + assert!(RainMetaDocumentV1Item::try_from(&invalid_item).is_err()); + Ok(()) + } + + fn parse_build(args: &[&str]) -> Build { + Build::try_parse_from(args).unwrap() + } + + /// Each arity guard fires with its own message, before any file IO: + /// the input path never exists and yet the mismatch is what errors. + #[test] + fn test_build_arity_guards() { + let b = parse_build(&[ + "build", + "-i", "does-not-exist.json", + "-m", "solidity-abi-v2", + "-m", "solidity-abi-v2", + ]); + assert_eq!( + build(b).unwrap_err().to_string(), + "1 inputs does not match 2 magic numbers." + ); + + let b = parse_build(&[ + "build", + "-i", "does-not-exist.json", + "-m", "solidity-abi-v2", + "-t", "json", + "-t", "json", + ]); + assert_eq!( + build(b).unwrap_err().to_string(), + "1 inputs does not match 2 content types." + ); + + let b = parse_build(&[ + "build", + "-i", "does-not-exist.json", + "-m", "solidity-abi-v2", + "-t", "json", + "-e", "identity", + "-e", "identity", + ]); + assert_eq!( + build(b).unwrap_err().to_string(), + "1 inputs does not match 2 content encodings." + ); + + let b = parse_build(&[ + "build", + "-i", "does-not-exist.json", + "-m", "solidity-abi-v2", + "-t", "json", + "-e", "identity", + "-l", "en", + "-l", "en", + ]); + assert_eq!( + build(b).unwrap_err().to_string(), + "1 inputs does not match 2 content languages." + ); + } + + /// build() reads each input file, builds the document under the + /// global magic and writes it to the output path; the hex output + /// encoding is honored. + #[test] + fn test_build_reads_files_and_encodes_output() -> anyhow::Result<()> { + let mut input = tempfile::NamedTempFile::new()?; + input.write_all("[ ]".as_bytes())?; + let output = tempfile::NamedTempFile::new()?; + + let expected = build_bytes( + KnownMagic::RainMetaDocumentV1, + vec![BuildItem { + data: "[ ]".as_bytes().to_vec(), + magic: KnownMagic::SolidityAbiV2, + content_type: ContentType::Json, + content_encoding: ContentEncoding::Identity, + content_language: ContentLanguage::En, + }], + )?; + + let input_path = input.path().to_str().unwrap().to_string(); + let output_path = output.path().to_str().unwrap().to_string(); + + let b = parse_build(&[ + "build", + "-i", &input_path, + "-m", "solidity-abi-v2", + "-t", "json", + "-e", "identity", + "-l", "en", + "-o", &output_path, + ]); + build(b)?; + assert_eq!(std::fs::read(output.path())?, expected); + + let b = parse_build(&[ + "build", + "-i", &input_path, + "-m", "solidity-abi-v2", + "-t", "json", + "-e", "identity", + "-l", "en", + "-o", &output_path, + "-E", "hex", + ]); + build(b)?; + assert_eq!( + std::fs::read_to_string(output.path())?, + alloy::primitives::hex::encode_prefixed(&expected) + ); + Ok(()) + } } From b8b361ccb54afd6301ecfada6544b13cc7bcdd84 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sat, 22 Aug 2026 22:05:24 +0000 Subject: [PATCH 3/5] test: pin generate/metaboard tx-data error wrapping, pretty output, dir creation, subject and magic AMT g5-interpretercallermeta-v1 unit 4. Strengthens the nonexistent-file error to its wrapped message, the write_output snapshot to exact pretty JSON, adds parent-dir creation coverage, and pins the emitted meta_bytes to the rain meta document magic prefix and the subject to the keccak256 of the bare cbor item map via an independently constructed item. Co-Authored-By: Claude Fable 5 --- crates/cli/src/cli/generate.rs | 32 +++++++++++++++++++++++++++++++- crates/cli/src/metaboard.rs | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/cli/generate.rs b/crates/cli/src/cli/generate.rs index 090a5731..f5f3801b 100644 --- a/crates/cli/src/cli/generate.rs +++ b/crates/cli/src/cli/generate.rs @@ -124,7 +124,12 @@ mod tests { #[test] fn test_read_input_content_nonexistent_file() { let result = read_input_content(Some(PathBuf::from("/nonexistent/file.rain"))); - assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.to_string() + .contains("Failed to read file '/nonexistent/file.rain'"), + "unexpected error: {err}" + ); } #[test] @@ -144,6 +149,31 @@ mod tests { assert!(written_content.contains("0x1234567890abcdef")); assert!(written_content.contains("0xdeadbeef")); assert!(written_content.contains("0xcafebabe")); + + // The output is pretty-printed with two-space indentation, in + // field declaration order. + let expected = "{\n \"subject\": \"0x1234567890abcdef\",\n \"meta_bytes\": \"0xdeadbeef\",\n \"calldata\": \"0xcafebabe\"\n}"; + assert_eq!(written_content, expected); + } + + /// write_output creates missing parent directories of the output + /// path before writing. + #[test] + fn test_write_output_creates_parent_dirs() { + let deployment_data = DotrainSourceEmitData { + subject: "0x01".to_string(), + meta_bytes: "0x02".to_string(), + calldata: "0x03".to_string(), + }; + + let dir = tempfile::tempdir().unwrap(); + let nested = dir.path().join("a").join("b").join("out.json"); + write_output(&deployment_data, Some(nested.clone())).unwrap(); + + let written = fs::read_to_string(&nested).unwrap(); + assert!(written.contains("0x01")); + assert!(written.contains("0x02")); + assert!(written.contains("0x03")); } #[test] diff --git a/crates/cli/src/metaboard.rs b/crates/cli/src/metaboard.rs index 8469ad12..d2fbfa68 100644 --- a/crates/cli/src/metaboard.rs +++ b/crates/cli/src/metaboard.rs @@ -147,6 +147,24 @@ mod tests { let meta_bytes_from_field = alloy::hex::decode(deployment.meta_bytes.trim_start_matches("0x")).unwrap(); assert_eq!(decoded.meta.as_ref(), meta_bytes_from_field.as_slice()); + + // meta_bytes is a cbor-seq under the rain meta document magic + // prefix 0xff0a89c674ee7874. + assert!(deployment.meta_bytes.starts_with("0xff0a89c674ee7874")); + + // The subject is the keccak256 of the BARE cbor item map — no + // rain-meta-document magic prefix — pinned here against an + // independently constructed item. + let item = RainMetaDocumentV1Item { + payload: serde_bytes::ByteBuf::from(content.as_bytes().to_vec()), + magic: KnownMagic::DotrainSourceV1, + content_type: ContentType::OctetStream, + content_encoding: ContentEncoding::None, + content_language: ContentLanguage::None, + schema: None, + }; + let expected_subject = alloy::primitives::keccak256(item.cbor_encode().unwrap()); + assert_eq!(deployment.subject, hex::encode_prefixed(expected_subject)); } #[test] From 26f9b39abe5570f0e613bc5ba8a7a50b7f492684 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sat, 22 Aug 2026 22:08:40 +0000 Subject: [PATCH 4/5] =?UTF-8?q?test:=20pin=20CLI=20plumbing=20=E2=80=94=20?= =?UTF-8?q?output=20encodings,=20validate,=20schema=20show/ls,=20magic=20l?= =?UTF-8?q?s,=20solc=20artifact,=20subgraph=20constants=20=E2=80=94=20plus?= =?UTF-8?q?=20binary=20e2e=20stdin/stdout=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AMT g5-interpretercallermeta-v1 units 3 and 5. In-file tests pin output() binary/hex file writing, validate() normalizability, schema show's four schema arms, pretty flag and unsupported message, solc artifact component extraction (including the documented null passthrough), and the KnownSubgraphs URL table with of_chain mapping. The new tests/cli.rs drives the built binary to observe stdout/stdin: magic ls and schema ls listings, schema show stdout, schema-check's success count message, and generate source over stdin to stdout. Co-Authored-By: Claude Fable 5 --- crates/cli/src/cli/output.rs | 33 +++++++ crates/cli/src/cli/schema/show.rs | 72 +++++++++++++++ crates/cli/src/cli/validate.rs | 31 +++++++ crates/cli/src/solc/mod.rs | 50 +++++++++++ crates/cli/src/subgraph/mod.rs | 90 +++++++++++++++++++ crates/cli/tests/cli.rs | 143 ++++++++++++++++++++++++++++++ 6 files changed, 419 insertions(+) create mode 100644 crates/cli/tests/cli.rs diff --git a/crates/cli/src/cli/output.rs b/crates/cli/src/cli/output.rs index a1e25534..7c4d8103 100644 --- a/crates/cli/src/cli/output.rs +++ b/crates/cli/src/cli/output.rs @@ -32,3 +32,36 @@ pub fn output( } Ok(()) } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + + /// Binary encoding writes the bytes through unchanged. + #[test] + fn test_output_binary_writes_exact_bytes_to_file() { + let file = tempfile::NamedTempFile::new().unwrap(); + let path = file.path().to_path_buf(); + output( + &Some(path.clone()), + SupportedOutputEncoding::Binary, + &[0x00, 0x01, 0xff], + ) + .unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), vec![0x00, 0x01, 0xff]); + } + + /// Hex encoding writes 0x-prefixed lowercase hex. + #[test] + fn test_output_hex_writes_prefixed_hex_to_file() { + let file = tempfile::NamedTempFile::new().unwrap(); + let path = file.path().to_path_buf(); + output( + &Some(path.clone()), + SupportedOutputEncoding::Hex, + &[0x00, 0x01, 0xff], + ) + .unwrap(); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "0x0001ff"); + } +} diff --git a/crates/cli/src/cli/schema/show.rs b/crates/cli/src/cli/schema/show.rs index a0226f04..7ef01360 100644 --- a/crates/cli/src/cli/schema/show.rs +++ b/crates/cli/src/cli/schema/show.rs @@ -45,3 +45,75 @@ pub fn show(s: Show) -> anyhow::Result<()> { schema_string.as_bytes(), ) } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + + fn show_to_string(schema: KnownMeta, pretty_print: bool) -> anyhow::Result { + let file = tempfile::NamedTempFile::new().unwrap(); + let path = file.path().to_path_buf(); + show(Show { + schema, + output_path: Some(path.clone()), + pretty_print, + })?; + Ok(std::fs::read_to_string(&path).unwrap()) + } + + /// Each supported meta produces its own schema; compact output by + /// default (no newlines). + #[test] + fn test_show_op_v1_schema_compact() { + let s = show_to_string(KnownMeta::OpV1, false).unwrap(); + let v: serde_json::Value = serde_json::from_str(&s).unwrap(); + assert_eq!(v["title"], "OpMeta."); + assert!(!s.contains('\n')); + } + + /// The pretty flag pretty-prints the same schema. + #[test] + fn test_show_pretty_print() { + let s = show_to_string(KnownMeta::OpV1, true).unwrap(); + assert!(s.starts_with("{\n")); + let v: serde_json::Value = serde_json::from_str(&s).unwrap(); + assert_eq!(v["title"], "OpMeta."); + } + + /// All four supported arms return the schema of their own meta type. + #[test] + fn test_show_supported_schemas_are_distinct() { + let op = show_to_string(KnownMeta::OpV1, false).unwrap(); + assert!(op.contains("OpMeta")); + let authoring = show_to_string(KnownMeta::AuthoringMetaV1, false).unwrap(); + assert!(authoring.contains("AuthoringMeta")); + let solidity = show_to_string(KnownMeta::SolidityAbiV2, false).unwrap(); + assert!(solidity.contains("SolidityAbi")); + let caller = show_to_string(KnownMeta::InterpreterCallerMetaV1, false).unwrap(); + assert!(caller.contains("InterpreterCallerMeta")); + for pair in [ + (&op, &authoring), + (&op, &solidity), + (&op, &caller), + (&authoring, &solidity), + (&authoring, &caller), + (&solidity, &caller), + ] { + assert_ne!(pair.0, pair.1); + } + } + + /// Metas without a JSON schema error with the exact unsupported + /// message. + #[test] + fn test_show_unsupported_meta_error() { + let file = tempfile::NamedTempFile::new().unwrap(); + let err = show(Show { + schema: KnownMeta::DotrainV1, + output_path: Some(file.path().to_path_buf()), + pretty_print: false, + }) + .unwrap_err(); + assert_eq!(err.to_string(), "Unsupported for dotrain-v1 meta"); + } +} diff --git a/crates/cli/src/cli/validate.rs b/crates/cli/src/cli/validate.rs index eaf00915..7578d0db 100644 --- a/crates/cli/src/cli/validate.rs +++ b/crates/cli/src/cli/validate.rs @@ -20,3 +20,34 @@ pub fn validate(v: Validate) -> anyhow::Result<()> { let _normalized = v.meta.normalize(&data)?; Ok(()) } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + use std::io::Write; + + /// A meta that normalizes is valid. + #[test] + fn test_validate_ok_for_valid_meta() { + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"[]").unwrap(); + let v = Validate { + meta: KnownMeta::SolidityAbiV2, + input_path: file.path().to_path_buf(), + }; + assert!(validate(v).is_ok()); + } + + /// A meta that does not normalize is invalid: validity IS + /// normalizability. + #[test] + fn test_validate_err_for_invalid_meta() { + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"{\"not\": \"an abi\"}").unwrap(); + let v = Validate { + meta: KnownMeta::SolidityAbiV2, + input_path: file.path().to_path_buf(), + }; + assert!(validate(v).is_err()); + } +} diff --git a/crates/cli/src/solc/mod.rs b/crates/cli/src/solc/mod.rs index 1e6d10e0..abbe30cb 100644 --- a/crates/cli/src/solc/mod.rs +++ b/crates/cli/src/solc/mod.rs @@ -27,3 +27,53 @@ pub fn extract_artifact_component_json( ArtifactComponent::DeployedBytecode => Ok(json["deployedBytecode"].clone()), } } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + + fn artifact_json() -> Vec { + serde_json::json!({ + "abi": [{ "type": "function", "name": "foo" }], + "bytecode": { "object": "0x6001" }, + "deployedBytecode": { "object": "0x6002" } + }) + .to_string() + .into_bytes() + } + + /// Each component arm extracts exactly its own key. + #[test] + fn test_extract_each_component() { + let data = artifact_json(); + assert_eq!( + extract_artifact_component_json(ArtifactComponent::Abi, &data).unwrap(), + serde_json::json!([{ "type": "function", "name": "foo" }]) + ); + assert_eq!( + extract_artifact_component_json(ArtifactComponent::Bytecode, &data).unwrap(), + serde_json::json!({ "object": "0x6001" }) + ); + assert_eq!( + extract_artifact_component_json(ArtifactComponent::DeployedBytecode, &data).unwrap(), + serde_json::json!({ "object": "0x6002" }) + ); + } + + /// Documented: no null check is performed — a missing component is + /// returned as JSON null, not an error. + #[test] + fn test_missing_component_returns_null() { + assert_eq!( + extract_artifact_component_json(ArtifactComponent::Abi, b"{}").unwrap(), + serde_json::Value::Null + ); + } + + /// Non-utf8 and non-json inputs error. + #[test] + fn test_invalid_input_errors() { + assert!(extract_artifact_component_json(ArtifactComponent::Abi, &[0xff, 0xfe]).is_err()); + assert!(extract_artifact_component_json(ArtifactComponent::Abi, b"not json").is_err()); + } +} diff --git a/crates/cli/src/subgraph/mod.rs b/crates/cli/src/subgraph/mod.rs index 91cba73b..a86817dc 100644 --- a/crates/cli/src/subgraph/mod.rs +++ b/crates/cli/src/subgraph/mod.rs @@ -58,3 +58,93 @@ impl KnownSubgraphs { } } } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::*; + + const ETH_LEGACY: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-ethereum"; + const ETH_NP: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-np-eth"; + const ETH_NPE2: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-npe2-eth"; + const POLY_LEGACY: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-polygon"; + const POLY_NP: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-np-matic"; + const POLY_NPE2: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-npe2-mati"; + const MUMBAI_LEGACY: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry"; + const MUMBAI_NP: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-np"; + const MUMBAI_NPE2: &str = + "https://api.thegraph.com/subgraphs/name/rainlanguage/interpreter-registry-npe2"; + + /// The per-network triples are [legacy, np, npe2] with exactly these + /// URLs. + #[test] + fn test_network_triples_are_exact() { + assert_eq!(KnownSubgraphs::ETHEREUM, [ETH_LEGACY, ETH_NP, ETH_NPE2]); + assert_eq!(KnownSubgraphs::POLYGON, [POLY_LEGACY, POLY_NP, POLY_NPE2]); + assert_eq!( + KnownSubgraphs::MUMBAI, + [MUMBAI_LEGACY, MUMBAI_NP, MUMBAI_NPE2] + ); + } + + /// The flavor slices pick the same column from every network, and ALL + /// concatenates the three networks in order. + #[test] + fn test_flavor_slices_and_all() { + assert_eq!( + KnownSubgraphs::LEGACY, + [ETH_LEGACY, POLY_LEGACY, MUMBAI_LEGACY] + ); + assert_eq!(KnownSubgraphs::NP, [ETH_NP, POLY_NP, MUMBAI_NP]); + assert_eq!(KnownSubgraphs::NPE2, [ETH_NPE2, POLY_NPE2, MUMBAI_NPE2]); + assert_eq!( + KnownSubgraphs::ALL, + [ + ETH_LEGACY, + ETH_NP, + ETH_NPE2, + POLY_LEGACY, + POLY_NP, + POLY_NPE2, + MUMBAI_LEGACY, + MUMBAI_NP, + MUMBAI_NPE2, + ] + ); + } + + /// of_chain maps 1/137/80001 to their networks. + #[test] + fn test_of_chain_known_networks() { + assert_eq!( + KnownSubgraphs::of_chain(1).unwrap(), + KnownSubgraphs::ETHEREUM + ); + assert_eq!( + KnownSubgraphs::of_chain(137).unwrap(), + KnownSubgraphs::POLYGON + ); + assert_eq!( + KnownSubgraphs::of_chain(80001).unwrap(), + KnownSubgraphs::MUMBAI + ); + } + + /// Every other chain id is unsupported. + #[test] + fn test_of_chain_unknown_network_errors() { + for id in [0u64, 2, 100, 8453, u64::MAX] { + assert!(matches!( + KnownSubgraphs::of_chain(id), + Err(Error::UnsupportedNetwork) + )); + } + } +} diff --git a/crates/cli/tests/cli.rs b/crates/cli/tests/cli.rs new file mode 100644 index 00000000..3f225a2b --- /dev/null +++ b/crates/cli/tests/cli.rs @@ -0,0 +1,143 @@ +//! End-to-end tests of the rain-metadata binary's stdin/stdout behaviour, +//! which in-process unit tests cannot observe. +#![cfg(not(target_family = "wasm"))] + +use std::io::Write; +use std::process::{Command, Stdio}; + +fn bin() -> Command { + Command::new(env!("CARGO_BIN_EXE_rain-metadata")) +} + +/// `magic ls` prints every known magic number, in declaration order, as +/// `{:#x} {kebab-name}` lines. Expected list is derived from the magic +/// number table of the rain metadata-v1 spec: +/// https://github.com/rainprotocol/specs/blob/main/metadata-v1.md +#[test] +fn magic_ls_prints_all_known_magic_numbers() { + let out = bin().args(["magic", "ls"]).output().unwrap(); + assert!(out.status.success()); + let stdout = String::from_utf8(out.stdout).unwrap(); + let expected = "\ +0xff0a89c674ee7874 rain-meta-document-v1 +0xffe5282f43e495b4 op-meta-v1 +0xffdac2f2f37be894 dotrain-v1 +0xff1c198cec3b48a7 rainlang-v1 +0xffe5ffb4a3ff2cde solidity-abi-v2 +0xffe9e3a02ca8e235 authoring-meta-v1 +0xff52fe42f1a05093 authoring-meta-v2 +0xffc21bbf86cc199b interpreter-caller-meta-v1 +0xffdb988a8cd04d32 expression-deployer-v2-bytecode-v1 +0xff13109e41336ff2 rainlang-source-v1 +0xffb2637608c09e38 address-list +0xffa15ef0fc437099 dotrain-source-v1 +0xffda7b2fb167c286 order-builder-state-v1 +0xff7a1507ba4419ca raindex-signed-context-oracle-v1 +0xffa8e8a9b9cf4a31 oa-schema +0xff9fae3cc645f463 oa-hash-list +0xffc47a6299e8a911 oa-structure +0xff8cd2927c8c86cb oa-token-image +0xffbc38eb14ad2209 oa-token-credential-links +"; + assert_eq!(stdout, expected); +} + +/// `schema ls` prints every known meta identifier in declaration order. +#[test] +fn schema_ls_prints_all_known_metas() { + let out = bin().args(["schema", "ls"]).output().unwrap(); + assert!(out.status.success()); + let stdout = String::from_utf8(out.stdout).unwrap(); + let expected = "\ +op-v1 +dotrain-v1 +rainlang-v1 +solidity-abi-v2 +authoring-meta-v1 +authoring-meta-v2 +interpreter-caller-meta-v1 +expression-deployer-v2-bytecode-v1 +rainlang-source-v1 +address-list +dotrain-source-v1 +order-builder-state-v1 +raindex-signed-context-oracle-v1 +"; + assert_eq!(stdout, expected); +} + +/// `schema show` writes the schema to stdout when no output path is +/// given (the stdout branch of cli::output::output). +#[test] +fn schema_show_prints_op_meta_schema_to_stdout() { + let out = bin().args(["schema", "show", "op-v1"]).output().unwrap(); + assert!(out.status.success()); + let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + assert_eq!(v["title"], "OpMeta."); +} + +/// `schema-check` reports the number of verified entities and the source +/// label on success. +#[test] +fn schema_check_prints_verified_entity_count() { + let dir = tempfile::tempdir().unwrap(); + let source = dir.path().join("schema.graphql"); + std::fs::write( + &source, + "type MetaBoard @entity { id: Bytes! }\ntype MetaV1 @entity { id: ID! }\n", + ) + .unwrap(); + let consumer = dir.path().join("consumer.graphql"); + std::fs::write( + &consumer, + "type MetaBoard { id: Bytes! }\ntype MetaV1 { id: ID! }\n", + ) + .unwrap(); + + let out = bin() + .args([ + "schema-check", + "--source", + source.to_str().unwrap(), + "--consumer", + consumer.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(out.status.success()); + let stdout = String::from_utf8(out.stdout).unwrap(); + assert_eq!(stdout, "schema check ok: 2 entities verified against source\n"); +} + +/// `generate source` reads the dotrain content from stdin when no input +/// path is given, and writes the emit data JSON to stdout when no output +/// path is given. +#[test] +fn generate_source_reads_stdin_and_writes_stdout() { + let content = "#main _ _: int-add(1 2);"; + let mut child = bin() + .args(["generate", "source"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .as_mut() + .unwrap() + .write_all(content.as_bytes()) + .unwrap(); + let out = child.wait_with_output().unwrap(); + assert!(out.status.success()); + + let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap(); + let subject = v["subject"].as_str().unwrap(); + assert!(subject.starts_with("0x")); + assert_eq!(subject.len(), 66); + // The meta bytes carry the rain meta document magic prefix. + assert!(v["meta_bytes"] + .as_str() + .unwrap() + .starts_with("0xff0a89c674ee7874")); + assert!(v["calldata"].as_str().unwrap().starts_with("0x")); +} From babfb8d3e0fc86a8dbd2c141f6502d90331ffc5e Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sat, 22 Aug 2026 22:25:00 +0000 Subject: [PATCH 5/5] style: rustfmt the AMT test additions Co-Authored-By: Claude Fable 5 --- crates/cli/src/cli/build.rs | 93 ++++++++++++------- .../src/meta/types/interpreter_caller/v1.rs | 15 +-- crates/cli/tests/cli.rs | 5 +- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/crates/cli/src/cli/build.rs b/crates/cli/src/cli/build.rs index 3cd9ede1..7e579cb7 100644 --- a/crates/cli/src/cli/build.rs +++ b/crates/cli/src/cli/build.rs @@ -354,9 +354,12 @@ mod tests { fn test_build_arity_guards() { let b = parse_build(&[ "build", - "-i", "does-not-exist.json", - "-m", "solidity-abi-v2", - "-m", "solidity-abi-v2", + "-i", + "does-not-exist.json", + "-m", + "solidity-abi-v2", + "-m", + "solidity-abi-v2", ]); assert_eq!( build(b).unwrap_err().to_string(), @@ -365,10 +368,14 @@ mod tests { let b = parse_build(&[ "build", - "-i", "does-not-exist.json", - "-m", "solidity-abi-v2", - "-t", "json", - "-t", "json", + "-i", + "does-not-exist.json", + "-m", + "solidity-abi-v2", + "-t", + "json", + "-t", + "json", ]); assert_eq!( build(b).unwrap_err().to_string(), @@ -377,11 +384,16 @@ mod tests { let b = parse_build(&[ "build", - "-i", "does-not-exist.json", - "-m", "solidity-abi-v2", - "-t", "json", - "-e", "identity", - "-e", "identity", + "-i", + "does-not-exist.json", + "-m", + "solidity-abi-v2", + "-t", + "json", + "-e", + "identity", + "-e", + "identity", ]); assert_eq!( build(b).unwrap_err().to_string(), @@ -390,12 +402,18 @@ mod tests { let b = parse_build(&[ "build", - "-i", "does-not-exist.json", - "-m", "solidity-abi-v2", - "-t", "json", - "-e", "identity", - "-l", "en", - "-l", "en", + "-i", + "does-not-exist.json", + "-m", + "solidity-abi-v2", + "-t", + "json", + "-e", + "identity", + "-l", + "en", + "-l", + "en", ]); assert_eq!( build(b).unwrap_err().to_string(), @@ -428,25 +446,38 @@ mod tests { let b = parse_build(&[ "build", - "-i", &input_path, - "-m", "solidity-abi-v2", - "-t", "json", - "-e", "identity", - "-l", "en", - "-o", &output_path, + "-i", + &input_path, + "-m", + "solidity-abi-v2", + "-t", + "json", + "-e", + "identity", + "-l", + "en", + "-o", + &output_path, ]); build(b)?; assert_eq!(std::fs::read(output.path())?, expected); let b = parse_build(&[ "build", - "-i", &input_path, - "-m", "solidity-abi-v2", - "-t", "json", - "-e", "identity", - "-l", "en", - "-o", &output_path, - "-E", "hex", + "-i", + &input_path, + "-m", + "solidity-abi-v2", + "-t", + "json", + "-e", + "identity", + "-l", + "en", + "-o", + &output_path, + "-E", + "hex", ]); build(b)?; assert_eq!( diff --git a/crates/cli/src/meta/types/interpreter_caller/v1.rs b/crates/cli/src/meta/types/interpreter_caller/v1.rs index 80aabd95..3ad685f1 100644 --- a/crates/cli/src/meta/types/interpreter_caller/v1.rs +++ b/crates/cli/src/meta/types/interpreter_caller/v1.rs @@ -293,17 +293,11 @@ mod tests { fn test_length_constraints() { let mut v = valid_json(); *v.pointer_mut("/methods").unwrap() = serde_json::json!([]); - assert!(matches!( - parse(&v).unwrap_err(), - Error::ValidationErrors(_) - )); + assert!(matches!(parse(&v).unwrap_err(), Error::ValidationErrors(_))); let mut v = valid_json(); *v.pointer_mut("/methods/0/inputs").unwrap() = serde_json::json!([]); - assert!(matches!( - parse(&v).unwrap_err(), - Error::ValidationErrors(_) - )); + assert!(matches!(parse(&v).unwrap_err(), Error::ValidationErrors(_))); let column = serde_json::json!({ "name": "Col" }); let mut v = valid_json(); @@ -314,10 +308,7 @@ mod tests { let mut v = valid_json(); *v.pointer_mut("/methods/0/expressions/0/contextColumns") .unwrap() = serde_json::Value::Array(vec![column; 256]); - assert!(matches!( - parse(&v).unwrap_err(), - Error::ValidationErrors(_) - )); + assert!(matches!(parse(&v).unwrap_err(), Error::ValidationErrors(_))); } /// TryFrom> and TryFrom<&[u8]> validate after parsing: diff --git a/crates/cli/tests/cli.rs b/crates/cli/tests/cli.rs index 3f225a2b..a6306d12 100644 --- a/crates/cli/tests/cli.rs +++ b/crates/cli/tests/cli.rs @@ -106,7 +106,10 @@ fn schema_check_prints_verified_entity_count() { .unwrap(); assert!(out.status.success()); let stdout = String::from_utf8(out.stdout).unwrap(); - assert_eq!(stdout, "schema check ok: 2 entities verified against source\n"); + assert_eq!( + stdout, + "schema check ok: 2 entities verified against source\n" + ); } /// `generate source` reads the dotrain content from stdin when no input