use super::accessors::{swift_build_accessor, swift_stringy_aggregator_contains_assert};
use crate::e2e::field_access::FieldResolver;
use std::collections::{HashMap, HashSet};
fn make_resolver_tool_calls() -> FieldResolver {
let mut optional = HashSet::new();
optional.insert("choices.message.tool_calls".to_string());
let mut arrays = HashSet::new();
arrays.insert("choices".to_string());
FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
}
#[test]
fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
let resolver = make_resolver_tool_calls();
let (accessor, has_optional) =
swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
assert!(
accessor.contains("toolCalls()?[0]"),
"expected `toolCalls()?[0]` for optional tool_calls, got: {accessor}"
);
assert!(
!accessor.contains("?[0]?"),
"must not emit trailing `?` after subscript index: {accessor}"
);
assert!(has_optional, "expected has_optional=true for optional field chain");
assert!(
accessor.contains("[0].function"),
"expected `.function` (non-optional) after subscript: {accessor}"
);
}
#[test]
fn contains_against_vec_dto_aggregates_stringy_accessors() {
use crate::e2e::field_access::{StringyField, StringyFieldKind, SwiftFirstClassMap};
let mut stringy_fields_by_type: HashMap<String, Vec<StringyField>> = HashMap::new();
stringy_fields_by_type.insert(
"ImportInfo".to_string(),
vec![
StringyField {
name: "source".to_string(),
kind: StringyFieldKind::Plain,
},
StringyField {
name: "items".to_string(),
kind: StringyFieldKind::Vec,
},
StringyField {
name: "alias".to_string(),
kind: StringyFieldKind::Optional,
},
],
);
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut process_fields = HashMap::new();
process_fields.insert("imports".to_string(), "ImportInfo".to_string());
field_types.insert("ProcessResult".to_string(), process_fields);
let mut arrays = HashSet::new();
arrays.insert("imports".to_string());
let map = SwiftFirstClassMap {
first_class_types: HashSet::new(),
field_types,
vec_field_names: HashSet::new(),
root_type: None,
stringy_fields_by_type,
};
let resolver = FieldResolver::new_with_swift_first_class(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&arrays,
&HashSet::new(),
&HashMap::new(),
map,
)
.with_swift_root_type(Some("ProcessResult".to_string()));
let line = swift_stringy_aggregator_contains_assert(Some("imports"), "result", &resolver, "\"os\"")
.expect("aggregator should fire for Vec<ImportInfo> contains");
assert!(
line.contains("result.imports().contains(where: { item in"),
"expected contains(where:) over result.imports(): {line}"
);
assert!(
line.contains("texts.append(item.source().toString())"),
"expected plain source() accessor: {line}"
);
assert!(
line.contains("texts.append(contentsOf: item.items().map { $0.as_str().toString() })"),
"expected vec items() flattened via .map as_str(): {line}"
);
assert!(
line.contains("if let v = item.alias()"),
"expected optional alias() unwrap: {line}"
);
assert!(
line.contains("$0.contains(\"os\")"),
"expected substring contains over expected value: {line}"
);
assert!(!line.contains("$0 == \"os\""), "must not use exact equality: {line}");
}
#[test]
fn contains_aggregator_skips_when_only_one_stringy_field() {
use crate::e2e::field_access::{StringyField, StringyFieldKind, SwiftFirstClassMap};
let mut stringy_fields_by_type: HashMap<String, Vec<StringyField>> = HashMap::new();
stringy_fields_by_type.insert(
"TagInfo".to_string(),
vec![StringyField {
name: "name".to_string(),
kind: StringyFieldKind::Plain,
}],
);
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut root_fields = HashMap::new();
root_fields.insert("tags".to_string(), "TagInfo".to_string());
field_types.insert("Root".to_string(), root_fields);
let mut arrays = HashSet::new();
arrays.insert("tags".to_string());
let map = SwiftFirstClassMap {
first_class_types: HashSet::new(),
field_types,
vec_field_names: HashSet::new(),
root_type: None,
stringy_fields_by_type,
};
let resolver = FieldResolver::new_with_swift_first_class(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&arrays,
&HashSet::new(),
&HashMap::new(),
map,
)
.with_swift_root_type(Some("Root".to_string()));
assert!(
swift_stringy_aggregator_contains_assert(Some("tags"), "result", &resolver, "\"x\"").is_none(),
"single-stringy-field types must not trigger the aggregator"
);
}
#[test]
fn chained_optional_only_emits_question_mark_on_first_optional() {
let mut optional = HashSet::new();
optional.insert("summary".to_string());
let resolver = FieldResolver::new(
&HashMap::new(),
&optional,
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let (accessor, has_optional) = swift_build_accessor("summary.strategy", "result", &resolver);
assert!(
accessor.contains("summary()?"),
"expected `summary()?` for optional summary field: {accessor}"
);
assert!(
!accessor.contains("strategy()?"),
"must not emit `?` after already-unwrapped optional field: {accessor}"
);
assert_eq!(
accessor, "result.summary()?.strategy()",
"expected `result.summary()?.strategy()`, got: {accessor}"
);
assert!(has_optional, "expected has_optional=true for chain with optional root");
}
#[test]
fn test_file_renders_env_vars_in_class_setup() {
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
let mut e2e_config = E2eConfig::default();
e2e_config.env.insert("ZEBRA".to_string(), "z_value".to_string());
e2e_config.env.insert("APPLE".to_string(), "a_value".to_string());
e2e_config.env.insert("BANANA".to_string(), "b_value".to_string());
let output = super::test_file::render_test_file(
"smoke",
&[],
&e2e_config,
"TestModule",
"TestCase",
"testFunction",
"result",
&[],
false,
None,
&Default::default(),
&ResolvedCrateConfig::default(),
&[],
false,
&[],
);
assert!(output.contains("APPLE"), "expected APPLE env var in output");
assert!(output.contains("BANANA"), "expected BANANA env var in output");
assert!(output.contains("ZEBRA"), "expected ZEBRA env var in output");
let apple_pos = output.find("APPLE").unwrap();
let banana_pos = output.find("BANANA").unwrap();
let zebra_pos = output.find("ZEBRA").unwrap();
assert!(
apple_pos < banana_pos && banana_pos < zebra_pos,
"env vars must be sorted alphabetically, got positions APPLE={}, BANANA={}, ZEBRA={}",
apple_pos,
banana_pos,
zebra_pos
);
assert!(
output.contains("setenv(key, val, 0)"),
"expected setenv(key, val, 0) calls in output"
);
}
#[test]
fn test_file_renders_no_env_block_when_env_empty() {
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
let e2e_config = E2eConfig::default();
let output = super::test_file::render_test_file(
"smoke",
&[],
&e2e_config,
"TestModule",
"TestCase",
"testFunction",
"result",
&[],
false,
None,
&Default::default(),
&ResolvedCrateConfig::default(),
&[],
false,
&[],
);
assert!(
!output.contains("setenv"),
"empty env should not produce any setenv calls"
);
}
#[test]
fn app_harness_renders_fixtures_json_chunks_without_multiline_string_syntax_error() {
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::FixtureGroup;
let group = FixtureGroup {
category: "test".to_string(),
fixtures: vec![],
};
let e2e_config = E2eConfig::default();
let output = super::project::render_app_harness(&e2e_config, &[group], "TestModule");
assert!(
!output.contains("\"\"\"{{"),
"output must not have multiline string opening followed by JSON object on same line"
);
assert!(
!output.contains("\"\"\" {"),
"output must not have multiline string opening followed by space and JSON on same line"
);
assert!(
output.contains("let _FIXTURES_JSON: String = ["),
"expected array literal pattern: let _FIXTURES_JSON: String = ["
);
assert!(
output.contains("].joined()"),
"expected .joined() call to concatenate chunks"
);
assert!(!output.is_empty(), "rendered output should not be empty");
}