mod common;
use common::*;
use zenith_core::format::format_document;
const WITH_ASSETS: &str = r##"zenith version=1 {
project id="proj.assets" name="Assets Test"
assets {
asset id="asset.logo" kind="svg" src="assets/logo.svg" sha256="deadbeef"
asset id="asset.hero" kind="image" src="assets/hero.png"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.assets" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
#[test]
fn test_assets_parse_fields() {
let adapter = KdlAdapter;
let doc = adapter
.parse(WITH_ASSETS.as_bytes())
.expect("parse must succeed");
let assets = &doc.assets.assets;
assert_eq!(assets.len(), 2, "expected 2 asset declarations");
let logo = &assets[0];
assert_eq!(logo.id, "asset.logo");
assert_eq!(logo.kind, zenith_core::AssetKind::Svg);
assert_eq!(logo.src, "assets/logo.svg");
assert_eq!(logo.sha256.as_deref(), Some("deadbeef"));
let hero = &assets[1];
assert_eq!(hero.id, "asset.hero");
assert_eq!(hero.kind, zenith_core::AssetKind::Image);
assert_eq!(hero.src, "assets/hero.png");
assert!(hero.sha256.is_none(), "sha256 should be None when absent");
}
#[test]
fn test_assets_round_trip_ast_equality() {
let adapter = KdlAdapter;
let doc_orig = adapter
.parse(WITH_ASSETS.as_bytes())
.expect("original parse");
let formatted = format_document(&doc_orig).expect("format");
let doc2 = adapter.parse(&formatted).expect("re-parse after format");
let a1 = &doc_orig.assets.assets;
let a2 = &doc2.assets.assets;
assert_eq!(a1.len(), a2.len(), "asset count must survive round-trip");
for (orig, reparsed) in a1.iter().zip(a2.iter()) {
assert_eq!(orig.id, reparsed.id);
assert_eq!(orig.kind, reparsed.kind);
assert_eq!(orig.src, reparsed.src);
assert_eq!(orig.sha256, reparsed.sha256);
}
}
#[test]
fn test_assets_format_idempotency() {
let adapter = KdlAdapter;
let doc = adapter
.parse(WITH_ASSETS.as_bytes())
.expect("parse must succeed");
let s1 = format_document(&doc).expect("format 1");
let doc2 = adapter.parse(&s1).expect("parse after first format");
let s2 = format_document(&doc2).expect("format 2");
assert_eq!(
String::from_utf8(s1).unwrap(),
String::from_utf8(s2).unwrap(),
"assets format must be idempotent"
);
}
#[test]
fn test_assets_canonical_property_order() {
let adapter = KdlAdapter;
let doc = adapter
.parse(WITH_ASSETS.as_bytes())
.expect("parse must succeed");
let out = format_document(&doc).expect("format");
let text = String::from_utf8(out).unwrap();
let logo_line = text
.lines()
.find(|l| l.trim_start().starts_with("asset") && l.contains("asset.logo"))
.expect("must find logo asset line");
let pos_id = logo_line.find("id=").expect("id= must be present");
let pos_kind = logo_line.find("kind=").expect("kind= must be present");
let pos_src = logo_line.find("src=").expect("src= must be present");
let pos_sha256 = logo_line.find("sha256=").expect("sha256= must be present");
assert!(pos_id < pos_kind, "id must come before kind");
assert!(pos_kind < pos_src, "kind must come before src");
assert!(pos_src < pos_sha256, "src must come before sha256");
}
#[test]
fn test_asset_ai_provenance_parse_fields() {
let src = r##"zenith version=1 {
assets {
asset id="asset.gen" kind="image" src="assets/gen.png" ai-prompt="a red fox" ai-model="dall-e-3" ai-provider="openai" ai-seed=42 ai-generation-date="2024-01-15" ai-license="CC0-1.0" ai-source-rights="none" ai-safety-status="approved" ai-reuse-policy="free"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ai" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse must succeed");
let decl = &doc.assets.assets[0];
assert_eq!(decl.ai_prompt.as_deref(), Some("a red fox"));
assert_eq!(decl.ai_model.as_deref(), Some("dall-e-3"));
assert_eq!(decl.ai_provider.as_deref(), Some("openai"));
assert_eq!(decl.ai_seed, Some(42_i64));
assert_eq!(decl.ai_generation_date.as_deref(), Some("2024-01-15"));
assert_eq!(decl.ai_license.as_deref(), Some("CC0-1.0"));
assert_eq!(decl.ai_source_rights.as_deref(), Some("none"));
assert_eq!(decl.ai_safety_status.as_deref(), Some("approved"));
assert_eq!(decl.ai_reuse_policy.as_deref(), Some("free"));
}
#[test]
fn test_asset_ai_provenance_round_trip_ast_equality() {
let src = r##"zenith version=1 {
assets {
asset id="asset.gen" kind="image" src="assets/gen.png" ai-prompt="a red fox" ai-model="dall-e-3" ai-provider="openai" ai-seed=42 ai-generation-date="2024-01-15" ai-license="CC0-1.0" ai-source-rights="none" ai-safety-status="approved" ai-reuse-policy="free"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ai" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let doc1 = adapter.parse(src.as_bytes()).expect("parse 1");
let formatted = format_document(&doc1).expect("format");
let doc2 = adapter.parse(&formatted).expect("parse 2");
let d1 = &doc1.assets.assets[0];
let d2 = &doc2.assets.assets[0];
assert_eq!(d1.ai_prompt, d2.ai_prompt);
assert_eq!(d1.ai_model, d2.ai_model);
assert_eq!(d1.ai_provider, d2.ai_provider);
assert_eq!(d1.ai_seed, d2.ai_seed);
assert_eq!(d1.ai_generation_date, d2.ai_generation_date);
assert_eq!(d1.ai_license, d2.ai_license);
assert_eq!(d1.ai_source_rights, d2.ai_source_rights);
assert_eq!(d1.ai_safety_status, d2.ai_safety_status);
assert_eq!(d1.ai_reuse_policy, d2.ai_reuse_policy);
}
#[test]
fn test_asset_ai_provenance_format_idempotency() {
let src = r##"zenith version=1 {
assets {
asset id="asset.gen" kind="image" src="assets/gen.png" ai-prompt="a red fox" ai-model="dall-e-3" ai-provider="openai" ai-seed=42 ai-generation-date="2024-01-15" ai-license="CC0-1.0" ai-source-rights="none" ai-safety-status="approved" ai-reuse-policy="free"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ai" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let s1 = format_document(&doc).expect("format 1");
let doc2 = adapter.parse(&s1).expect("parse after format 1");
let s2 = format_document(&doc2).expect("format 2");
assert_eq!(
String::from_utf8(s1).unwrap(),
String::from_utf8(s2).unwrap(),
"AI-provenance asset format must be idempotent"
);
}
#[test]
fn test_asset_ai_provenance_absent_byte_identity() {
let src = r##"zenith version=1 {
assets {
asset id="asset.plain" kind="image" src="assets/plain.png"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.plain" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let out = format_document(&doc).expect("format");
let text = String::from_utf8(out).unwrap();
assert!(
!text.contains("ai-"),
"formatted output must not contain \"ai-\" when no provenance fields are set; got:\n{text}"
);
}
#[test]
fn test_asset_ai_provenance_string_escaping_round_trip() {
let prompt_value = "a \"quoted\" word\nand a newline".to_owned();
let src = r##"zenith version=1 {
assets {
asset id="asset.esc" kind="image" src="assets/esc.png"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.esc" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let mut doc = adapter.parse(src.as_bytes()).expect("parse");
doc.assets.assets[0].ai_prompt = Some(prompt_value.clone());
let formatted = format_document(&doc).expect("format");
let doc2 = adapter.parse(&formatted).expect("re-parse after format");
assert_eq!(
doc2.assets.assets[0].ai_prompt.as_deref(),
Some(prompt_value.as_str()),
"ai-prompt with embedded quote and newline must survive format → parse"
);
}
#[test]
fn test_asset_ai_provenance_canonical_order() {
let src = r##"zenith version=1 {
assets {
asset id="asset.ord" kind="image" src="assets/ord.png" sha256="abc123" ai-prompt="fox" ai-reuse-policy="free" zzz-unknown="yes"
}
tokens format="zenith-token-v1" {
}
styles {
}
document id="doc.ord" {
page id="page.one" w=(px)640 h=(px)360 {
}
}
}
"##;
let adapter = KdlAdapter;
let doc = adapter.parse(src.as_bytes()).expect("parse");
let out = format_document(&doc).expect("format");
let text = String::from_utf8(out).unwrap();
let asset_line = text
.lines()
.find(|l| l.trim_start().starts_with("asset") && l.contains("asset.ord"))
.expect("must find the asset.ord line");
let pos_sha256 = asset_line.find("sha256=").expect("sha256= must be present");
let pos_ai_prompt = asset_line
.find("ai-prompt=")
.expect("ai-prompt= must be present");
let pos_ai_reuse = asset_line
.find("ai-reuse-policy=")
.expect("ai-reuse-policy= must be present");
let pos_unknown = asset_line
.find("zzz-unknown=")
.expect("zzz-unknown= must be present");
assert!(
pos_sha256 < pos_ai_prompt,
"sha256 must come before ai-prompt"
);
assert!(
pos_ai_reuse < pos_unknown,
"ai-reuse-policy must come before unknown props"
);
}